Motion · Rhythm
Montage rhythm moves
Three montage-rhythm styles: drop-blackout-slam, a blackout wind-up drop; wright-triple-cut, a triple-clack close-up cut; and domino-cascade, a domino-chain entrance.
Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player
Variants
Mid-playback, one frame cuts to a dead-silent pure black hold for 12f, then the main visual slams in with a screen shake plus a bright ring.
The one beat right before the film's single biggest climax; the entrance of a launch slogan/big text
Three 10f extreme close-ups hard-cut in rapid succession (each "still 4 – move 3 – still 3"), the third snaps back to a wide shot revealing the result.
A sketch of "the operating process": three-step setup, one-click-done style narratives
The title slams down → a shockwave bounces the card row up → the last card knocks the sidebar sliding into place, momentum transferring stage by stage.
A narrative version of a whole page entering at the open; replaces a plain, one-by-one fade-in
When to use
Paragraph-level rhythm design: wind-up and burst (A), process sketch (B), opening chain reaction (C); complements beat-cut-moves (which arranges cut spacing) — these three manage the "shape of a section's breathing".
Intention
beat-cut-moves manages how cut intervals are spaced; these three styles manage a bigger layer of rhythmic storytelling: A is "silence as wind-up" — one beat before the drop cuts to a dead-silent full black hold, so the burst lands harder, used for the one shot right before the film's biggest peak; B is a "process triple" — three extreme close-ups clack in rapid succession, the third snaps back to a wide shot revealing the result, an Edgar Wright-style edit that turns "it's this simple" into muscle memory; C is "momentum transfer" — a title slams down and shakes cards loose, each element's entrance triggered by the previous one's impact, a Rube Goldberg-style telling of a page's content entering as a chain reaction.
Duration & energy
- Duration: A: 4.3s / B: 4.3s / C: 5s
- Energy: High
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
beat-cut-moves 管切点间隔怎么排,这三式管更大一层的节奏叙事: A 是"静默即蓄力"——drop 前一拍切全黑死寂憋一整拍,爆开才够响 (EDM 演出 blackout 惯例),给全片最高潮的前一秒用;B 是"流程三连" ——三个超近特写咔哒咔哒咔哒连打(按下/拨动/翻牌),第三声甩回全景 亮结果,Edgar Wright 式把"操作很简单"剪成肌肉记忆;C 是"动量传递" ——标题砸落震起卡片、卡片落地撞进侧边栏,每个元素的入场由上一个的 撞击触发,Rube Goldberg 式把一页内容的入场讲成连锁反应。
三式选型
| 式 | 做法 | 适用 | |----|------|------| | A drop-blackout-slam 黑场蓄爆 | 正常播放中一帧切纯黑死寂 12f,然后主视觉带震屏+亮环砸入 | 全片唯一最高潮的前一拍;发布口号/大字的登场 | | B wright-triple-cut 三连特写 | 三个 10f 超近特写硬切连打(各"静4-动3-静3"),第三声甩回全景亮结果 | "操作流程"速写:三步设置、一键完成类叙事 | | C domino-cascade 连锁入场 | 标题砸落→震波弹起卡片列→末卡撞滑侧边栏进场,动量方向逐级传递 | 开场整页入场的叙事化版本;替代平铺直叙的逐个 fade-in |
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A 铺垫 | 前段画面轻微 scale 呼吸 1.0↔1.02 + 节拍点每 12f 闪 | 必须先让"片子在正常播"可感,黑掉才有落差 | | A 黑场 | 一帧直切纯黑(#0c0c0c),12f 完全无物 | 命门:黑场里放任何东西(logo/微光)都在泄压——死寂才是蓄力 | | A 爆入 | 主视觉 scale 1.35→1.0(5f out cubic)+ 10px 震屏 exp(-t/2.5) 12f 收干 + 亮环 80→900px 16f 消散 | 三件事同帧起爆;震屏 t≥14 强制归零保真静止 | | B 特写结构 | 每个 10f:静 4f + 动作 3f + 静 3f;动作用 p<0.5 二值跳变保干脆 | 动作 3f 是"咔哒"的物理时长;拉长成 6f 就是普通交互动画 | | B 构图统一 | 三特写同倍率感、主体都在屏心 | 构图乱跳读作三段素材硬拼,连切节奏散 | | B 甩回 | 6f whip(Easing.poly(5) out)+ 2 层错帧副本(opacity 0.35/0.18)模拟糊 | 甩回落点的"结果"要提亮泛光——三连咔哒的答案必须亮着 | | C 动量链 | 每级 startFrame = 上一级 impact 帧;初速方向继承上一撞击方向 | 命门:竖砸→竖弹→(末卡歪 3° 给出横向)→横滑,方向没有出处则"连锁"读作巧合 | | C 撞击震动 | 4f 衰减序列 [A,-0.6A,0.3A,-0.12A,0],两次幅度 10/6px 递减 | 幅度递减表现能量损耗;等幅两震读作重复播放 | | C 卡片弹跳 | 依次隔 5f 各弹 60px 抛物线 12f | 40px/3f 间隔在正常速度下读不清传递感(本批实渲加码) | | 收尾 | A 爆入后 ≥50f / B 甩回后 ≥45f / C 全链落定后 ≥50f 真静止 | 三式都是重拳,hold 按平常两倍给(同 beat-cut B 判例) |
已知坑
- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,
首次实战须以真实素材回验
- A 式全片 ≤1 次且必须配声design:黑场同帧音频也切静、爆开同帧音爆
(sound-design §4.5)——无声版黑场读作播放器卡了 12 帧
- A 的震屏与 impact-feedback、C 的震动与 domino 内部震动都属震屏族,
同一镜头只能有一个震源
- B 式三个特写 10f/个节奏很快是设计意图(已验证可读),但特写内容换成
真实 UI 后若操作复杂(不是一眼动作),拉到 12–14f
- B 式甩回与 shot-transitions E 式甩镜同技术根——B 的 whip 是镜内收尾
不是转场,别在 B 之后再接 E 式(连甩读作晕)
- C 式链最多三级(标题→卡片→侧栏);四级以上观众跟不住因果链,
后面的入场退化回普通错峰
参考实现
demos/rhythm/montage-rhythm-moves/ (DominoCascade.tsx / DropBlackoutSlam.tsx / WrightTripleCut.tsx)
Reference implementation (Remotion)
- demos/rhythm/montage-rhythm-moves/DominoCascade.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/montage-rhythm-moves/DominoCascade.tsx
- demos/rhythm/montage-rhythm-moves/DropBlackoutSlam.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/montage-rhythm-moves/DropBlackoutSlam.tsx
- demos/rhythm/montage-rhythm-moves/WrightTripleCut.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/montage-rhythm-moves/WrightTripleCut.tsx
The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.
Everything on this page is also machine-readable. Point your agent at the JSON, or add the gimgs MCP server and ask it to search motion templates.
- Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
- Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
- Register the component as a <Composition> in a Remotion project and render with `npx remotion render`, or re-implement the recipe in your own engine.
curl -s https://gimgs.net/motion/montage-rhythm-moves.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/rhythm/montage-rhythm-moves/DominoCascade.tsx, demos/rhythm/montage-rhythm-moves/DropBlackoutSlam.tsx, demos/rhythm/montage-rhythm-moves/WrightTripleCut.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("montage-rhythm-moves")Prompt
Use the video-shotcraft shot recipe "Montage rhythm moves" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/montage-rhythm-moves.json Raw TSX: https://gimgs.net/motion/montage-rhythm-moves/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/rhythm/montage-rhythm-moves/DominoCascade.tsx, demos/rhythm/montage-rhythm-moves/DropBlackoutSlam.tsx, demos/rhythm/montage-rhythm-moves/WrightTripleCut.tsx; imports demos/_fixtures and assets/lib) Spec: 1920x1080, 30 fps, 4.39s 1. Read the JSON and follow its "prompt" section (when to use, intention, parameter table, known pitfalls). 2. Adapt it for: <describe what you want>. 3. Either clone the repo, register the component as a <Composition> in a Remotion project and run npx remotion render <CompositionId> out.mp4 — or re-implement the recipe in your own engine (HTML/WAAPI works: keep the same timing and easing).
// ===== 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 (
<div style={{ width: 1920, height: 1080, background: G.bg, overflow: 'hidden', position: 'relative' }}>
{/* 全画面震动容器 */}
<div style={{ position: 'absolute', inset: 0, transform: `translateY(${shakeY}px)` }}>
{/* 地板线:卡片落点 */}
<div style={{ position: 'absolute', left: ROW_LEFT - 30, top: 928, width: ROW_W + 60, height: 6, background: G.bar, borderRadius: 3 }} />
{/* ① 砸落的标题 */}
<div style={{ position: 'absolute', left: 240, width: 1680, top: titleTop, display: 'flex', justifyContent: 'center' }}>
<TitleBlock text="CHAIN REACTION" size={120} />
</div>
{/* ② 被震弹起的 4 张卡片 */}
{[0, 1, 2, 3].map((i) => (
<div
key={i}
style={{
position: 'absolute',
left: ROW_LEFT + i * (CARD_W + GAP),
top: CARD_TOP,
transform: `translateY(${cardDy(i)}px)${i === 3 ? ` rotate(${lastCardRot}deg)` : ''}`,
transformOrigin: '50% 100%',
}}
>
<Card w={CARD_W} h={CARD_H} seed={i + 2} />
</div>
))}
{/* ③ 被撞滑进场的侧边栏 */}
<div
style={{
position: 'absolute', left: 0, top: 0, width: 240, height: 1080,
background: G.side, transform: `translateX(${sideX}px)`,
padding: '32px 24px', boxSizing: 'border-box',
display: 'flex', flexDirection: 'column', gap: 22,
}}
>
<div style={{ width: 44, height: 44, borderRadius: 10, background: '#777775' }} />
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} style={{ height: 13, width: `${58 + ((i * 31) % 38)}%`, background: G.sideBar, borderRadius: 6 }} />
))}
</div>
</div>
</div>
);
};
// ===== 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 (
<div style={{ width: 1920, height: 1080, background: G.bg, overflow: 'hidden', position: 'relative' }}>
<div style={{ width: 1920, height: 1080, transform: `scale(${breathe})`, transformOrigin: '50% 50%' }}>
<FakeDashboard variant="B" />
</div>
{/* 右下角节拍点 */}
<div
style={{
position: 'absolute',
right: 56,
bottom: 48,
width: 36,
height: 36,
borderRadius: 18,
background: G.ink,
opacity: beatOn,
transform: `scale(${beatScale})`,
}}
/>
</div>
);
}
// ===== 段 2:帧 50–61 纯黑死寂,屏上完全无物 =====
if (f < SLAM) {
return <div style={{ width: 1920, height: 1080, background: '#0c0c0c' }} />;
}
// ===== 段 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 (
<div style={{ width: 1920, height: 1080, background: G.ink, overflow: 'hidden', position: 'relative' }}>
<div
style={{
width: 1920,
height: 1080,
transform: `translate(${shakeX}px, ${shakeY}px)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
}}
>
{/* 亮环:快速扩散消散 */}
{t < 16 && (
<div
style={{
position: 'absolute',
left: 960 - ringR,
top: 540 - ringR,
width: ringR * 2,
height: ringR * 2,
borderRadius: '50%',
border: '10px solid #ffffff',
opacity: ringOpacity,
}}
/>
)}
<div
style={{
fontFamily: 'Helvetica, Arial, sans-serif',
fontWeight: 800,
fontSize: 340,
color: '#ffffff',
letterSpacing: -6,
transform: `scale(${slamScale})`,
lineHeight: 1,
}}
>
DROP
</div>
</div>
</div>
);
};
// ===== 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 }) => (
<div style={{ width: 1920, height: 1080, background: G.panel, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{
width: 1400, height: 800, background: G.card, border: `4px solid ${G.border}`,
borderRadius: 28, boxShadow: '0 6px 24px rgba(0,0,0,0.08)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{children}
</div>
</div>
);
// 结果卡片(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 <FakeDashboard variant="A" />;
}
// ===== 特写一(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 (
<CloseupStage>
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{
width: 620, height: 190, borderRadius: 40, background: btnBg,
border: `5px solid ${G.mid}`, boxSizing: 'border-box',
transform: `translateY(${btnY}px)`,
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 14,
}}>
<div style={{ width: 260, height: 30, borderRadius: 15, background: p < 0.5 ? G.card : G.line }} />
</div>
{/* 圆形光标点:悬停在按钮中心,按下缩一圈 */}
<div style={{
position: 'absolute', left: '50%', top: '50%',
width: 96, height: 96, marginLeft: -48, marginTop: -48 + btnY,
borderRadius: 48, background: G.ink, border: '8px solid #ffffff',
boxSizing: 'border-box', transform: `scale(${cursorScale})`,
boxShadow: '0 4px 14px rgba(0,0,0,0.3)',
}} />
</div>
</CloseupStage>
);
}
// ===== 特写二(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 (
<CloseupStage>
<div style={{
position: 'relative', width: trackW, height: trackH, borderRadius: trackH / 2,
background: trackBg, boxSizing: 'border-box',
}}>
<div style={{
position: 'absolute', top: pad, left: knobX,
width: knob, height: knob, borderRadius: knob / 2, background: G.card,
boxShadow: '0 6px 18px rgba(0,0,0,0.35)',
}} />
</div>
</CloseupStage>
);
}
// ===== 特写三(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 (
<CloseupStage>
<div style={{ perspective: 1200 }}>
<div style={{
width: 440, height: 560, borderRadius: 32, background: G.ink,
display: 'flex', alignItems: 'center', justifyContent: 'center',
transform: `rotateX(${angle}deg)`, backfaceVisibility: 'hidden',
boxShadow: '0 8px 28px rgba(0,0,0,0.25)',
}}>
<div style={{
fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800,
fontSize: 420, color: '#ffffff', lineHeight: 1,
}}>
{digit}
</div>
</div>
</div>
</CloseupStage>
);
}
// ===== 帧 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) => (
<div key={key} style={{ position: 'absolute', left: 0, top: 0, transform: `translateX(${x}px)`, opacity }}>
<FakeDashboard variant="A" />
</div>
);
return (
<div style={{ width: 1920, height: 1080, background: G.bg, overflow: 'hidden', position: 'relative' }}>
{speeding && dash(whipX + 220, 0.18, 'echo2')}
{speeding && dash(whipX + 110, 0.35, 'echo1')}
{dash(whipX, 1, 'main')}
{/* 结果卡片提亮泛光 */}
<div style={{
position: 'absolute',
left: RESULT.left, top: RESULT.top, width: RESULT.w, height: RESULT.h,
transform: `translateX(${whipX}px) scale(${pop})`,
borderRadius: 14, boxSizing: 'border-box',
background: `rgba(255,255,255,${0.72 * glow})`,
border: `5px solid rgba(47,47,47,${glow})`,
boxShadow: `0 0 ${70 * glow}px rgba(255,255,255,${0.95 * glow}), 0 0 ${26 * glow}px rgba(47,47,47,${0.28 * glow})`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
opacity: glow,
}}>
<div style={{
fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800,
fontSize: 260, color: G.ink, lineHeight: 1,
}}>
1
</div>
</div>
</div>
);
};
- Read the recipePrompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
- Get the codetool: git
git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install - Register + rendertool: remotion
npx remotion render <CompositionId> out.mp4 # component: demos/rhythm/montage-rhythm-moves/DominoCascade.tsx, demos/rhythm/montage-rhythm-moves/DropBlackoutSlam.tsx, demos/rhythm/montage-rhythm-moves/WrightTripleCut.tsx - Or re-implementPort the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
| param | type | default | description |
|---|---|---|---|
| — | |||
More in this category

Trailer grammar: three beats
Three trailer-grammar moves — trailer-bumper, a fast-cut hook up front; card-footage-cadence, text cards interleaved with dialogue footage; smash-cut, a slam into stillness.

Speed ramp and freeze-annotate
Two frame-remap pacing moves — speed-ramp (fast → 0.2x gaze → fast) and freeze-annotate (flowing → freeze with a circled callout → unfreeze).

Spectrum-morph underline
Spectrum UI morph — the title underline splits into vertical bars that pulse like a spectrum for two bars, then closes back into a straight line; music visualization living inside the UI.

Smear-frame afterimages
Afterimage smear — a card drags 4 crisp, countable clones while panning fast, merging into one on landing; an animated take on motion blur.