// ===== demos/typography/split-flap-title/SplitFlapFlip.tsx ===== import React from 'react'; import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion'; import { FakeDashboard, G } from '../../_fixtures/Fixtures'; // split-flap-flip:机场翻牌字。每字符一个深底翻牌格(上下两半), // 逐格翻过 3 个乱码中间态后咔哒停在目标字,左→右 4f 级联成波。 // 节拍:0–21 建立(整排乱码静止)→ 22 起级联翻牌 → 78 全部停定 → 静止到 140。 const TEXT = 'SHIP FASTER'; const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#$%&'; const START = 22; // 级联起始帧 const STAGGER = 4; // 字符间级联延迟 const FLIP = 5; // 单次翻牌时长 const NFLIP = 3; // 每字符翻 3 次(2 个乱码中间态 + 1 次落到目标字) const CELL_W = 118; const CELL_H = 156; // seed 正弦哈希(禁 Math.random) const rnd = (a: number) => { const x = Math.sin(a * 127.3) * 43758.5453; return x - Math.floor(x); }; const garble = (i: number, k: number) => CHARSET[Math.floor(rnd(i * 7.13 + k * 3.71 + 1) * CHARSET.length)]; const FLAP_BG = '#262624'; const FLAP_INK = '#f4f4f2'; // 半格:上/下半各自 overflow hidden,内部整字定位错半格露出对应一半 const Half: React.FC<{ ch: string; part: 'top' | 'bottom' }> = ({ ch, part }) => (
{ch}
); const FlapCell: React.FC<{ target: string; i: number; frame: number }> = ({ target, i, frame, }) => { // 该格的字符序列:2 个乱码 → 1 个乱码 → 目标字(首态也是乱码,建立段可见) const seq = [garble(i, 0), garble(i, 1), garble(i, 2), target]; const local = frame - (START + i * STAGGER); const done = local >= NFLIP * FLIP; // 停定咔哒:整格下沉回弹(1px 肉眼无感,放大到 6px 才有"咔哒") const clickY = done ? interpolate(local, [15, 17, 19, 22], [0, 6, -1.5, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.quad), }) : 0; let topCh = seq[0]; let bottomCh = seq[0]; let flap: React.ReactNode = null; if (done) { topCh = target; bottomCh = target; } else if (local > 0) { const k = Math.min(NFLIP - 1, Math.floor(local / FLIP)); const from = seq[k]; const to = seq[k + 1]; const p = Easing.in(Easing.quad)((local - k * FLIP) / FLIP); // 重力感:越掉越快 topCh = to; // 上半静态:翻开后露出下一字符的上半 bottomCh = from; // 下半静态:保持旧字符直到活动叶盖下来 if (p < 0.5) { // 前半程:旧字符上半叶 0→-90 掉下 const deg = p * 2 * 90; flap = (
); } else { // 后半程:新字符下半叶 90→0 拍下盖住旧下半 const deg = 90 - (p - 0.5) * 2 * 90; flap = (
); } } return (
{flap} {/* 中缝铰链线 */}
); }; export const SplitFlapFlip: React.FC = () => { const frame = useCurrentFrame(); let letterIdx = 0; return ( {/* 背景假页面压暗,突出翻牌板 */}
{TEXT.split('').map((ch, idx) => { if (ch === ' ') { return
; } const i = letterIdx++; return ; })}
); };