Files
WebDrop/web/src/app/Splash.tsx
T
2026-10-02 00:59:37 +08:00

62 lines
2.0 KiB
TypeScript

import { useEffect, useState, type AnimationEvent } from 'react';
import styles from './Splash.module.css';
/** 单线手写 WebDrop,逐笔画出。每个字母一条路径,按序号错开 */
const STROKES = [
'M4 14 L14 58 L24 30 L34 58 L44 14',
'M52 42 H76 C76 28 66 26 60 31 C50 38 53 58 63 58 C69 59 74 56 77 52',
'M88 8 L88 58 M88 46 C92 34 106 36 106 47 C106 59 92 62 88 52',
'M120 14 L120 58 M120 14 C156 12 160 58 120 58',
'M172 30 L172 58 M172 42 C176 31 186 29 192 33',
'M204 46 C204 33 222 33 222 46 C222 59 204 59 204 46',
'M236 32 L236 74 M236 46 C240 34 256 36 256 48 C256 60 242 64 236 54',
];
const reduced = () => matchMedia('(prefers-reduced-motion: reduce)').matches;
interface Props {
ready: boolean;
onDone: () => void;
}
export function Splash({ ready, onDone }: Props) {
const [animDone, setAnimDone] = useState(reduced);
const [skipped, setSkipped] = useState(false);
const leaving = skipped || (animDone && ready);
useEffect(() => {
const skip = () => setSkipped(true);
window.addEventListener('keydown', skip);
return () => window.removeEventListener('keydown', skip);
}, []);
function onEnd(e: AnimationEvent) {
if (e.animationName.includes('draw') && e.target instanceof SVGElement && e.target.dataset.last) setAnimDone(true);
if (e.animationName.includes('leave') && e.target === e.currentTarget) onDone();
}
return (
<div
className={`${styles.splash} ${leaving ? styles.leaving : ''}`}
onClick={() => setSkipped(true)}
onAnimationEnd={onEnd}
role="status"
aria-label="加载中"
>
<svg viewBox="0 0 262 82" className={styles.word} aria-hidden="true">
{STROKES.map((d, i) => (
<path
key={d}
d={d}
pathLength={1}
className={styles.stroke}
style={{ '--i': i } as React.CSSProperties}
data-last={i === STROKES.length - 1 ? '1' : undefined}
/>
))}
</svg>
<p className={styles.hint}>加载中</p>
</div>
);
}