feat(core): onboarding paper unfolding animation (#5264)

This commit is contained in:
Cats Juice
2023-12-19 07:18:06 +00:00
parent 841385666e
commit d9f1cc60b9
5 changed files with 238 additions and 30 deletions

View File

@@ -1,43 +1,76 @@
import type { CSSProperties } from 'react';
import { type CSSProperties, useCallback, useState } from 'react';
import { articles } from './articles';
import { PaperSteps } from './paper-steps';
import * as styles from './style.css';
import type { ArticleId, ArticleOption } from './types';
interface OnboardingProps {
onOpenApp?: () => void;
}
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 (
<div className={styles.onboarding} data-is-desktop={environment.isDesktop}>
<div className={styles.offsetOrigin}>
{Object.entries(articles).map(([id, article]) => {
const { enterOptions, location } = article;
const style = {
'--fromX': `${enterOptions.fromX}vw`,
'--fromY': `${enterOptions.fromY}vh`,
'--fromZ': `${enterOptions.fromZ}px`,
'--toZ': `${enterOptions.toZ}px`,
'--fromRotateX': `${enterOptions.fromRotateX}deg`,
'--fromRotateY': `${enterOptions.fromRotateY}deg`,
'--fromRotateZ': `${enterOptions.fromRotateZ}deg`,
'--toRotateZ': `${enterOptions.toRotateZ}deg`,
{(Object.entries(articles) as Array<[ArticleId, ArticleOption]>).map(
([id, article]) => {
const { enterOptions, location } = article;
const style = {
zIndex: status.unfoldingId === id ? 1 : 0,
'--delay': `${enterOptions.delay}ms`,
'--duration': enterOptions.duration,
'--easing': enterOptions.easing,
'--fromX': `${enterOptions.fromX}vw`,
'--fromY': `${enterOptions.fromY}vh`,
'--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`,
'--offset-y': `${location.y || 0}px`,
} as CSSProperties;
'--delay': `${enterOptions.delay}ms`,
'--duration': enterOptions.duration,
'--easing': enterOptions.easing,
return (
<div style={style} key={id}>
<PaperSteps article={article} show={true} />
</div>
);
})}
'--offset-x': `${location.x || 0}px`,
'--offset-y': `${location.y || 0}px`,
} as CSSProperties;
return (
<div style={style} key={id}>
<PaperSteps
article={article}
show={status.activeId === null || status.activeId === id}
onFoldChange={onFoldChange}
onFoldChanged={onFoldChanged}
/>
</div>
);
}
)}
</div>
</div>
);

View File

@@ -1,18 +1,51 @@
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
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 {
show?: boolean;
article: ArticleOption;
onFoldChange?: (id: ArticleId, v: boolean) => void;
onFoldChanged?: (id: ArticleId, v: boolean) => void;
}
export const PaperSteps = ({ show, article }: PaperStepsProps) => {
const onFinished = useCallback(() => {
console.log('onFinished');
export const PaperSteps = ({
show,
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;
return <AnimateIn article={article} onFinished={onFinished} />;
return stage === 'enter' ? (
<AnimateIn article={article} onFinished={onEntered} />
) : stage === 'unfold' ? (
<Unfolding
article={article}
onChange={_onFoldChange}
onChanged={_onFoldChanged}
/>
) : null;
};

View File

@@ -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`,
},
},
});

View File

@@ -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>
);
};

View File

@@ -16,10 +16,24 @@ export const onboardingVars = {
bg: 'var(--affine-pure-white)',
// textColor: 'var(--affine-light-text-primary-color)',
textColor: '#121212',
borderColor: '#E3E2E4',
},
unfolding: {
sizeTransition: '0.3s ease',
transformTransition: '0.3s ease',
},
web: {
bg: '#fafafa', // TODO: use var
},
article: {
w: '1200px',
h: '800px',
},
edgeless: {
w: '1200px',
h: '800px',
},
};
export const perspective = style({