feat(core): onboarding paper enter animation (#5248)
This commit is contained in:
@@ -50,6 +50,7 @@
|
|||||||
"@toeverything/theme": "^0.7.20",
|
"@toeverything/theme": "^0.7.20",
|
||||||
"@vanilla-extract/css": "^1.13.0",
|
"@vanilla-extract/css": "^1.13.0",
|
||||||
"@vanilla-extract/dynamic": "^2.0.3",
|
"@vanilla-extract/dynamic": "^2.0.3",
|
||||||
|
"animejs": "^3.2.2",
|
||||||
"async-call-rpc": "^6.3.1",
|
"async-call-rpc": "^6.3.1",
|
||||||
"bytes": "^3.1.2",
|
"bytes": "^3.1.2",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
@@ -93,6 +94,7 @@
|
|||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@swc/core": "^1.3.93",
|
"@swc/core": "^1.3.93",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
|
"@types/animejs": "^3",
|
||||||
"@types/bytes": "^3.1.3",
|
"@types/bytes": "^3.1.3",
|
||||||
"@types/image-blob-reduce": "^4.1.3",
|
"@types/image-blob-reduce": "^4.1.3",
|
||||||
"@types/lodash-es": "^4.17.9",
|
"@types/lodash-es": "^4.17.9",
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import { article, articleWrapper, text, title } from '../curve-paper/paper.css';
|
||||||
|
import type { ArticleId, ArticleOption } from '../types';
|
||||||
|
|
||||||
|
const ids = ['0', '1', '2', '3', '4'] as Array<ArticleId>;
|
||||||
|
|
||||||
|
/** locate paper */
|
||||||
|
const paperLocations = {
|
||||||
|
'0': {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
'1': {
|
||||||
|
x: -240,
|
||||||
|
y: -100,
|
||||||
|
},
|
||||||
|
'2': {
|
||||||
|
x: 240,
|
||||||
|
y: -100,
|
||||||
|
},
|
||||||
|
'3': {
|
||||||
|
x: -480,
|
||||||
|
y: 40,
|
||||||
|
},
|
||||||
|
'4': {
|
||||||
|
x: 480,
|
||||||
|
y: 50,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** paper enter animation config */
|
||||||
|
const paperEnterAnimationOriginal = {
|
||||||
|
'0': {
|
||||||
|
curveCenter: 4,
|
||||||
|
curve: 292,
|
||||||
|
delay: 800,
|
||||||
|
fromZ: 1230,
|
||||||
|
fromX: -76,
|
||||||
|
fromY: 100,
|
||||||
|
fromRotateX: 185,
|
||||||
|
fromRotateY: -166,
|
||||||
|
fromRotateZ: 252,
|
||||||
|
toZ: 0,
|
||||||
|
// toX: 12,
|
||||||
|
// toY: -30,
|
||||||
|
toRotateZ: 6,
|
||||||
|
duration: '2s',
|
||||||
|
easing: 'ease',
|
||||||
|
},
|
||||||
|
'1': {
|
||||||
|
curveCenter: 4,
|
||||||
|
curve: 280,
|
||||||
|
delay: 0,
|
||||||
|
fromZ: 3697,
|
||||||
|
fromX: 25,
|
||||||
|
fromY: -93,
|
||||||
|
fromRotateX: 331,
|
||||||
|
fromRotateY: 360,
|
||||||
|
fromRotateZ: -257,
|
||||||
|
toZ: 0,
|
||||||
|
// toX: -18,
|
||||||
|
// toY: -28,
|
||||||
|
toRotateZ: -8,
|
||||||
|
duration: '2s',
|
||||||
|
easing: 'ease',
|
||||||
|
},
|
||||||
|
'2': {
|
||||||
|
curveCenter: 3,
|
||||||
|
curve: 660,
|
||||||
|
delay: 1700,
|
||||||
|
fromZ: 57379,
|
||||||
|
fromX: 2,
|
||||||
|
fromY: -77,
|
||||||
|
fromRotateX: 0,
|
||||||
|
fromRotateY: 0,
|
||||||
|
fromRotateZ: 0,
|
||||||
|
toZ: 0,
|
||||||
|
// toX: -3,
|
||||||
|
// toY: -21,
|
||||||
|
toRotateZ: 2,
|
||||||
|
duration: '2s',
|
||||||
|
easing: 'ease',
|
||||||
|
},
|
||||||
|
'3': {
|
||||||
|
curveCenter: 4,
|
||||||
|
curve: 260,
|
||||||
|
delay: 1500,
|
||||||
|
fromZ: 4303,
|
||||||
|
fromX: -37,
|
||||||
|
fromY: -100,
|
||||||
|
fromRotateX: 360,
|
||||||
|
fromRotateY: 360,
|
||||||
|
fromRotateZ: 8,
|
||||||
|
toZ: 0,
|
||||||
|
// toX: -30,
|
||||||
|
// toY: -9,
|
||||||
|
toRotateZ: 2,
|
||||||
|
duration: '2s',
|
||||||
|
easing: 'ease',
|
||||||
|
},
|
||||||
|
'4': {
|
||||||
|
curveCenter: 3,
|
||||||
|
curve: 270,
|
||||||
|
delay: 1571,
|
||||||
|
fromZ: 1876,
|
||||||
|
fromX: 65,
|
||||||
|
fromY: 48,
|
||||||
|
fromRotateX: 101,
|
||||||
|
fromRotateY: 188,
|
||||||
|
fromRotateZ: -200,
|
||||||
|
toZ: 0,
|
||||||
|
// toX: 24,
|
||||||
|
// toY: -2,
|
||||||
|
toRotateZ: 8,
|
||||||
|
duration: '2s',
|
||||||
|
easing: 'ease',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PaperEnterAnimation = (typeof paperEnterAnimationOriginal)[0];
|
||||||
|
export const paperEnterAnimations = paperEnterAnimationOriginal as Record<
|
||||||
|
any,
|
||||||
|
PaperEnterAnimation
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** Brief content */
|
||||||
|
const paperBriefs = {
|
||||||
|
'0': (
|
||||||
|
<div className={articleWrapper}>
|
||||||
|
<article className={article}>
|
||||||
|
<h1 className={title}>Breath of the Wild: Redefining Game Design</h1>
|
||||||
|
<p className={text}>
|
||||||
|
“With all the time you spend watching TV,” he tells me, “you could
|
||||||
|
have written a novel by now.” It’s hard to disagree with the sentiment
|
||||||
|
— writing a novel is undoubtedly a better use of time than watching TV
|
||||||
|
— but what about the hidden assumption? Such comments imply that time
|
||||||
|
is “fungible” — that time spent watching TV can just as easily be
|
||||||
|
spent writing a novel. And sadly, that’s just not the case.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
'1': (
|
||||||
|
<div className={articleWrapper}>
|
||||||
|
<article className={article}>
|
||||||
|
<h1 className={title}>Learning with earning with retrieval practice</h1>
|
||||||
|
<p className={text}>
|
||||||
|
Are there any specific techniques to make the process of learning more
|
||||||
|
effective?
|
||||||
|
</p>
|
||||||
|
<p className={text}>
|
||||||
|
Students often re-read, underline, or highlight materials, thinking
|
||||||
|
that it will help them learn better. But, the best method for really
|
||||||
|
turning information into long-term memory is to use what is called
|
||||||
|
‘retrieval practice’.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
'2': (
|
||||||
|
<div className={articleWrapper}>
|
||||||
|
<article className={article}>
|
||||||
|
<h1 className={title}>
|
||||||
|
Local-first software
|
||||||
|
<br />
|
||||||
|
You own your data, in spite of the cloud
|
||||||
|
</h1>
|
||||||
|
<p className={text}>
|
||||||
|
Cloud apps like Google Docs and Trello are popular because they enable
|
||||||
|
real-time collaboration with colleagues, and they make it easy for us
|
||||||
|
to access our work from all of our devices. However, by centralizing
|
||||||
|
data storage on servers, cloud apps also take away ownership and
|
||||||
|
agency from users. If a service shuts down, the software stops
|
||||||
|
functioning, and data created with that software is lost.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
'3': (
|
||||||
|
<div className={articleWrapper}>
|
||||||
|
<article className={article}>
|
||||||
|
<h1 className={title}>More Is Different</h1>
|
||||||
|
<p className={text}>
|
||||||
|
Broken symmetry and the nature of the hierarchical structure of
|
||||||
|
science
|
||||||
|
</p>
|
||||||
|
<p className={text}>
|
||||||
|
The reductionist hypothesis may still be a topic for controversy among
|
||||||
|
philosophers, but among the great majority of active scientists I
|
||||||
|
think it is accepted without questions. The workings of our minds and
|
||||||
|
bodies, and of all the animate or inanimate matter of which we have
|
||||||
|
any detailed knowledge, are assumed to be controlled by the same set
|
||||||
|
of fundamental laws, which except under certain extreme conditions we
|
||||||
|
feel we know pretty well.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
'4': (
|
||||||
|
<div className={articleWrapper}>
|
||||||
|
<article className={article}>
|
||||||
|
<h1 className={title}>HOWTO: Be more productive</h1>
|
||||||
|
<p className={text}>
|
||||||
|
“With all the time you spend watching TV,” he tells me, “you could
|
||||||
|
have written a novel by now.” It’s hard to disagree with the sentiment
|
||||||
|
— writing a novel is undoubtedly a better use of time than watching TV
|
||||||
|
— but what about the hidden assumption? Such comments imply that time
|
||||||
|
is “fungible” — that time spent watching TV can just as easily be
|
||||||
|
spent writing a novel. And sadly, that’s just not the case.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const articles: Record<ArticleId, ArticleOption> = ids.reduce(
|
||||||
|
(acc, id) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[id]: {
|
||||||
|
id,
|
||||||
|
location: paperLocations[id],
|
||||||
|
enterOptions: paperEnterAnimations[id],
|
||||||
|
brief: paperBriefs[id],
|
||||||
|
} satisfies ArticleOption,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{} as Record<ArticleId, ArticleOption>
|
||||||
|
);
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { createVar, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
import { onboardingVars } from '../style.css';
|
||||||
|
|
||||||
|
export const paperWidthVar = createVar();
|
||||||
|
export const paperHeightVar = createVar();
|
||||||
|
|
||||||
|
export const paper = style({
|
||||||
|
vars: {
|
||||||
|
[paperWidthVar]: onboardingVars.paper.w,
|
||||||
|
[paperHeightVar]: onboardingVars.paper.h,
|
||||||
|
},
|
||||||
|
|
||||||
|
width: paperWidthVar,
|
||||||
|
height: paperHeightVar,
|
||||||
|
position: 'relative',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const segment = style({
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
background: onboardingVars.paper.bg,
|
||||||
|
position: 'absolute',
|
||||||
|
top: `calc(var(--segments-up) / var(--segments) * 100%)`,
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
['&[data-root="true"]']: {
|
||||||
|
height: `calc(1 / var(--segments) * 100%)`,
|
||||||
|
},
|
||||||
|
['&[data-direction="up"]']: {
|
||||||
|
top: 'unset',
|
||||||
|
bottom: `100%`,
|
||||||
|
transformOrigin: 'bottom',
|
||||||
|
},
|
||||||
|
['&[data-direction="down"]']: {
|
||||||
|
top: `100%`,
|
||||||
|
transformOrigin: 'top',
|
||||||
|
},
|
||||||
|
['&[data-top="true"]']: {
|
||||||
|
borderTopLeftRadius: onboardingVars.paper.r,
|
||||||
|
borderTopRightRadius: onboardingVars.paper.r,
|
||||||
|
},
|
||||||
|
['&[data-bottom="true"]']: {
|
||||||
|
borderBottomLeftRadius: onboardingVars.paper.r,
|
||||||
|
borderBottomRightRadius: onboardingVars.paper.r,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const contentWrapper = style({
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const content = style({
|
||||||
|
padding: '16px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
fontFamily: 'var(--affine-font-family)',
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
[`${contentWrapper} > &`]: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: paperWidthVar,
|
||||||
|
height: paperHeightVar,
|
||||||
|
top: `calc((var(--index)) * -100%)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const articleWrapper = style({
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const article = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '12px',
|
||||||
|
color: onboardingVars.paper.textColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const title = style({
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: '26px',
|
||||||
|
});
|
||||||
|
export const text = style({
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: '22px',
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './paper.css';
|
||||||
|
import { Segments } from './segments';
|
||||||
|
|
||||||
|
export interface PaperProps {
|
||||||
|
segments: number;
|
||||||
|
centerIndex: number;
|
||||||
|
content: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Paper = (props: PaperProps) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.paper}>
|
||||||
|
<Segments
|
||||||
|
level={props.segments}
|
||||||
|
root={true}
|
||||||
|
index={props.segments}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { PropsWithChildren, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './paper.css';
|
||||||
|
|
||||||
|
export interface SegmentProps extends PropsWithChildren {
|
||||||
|
index: number;
|
||||||
|
level?: number;
|
||||||
|
direction?: 'up' | 'down';
|
||||||
|
content: ReactNode;
|
||||||
|
|
||||||
|
isTop?: boolean;
|
||||||
|
isBottom?: boolean;
|
||||||
|
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Segment({
|
||||||
|
children,
|
||||||
|
index,
|
||||||
|
direction,
|
||||||
|
content,
|
||||||
|
level,
|
||||||
|
isTop,
|
||||||
|
isBottom,
|
||||||
|
...attrs
|
||||||
|
}: SegmentProps) {
|
||||||
|
const style = { '--index': index } as React.CSSProperties;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.segment}
|
||||||
|
data-direction={direction}
|
||||||
|
data-level={level}
|
||||||
|
data-bottom={(direction === 'down' && level === 1) || isBottom}
|
||||||
|
data-top={(direction === 'up' && level === 1) || isTop}
|
||||||
|
{...attrs}
|
||||||
|
>
|
||||||
|
<div className={styles.contentWrapper} style={style}>
|
||||||
|
<div className={styles.content}>{content}</div>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { Segment } from './segment';
|
||||||
|
|
||||||
|
export interface SegmentsProps {
|
||||||
|
level?: number;
|
||||||
|
direction?: 'up' | 'down';
|
||||||
|
index: number;
|
||||||
|
root?: boolean;
|
||||||
|
centerIndex: number;
|
||||||
|
segments: number;
|
||||||
|
content: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Segments({
|
||||||
|
level,
|
||||||
|
direction,
|
||||||
|
root,
|
||||||
|
index,
|
||||||
|
centerIndex,
|
||||||
|
segments,
|
||||||
|
content,
|
||||||
|
}: SegmentsProps) {
|
||||||
|
if (!level) return null;
|
||||||
|
|
||||||
|
const inherits = { centerIndex, segments, content };
|
||||||
|
|
||||||
|
if (root) {
|
||||||
|
const up = centerIndex;
|
||||||
|
const down = segments - up - 1;
|
||||||
|
const vars = {
|
||||||
|
'--segments': segments,
|
||||||
|
'--segments-up': up,
|
||||||
|
'--segments-down': down,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Segment
|
||||||
|
data-root={true}
|
||||||
|
style={vars}
|
||||||
|
index={up}
|
||||||
|
content={content}
|
||||||
|
isTop={up === 0}
|
||||||
|
isBottom={down === 0}
|
||||||
|
>
|
||||||
|
<Segments index={up - 1} level={up} direction="up" {...inherits} />
|
||||||
|
<Segments index={up + 1} level={down} direction="down" {...inherits} />
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const children =
|
||||||
|
level === 1 ? null : (
|
||||||
|
<Segments
|
||||||
|
direction={direction}
|
||||||
|
index={direction === 'up' ? index - 1 : index + 1}
|
||||||
|
level={level - 1}
|
||||||
|
{...inherits}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Segment
|
||||||
|
direction={direction}
|
||||||
|
index={index}
|
||||||
|
content={content}
|
||||||
|
level={level}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
import { articles } from './articles';
|
||||||
|
import { PaperSteps } from './paper-steps';
|
||||||
|
import * as styles from './style.css';
|
||||||
|
|
||||||
|
interface OnboardingProps {
|
||||||
|
onOpenApp?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Onboarding = (_: OnboardingProps) => {
|
||||||
|
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`,
|
||||||
|
|
||||||
|
'--delay': `${enterOptions.delay}ms`,
|
||||||
|
'--duration': enterOptions.duration,
|
||||||
|
'--easing': enterOptions.easing,
|
||||||
|
|
||||||
|
'--offset-x': `${location.x || 0}px`,
|
||||||
|
'--offset-y': `${location.y || 0}px`,
|
||||||
|
} as CSSProperties;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={style} key={id}>
|
||||||
|
<PaperSteps article={article} show={true} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { AnimateIn } from './steps/animate-in';
|
||||||
|
import type { ArticleOption } from './types';
|
||||||
|
|
||||||
|
interface PaperStepsProps {
|
||||||
|
show?: boolean;
|
||||||
|
article: ArticleOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaperSteps = ({ show, article }: PaperStepsProps) => {
|
||||||
|
const onFinished = useCallback(() => {
|
||||||
|
console.log('onFinished');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!show) return null;
|
||||||
|
return <AnimateIn article={article} onFinished={onFinished} />;
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { keyframes, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
import { paperLocation } from '../style.css';
|
||||||
|
|
||||||
|
const moveInAnim = keyframes({
|
||||||
|
'0%': {
|
||||||
|
transform: `translateZ(var(--fromZ)) translateX(var(--fromX)) translateY(var(--fromY)) rotateX(var(--fromRotateX)) rotateY(var(--fromRotateY)) rotateZ(var(--fromRotateZ))`,
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: `translateZ(var(--toZ)) translateX(0) translateY(0) rotateX(0deg) rotateY(0deg) rotateZ(var(--toRotateZ))`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const moveIn = style([
|
||||||
|
paperLocation,
|
||||||
|
{
|
||||||
|
animation: `${moveInAnim} var(--duration) ease forwards`,
|
||||||
|
animationDelay: 'var(--delay)',
|
||||||
|
transform: 'translateY(100vh)', // hide on init
|
||||||
|
},
|
||||||
|
]);
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import anime from 'animejs';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { Paper, type PaperProps } from '../curve-paper/paper';
|
||||||
|
import * as paperStyles from '../curve-paper/paper.css';
|
||||||
|
import type { ArticleOption } from '../types';
|
||||||
|
import * as styles from './animate-in.css';
|
||||||
|
|
||||||
|
interface AnimateInProps {
|
||||||
|
paperProps?: PaperProps;
|
||||||
|
article: ArticleOption;
|
||||||
|
onFinished?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const easing = 'spring(5, 100, 10, 0)';
|
||||||
|
const animeSync = (params: Parameters<typeof anime>[0]) => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
anime({ ...params, complete: () => resolve(null) });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AnimateIn = ({
|
||||||
|
article,
|
||||||
|
paperProps,
|
||||||
|
onFinished,
|
||||||
|
}: AnimateInProps) => {
|
||||||
|
const { id: _id, enterOptions, brief } = article;
|
||||||
|
const id = `onboardingMoveIn${_id}`;
|
||||||
|
const segments = 4;
|
||||||
|
|
||||||
|
const rotateX = enterOptions.curve / segments;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all([
|
||||||
|
animeSync({
|
||||||
|
targets: `[data-id="${id}"] .${paperStyles.segment}[data-direction="up"]`,
|
||||||
|
rotateX: [-rotateX, 0],
|
||||||
|
easing,
|
||||||
|
delay: enterOptions.delay,
|
||||||
|
}),
|
||||||
|
animeSync({
|
||||||
|
targets: `[data-id="${id}"] ${paperStyles.segment}[data-direction="down"]`,
|
||||||
|
rotateX: [rotateX, 0],
|
||||||
|
easing,
|
||||||
|
delay: enterOptions.delay,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.then(() => {
|
||||||
|
onFinished?.();
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, [enterOptions.delay, id, rotateX, onFinished]);
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
...paperProps,
|
||||||
|
segments,
|
||||||
|
content: brief,
|
||||||
|
centerIndex: Math.min(segments - 1, Math.max(0, enterOptions.curveCenter)),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-id={id} className={styles.moveIn}>
|
||||||
|
<Paper {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { globalStyle, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
// in case that we need to support dark mode later
|
||||||
|
export const onboardingVars = {
|
||||||
|
window: {
|
||||||
|
bg: 'var(--affine-pure-white)',
|
||||||
|
shadow: 'var(--affine-shadow-1)',
|
||||||
|
transition: {
|
||||||
|
size: '0.3s ease',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
paper: {
|
||||||
|
w: '230px',
|
||||||
|
h: '302px',
|
||||||
|
r: '8px',
|
||||||
|
bg: 'var(--affine-pure-white)',
|
||||||
|
// textColor: 'var(--affine-light-text-primary-color)',
|
||||||
|
textColor: '#121212',
|
||||||
|
},
|
||||||
|
web: {
|
||||||
|
bg: '#fafafa', // TODO: use var
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const perspective = style({
|
||||||
|
perspective: '10000px',
|
||||||
|
transformStyle: 'preserve-3d',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const onboarding = style([
|
||||||
|
perspective,
|
||||||
|
{
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
// hack background color for web
|
||||||
|
'&::after': {
|
||||||
|
content: '',
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
background: onboardingVars.web.bg,
|
||||||
|
transform: 'translateZ(-1000px) scale(2)',
|
||||||
|
},
|
||||||
|
'&[data-is-desktop="true"]::after': {
|
||||||
|
content: 'unset',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
globalStyle(`${onboarding} *`, {
|
||||||
|
perspective: '10000px',
|
||||||
|
transformStyle: 'preserve-3d',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const offsetOrigin = style({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const paperLocation = style({
|
||||||
|
position: 'absolute',
|
||||||
|
left: `calc(var(--offset-x) - ${onboardingVars.paper.w} / 2)`,
|
||||||
|
top: `calc(var(--offset-y) - ${onboardingVars.paper.h} / 2)`,
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export type OnboardingStep = 'enter' | 'unfold' | 'mode-switch';
|
||||||
|
export type ArticleId = '0' | '1' | '2' | '3' | '4';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paper enter animation options
|
||||||
|
*/
|
||||||
|
export interface PaperEnterOptions {
|
||||||
|
// animation-curve
|
||||||
|
curveCenter: number;
|
||||||
|
curve: number;
|
||||||
|
|
||||||
|
// animation-move
|
||||||
|
fromZ: number;
|
||||||
|
fromX: number;
|
||||||
|
fromY: number;
|
||||||
|
fromRotateX: number;
|
||||||
|
fromRotateY: number;
|
||||||
|
fromRotateZ: number;
|
||||||
|
toZ: number;
|
||||||
|
toRotateZ: number;
|
||||||
|
|
||||||
|
// move-in animation config
|
||||||
|
duration: number | string;
|
||||||
|
delay: number;
|
||||||
|
easing: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticleOption {
|
||||||
|
/** article id */
|
||||||
|
id: ArticleId;
|
||||||
|
|
||||||
|
/** paper enter animation content */
|
||||||
|
brief: ReactNode;
|
||||||
|
|
||||||
|
/** paper enter animation configuration */
|
||||||
|
enterOptions: PaperEnterOptions;
|
||||||
|
|
||||||
|
/** Locate paper */
|
||||||
|
location: {
|
||||||
|
/** offset X */
|
||||||
|
x: number;
|
||||||
|
/** offset Y */
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { useCallback } from 'react';
|
||||||
import { redirect } from 'react-router-dom';
|
import { redirect } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Onboarding } from '../components/affine/onboarding/onboarding';
|
||||||
import {
|
import {
|
||||||
appConfigStorage,
|
appConfigStorage,
|
||||||
useAppConfigStorage,
|
useAppConfigStorage,
|
||||||
@@ -18,9 +19,9 @@ export const loader = () => {
|
|||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
const { jumpToIndex } = useNavigateHelper();
|
const { jumpToIndex } = useNavigateHelper();
|
||||||
const [onBoarding, setOnboarding] = useAppConfigStorage('onBoarding');
|
const [, setOnboarding] = useAppConfigStorage('onBoarding');
|
||||||
|
|
||||||
const openApp = () => {
|
const openApp = useCallback(() => {
|
||||||
if (environment.isDesktop) {
|
if (environment.isDesktop) {
|
||||||
window.apis.ui.handleOpenMainApp().catch(err => {
|
window.apis.ui.handleOpenMainApp().catch(err => {
|
||||||
console.log('failed to open main app', err);
|
console.log('failed to open main app', err);
|
||||||
@@ -29,24 +30,7 @@ export const Component = () => {
|
|||||||
jumpToIndex(RouteLogic.REPLACE);
|
jumpToIndex(RouteLogic.REPLACE);
|
||||||
setOnboarding(false);
|
setOnboarding(false);
|
||||||
}
|
}
|
||||||
};
|
}, [jumpToIndex, setOnboarding]);
|
||||||
|
|
||||||
return (
|
return <Onboarding onOpenApp={openApp} />;
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '8px',
|
|
||||||
height: '100vh',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button onClick={() => setOnboarding(!onBoarding)}>
|
|
||||||
Toggle onboarding
|
|
||||||
</Button>
|
|
||||||
onboarding page, onboarding mode is {onBoarding ? 'on' : 'off'}
|
|
||||||
<Button onClick={openApp}>Enter App</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { assert } from 'console';
|
import { assert } from 'console';
|
||||||
import { BrowserWindow } from 'electron';
|
import { BrowserWindow, screen } from 'electron';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import { mainWindowOrigin } from './constants';
|
import { mainWindowOrigin } from './constants';
|
||||||
@@ -27,17 +27,22 @@ async function createOnboardingWindow(additionalArguments: string[]) {
|
|||||||
|
|
||||||
assert(helperExposedMeta, 'helperExposedMeta should be defined');
|
assert(helperExposedMeta, 'helperExposedMeta should be defined');
|
||||||
|
|
||||||
|
// get user's screen size
|
||||||
|
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
|
||||||
|
|
||||||
const browserWindow = new BrowserWindow({
|
const browserWindow = new BrowserWindow({
|
||||||
width: 800,
|
width,
|
||||||
height: 600,
|
height,
|
||||||
frame: false,
|
frame: false,
|
||||||
show: false,
|
show: false,
|
||||||
closable: false,
|
closable: false,
|
||||||
minimizable: false,
|
minimizable: false,
|
||||||
maximizable: false,
|
maximizable: false,
|
||||||
fullscreenable: false,
|
fullscreenable: false,
|
||||||
skipTaskbar: true,
|
// skipTaskbar: true,
|
||||||
// transparent: true,
|
transparent: true,
|
||||||
|
backgroundColor: '#00FFFFFF',
|
||||||
|
hasShadow: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
webgl: true,
|
webgl: true,
|
||||||
preload: join(__dirname, './preload.js'),
|
preload: join(__dirname, './preload.js'),
|
||||||
@@ -46,7 +51,10 @@ async function createOnboardingWindow(additionalArguments: string[]) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
browserWindow.on('ready-to-show', () => {
|
browserWindow.on('ready-to-show', () => {
|
||||||
|
// TODO: add a timeout to avoid flickering, window is ready, but dom is not ready
|
||||||
|
setTimeout(() => {
|
||||||
browserWindow.show();
|
browserWindow.show();
|
||||||
|
}, 300);
|
||||||
});
|
});
|
||||||
|
|
||||||
await browserWindow.loadURL(
|
await browserWindow.loadURL(
|
||||||
|
|||||||
16
yarn.lock
16
yarn.lock
@@ -379,6 +379,7 @@ __metadata:
|
|||||||
"@swc/core": "npm:^1.3.93"
|
"@swc/core": "npm:^1.3.93"
|
||||||
"@testing-library/react": "npm:^14.0.0"
|
"@testing-library/react": "npm:^14.0.0"
|
||||||
"@toeverything/theme": "npm:^0.7.20"
|
"@toeverything/theme": "npm:^0.7.20"
|
||||||
|
"@types/animejs": "npm:^3"
|
||||||
"@types/bytes": "npm:^3.1.3"
|
"@types/bytes": "npm:^3.1.3"
|
||||||
"@types/image-blob-reduce": "npm:^4.1.3"
|
"@types/image-blob-reduce": "npm:^4.1.3"
|
||||||
"@types/lodash-es": "npm:^4.17.9"
|
"@types/lodash-es": "npm:^4.17.9"
|
||||||
@@ -386,6 +387,7 @@ __metadata:
|
|||||||
"@types/webpack-env": "npm:^1.18.2"
|
"@types/webpack-env": "npm:^1.18.2"
|
||||||
"@vanilla-extract/css": "npm:^1.13.0"
|
"@vanilla-extract/css": "npm:^1.13.0"
|
||||||
"@vanilla-extract/dynamic": "npm:^2.0.3"
|
"@vanilla-extract/dynamic": "npm:^2.0.3"
|
||||||
|
animejs: "npm:^3.2.2"
|
||||||
async-call-rpc: "npm:^6.3.1"
|
async-call-rpc: "npm:^6.3.1"
|
||||||
bytes: "npm:^3.1.2"
|
bytes: "npm:^3.1.2"
|
||||||
clsx: "npm:^2.0.0"
|
clsx: "npm:^2.0.0"
|
||||||
@@ -14324,6 +14326,13 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@types/animejs@npm:^3":
|
||||||
|
version: 3.1.12
|
||||||
|
resolution: "@types/animejs@npm:3.1.12"
|
||||||
|
checksum: 8ea5d0440236b87042ad012c0bfd90a38cf38688b8b28ad750d21eb01a3943c9256f0ec79ea0b40aad6e500ee177ed43d5aeec8f875f904b61f195e5e305d2ce
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/argparse@npm:1.0.38":
|
"@types/argparse@npm:1.0.38":
|
||||||
version: 1.0.38
|
version: 1.0.38
|
||||||
resolution: "@types/argparse@npm:1.0.38"
|
resolution: "@types/argparse@npm:1.0.38"
|
||||||
@@ -16580,6 +16589,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"animejs@npm:^3.2.2":
|
||||||
|
version: 3.2.2
|
||||||
|
resolution: "animejs@npm:3.2.2"
|
||||||
|
checksum: 7abdb56f415c666ba02f4e64fdbb10d457fed7e3711b0f006f97e48e5650097013397d890e8ceb31e9e06b73bf6dfd9202309d0dae0fc0b5190aa7c4e0ab7054
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"ansi-align@npm:^3.0.1":
|
"ansi-align@npm:^3.0.1":
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
resolution: "ansi-align@npm:3.0.1"
|
resolution: "ansi-align@npm:3.0.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user