mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +08:00
feat(core): onboarding paper unfolding animation (#5264)
This commit is contained in:
@@ -1,43 +1,76 @@
|
|||||||
import type { CSSProperties } from 'react';
|
import { type CSSProperties, useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { articles } from './articles';
|
import { articles } from './articles';
|
||||||
import { PaperSteps } from './paper-steps';
|
import { PaperSteps } from './paper-steps';
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
|
import type { ArticleId, ArticleOption } from './types';
|
||||||
|
|
||||||
interface OnboardingProps {
|
interface OnboardingProps {
|
||||||
onOpenApp?: () => void;
|
onOpenApp?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Onboarding = (_: OnboardingProps) => {
|
export const Onboarding = (_: OnboardingProps) => {
|
||||||
|
const [status, setStatus] = useState<{
|
||||||
|
activeId: ArticleId | null;
|
||||||
|
unfoldingId: ArticleId | null;
|
||||||
|
}>({ activeId: null, unfoldingId: null });
|
||||||
|
|
||||||
|
const onFoldChange = useCallback((id: ArticleId, v: boolean) => {
|
||||||
|
setStatus(s => {
|
||||||
|
return {
|
||||||
|
activeId: v ? null : s.activeId,
|
||||||
|
unfoldingId: v ? null : id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onFoldChanged = useCallback((id: ArticleId, v: boolean) => {
|
||||||
|
setStatus(s => {
|
||||||
|
return {
|
||||||
|
activeId: v ? null : id,
|
||||||
|
unfoldingId: s.unfoldingId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.onboarding} data-is-desktop={environment.isDesktop}>
|
<div className={styles.onboarding} data-is-desktop={environment.isDesktop}>
|
||||||
<div className={styles.offsetOrigin}>
|
<div className={styles.offsetOrigin}>
|
||||||
{Object.entries(articles).map(([id, article]) => {
|
{(Object.entries(articles) as Array<[ArticleId, ArticleOption]>).map(
|
||||||
const { enterOptions, location } = article;
|
([id, article]) => {
|
||||||
const style = {
|
const { enterOptions, location } = article;
|
||||||
'--fromX': `${enterOptions.fromX}vw`,
|
const style = {
|
||||||
'--fromY': `${enterOptions.fromY}vh`,
|
zIndex: status.unfoldingId === id ? 1 : 0,
|
||||||
'--fromZ': `${enterOptions.fromZ}px`,
|
|
||||||
'--toZ': `${enterOptions.toZ}px`,
|
|
||||||
'--fromRotateX': `${enterOptions.fromRotateX}deg`,
|
|
||||||
'--fromRotateY': `${enterOptions.fromRotateY}deg`,
|
|
||||||
'--fromRotateZ': `${enterOptions.fromRotateZ}deg`,
|
|
||||||
'--toRotateZ': `${enterOptions.toRotateZ}deg`,
|
|
||||||
|
|
||||||
'--delay': `${enterOptions.delay}ms`,
|
'--fromX': `${enterOptions.fromX}vw`,
|
||||||
'--duration': enterOptions.duration,
|
'--fromY': `${enterOptions.fromY}vh`,
|
||||||
'--easing': enterOptions.easing,
|
'--fromZ': `${enterOptions.fromZ}px`,
|
||||||
|
'--toZ': `${enterOptions.toZ}px`,
|
||||||
|
'--fromRotateX': `${enterOptions.fromRotateX}deg`,
|
||||||
|
'--fromRotateY': `${enterOptions.fromRotateY}deg`,
|
||||||
|
'--fromRotateZ': `${enterOptions.fromRotateZ}deg`,
|
||||||
|
'--toRotateZ': `${enterOptions.toRotateZ}deg`,
|
||||||
|
|
||||||
'--offset-x': `${location.x || 0}px`,
|
'--delay': `${enterOptions.delay}ms`,
|
||||||
'--offset-y': `${location.y || 0}px`,
|
'--duration': enterOptions.duration,
|
||||||
} as CSSProperties;
|
'--easing': enterOptions.easing,
|
||||||
|
|
||||||
return (
|
'--offset-x': `${location.x || 0}px`,
|
||||||
<div style={style} key={id}>
|
'--offset-y': `${location.y || 0}px`,
|
||||||
<PaperSteps article={article} show={true} />
|
} as CSSProperties;
|
||||||
</div>
|
|
||||||
);
|
return (
|
||||||
})}
|
<div style={style} key={id}>
|
||||||
|
<PaperSteps
|
||||||
|
article={article}
|
||||||
|
show={status.activeId === null || status.activeId === id}
|
||||||
|
onFoldChange={onFoldChange}
|
||||||
|
onFoldChanged={onFoldChanged}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,51 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { AnimateIn } from './steps/animate-in';
|
import { AnimateIn } from './steps/animate-in';
|
||||||
import type { ArticleOption } from './types';
|
import { Unfolding } from './steps/unfolding';
|
||||||
|
import type { ArticleId, OnboardingStep } from './types';
|
||||||
|
import { type ArticleOption } from './types';
|
||||||
|
|
||||||
interface PaperStepsProps {
|
interface PaperStepsProps {
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
article: ArticleOption;
|
article: ArticleOption;
|
||||||
|
onFoldChange?: (id: ArticleId, v: boolean) => void;
|
||||||
|
onFoldChanged?: (id: ArticleId, v: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PaperSteps = ({ show, article }: PaperStepsProps) => {
|
export const PaperSteps = ({
|
||||||
const onFinished = useCallback(() => {
|
show,
|
||||||
console.log('onFinished');
|
article,
|
||||||
|
onFoldChange,
|
||||||
|
onFoldChanged,
|
||||||
|
}: PaperStepsProps) => {
|
||||||
|
const [stage, setStage] = useState<OnboardingStep>('enter');
|
||||||
|
|
||||||
|
const onEntered = useCallback(() => {
|
||||||
|
setStage('unfold');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const _onFoldChange = useCallback(
|
||||||
|
(v: boolean) => {
|
||||||
|
onFoldChange?.(article.id, v);
|
||||||
|
},
|
||||||
|
[onFoldChange, article.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const _onFoldChanged = useCallback(
|
||||||
|
(v: boolean) => {
|
||||||
|
onFoldChanged?.(article.id, v);
|
||||||
|
},
|
||||||
|
[onFoldChanged, article.id]
|
||||||
|
);
|
||||||
|
|
||||||
if (!show) return null;
|
if (!show) return null;
|
||||||
return <AnimateIn article={article} onFinished={onFinished} />;
|
return stage === 'enter' ? (
|
||||||
|
<AnimateIn article={article} onFinished={onEntered} />
|
||||||
|
) : stage === 'unfold' ? (
|
||||||
|
<Unfolding
|
||||||
|
article={article}
|
||||||
|
onChange={_onFoldChange}
|
||||||
|
onChanged={_onFoldChanged}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { keyframes, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
import { onboardingVars, paperLocation } from '../style.css';
|
||||||
|
|
||||||
|
const unfolding = onboardingVars.unfolding;
|
||||||
|
|
||||||
|
const shadowIn = keyframes({
|
||||||
|
from: { boxShadow: `0px 0px 0px rgba(0, 0, 0, 0)` },
|
||||||
|
to: { boxShadow: `0px 0px 4px rgba(66, 65, 73, 0.14)` },
|
||||||
|
});
|
||||||
|
const borderIn = keyframes({
|
||||||
|
from: { opacity: 0 },
|
||||||
|
to: { opacity: 1 },
|
||||||
|
});
|
||||||
|
const fadeOut = keyframes({
|
||||||
|
from: { opacity: 1 },
|
||||||
|
to: { opacity: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const unfoldingWrapper = style([
|
||||||
|
paperLocation,
|
||||||
|
{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
|
||||||
|
transform: 'rotate(var(--toRotateZ))',
|
||||||
|
cursor: 'pointer',
|
||||||
|
|
||||||
|
backgroundColor: onboardingVars.paper.bg,
|
||||||
|
borderRadius: onboardingVars.paper.r,
|
||||||
|
width: onboardingVars.paper.w,
|
||||||
|
height: onboardingVars.paper.h,
|
||||||
|
|
||||||
|
// animate in
|
||||||
|
boxShadow: `0px 0px 0px rgba(0, 0, 0, 0)`,
|
||||||
|
animation: `${shadowIn} 0.5s ease forwards`,
|
||||||
|
|
||||||
|
transition: `all 0.23s ease, width ${unfolding.sizeTransition}, height ${unfolding.sizeTransition}, transform ${unfolding.transformTransition}`,
|
||||||
|
|
||||||
|
'::before': {
|
||||||
|
// hack border
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
border: `1px solid ${onboardingVars.paper.borderColor}`,
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
animation: `${borderIn} 0.5s ease forwards`,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&[data-fold="false"]': {
|
||||||
|
vars: {
|
||||||
|
'--toRotateZ': '0deg',
|
||||||
|
},
|
||||||
|
width: onboardingVars.article.w,
|
||||||
|
height: onboardingVars.article.h,
|
||||||
|
left: `calc(0 - ${onboardingVars.article.w} / 2)`,
|
||||||
|
top: `calc(0 - ${onboardingVars.article.h} / 2)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const unfoldingContent = style({
|
||||||
|
width: onboardingVars.paper.w,
|
||||||
|
height: onboardingVars.paper.h,
|
||||||
|
|
||||||
|
padding: '16px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
fontFamily: 'var(--affine-font-family)',
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&.leave': {
|
||||||
|
animation: `${fadeOut} 0.1s ease forwards`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import type { ArticleOption } from '../types';
|
||||||
|
import * as styles from './unfolding.css';
|
||||||
|
|
||||||
|
interface UnfoldingProps {
|
||||||
|
onChange?: (e: boolean) => void;
|
||||||
|
onChanged?: (e: boolean) => void;
|
||||||
|
article: ArticleOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Unfolding = ({ article, onChange, onChanged }: UnfoldingProps) => {
|
||||||
|
const [fold, setFold] = useState(true);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const toggleFold = useCallback(() => {
|
||||||
|
setFold(!fold);
|
||||||
|
return !fold;
|
||||||
|
}, [fold]);
|
||||||
|
|
||||||
|
const onPaperClick = useCallback(() => {
|
||||||
|
const isFold = toggleFold();
|
||||||
|
onChange?.(isFold);
|
||||||
|
|
||||||
|
if (ref.current) {
|
||||||
|
const handler = () => {
|
||||||
|
onChanged?.(isFold);
|
||||||
|
};
|
||||||
|
ref.current.addEventListener('transitionend', handler, { once: true });
|
||||||
|
return () => ref.current?.removeEventListener('transitionend', handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [toggleFold, onChange, onChanged]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-fold={fold}
|
||||||
|
className={styles.unfoldingWrapper}
|
||||||
|
onClick={onPaperClick}
|
||||||
|
>
|
||||||
|
<div className={clsx(styles.unfoldingContent, !fold && 'leave')}>
|
||||||
|
{article.brief}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -16,10 +16,24 @@ export const onboardingVars = {
|
|||||||
bg: 'var(--affine-pure-white)',
|
bg: 'var(--affine-pure-white)',
|
||||||
// textColor: 'var(--affine-light-text-primary-color)',
|
// textColor: 'var(--affine-light-text-primary-color)',
|
||||||
textColor: '#121212',
|
textColor: '#121212',
|
||||||
|
borderColor: '#E3E2E4',
|
||||||
|
},
|
||||||
|
unfolding: {
|
||||||
|
sizeTransition: '0.3s ease',
|
||||||
|
transformTransition: '0.3s ease',
|
||||||
},
|
},
|
||||||
web: {
|
web: {
|
||||||
bg: '#fafafa', // TODO: use var
|
bg: '#fafafa', // TODO: use var
|
||||||
},
|
},
|
||||||
|
|
||||||
|
article: {
|
||||||
|
w: '1200px',
|
||||||
|
h: '800px',
|
||||||
|
},
|
||||||
|
edgeless: {
|
||||||
|
w: '1200px',
|
||||||
|
h: '800px',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const perspective = style({
|
export const perspective = style({
|
||||||
|
|||||||
Reference in New Issue
Block a user