{
  "slug": "trailer-grammar-moves",
  "title": "Trailer grammar: three beats",
  "tagline": "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.",
  "description": "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 ",
  "category": "rhythm",
  "tags": [
    "shotcraft",
    "remotion",
    "rhythm",
    "opening",
    "transition"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.72,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/trailer-grammar-moves/poster-f5e93454e2ac.jpg",
    "video": "https://gimgs.net/motion/media/trailer-grammar-moves/video-2a08f5853d40.mp4"
  },
  "variants": [
    {
      "key": "trailer-bumper",
      "label": "trailer-bumper",
      "description": "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",
      "use": "The hook before the main film starts; a cold open",
      "video": "https://gimgs.net/motion/media/trailer-grammar-moves/video-2a08f5853d40.mp4",
      "poster": "https://gimgs.net/motion/media/trailer-grammar-moves/poster-f5e93454e2ac.jpg"
    },
    {
      "key": "card-footage-cadence",
      "label": "card-footage-cadence",
      "description": "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",
      "use": "The middle passage's image-and-text conversation; a triple selling-point chain",
      "video": "https://gimgs.net/motion/media/trailer-grammar-moves/video-808dc0c69553.mp4",
      "poster": "https://gimgs.net/motion/media/trailer-grammar-moves/poster-card-footage-cadence-3770590dad5b.jpg"
    },
    {
      "key": "smash-cut",
      "label": "smash-cut",
      "description": "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",
      "use": "The full stop closing the film's biggest climax",
      "video": "https://gimgs.net/motion/media/trailer-grammar-moves/video-fb7032e0aeea.mp4",
      "poster": "https://gimgs.net/motion/media/trailer-grammar-moves/poster-smash-cut-f53ebaf29896.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/trailer-grammar-moves.html",
  "json_url": "https://gimgs.net/motion/trailer-grammar-moves.json",
  "template_url": "https://gimgs.net/motion/trailer-grammar-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/trailer-grammar-moves.md",
  "updated_at": "2026-09-24T07:40:29.811Z",
  "prompt": "## When to use\n\nThe 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\n\n## Intention\n\nThe 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.\n\n## Duration & energy\n\n- Duration: A: ~4.7s / B: ~5s / C: ~4.5s\n- Energy: A high / B medium / C high\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n库内节奏卡各管一段：beat-cut 管切点排布、montage-rhythm 管段落呼吸、\nrhythm-interrupt 管打断。本卡管**预告片的结构性时刻**——不是某个\n镜头怎么动，是整片的三个关节怎么接：A 是开场钩——正片前 0.9 秒塞\n三个最抓眼镜头速剪 + 黑场静默一拍再开场，预告片的\"预告\"；B 是中段\n对话——UI 镜头与黑底短语卡互相接拍交替硬切，画面与文字都踩拍点，\n第三幕结构；C 是高潮句号——满屏动势轰鸣最高潮一帧硬切成整齐静止\n全景死寂，喧闹→死寂。三式合用即一支预告片的骨架。\n\n## 三式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A trailer-bumper | 三镜头各 9f 等长硬切（0/9/18），每镜内部 scale 1→1.04 微推保活；27–33f 纯黑静默，33f 起标题 16f 淡入 + 44px out-cubic 微升 | 正片开场前的钩子；cold open |\n| B card-footage-cadence | 七段条件挂载分段（14/22/34/42/52/62 切点）：UI 段带微动（缓推/裁切横移），字卡段黑底白字 1.05→1 落定微缩 | 中段画面与文字对话；卖点三连 |\n| C smash-cut | 轰鸣段全 Easing.in(quad)：背景推近 1→1.55 + rotate 1.8°、5 张飞卡错峰加速冲脸 + 速度门控模糊；42f 一帧硬切无动画属性的静止全景 | 全片最高潮的收束句号 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A 速剪 | 三镜等长 9f，零过渡；镜头 3 用 origin 58%/40% 放大 2.2x 怼脸特写 | 节拍感靠等长；三镜构图必须明显不同 |\n| A 微推 | 每镜内部 scale 1→1.04 线性 | 静帧三连读作幻灯片——命门 |\n| A 黑场 | 27–33f 纯黑 #000 空 div，6f | 黑场里放任何东西都毁掉\"静默一拍\" |\n| B 交替 | UI 段 12f 级 ↔ 字卡 8f 级硬切，条件挂载分段=天然零过渡 | 段长可变但要读出\"接拍\"感 |\n| B 字卡 | 黑底白字居中，落定 1.05→1 out-cubic 5f 内完成，其余全静 | 字卡的静 ↔ UI 段的动，质感对比即节奏 |\n| C 切点 | 42f 一帧完成全部反差；切前 3f 动势仍在加速 | 绝不减速迎接切点——命门 |\n| C 模糊 | 飞卡 blur = 1+4p（p∝瞬时速度），背景恒 1.5px 衬前景 | 速度门控：快才糊，不是全程糊 |\n| 收尾 | A 静止 91f / B 45f / C 93f（≥40f） | C 死寂段直接 return 无动画子树，帧函数级真静止 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- A 黑场必须纯黑无物（drop-blackout 判例同族）——黑场是喘息，\n  不是又一张字卡\n- B 与 cel-flash-stomp/beat-cut 字卡段的分工：那些是字在表演\n  （字自己是主角），这是字与画对话（互相接拍）——别混用\n- C 死寂段绝对真静止、音乐同帧骤停——声音强依赖，无声版的 smash cut\n  只剩一半（sound-design §4.5）；A 三连切每切一声打点、黑场全消音\n- C 与 drop-blackout-slam 的区别：那是黑场蓄力再爆（静→爆），\n  这是爆完瞬间入定（爆→静）——方向相反，别接错位置\n\n## 参考实现\ndemos/rhythm/trailer-grammar-moves/\n（CardFootageCadence.tsx / SmashCut.tsx / TrailerBumper.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx\n- demos/rhythm/trailer-grammar-moves/SmashCut.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/SmashCut.tsx\n- demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx\n\nThe component imports shared fixtures from `demos/_fixtures` and helpers from `assets/lib` in the repo; SKILL.md there documents the render workflow.\n",
  "flow": [
    {
      "step": "Read the recipe",
      "note": "Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points."
    },
    {
      "step": "Get the code",
      "tool": "git",
      "cmd": "git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install"
    },
    {
      "step": "Register + render",
      "tool": "remotion",
      "cmd": "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"
    },
    {
      "step": "Or re-implement",
      "note": "Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing."
    }
  ],
  "files": [
    {
      "path": "demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx"
    },
    {
      "path": "demos/rhythm/trailer-grammar-moves/SmashCut.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/SmashCut.tsx"
    },
    {
      "path": "demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx"
    }
  ],
  "contract": "React/Remotion composition (TSX) from the open-source video-shotcraft library (Apache-2.0). It is not a browser-playable template: clone the repo, register the component in a Remotion project and render with `npx remotion render`. The recipe (prompt) describes timing, easing and parameters so you can also re-implement the shot in any engine.",
  "render": {
    "engine": "remotion",
    "frames": 142,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/SmashCut.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx"
    ],
    "note": "Clone https://github.com/Vincentwei1021/video-shotcraft (npm install). The component lives under demos/{category}/{name}/ and imports shared fixtures from demos/_fixtures and helpers from assets/lib. Copy it (with those imports) into a Remotion project, register a <Composition> with the template's width/height/fps/duration in Root.tsx, then `npx remotion render <CompositionId> out.mp4`. SKILL.md in the repo documents the full Claude Code / Codex workflow. The prompt below is the shot recipe (timing, easing, parameter table, pitfalls) — enough to re-implement the shot in HTML/WAAPI or another engine if you prefer."
  },
  "source": "// ===== demos/rhythm/trailer-grammar-moves/CardFootageCadence.tsx =====\n// card-footage-cadence｜字卡穿插节奏\n// UI 镜头 ↔ 黑底字卡像对话接拍：0–14f UI A 缓推 → 硬切 8f 字卡 SHIP →\n// 硬切 12f UI B 1.6x 裁切缓移 → 8f 字卡 FASTER → 10f UI A 2x 裁切 →\n// 10f 字卡 TODAY(下划线) → 62f 硬切收尾全景定格。\n// UI 段全部带微动（推/移），字卡段除落定微缩(1.05→1, out-cubic)外全静，\n// 动↔静对比即手法本体。所有切换零过渡（条件挂载，无 crossfade）。\n// 收尾 62–105f 轻推收完，105–150f 真静止 45f ≥ 40f。帧确定，无随机。\nimport React from 'react';\nimport { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { FakeDashboard } from '../../_fixtures/Fixtures';\n\nconst CLAMP = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;\n\n// 黑底白字字卡：落定微缩 1.05→1（out-cubic，段内前 5f 完成），其余全静\nconst TitleCard: React.FC<{ text: string; local: number; underline?: boolean }> = ({\n  text,\n  local,\n  underline = false,\n}) => {\n  const scale = interpolate(local, [0, 5], [1.05, 1], {\n    ...CLAMP,\n    easing: Easing.out(Easing.cubic),\n  });\n  return (\n    <AbsoluteFill\n      style={{ background: '#0d0d0d', alignItems: 'center', justifyContent: 'center' }}\n    >\n      <div\n        style={{\n          transform: `scale(${scale})`,\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          gap: 26,\n        }}\n      >\n        <div\n          style={{\n            fontFamily: 'Helvetica, Arial, sans-serif',\n            fontWeight: 800,\n            fontSize: 170,\n            color: '#ffffff',\n            letterSpacing: 2,\n            lineHeight: 1,\n          }}\n        >\n          {text}\n        </div>\n        {underline && (\n          <div style={{ width: 560, height: 14, background: '#ffffff', borderRadius: 7 }} />\n        )}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// UI 镜头：包一层 transform 做缓推/缓移，硬切由外层条件挂载保证\nconst UiShot: React.FC<{\n  variant: 'A' | 'B';\n  transform: string;\n  origin?: string;\n}> = ({ variant, transform, origin = '50% 50%' }) => (\n  <AbsoluteFill style={{ overflow: 'hidden', background: '#ececea' }}>\n    <div style={{ width: 1920, height: 1080, transform, transformOrigin: origin }}>\n      <FakeDashboard variant={variant} />\n    </div>\n  </AbsoluteFill>\n);\n\nexport const CardFootageCadence: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // ---- 分段：切点 14 / 22 / 34 / 42 / 52 / 62，总长 150 ----\n\n  // 段1 0–14f：UI A 全景缓推 1.0→1.08\n  if (frame < 14) {\n    const s = interpolate(frame, [0, 14], [1, 1.08], CLAMP);\n    return <UiShot variant=\"A\" transform={`scale(${s})`} />;\n  }\n\n  // 段2 14–22f：字卡 SHIP\n  if (frame < 22) {\n    return <TitleCard text=\"SHIP\" local={frame - 14} />;\n  }\n\n  // 段3 22–34f：UI B 1.6x 裁切 + 横向缓移（120px 内容位移，屏上 ~192px）。\n  // origin 偏左让列表行图标入画，读得出是 UI（QA 后调：50%→35%）\n  if (frame < 34) {\n    const tx = interpolate(frame - 22, [0, 12], [60, -60], CLAMP);\n    return (\n      <UiShot variant=\"B\" transform={`scale(1.6) translateX(${tx}px)`} origin=\"35% 50%\" />\n    );\n  }\n\n  // 段4 34–42f：字卡 FASTER\n  if (frame < 42) {\n    return <TitleCard text=\"FASTER\" local={frame - 34} />;\n  }\n\n  // 段5 42–52f：UI A 另一处 2x 裁切（左上卡片区）+ 继续缓推 2.0→2.14\n  if (frame < 52) {\n    const s = interpolate(frame - 42, [0, 10], [2, 2.14], CLAMP);\n    return <UiShot variant=\"A\" transform={`scale(${s})`} origin=\"32% 30%\" />;\n  }\n\n  // 段6 52–62f：字卡 TODAY + 下划线\n  if (frame < 62) {\n    return <TitleCard text=\"TODAY\" local={frame - 52} underline />;\n  }\n\n  // 段7 62–150f：收尾全景 UI 定格。62–105f 极缓推收完（out-cubic），\n  // 105f 后 transform 值恒为 1.02，105–150f 真静止 45f。\n  const s = interpolate(frame, [62, 105], [1, 1.02], {\n    ...CLAMP,\n    easing: Easing.out(Easing.cubic),\n  });\n  return <UiShot variant=\"B\" transform={`scale(${s})`} />;\n};\n\n\n// ===== demos/rhythm/trailer-grammar-moves/SmashCut.tsx =====\n// smash-cut｜猛切（喧闹→死寂）\n// 0–41f 轰鸣段：5 张 Card 多方向高速飞掠冲镜（穿屏 translate + scale 1.5→3\n// 冲脸 + 微 rotate，各卡错峰跑两轮，第二轮更快=整体仍在加速），背景\n// FakeDashboard A 以 ease-in 持续加速推近 1→1.55 并滚动 rotate 1.8°——\n// 切点前一刻动势最猛。42f 一帧硬切 variant B 整齐静止全景，无一物在动，\n// 停满 93f（>50f）。反差即手法本体。总 135f。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, Card, FakeDashboard } from '../../_fixtures/Fixtures';\n\nconst CUT = 42; // 硬切帧：>=42 全静止\n\n// 每张飞卡：两轮 pass，第二轮更短（更快），ease-in 让每一程都在加速。\n// from/to 为屏幕外起终点（中心系坐标），飞行中 scale 1.5→3 冲脸。\ntype Fly = {\n  from: [number, number];\n  to: [number, number];\n  rot: [number, number]; // 微旋转 起→终\n  seed: number;\n  w: number;\n  h: number;\n};\n\nconst FLIES: Fly[] = [\n  { from: [-1400, -120], to: [1400, 60], rot: [-6, 5], seed: 1, w: 440, h: 290 },\n  { from: [1400, 180], to: [-1400, -100], rot: [7, -4], seed: 2, w: 400, h: 260 },\n  { from: [-300, -900], to: [200, 900], rot: [-3, 8], seed: 3, w: 460, h: 300 },\n  { from: [-1300, 800], to: [1300, -750], rot: [5, -7], seed: 4, w: 420, h: 280 },\n  { from: [1350, -780], to: [-1350, 820], rot: [-8, 4], seed: 5, w: 480, h: 310 },\n];\n\n// 卡 i 的第 k 轮：start = i*4 + k*20，时长 16 / 12（第二轮更快）。\n// card4 第二轮 36–48f 在切点 42f 被硬切截断——正中\"绝不减速迎接切点\"。\nconst passWindow = (i: number, k: number): [number, number] => {\n  const start = i * 4 + k * 20;\n  const dur = k === 0 ? 16 : 12;\n  return [start, start + dur];\n};\n\nconst FlyCard: React.FC<{ fly: Fly; i: number; frame: number }> = ({ fly, i, frame }) => {\n  // 找当前活跃的 pass（两轮）\n  let active: [number, number] | null = null;\n  for (let k = 0; k < 2; k++) {\n    const [s, e] = passWindow(i, k);\n    if (frame >= s && frame < e) { active = [s, e]; break; }\n  }\n  if (!active) return null;\n  const [s, e] = active;\n  // ease-in：全程加速，越接近终点越快\n  const p = interpolate(frame, [s, e], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.quad),\n  });\n  const x = fly.from[0] + (fly.to[0] - fly.from[0]) * p;\n  const y = fly.from[1] + (fly.to[1] - fly.from[1]) * p;\n  const rot = fly.rot[0] + (fly.rot[1] - fly.rot[0]) * p;\n  const scale = 1.5 + 1.5 * p; // 1.5 → 3 冲脸\n  // 速度门控方向模糊感：ease-in 下瞬时速度 ∝ p，速度越快越糊\n  const blur = 1 + 4 * p;\n  return (\n    <div style={{\n      position: 'absolute', left: 960 - fly.w / 2, top: 540 - fly.h / 2,\n      transform: `translate(${x}px, ${y}px) rotate(${rot}deg) scale(${scale})`,\n      filter: `blur(${blur}px)`,\n    }}>\n      <Card w={fly.w} h={fly.h} seed={fly.seed}\n        style={{ boxShadow: '0 12px 40px rgba(0,0,0,0.25)' }} />\n    </div>\n  );\n};\n\nexport const SmashCut: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // —— 死寂段：42f 起 variant B 整齐静止全景，无任何动画属性 ——\n  if (frame >= CUT) {\n    return <FakeDashboard variant=\"B\" />;\n  }\n\n  // —— 轰鸣段：背景 ease-in 加速推近 + 滚动，切点前 3f 仍在加速 ——\n  const bgScale = interpolate(frame, [0, CUT], [1, 1.55], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.quad),\n  });\n  const bgRot = interpolate(frame, [0, CUT], [0, 1.8], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.quad),\n  });\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, overflow: 'hidden', position: 'relative' }}>\n      <div style={{\n        width: 1920, height: 1080,\n        transform: `scale(${bgScale}) rotate(${bgRot}deg)`,\n        transformOrigin: '50% 50%',\n        filter: 'blur(1.5px)', // 背景轻糊，衬前景飞卡\n      }}>\n        <FakeDashboard variant=\"A\" />\n      </div>\n      {FLIES.map((fly, i) => (\n        <FlyCard key={i} fly={fly} i={i} frame={frame} />\n      ))}\n    </div>\n  );\n};\n\n\n// ===== demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx =====\n// trailer-bumper｜前置速剪预告\n// 结构即全部：三连 9f 速剪（A 整页 / B 整页 / A 2.2x 怼脸特写）→ 6f 纯黑喘息\n// → 正式开场大标题淡入+微升 16f 定格。每个速剪镜头内部带 1→1.04 轻推近。\n// 收尾真静止：49f 后所有动画结束，静止 91f（>40f）。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, FakeDashboard } from '../../_fixtures/Fixtures';\n\n// 时间轴（帧）\nconst CUT_1 = 0;   // 镜头1：variant A 整页\nconst CUT_2 = 9;   // 镜头2：variant B 整页\nconst CUT_3 = 18;  // 镜头3：variant A 2.2x 怼脸特写\nconst BLACK = 27;  // 纯黑静默\nconst TITLE = 33;  // 正式开场\nconst TITLE_IN = 16; // 标题淡入+微升时长\n\n// 速剪镜头内部轻推近：镜头内 local 0→9f，scale 1→1.04（线性，活镜头感）\nconst push = (frame: number, start: number) =>\n  interpolate(frame, [start, start + 9], [1, 1.04], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\nexport const TrailerBumper: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // —— 段落 1–3：三连速剪 ——\n  if (frame < BLACK) {\n    let inner: React.ReactNode;\n    if (frame < CUT_2) {\n      // 镜头1：A 整页 + 轻推近\n      inner = (\n        <div style={{ width: 1920, height: 1080, transform: `scale(${push(frame, CUT_1)})`, transformOrigin: '50% 50%' }}>\n          <FakeDashboard variant=\"A\" />\n        </div>\n      );\n    } else if (frame < CUT_3) {\n      // 镜头2：B 整页 + 轻推近\n      inner = (\n        <div style={{ width: 1920, height: 1080, transform: `scale(${push(frame, CUT_2)})`, transformOrigin: '50% 50%' }}>\n          <FakeDashboard variant=\"B\" />\n        </div>\n      );\n    } else {\n      // 镜头3：A 的 2.2x 放大裁切怼脸特写（对准中列卡片），叠加同样的轻推近\n      const s = 2.2 * push(frame, CUT_3);\n      inner = (\n        <div style={{ width: 1920, height: 1080, transform: `scale(${s})`, transformOrigin: '58% 40%' }}>\n          <FakeDashboard variant=\"A\" />\n        </div>\n      );\n    }\n    return (\n      <div style={{ width: 1920, height: 1080, overflow: 'hidden', background: G.bg }}>\n        {inner}\n      </div>\n    );\n  }\n\n  // —— 段落 4：纯黑静默 6f，必须纯黑无物 ——\n  if (frame < TITLE) {\n    return <div style={{ width: 1920, height: 1080, background: '#000000' }} />;\n  }\n\n  // —— 段落 5：正式开场——浅底大标题淡入+微升 16f 后定格 ——\n  const opacity = interpolate(frame, [TITLE, TITLE + TITLE_IN], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const rise = interpolate(frame, [TITLE, TITLE + TITLE_IN], [44, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n  return (\n    <div\n      style={{\n        width: 1920,\n        height: 1080,\n        background: G.bg,\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n        overflow: 'hidden',\n      }}\n    >\n      <div\n        style={{\n          opacity,\n          transform: `translateY(${rise}px)`,\n          fontFamily: 'Helvetica, Arial, sans-serif',\n          fontWeight: 800,\n          fontSize: 168,\n          color: G.ink,\n          letterSpacing: -2,\n          textAlign: 'center',\n        }}\n      >\n        THE LAUNCH\n      </div>\n    </div>\n  );\n};\n\n\n"
}