Motion · Nhịp & montage
Chuyển động nhịp điệu dựng phim
Ba kiểu nhịp điệu dựng phim: drop-blackout-slam — dồn nén trong màn đen rồi bùng nổ; wright-triple-cut — ba tiếng cách cận cảnh liên hoàn; và domino-cascade — nhập cảnh dây chuyền domino.
Nguồn: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composition Remotion (React/TSX) — có video preview, không có player trong trình duyệt
Biến thể
Đang phát bình thường thì cắt sang một khung hình đen tuyền chết lặng giữ 12f, rồi hình ảnh chủ đạo đập vào kèm rung màn hình + vòng sáng.
Nhịp ngay trước cao trào lớn nhất duy nhất của phim; cảnh vào của khẩu hiệu ra mắt/chữ lớn
Ba cận cảnh cực gần 10f cắt cứng liên tiếp (mỗi cận cảnh "tĩnh 4 - động 3 - tĩnh 3"), tiếng thứ ba bật về toàn cảnh lộ kết quả.
Phác thảo "quy trình thao tác": kể chuyện kiểu cài đặt ba bước, hoàn thành một chạm
Tiêu đề đập xuống → sóng rung làm hàng thẻ bật lên → thẻ cuối va trượt thanh bên vào cảnh, động lượng truyền qua từng giai đoạn.
Phiên bản kể chuyện của việc cả trang vào cảnh ở đầu phim; thay thế kiểu fade-in từng mục đơn điệu
Khi nào dùng
Thiết kế nhịp điệu cấp đoạn: dồn nén rồi bùng nổ (A), phác thảo quy trình (B), chuỗi phản ứng mở đầu (C); bổ trợ cho beat-cut-moves (quản khoảng cách các điểm cắt) — ba kiểu này quản "hình dạng hơi thở của cả đoạn".
Ý đồ
beat-cut-moves quản cách sắp khoảng cách các điểm cắt, ba kiểu này quản một lớp kể chuyện nhịp điệu lớn hơn: A là "im lặng chính là dồn nén" — một nhịp trước khi drop, cắt sang màn đen chết lặng giữ nguyên, để cú bùng nổ vang hơn, dùng cho khoảnh khắc ngay trước cao trào lớn nhất phim; B là "bộ ba quy trình" — ba cận cảnh cực gần lách cách liên tiếp, tiếng thứ ba bật về toàn cảnh lộ ra kết quả, kiểu dựng phim của Edgar Wright biến "thao tác rất đơn giản" thành trí nhớ cơ bắp; C là "truyền động lượng" — tiêu đề đập xuống làm rung bật các thẻ, mỗi phần tử vào cảnh được kích hoạt bởi va chạm của phần tử trước đó, kiểu Rube Goldberg kể việc nội dung một trang vào cảnh như một chuỗi phản ứng dây chuyền.
Thời lượng & năng lượng
- Thời lượng: A: 4.3s / B: 4.3s / C: 5s
- Năng lượng: Cao
Công thức cảnh quay (bản gốc tiếng Trung — timing, easing, bảng tham số, lỗi hay gặp)
意图
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)
Mã nguồn tham chiếu (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
Component import fixture chung từ demos/_fixtures và helper từ assets/lib của repo; xem SKILL.md trong repo để biết quy trình render.
Mọi thứ trên trang này đều có bản máy đọc. Đưa agent link JSON, hoặc thêm MCP server gimgs rồi bảo nó tìm motion template.
- Tải JSON — trong đó có công thức cảnh quay (timing, easing, bảng tham số, lỗi hay gặp) và toàn bộ source TSX.
- Clone repo video-shotcraft; component import fixture chung từ demos/_fixtures và helper từ assets/lib.
- Đăng ký component thành <Composition> trong project Remotion rồi render bằng `npx remotion render`, hoặc dựng lại công thức bằng engine của bạn.
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 "Chuyển động nhịp điệu dựng phim" (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 |
|---|---|---|---|
| — | |||
Cùng danh mục

Ngữ pháp trailer: ba nhịp
Ba thủ pháp ngữ pháp trailer — trailer-bumper cắt nhanh móc câu mở đầu, card-footage-cadence xen thẻ chữ với cảnh đối thoại, smash-cut đập cắt vào tĩnh lặng.

Đổi tốc độ và đóng băng chú thích
Hai thủ pháp nhịp điệu remap số khung hình phi tuyến — speed-ramp (nhanh → 0.2x nhìn kỹ → nhanh) và freeze-annotate (chảy → đóng băng khoanh chú thích → tan băng).

Gạch chân hóa phổ tần
UI hóa phổ tần — gạch chân tiêu đề tách thành dải cột dọc nhảy theo phổ tần suốt hai nhịp, rồi thu lại thành đường thẳng; hình ảnh hóa nhạc sống trong UI.

Ảnh bóng lặp kiểu smear
Ảnh bóng smear — thẻ để lại 4 bản sao bán trong suốt rõ, đếm được khi lướt ngang nhanh, gộp lại làm một lúc dừng; phương án dựng hình thay cho motion blur.