// ===== demos/typography/type-entrance-moves/LetterDropPhysics.tsx =====
// 字符坠落堆积(letter-drop-physics)——FallingLetterAnimation。
// "GRAVITY" 7 字符各自绝对定位,第 i 字符从帧 10+i*5 起下落:
// ① 重力加速 y = D*(t/24)^2 掉 720px 到基线(地板线可见);
// ② 落地后 2 次衰减弹跳(高度 30% / 9%,抛物线 4u(1-u) 拼段),
// 落地瞬间 rotate 到 seed hash ±6° 小歪角并保持;
// ③ 帧 110 一拍:6f ease-out 全体齐整回正(rotate→0、偏移→0、scale 1.06→1),
// 帧 116–150 真静止(≥25f)收尾。
import React from 'react';
import { useCurrentFrame, Easing } from 'remotion';
import { G } from '../../_fixtures/Fixtures';
// 确定性伪随机
const h = (n: number) => {
const s = Math.sin(n * 127.3) * 43758.5453;
return s - Math.floor(s);
};
const WORD = 'GRAVITY';
const SLOT_W = 150; // 每字符槽宽
const FONT = 140;
const WORD_W = WORD.length * SLOT_W; // 1050
const LEFT = (1920 - WORD_W) / 2; // 435
const REST_TOP = 452; // 字符落定后的 top
const FLOOR_Y = REST_TOP + 152; // 地板线(视觉基线)
const DROP = 720; // 下落距离
const T_FALL = 24; // 落到基线用时
const T_B1 = 16; // 第一次弹跳时长(高 30%)
const T_B2 = 8; // 第二次弹跳时长(高 9%)
const SNAP = 110; // 齐整回正的一拍
const SNAP_DUR = 6;
const easeOutCubic = Easing.out(Easing.cubic);
const clamp01 = (x: number) => Math.min(1, Math.max(0, x));
// 单字符纵向位移(相对基线,负 = 在上方)
const dropY = (t: number): number => {
if (t <= 0) return -DROP;
if (t < T_FALL) return -DROP + DROP * (t / T_FALL) ** 2; // 重力加速
if (t < T_FALL + T_B1) {
const u = (t - T_FALL) / T_B1;
return -DROP * 0.3 * 4 * u * (1 - u); // 弹跳 1:30%
}
if (t < T_FALL + T_B1 + T_B2) {
const u = (t - T_FALL - T_B1) / T_B2;
return -DROP * 0.09 * 4 * u * (1 - u); // 弹跳 2:9%
}
return 0;
};
export const LetterDropPhysics: React.FC = () => {
const frame = useCurrentFrame();
// 帧 110 起的齐整回正进度
const snap = easeOutCubic(clamp01((frame - SNAP) / SNAP_DUR));
return (
{/* 地板线:字符落点的视觉基线 */}
{WORD.split('').map((ch, i) => {
const start = 10 + i * 5;
const t = frame - start;
const y = dropY(t);
// 落地瞬间歪到 seed 小角度(±6°),落地前为 0
const tiltTarget = (h(i + 1) - 0.5) * 12;
const landP = clamp01((t - T_FALL) / 6);
const restJitter = (h(i + 11) - 0.5) * 10; // 落定后的 ±5px 竖向错位
// snap 一拍:歪角与错位齐整归零,scale 1.06→1 脉冲
const rot = tiltTarget * landP * (1 - snap);
const jitter = restJitter * landP * (1 - snap);
const scale = frame < SNAP ? 1 : 1 + 0.06 * (1 - snap);
return (
{ch}
);
})}
);
};
// ===== demos/typography/type-entrance-moves/ScrambleDecode.tsx =====
// 乱码解码字(scramble-decode)——终端黑客感文字入场。
// 标题 "DECODE SPEED" 每字符先高速跳随机字母/数字(每 2f 换一个,seed hash 取字符),
// 从左到右逐个锁定:第 i 个字符在帧 20+i*6 锁定为真字符,锁定瞬间该字符反色闪
// (G.ink 色块白字 2f)随即恢复。跳动期字符 G.mid,锁定后 G.ink。
// 关键帧:0–20f 全员乱跳 → 20–86f 从左到右逐个锁定 → 87–130f 完全静止收尾。
import React from 'react';
import { useCurrentFrame } from 'remotion';
import { G } from '../../_fixtures/Fixtures';
const TEXT = 'DECODE SPEED';
const CHARSET = 'ABCDEF0123456789#$%&';
const LOCK_START = 20; // 第 0 个字符锁定帧
const LOCK_STEP = 6; // 相邻字符锁定间隔
const FLASH_LEN = 2; // 锁定反色闪持续帧数
// 帧确定伪随机
const h = (n: number) => {
const s = Math.sin(n * 127.3) * 43758.5453;
return s - Math.floor(s);
};
export const ScrambleDecode: React.FC = () => {
const frame = useCurrentFrame();
const chars = TEXT.split('');
const lockedCount = chars.filter((c, i) => c === ' ' || frame >= LOCK_START + i * LOCK_STEP).length;
const allLocked = lockedCount === chars.length;
return (
{chars.map((ch, i) => {
if (ch === ' ') {
return ;
}
const lockFrame = LOCK_START + i * LOCK_STEP;
const locked = frame >= lockFrame;
const flashing = locked && frame < lockFrame + FLASH_LEN;
// 跳动期:每 2 帧换一个伪随机字符
const tick = Math.floor(frame / 2);
const scrambleChar = CHARSET[Math.floor(h(i * 101 + tick * 7 + 13) * CHARSET.length)];
const shown = locked ? ch : scrambleChar;
return (
{shown}
);
})}
{/* 底部进度提示条:已锁定字符数比例,帮助读出"从左到右扫过"的推进感 */}
);
};