Motion · Rhythm
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.
Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player
Variants
Three shots, each a 9f hard cut of equal length (0/9/18), each with an internal 1→1.04 micro push to keep it alive; 27–33f pure black silence, then at 33f the title fades in over 16f with a 44px out-cubic micro-rise
The hook before the main film starts; a cold open
Seven conditionally mounted segments (cut points at 14/22/34/42/52/62): UI segments carry micro-motion (slow push / crop pan), text-card segments are white-on-black settling from 1.05 to 1 with a slight shrink
The middle passage's image-and-text conversation; a triple selling-point chain
The roaring segment runs entirely on Easing.in(quad): background pushes 1→1.55 plus 1.8° rotate, 5 cards fly in staggered and accelerating toward camera with velocity-gated blur; a single hard cut at 42f to a perfectly still wide shot with no animated properties
The full stop closing the film's biggest climax
When to use
The trailer's three structural moments: how the open hooks (A), how the middle converses (B), how the climax closes (C); combined the three form a trailer's skeleton
Intention
The library's rhythm cards each handle one layer: beat-cut manages cut placement, montage-rhythm manages passage breathing, rhythm-interrupt manages interruption. This card handles a trailer's structural moments — not how one shot moves, but how the whole film's three joints connect: A is the opening hook — 0.9 seconds of the most eye-catching shots fast-cut together before the main title, plus a black-screen silent beat before the film opens, "the trailer's trailer"; B is the middle conversation — UI shots alternate hard-cutting with black-background short-phrase cards, both image and text hitting the beat, the third-act structure; C is the climactic full stop — a screen full of roaring motion hard-cuts at its peak into a perfectly still, dead-silent wide shot, noise into silence. Combined, the three form a trailer's skeleton.
Duration & energy
- Duration: A: ~4.7s / B: ~5s / C: ~4.5s
- Energy: A high / B medium / C high
Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)
意图
库内节奏卡各管一段:beat-cut 管切点排布、montage-rhythm 管段落呼吸、 rhythm-interrupt 管打断。本卡管预告片的结构性时刻——不是某个 镜头怎么动,是整片的三个关节怎么接:A 是开场钩——正片前 0.9 秒塞 三个最抓眼镜头速剪 + 黑场静默一拍再开场,预告片的"预告";B 是中段 对话——UI 镜头与黑底短语卡互相接拍交替硬切,画面与文字都踩拍点, 第三幕结构;C 是高潮句号——满屏动势轰鸣最高潮一帧硬切成整齐静止 全景死寂,喧闹→死寂。三式合用即一支预告片的骨架。
三式选型
| 式 | 做法 | 适用 | |----|------|------| | A trailer-bumper | 三镜头各 9f 等长硬切(0/9/18),每镜内部 scale 1→1.04 微推保活;27–33f 纯黑静默,33f 起标题 16f 淡入 + 44px out-cubic 微升 | 正片开场前的钩子;cold open | | B card-footage-cadence | 七段条件挂载分段(14/22/34/42/52/62 切点):UI 段带微动(缓推/裁切横移),字卡段黑底白字 1.05→1 落定微缩 | 中段画面与文字对话;卖点三连 | | C smash-cut | 轰鸣段全 Easing.in(quad):背景推近 1→1.55 + rotate 1.8°、5 张飞卡错峰加速冲脸 + 速度门控模糊;42f 一帧硬切无动画属性的静止全景 | 全片最高潮的收束句号 |
参数表
| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A 速剪 | 三镜等长 9f,零过渡;镜头 3 用 origin 58%/40% 放大 2.2x 怼脸特写 | 节拍感靠等长;三镜构图必须明显不同 | | A 微推 | 每镜内部 scale 1→1.04 线性 | 静帧三连读作幻灯片——命门 | | A 黑场 | 27–33f 纯黑 #000 空 div,6f | 黑场里放任何东西都毁掉"静默一拍" | | B 交替 | UI 段 12f 级 ↔ 字卡 8f 级硬切,条件挂载分段=天然零过渡 | 段长可变但要读出"接拍"感 | | B 字卡 | 黑底白字居中,落定 1.05→1 out-cubic 5f 内完成,其余全静 | 字卡的静 ↔ UI 段的动,质感对比即节奏 | | C 切点 | 42f 一帧完成全部反差;切前 3f 动势仍在加速 | 绝不减速迎接切点——命门 | | C 模糊 | 飞卡 blur = 1+4p(p∝瞬时速度),背景恒 1.5px 衬前景 | 速度门控:快才糊,不是全程糊 | | 收尾 | A 静止 91f / B 45f / C 93f(≥40f) | C 死寂段直接 return 无动画子树,帧函数级真静止 |
已知坑
- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,
首次实战须以真实素材回验
- A 黑场必须纯黑无物(drop-blackout 判例同族)——黑场是喘息,
不是又一张字卡
- B 与 cel-flash-stomp/beat-cut 字卡段的分工:那些是字在表演
(字自己是主角),这是字与画对话(互相接拍)——别混用
- C 死寂段绝对真静止、音乐同帧骤停——声音强依赖,无声版的 smash cut
只剩一半(sound-design §4.5);A 三连切每切一声打点、黑场全消音
- C 与 drop-blackout-slam 的区别:那是黑场蓄力再爆(静→爆),
这是爆完瞬间入定(爆→静)——方向相反,别接错位置
参考实现
demos/rhythm/trailer-grammar-moves/ (CardFootageCadence.tsx / SmashCut.tsx / TrailerBumper.tsx)
Reference implementation (Remotion)
- demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx
- demos/rhythm/trailer-grammar-moves/SmashCut.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/SmashCut.tsx
- demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/TrailerBumper.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/trailer-grammar-moves.jsongit clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx, demos/rhythm/trailer-grammar-moves/SmashCut.tsx, demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx
npx remotion render <CompositionId> out.mp4claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("trailer-grammar-moves")Prompt
Use the video-shotcraft shot recipe "Trailer grammar: three beats" (Remotion composition, Apache-2.0) published on gimgs.net. Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/trailer-grammar-moves.json Raw TSX: https://gimgs.net/motion/trailer-grammar-moves/template.html Repository: https://github.com/Vincentwei1021/video-shotcraft (component: demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx, demos/rhythm/trailer-grammar-moves/SmashCut.tsx, demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx; imports demos/_fixtures and assets/lib) Spec: 1920x1080, 30 fps, 4.72s 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/trailer-grammar-moves/CardFootageCadence.tsx =====
// card-footage-cadence|字卡穿插节奏
// UI 镜头 ↔ 黑底字卡像对话接拍:0–14f UI A 缓推 → 硬切 8f 字卡 SHIP →
// 硬切 12f UI B 1.6x 裁切缓移 → 8f 字卡 FASTER → 10f UI A 2x 裁切 →
// 10f 字卡 TODAY(下划线) → 62f 硬切收尾全景定格。
// UI 段全部带微动(推/移),字卡段除落定微缩(1.05→1, out-cubic)外全静,
// 动↔静对比即手法本体。所有切换零过渡(条件挂载,无 crossfade)。
// 收尾 62–105f 轻推收完,105–150f 真静止 45f ≥ 40f。帧确定,无随机。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';
import { FakeDashboard } from '../../_fixtures/Fixtures';
const CLAMP = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;
// 黑底白字字卡:落定微缩 1.05→1(out-cubic,段内前 5f 完成),其余全静
const TitleCard: React.FC<{ text: string; local: number; underline?: boolean }> = ({
text,
local,
underline = false,
}) => {
const scale = interpolate(local, [0, 5], [1.05, 1], {
...CLAMP,
easing: Easing.out(Easing.cubic),
});
return (
<AbsoluteFill
style={{ background: '#0d0d0d', alignItems: 'center', justifyContent: 'center' }}
>
<div
style={{
transform: `scale(${scale})`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 26,
}}
>
<div
style={{
fontFamily: 'Helvetica, Arial, sans-serif',
fontWeight: 800,
fontSize: 170,
color: '#ffffff',
letterSpacing: 2,
lineHeight: 1,
}}
>
{text}
</div>
{underline && (
<div style={{ width: 560, height: 14, background: '#ffffff', borderRadius: 7 }} />
)}
</div>
</AbsoluteFill>
);
};
// UI 镜头:包一层 transform 做缓推/缓移,硬切由外层条件挂载保证
const UiShot: React.FC<{
variant: 'A' | 'B';
transform: string;
origin?: string;
}> = ({ variant, transform, origin = '50% 50%' }) => (
<AbsoluteFill style={{ overflow: 'hidden', background: '#ececea' }}>
<div style={{ width: 1920, height: 1080, transform, transformOrigin: origin }}>
<FakeDashboard variant={variant} />
</div>
</AbsoluteFill>
);
export const CardFootageCadence: React.FC = () => {
const frame = useCurrentFrame();
// ---- 分段:切点 14 / 22 / 34 / 42 / 52 / 62,总长 150 ----
// 段1 0–14f:UI A 全景缓推 1.0→1.08
if (frame < 14) {
const s = interpolate(frame, [0, 14], [1, 1.08], CLAMP);
return <UiShot variant="A" transform={`scale(${s})`} />;
}
// 段2 14–22f:字卡 SHIP
if (frame < 22) {
return <TitleCard text="SHIP" local={frame - 14} />;
}
// 段3 22–34f:UI B 1.6x 裁切 + 横向缓移(120px 内容位移,屏上 ~192px)。
// origin 偏左让列表行图标入画,读得出是 UI(QA 后调:50%→35%)
if (frame < 34) {
const tx = interpolate(frame - 22, [0, 12], [60, -60], CLAMP);
return (
<UiShot variant="B" transform={`scale(1.6) translateX(${tx}px)`} origin="35% 50%" />
);
}
// 段4 34–42f:字卡 FASTER
if (frame < 42) {
return <TitleCard text="FASTER" local={frame - 34} />;
}
// 段5 42–52f:UI A 另一处 2x 裁切(左上卡片区)+ 继续缓推 2.0→2.14
if (frame < 52) {
const s = interpolate(frame - 42, [0, 10], [2, 2.14], CLAMP);
return <UiShot variant="A" transform={`scale(${s})`} origin="32% 30%" />;
}
// 段6 52–62f:字卡 TODAY + 下划线
if (frame < 62) {
return <TitleCard text="TODAY" local={frame - 52} underline />;
}
// 段7 62–150f:收尾全景 UI 定格。62–105f 极缓推收完(out-cubic),
// 105f 后 transform 值恒为 1.02,105–150f 真静止 45f。
const s = interpolate(frame, [62, 105], [1, 1.02], {
...CLAMP,
easing: Easing.out(Easing.cubic),
});
return <UiShot variant="B" transform={`scale(${s})`} />;
};
// ===== demos/rhythm/trailer-grammar-moves/SmashCut.tsx =====
// smash-cut|猛切(喧闹→死寂)
// 0–41f 轰鸣段:5 张 Card 多方向高速飞掠冲镜(穿屏 translate + scale 1.5→3
// 冲脸 + 微 rotate,各卡错峰跑两轮,第二轮更快=整体仍在加速),背景
// FakeDashboard A 以 ease-in 持续加速推近 1→1.55 并滚动 rotate 1.8°——
// 切点前一刻动势最猛。42f 一帧硬切 variant B 整齐静止全景,无一物在动,
// 停满 93f(>50f)。反差即手法本体。总 135f。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, Card, FakeDashboard } from '../../_fixtures/Fixtures';
const CUT = 42; // 硬切帧:>=42 全静止
// 每张飞卡:两轮 pass,第二轮更短(更快),ease-in 让每一程都在加速。
// from/to 为屏幕外起终点(中心系坐标),飞行中 scale 1.5→3 冲脸。
type Fly = {
from: [number, number];
to: [number, number];
rot: [number, number]; // 微旋转 起→终
seed: number;
w: number;
h: number;
};
const FLIES: Fly[] = [
{ from: [-1400, -120], to: [1400, 60], rot: [-6, 5], seed: 1, w: 440, h: 290 },
{ from: [1400, 180], to: [-1400, -100], rot: [7, -4], seed: 2, w: 400, h: 260 },
{ from: [-300, -900], to: [200, 900], rot: [-3, 8], seed: 3, w: 460, h: 300 },
{ from: [-1300, 800], to: [1300, -750], rot: [5, -7], seed: 4, w: 420, h: 280 },
{ from: [1350, -780], to: [-1350, 820], rot: [-8, 4], seed: 5, w: 480, h: 310 },
];
// 卡 i 的第 k 轮:start = i*4 + k*20,时长 16 / 12(第二轮更快)。
// card4 第二轮 36–48f 在切点 42f 被硬切截断——正中"绝不减速迎接切点"。
const passWindow = (i: number, k: number): [number, number] => {
const start = i * 4 + k * 20;
const dur = k === 0 ? 16 : 12;
return [start, start + dur];
};
const FlyCard: React.FC<{ fly: Fly; i: number; frame: number }> = ({ fly, i, frame }) => {
// 找当前活跃的 pass(两轮)
let active: [number, number] | null = null;
for (let k = 0; k < 2; k++) {
const [s, e] = passWindow(i, k);
if (frame >= s && frame < e) { active = [s, e]; break; }
}
if (!active) return null;
const [s, e] = active;
// ease-in:全程加速,越接近终点越快
const p = interpolate(frame, [s, e], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad),
});
const x = fly.from[0] + (fly.to[0] - fly.from[0]) * p;
const y = fly.from[1] + (fly.to[1] - fly.from[1]) * p;
const rot = fly.rot[0] + (fly.rot[1] - fly.rot[0]) * p;
const scale = 1.5 + 1.5 * p; // 1.5 → 3 冲脸
// 速度门控方向模糊感:ease-in 下瞬时速度 ∝ p,速度越快越糊
const blur = 1 + 4 * p;
return (
<div style={{
position: 'absolute', left: 960 - fly.w / 2, top: 540 - fly.h / 2,
transform: `translate(${x}px, ${y}px) rotate(${rot}deg) scale(${scale})`,
filter: `blur(${blur}px)`,
}}>
<Card w={fly.w} h={fly.h} seed={fly.seed}
style={{ boxShadow: '0 12px 40px rgba(0,0,0,0.25)' }} />
</div>
);
};
export const SmashCut: React.FC = () => {
const frame = useCurrentFrame();
// —— 死寂段:42f 起 variant B 整齐静止全景,无任何动画属性 ——
if (frame >= CUT) {
return <FakeDashboard variant="B" />;
}
// —— 轰鸣段:背景 ease-in 加速推近 + 滚动,切点前 3f 仍在加速 ——
const bgScale = interpolate(frame, [0, CUT], [1, 1.55], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad),
});
const bgRot = interpolate(frame, [0, CUT], [0, 1.8], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad),
});
return (
<div style={{ width: 1920, height: 1080, background: G.bg, overflow: 'hidden', position: 'relative' }}>
<div style={{
width: 1920, height: 1080,
transform: `scale(${bgScale}) rotate(${bgRot}deg)`,
transformOrigin: '50% 50%',
filter: 'blur(1.5px)', // 背景轻糊,衬前景飞卡
}}>
<FakeDashboard variant="A" />
</div>
{FLIES.map((fly, i) => (
<FlyCard key={i} fly={fly} i={i} frame={frame} />
))}
</div>
);
};
// ===== demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx =====
// trailer-bumper|前置速剪预告
// 结构即全部:三连 9f 速剪(A 整页 / B 整页 / A 2.2x 怼脸特写)→ 6f 纯黑喘息
// → 正式开场大标题淡入+微升 16f 定格。每个速剪镜头内部带 1→1.04 轻推近。
// 收尾真静止:49f 后所有动画结束,静止 91f(>40f)。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, FakeDashboard } from '../../_fixtures/Fixtures';
// 时间轴(帧)
const CUT_1 = 0; // 镜头1:variant A 整页
const CUT_2 = 9; // 镜头2:variant B 整页
const CUT_3 = 18; // 镜头3:variant A 2.2x 怼脸特写
const BLACK = 27; // 纯黑静默
const TITLE = 33; // 正式开场
const TITLE_IN = 16; // 标题淡入+微升时长
// 速剪镜头内部轻推近:镜头内 local 0→9f,scale 1→1.04(线性,活镜头感)
const push = (frame: number, start: number) =>
interpolate(frame, [start, start + 9], [1, 1.04], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
export const TrailerBumper: React.FC = () => {
const frame = useCurrentFrame();
// —— 段落 1–3:三连速剪 ——
if (frame < BLACK) {
let inner: React.ReactNode;
if (frame < CUT_2) {
// 镜头1:A 整页 + 轻推近
inner = (
<div style={{ width: 1920, height: 1080, transform: `scale(${push(frame, CUT_1)})`, transformOrigin: '50% 50%' }}>
<FakeDashboard variant="A" />
</div>
);
} else if (frame < CUT_3) {
// 镜头2:B 整页 + 轻推近
inner = (
<div style={{ width: 1920, height: 1080, transform: `scale(${push(frame, CUT_2)})`, transformOrigin: '50% 50%' }}>
<FakeDashboard variant="B" />
</div>
);
} else {
// 镜头3:A 的 2.2x 放大裁切怼脸特写(对准中列卡片),叠加同样的轻推近
const s = 2.2 * push(frame, CUT_3);
inner = (
<div style={{ width: 1920, height: 1080, transform: `scale(${s})`, transformOrigin: '58% 40%' }}>
<FakeDashboard variant="A" />
</div>
);
}
return (
<div style={{ width: 1920, height: 1080, overflow: 'hidden', background: G.bg }}>
{inner}
</div>
);
}
// —— 段落 4:纯黑静默 6f,必须纯黑无物 ——
if (frame < TITLE) {
return <div style={{ width: 1920, height: 1080, background: '#000000' }} />;
}
// —— 段落 5:正式开场——浅底大标题淡入+微升 16f 后定格 ——
const opacity = interpolate(frame, [TITLE, TITLE + TITLE_IN], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const rise = interpolate(frame, [TITLE, TITLE + TITLE_IN], [44, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
return (
<div
style={{
width: 1920,
height: 1080,
background: G.bg,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
}}
>
<div
style={{
opacity,
transform: `translateY(${rise}px)`,
fontFamily: 'Helvetica, Arial, sans-serif',
fontWeight: 800,
fontSize: 168,
color: G.ink,
letterSpacing: -2,
textAlign: 'center',
}}
>
THE LAUNCH
</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/trailer-grammar-moves/CardFootageCadence.tsx, demos/rhythm/trailer-grammar-moves/SmashCut.tsx, demos/rhythm/trailer-grammar-moves/TrailerBumper.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

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.

Sakuga timing shift
On-3s cuts to on-1s — an element moves in choppy 3-frame steps, then snaps to buttery frame-by-frame speed at the climax.