Motion · Ritmo

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.

1920×108016:930 fps4.72s · 142 quadrosremotionLicença: Apache-2.0#shotcraft#remotion#rhythm#opening#transition

Fonte: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composição Remotion (React/TSX) — vídeo de prévia, sem player no navegador

Variantes

trailer-bumper
三镜头各 9f 等长硬切(0/9/18),每镜内部 scale 1→1.04 微推保活;27-33f 纯黑静默,33f 起标题 16f 淡入 + 44px out-cubic 微升
正片开场前的钩子;cold open
card-footage-cadence
七段条件挂载分段(14/22/34/42/52/62 切点):UI 段带微动(缓推/裁切横移),字卡段黑底白字 1.05→1 落定微缩
中段画面与文字对话;卖点三连
smash-cut
轰鸣段全 Easing.in(quad):背景推近 1→1.55 + rotate 1.8°、5 张飞卡错峰加速冲脸 + 速度门控模糊;42f 一帧硬切无动画属性的静止全景
全片最高潮的收束句号

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.

Tudo nesta página também é legível por máquina. Aponte seu agente para o JSON ou adicione o servidor MCP do gimgs e peça para buscar templates de motion.

  1. Baixe o JSON — contém a receita do plano (timing, easing, tabela de parâmetros, armadilhas) e o código TSX completo.
  2. Clone o repositório video-shotcraft; o componente importa fixtures de demos/_fixtures e helpers de assets/lib.
  3. Registre o componente como <Composition> em um projeto Remotion e renderize com `npx remotion render`, ou reimplemente a receita no seu próprio engine.
curl -s https://gimgs.net/motion/trailer-grammar-moves.json
git 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.mp4
claude 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).

JSON · Markdown · llms.txt

10,290 chars · Apache-2.0Abrir template bruto ↗
// ===== 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>
  );
};


  1. Read the recipe
    Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
  2. Get the code
    tool: git
    git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install
  3. Register + render
    tool: 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
  4. Or re-implement
    Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
paramtypedefaultdescription
—

Mais nesta categoria