// ===== demos/rhythm/montage-rhythm-moves/DominoCascade.tsx ===== // 多米诺连锁入场(domino-cascade)——Rube Goldberg / OK Go MV。 // 三级动量链,每级 startFrame = 上一级 impact 帧: // ① 帧 36–51 标题 "CHAIN REACTION" 从画外顶 ease-in(cubic) 砸落上半屏, // impact 帧 51 全画面竖向震一拍(4f 衰减); // ② 帧 51 起下方 4 张卡片被震得依次(隔 5f)向上弹 60px 抛物线落回(12f), // 末卡落地帧 78 = 第二次撞击(再震一拍 + 末卡向左歪 3° 给出横向动量); // ③ 帧 78–100 左侧深色侧边栏被横向撞滑进场,Easing.out(cubic) 带过冲回弹;帧 100–150 全体真静止。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G, Card, TitleBlock } from '../../_fixtures/Fixtures'; const easeInCubic = Easing.in(Easing.cubic); const easeOutCubic = Easing.out(Easing.cubic); // —— 关键帧 —— const TITLE_START = 36; // 砸落开始(前 36f hold 读布景) const IMPACT_1 = 51; // 标题落地 = 第一次撞击 const CARD_STAGGER = 5; const CARD_DUR = 12; const IMPACT_2 = IMPACT_1 + 3 * CARD_STAGGER + CARD_DUR; // 78,末卡落地 const SIDE_END = IMPACT_2 + 14; // 92 侧边栏到位(过冲点) const SIDE_SETTLE = SIDE_END + 8; // 100 回弹结束,此后真静止 // 撞击震动:一拍,4f 内衰减归零 const shake = (f: number, at: number, amp: number) => { if (f < at || f > at + 4) return 0; const seq = [amp, -amp * 0.6, amp * 0.3, -amp * 0.12, 0]; return seq[f - at]; }; // 卡片行几何:内容中心 1080(给左侧 240 侧边栏留出位置) const CARD_W = 340; const CARD_H = 220; const GAP = 40; const ROW_W = 4 * CARD_W + 3 * GAP; // 1480 const ROW_LEFT = 1080 - ROW_W / 2; // 340 const CARD_TOP = 700; // 卡片底边 920,落在地板线上 export const DominoCascade: React.FC = () => { const frame = useCurrentFrame(); // ① 标题砸落:画外顶 → 上半屏,ease-in 加速读作"砸" const titleTop = interpolate(frame, [TITLE_START, IMPACT_1], [-260, 240], { easing: easeInCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // 两次撞击的全画面竖向震动 const shakeY = shake(frame, IMPACT_1, 10) + shake(frame, IMPACT_2, 6); // ② 卡片弹跳:抛物线 4t(1-t),依次隔 5f const cardDy = (i: number) => { const s = IMPACT_1 + i * CARD_STAGGER; const t = Math.min(1, Math.max(0, (frame - s) / CARD_DUR)); return -60 * 4 * t * (1 - t); }; // 末卡落地时向左歪 3°(横向动量的可见出处),随后回正 const lastCardRot = interpolate( frame, [IMPACT_2 - 9, IMPACT_2, SIDE_END], [0, -3, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }, ); // ③ 侧边栏横向撞滑:带初速滑入 + 过冲回弹 let sideX: number; if (frame < IMPACT_2) { sideX = -260; } else if (frame < SIDE_END) { const t = (frame - IMPACT_2) / (SIDE_END - IMPACT_2); sideX = -260 + 272 * easeOutCubic(t); // 冲到 +12(约 5% 过冲) } else if (frame < SIDE_SETTLE) { const t = (frame - SIDE_END) / (SIDE_SETTLE - SIDE_END); sideX = 12 * (1 - Easing.inOut(Easing.quad)(t)); } else { sideX = 0; } return (
{/* 全画面震动容器 */}
{/* 地板线:卡片落点 */}
{/* ① 砸落的标题 */}
{/* ② 被震弹起的 4 张卡片 */} {[0, 1, 2, 3].map((i) => (
))} {/* ③ 被撞滑进场的侧边栏 */}
{Array.from({ length: 8 }).map((_, i) => (
))}
); }; // ===== demos/rhythm/montage-rhythm-moves/DropBlackoutSlam.tsx ===== // 落拍黑场爆开(drop-blackout-slam)——EDM concert blackout 的节奏手法: // 帧 0–49:假面板正常"播放"(scale 呼吸 1.0↔1.02)+右下角节拍点每 12f 闪一下; // 帧 50–61:一帧切纯黑 #0c0c0c,整整 12f 死寂,屏上完全无物(蓄力全靠这一拍静默); // 帧 62:白色大标题 "DROP" 从 scale 1.35 撞到 1.0(5f cubic out),同帧整屏 // 10px 震屏指数衰减(τ≈2.5f,约 12f 收干),底色变 G.ink 并从中心泛出一圈 // 快速消散的亮环;帧 ~80 起全静止到 130,收尾 ≥40f 真静止。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { FakeDashboard, G } from '../../_fixtures/Fixtures'; // 帧确定伪随机(硬规矩:禁 Math.random) const h = (n: number) => { const s = Math.sin(n * 127.3) * 43758.5453; return s - Math.floor(s); }; const BLACK_IN = 50; // 切黑帧 const SLAM = 62; // 爆入帧 export const DropBlackoutSlam: React.FC = () => { const f = useCurrentFrame(); // ===== 段 1:帧 0–49 正常播放 ===== if (f < BLACK_IN) { // scale 呼吸 1.0↔1.02,周期 48f const breathe = 1.01 + 0.01 * Math.sin((f / 48) * Math.PI * 2); // 节拍点:每 12f 闪一下(亮 4f 衰减) const beatPhase = f % 12; const beatOn = interpolate(beatPhase, [0, 1, 5], [0.25, 1, 0.25], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const beatScale = interpolate(beatPhase, [0, 1, 5], [1, 1.5, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return (
{/* 右下角节拍点 */}
); } // ===== 段 2:帧 50–61 纯黑死寂,屏上完全无物 ===== if (f < SLAM) { return
; } // ===== 段 3:帧 62 起爆入 ===== const t = f - SLAM; // 标题:scale 1.35 → 1.0,5f cubic out const slamScale = interpolate(t, [0, 5], [1.35, 1], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // 震屏:10px 指数衰减,τ≈2.5f,~12f 收干;t≥14 强制归零保证结尾真静止 const amp = t >= 14 ? 0 : 10 * Math.exp(-t / 2.5); const shakeX = amp === 0 ? 0 : (h(f * 3.7 + 1) - 0.5) * 2 * amp; const shakeY = amp === 0 ? 0 : (h(f * 7.1 + 2) - 0.5) * 2 * amp; // 亮环:从中心快速扩散并消散,~16f 走完(t≥16 后 opacity=0 且不再画) const ringR = interpolate(t, [0, 16], [80, 900], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const ringOpacity = interpolate(t, [0, 3, 16], [0.85, 0.55, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return (
{/* 亮环:快速扩散消散 */} {t < 16 && (
)}
DROP
); }; // ===== demos/rhythm/montage-rhythm-moves/WrightTripleCut.tsx ===== // 三连咔哒特写(wright-triple-cut)——Edgar Wright《僵尸肖恩》流程三连快切: // 帧 0–24 全景 hold(FakeDashboard A);随后三个 10f 特写硬切连打,每个特写 // 前 4f 静止、中 3f 动作、后 3f 静止:①25–34 光标按下按钮(圆点缩一圈+按钮变深) // ②35–44 开关左拨右(圆钮 3f 滑动+轨道 G.mid→G.ink) ③45–54 大数字 rotateX 翻牌 0→1; // 帧 55 甩回全景:6f whip translateX 滑入,高速段 3 层错帧副本模拟运动模糊, // 中上卡片提亮泛光+大 "1" 标记结果;帧 ~68 起全静止到 130(真静止 ≥60f)。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { FakeDashboard, G } from '../../_fixtures/Fixtures'; const HOLD_END = 25; // 全景 hold 结束 const C1 = 25; // 特写一:光标按下 const C2 = 35; // 特写二:开关 const C3 = 45; // 特写三:翻牌 const WHIP = 55; // 甩回全景 // 特写统一舞台:G.panel 底 + 居中大白卡,主体屏心、同倍率感 const CloseupStage: React.FC<{ children: React.ReactNode }> = ({ children }) => (
{children}
); // 结果卡片(3×2 网格中上格)的几何:sidebar 220 + padding 36,列宽 524,行高 454 const RESULT = { left: 808, top: 108, w: 524, h: 454 }; export const WrightTripleCut: React.FC = () => { const f = useCurrentFrame(); // ===== 帧 0–24:全景 hold ===== if (f < HOLD_END) { return ; } // ===== 特写一(25–34):光标从悬停到按下,按钮变深 ===== if (f < C2) { const t = f - C1; // 局部 0–9;动作窗 4–6 const p = interpolate(t, [4, 7], [0, 1], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const cursorScale = 1 - 0.3 * p; // 1 → 0.7 缩一圈 const btnBg = p < 0.5 ? G.bar : '#5c5c5a'; // 按下瞬间变深 const btnY = 8 * p; // 按钮被压下 8px return (
{/* 圆形光标点:悬停在按钮中心,按下缩一圈 */}
); } // ===== 特写二(35–44):开关从左拨到右 ===== if (f < C3) { const t = f - C2; const p = interpolate(t, [4, 7], [0, 1], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const trackW = 560; const trackH = 240; const knob = 192; const pad = 24; const knobX = pad + p * (trackW - knob - pad * 2); // 左 → 右 const trackBg = p < 0.5 ? G.mid : G.ink; return (
); } // ===== 特写三(45–54):大数字翻牌 0 → 1(rotateX) ===== if (f < WHIP) { const t = f - C3; const p = interpolate(t, [4, 7], [0, 1], { easing: Easing.inOut(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const showOne = p >= 0.5; const angle = showOne ? (1 - p) * 2 * 90 : -p * 2 * 90; // 0→-90 折走,90→0 翻入 const digit = showOne ? '1' : '0'; return (
{digit}
); } // ===== 帧 55 起:甩回全景 + 结果卡片亮起 ===== const t = f - WHIP; // whip:6f 从右侧 900px 高速滑入,poly(5) out 急减速;t≥6 恒为 0(真静止) const whipX = t >= 6 ? 0 : interpolate(t, [0, 6], [900, 0], { easing: Easing.out(Easing.poly(5)), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const speeding = t < 4; // 高速段:3 层错帧副本模拟模糊感 // 结果卡片:泛光 57–84 缓升后恒定(帧 84 起真静止);弹一下 57–64 后恒为 1 const glow = interpolate(f, [57, 84], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }); const pop = f >= 64 ? 1 : interpolate(f, [57, 60, 64], [1, 1.07, 1], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const dash = (x: number, opacity: number, key: string) => (
); return (
{speeding && dash(whipX + 220, 0.18, 'echo2')} {speeding && dash(whipX + 110, 0.35, 'echo1')} {dash(whipX, 1, 'main')} {/* 结果卡片提亮泛光 */}
1
); };