// ===== demos/ui-entrance/paper-craft-moves/MaskingTapeSlap.tsx ===== // masking-tape-slap —— 纸胶带拍定 // 一张 Card 轻飘入位后悬着微晃(±1.5° 正弦 + 5px 上下浮),两条半透明纸胶带 // 先后从画外拍在对角(scale 1.45→1 + rotate 过冲 + 一帧压扁)。第一条拍下晃动减半, // 第二条拍下同帧卡片停晃、投影瞬间变薄、整卡 2px 下沉——"按死"的一瞬是主角。 // 帧确定性:全部由 frame 派生,无随机。收尾 f86 后真静止 54f。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G, Card, TitleBlock } from '../../_fixtures/Fixtures'; const CARD_W = 560; const CARD_H = 350; const CX = (1920 - CARD_W) / 2; const CY = (1080 - CARD_H) / 2 + 40; const FLOAT_START = 12; // 开头 12f 空场静置 const FLOAT_END = 38; const SLAP1 = 58; const SLAP2 = 82; const APPROACH = 6; // 胶带从画外扑向卡面的帧数 const FREEZE = 2; // 拍死后晃动归零帧数 // 悬浮晃动幅度包络:入位后升起 → 第一条胶带拍下后减半 → 第二条拍下冻结(由外层处理) const amp = (f: number): number => { const rise = interpolate(f, [FLOAT_END, FLOAT_END + 8], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const damp = interpolate(f, [SLAP1, SLAP1 + 4], [1, 0.45], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return rise * damp; }; const rawRot = (f: number): number => amp(f) * 1.5 * Math.sin((f - FLOAT_END) * 0.16); const rawBob = (f: number): number => amp(f) * 5 * Math.sin((f - FLOAT_END) * 0.11); // 第二条拍下(SLAP2)同帧起 2f 内把晃动按死到 0 const frozen = (f: number, raw: (x: number) => number): number => f <= SLAP2 ? raw(f) : interpolate(f, [SLAP2, SLAP2 + FREEZE], [raw(SLAP2), 0], { extrapolateRight: 'clamp', }); const Tape: React.FC<{ frame: number; land: number; cx: number; // 胶带中心点(世界坐标) cy: number; rot: number; // 落定角度 fromX: number; // 画外来向偏移 fromY: number; }> = ({ frame, land, cx, cy, rot, fromX, fromY }) => { if (frame < land - APPROACH) return null; // 条件卸载:拍上前真不存在 const t = interpolate(frame, [land - APPROACH, land], [0, 1], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const scale = interpolate(t, [0, 1], [1.45, 1]); const dx = fromX * (1 - t); const dy = fromY * (1 - t); const opacity = interpolate(frame, [land - APPROACH, land - APPROACH + 2], [0, 0.85], { extrapolateRight: 'clamp', }); // rotate 过冲:来时欠 16° → 落帧过 7° → 4f 内回正 const r = interpolate(frame, [land - APPROACH, land, land + 4], [rot - 16, rot + 7, rot], { easing: Easing.out(Easing.quad), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // 一帧压扁:落帧 scaleY 0.72,次帧 0.9,随后复原 const sy = frame === land ? 0.72 : frame === land + 1 ? 0.9 : 1; return (
); }; export const MaskingTapeSlap: React.FC = () => { const frame = useCurrentFrame(); // 卡片飘入:从上方 -120px 缓落 const floatY = interpolate(frame, [FLOAT_START, FLOAT_END], [-120, 0], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const floatOp = interpolate(frame, [FLOAT_START, FLOAT_START + 10], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const rot = frozen(frame, rawRot); const bob = frozen(frame, rawBob); // 按死:2px 下沉 + 投影瞬间变薄 const sink = interpolate(frame, [SLAP2, SLAP2 + FREEZE], [0, 2], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const shOff = interpolate(frame, [SLAP2, SLAP2 + FREEZE], [16, 3], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const shBlur = interpolate(frame, [SLAP2, SLAP2 + FREEZE], [34, 8], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const shAlpha = interpolate(frame, [SLAP2, SLAP2 + FREEZE], [0.22, 0.1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return (
{/* 两条胶带钉在卡片对角(世界坐标,卡片在其下滑动微晃) */}
); }; // ===== demos/ui-entrance/paper-craft-moves/PopupBookRise.tsx ===== // popup-book-rise —— 立体书立起 // FakeDashboard(A) 打平躺下(场景 rotateX 75° 透视俯视),6 张卡片是贴在页上的 // 纸片,沿各自底边从平躺错峰立起(rotateX 90°→-5° 过冲→0° 回弹,即立到 95° 再回 90°), // 根部投影随立起角度收窄变淡。全部立起后整个场景轻微回正(75°→68°)收尾。 // 卡片用独立网格复刻 dashboard A 区布局(Fixtures 内嵌卡无法单独驱动)。 // 收尾 f108 后真静止 ≥52f。帧确定性:全由 frame 派生。 import React from 'react'; import { useCurrentFrame, interpolate, spring, Easing } from 'remotion'; import { G, Card } from '../../_fixtures/Fixtures'; const FPS = 30; const HOLD = 14; // 开头静置 const STAGGER = 7; const RISE_DUR = 34; // spring 视觉收敛帧数 const LAST_START = HOLD + 5 * STAGGER; // 49 const SETTLE = LAST_START + RISE_DUR; // 83:全部立起 const REST = SETTLE + 25; // 108:场景回正完成 // dashboard A 区几何(照抄 FakeDashboard:侧栏 220 + 顶栏 72 + padding 36 + gap 28) const AREA_X = 220 + 36; const AREA_Y = 72 + 36; const AREA_W = 1920 - 220 - 72; const AREA_H = 1080 - 72 - 72; const GAP = 28; const CELL_W = (AREA_W - 2 * GAP) / 3; const CELL_H = (AREA_H - GAP) / 2; const PageCard: React.FC<{ i: number; frame: number }> = ({ i, frame }) => { const col = i % 3; const row = Math.floor(i / 3); // 远排(row 0)先立,近排后立;同排从左到右 const order = row === 0 ? col : 3 + col; const start = HOLD + order * STAGGER; const s = spring({ frame: frame - start, fps: FPS, config: { damping: 11, stiffness: 130, mass: 0.9 }, durationInFrames: RISE_DUR, durationRestThreshold: 0.0001, }); // 平躺(贴页面 = local 0°)→ 立起(垂直页面 = local -90°,顶边朝观众翻起), // spring 过冲自然冲过 -90° 到约 -95°(纸的韧性)再回弹。 const rx = interpolate(s, [0, 1], [0, -90]); // 根部投影:躺平时长影(卡片盖在页面上),立起后收成窄条 const lie = 1 - Math.min(Math.abs(rx) / 90, 1); // 1 = 躺平, 0 = 立直 const shH = 14 + 90 * Math.max(lie, 0); const shAlpha = 0.1 + 0.16 * Math.max(lie, 0); return (
{/* 根部投影贴在页面上,不随卡片立起 */}
); }; export const PopupBookRise: React.FC = () => { const frame = useCurrentFrame(); // 场景(书页)俯视角:全程 75°,全部立起后轻微回正到 68° const sceneRx = interpolate(frame, [SETTLE, REST], [75, 68], { easing: Easing.inOut(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return (
{/* 书页底板:dashboard 的壳(侧栏+顶栏+空白页面) */}
{Array.from({ length: 7 }).map((_, i) => (
))}
{/* 6 张纸片卡沿底边立起 */} {Array.from({ length: 6 }).map((_, i) => ( ))}
); };