// ===== 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 }) => (
);
// 结果卡片(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 (
);
}
// ===== 帧 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')}
{/* 结果卡片提亮泛光 */}
);
};