{
  "slug": "input-trigger-moves",
  "title": "Input trigger moves",
  "tagline": "Two input-trigger styles: cursor-performance, a cursor performance push-in click, and keycap-smash-cut, a keycap-fuse detonation hard cut.",
  "description": "Two input-trigger styles: cursor-performance, a cursor performance push-in click, and keycap-smash-cut, a keycap-fuse detonation hard cut.",
  "category": "interaction",
  "tags": [
    "shotcraft",
    "remotion",
    "interaction",
    "opening"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.06,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/input-trigger-moves/poster-f308a0d8a879.jpg",
    "video": "https://gimgs.net/motion/media/input-trigger-moves/video-e5404b536f2b.mp4"
  },
  "variants": [
    {
      "key": "cursor-performance",
      "label": "cursor-performance · Cursor performance click",
      "description": "The cursor slides in along three bezier arcs with a wrist-flick overshoot at the end, brightening on hover; on the click frame the button depresses plus a ripple, the camera pushes in 1.4x, holds two beats, then eases back out.",
      "use": "A demo close-up of a core button/interaction",
      "video": "https://gimgs.net/motion/media/input-trigger-moves/video-e5404b536f2b.mp4",
      "poster": "https://gimgs.net/motion/media/input-trigger-moves/poster-f308a0d8a879.jpg"
    },
    {
      "key": "keycap-smash-cut",
      "label": "keycap-smash-cut · Keycap fuse smash cut",
      "description": "Keycap hovers breathing → 3f squash with a bright ring as the fuse → 30f of cards rushing at the camera from all sides, accelerating with a roar → the most violent-motion frame hard-cuts to a still full view, the keycap embedded in the top bar.",
      "use": "The film's opening verdict; the highest energy",
      "video": "https://gimgs.net/motion/media/input-trigger-moves/video-5e1b87048b06.mp4",
      "poster": "https://gimgs.net/motion/media/input-trigger-moves/poster-keycap-smash-cut-2babef9bb4fb.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/input-trigger-moves.html",
  "json_url": "https://gimgs.net/motion/input-trigger-moves.json",
  "template_url": "https://gimgs.net/motion/input-trigger-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/input-trigger-moves.md",
  "updated_at": "2026-09-24T07:40:03.007Z",
  "prompt": "## When to use\n\nFirst-person segments of a launch film: demoing the core interaction, the opening as the climax; the audience is \"using\" the product, not \"watching\" it.\n\n## Intention\n\nThe first-person grammar of a launch film — the audience isn't \"watching\" the product, they're \"using\" it: the cursor is the hand, the keycap is the fingertip, the trigger itself is the narrative. Both styles share one thing: **the interaction is the trigger, the product visuals are the gunshot**. A is a slow trigger — a cursor with visible personality glides in and clicks, the camera pushes in toward the click point then eases back out, a round trip (unlike crash-zoom: this is slow, anchored to the cursor, and comes back); C is the fuse — a fake-3D keycap hovers breathing, squashes with a bright rim overflow as the fuse, and pressing it detonates a full-screen roar, the most climactic frame hard-cutting into a perfectly still full view — press → burst → verdict. Pick A to demo an interaction, C for an opening that's already the climax.\n\n## Duration & energy\n\n- Duration: A: ~5s / C: ~5s\n- Energy: A: Medium / C: High\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n发布片的第一人称语法——观众不是在\"看\"产品，是在\"用\"产品：光标\n是手、键帽是指尖、触发即叙事。两式共同点：**交互动作当扳机，产品\n画面当枪响**。A 是慢扳机——放大光标带性格地滑入点击，镜头以点击点\n推近再缓退，有去有回（区别 crash-zoom：慢速、锚定光标、会回来）；\nC 是引信——假 3D 键帽（⌘K）呼吸悬浮，3f 压扁+底部亮环溢出当引信，\n按下引爆 30f 满屏轰鸣，最高潮一帧猛切成整齐静止全景，\n按下→爆发→定论。选型：演示交互用 A，开场即高潮用 C。\n\n## 两式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A cursor-performance | 光标三次贝塞尔弧线滑入+末端甩腕过冲，悬停提亮，点击帧按钮下陷+涟漪+推近 1.4x 停两拍缓退 | 核心按钮/交互的演示特写 |\n| C keycap-smash-cut | 键帽呼吸悬浮→3f 压扁+亮环引信 + 30f 卡片四面冲镜持续加速轰鸣 + 动势最猛一帧硬切静止全景、键帽嵌顶栏 | 全片开场定论；能量最高 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A 甩腕 | 贝塞尔 t 到 f24 过冲 1.05 再 6f 拐回 | 无过冲的直线滑入没有\"性格\" |\n| A 悬停提亮 | 深底 #2f2f2f→#555553（4f）+ scale 1.05 | 白底提亮不可见（提亮判例），深色底才成立 |\n| A 涟漪 | 直径 60→380 out-cubic 22f 扩散 / opacity 0.9→0 线性 26f 消散，解耦；f66 条件卸载 | 扩散消散同曲线读作一坨淡出 |\n| A 推近 | f40–52 scale 1→1.4（与点击同帧起跳）→停 20f→18f inOut 退回；origin 钉点击点 | 光标与按钮必须同缩放层，否则推近漂移 |\n| C 引信压扁 | box-shadow 厚度 12→3px、3f 完成 + 下沉 14px + 键面变暗；亮环 out-cubic 90→560px 扩散、线性消散解耦 | 厚度被压扁的 3f 是灵魂——命门；无厚度差只是变色 |\n| C 轰鸣 | 28–58f 六物两轮 pass（第二轮更短）Easing.in(quad) 加速 + scale 1.5→3 + blur 1→5px 速度门控 | 全程必须一直在加速，匀速轰鸣泄劲 |\n| C 猛切 | f58 一帧 return 静止 JSX，键帽 44px 嵌顶栏，死寂 ≥82f | 切后画面不能有任何随帧属性 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 全片输入触发 ≤2 次——每次都该是段落级开关；连按三次键帽\n  观众就知道这只是转场花活，扳机失效\n- A/C 声音强依赖：click 采样/键击采样与触发帧同帧\n  （sound-design §4.5）；无声的按下读作画面自己动了\n- C 的猛切后键帽必须在新场景里回收（嵌顶栏呼应引信）——否则\n  只是两个手法排队，不是组合——组合命门\n- C 的轰鸣段与 smash-cut 卡（trailer-grammar-moves C 式）同源，\n  同片二选一\n\n## 参考实现\ndemos/interaction/input-trigger-moves/\n（CursorPerformancePunchIn.tsx / KeycapSmashCut.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/interaction/input-trigger-moves/CursorPerformancePunchIn.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/input-trigger-moves/CursorPerformancePunchIn.tsx\n- demos/interaction/input-trigger-moves/KeycapSmashCut.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/input-trigger-moves/KeycapSmashCut.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/interaction/input-trigger-moves/CursorPerformancePunchIn.tsx, demos/interaction/input-trigger-moves/KeycapSmashCut.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/interaction/input-trigger-moves/CursorPerformancePunchIn.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/input-trigger-moves/CursorPerformancePunchIn.tsx"
    },
    {
      "path": "demos/interaction/input-trigger-moves/KeycapSmashCut.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/input-trigger-moves/KeycapSmashCut.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": 152,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/input-trigger-moves/CursorPerformancePunchIn.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/input-trigger-moves/KeycapSmashCut.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/interaction/input-trigger-moves/CursorPerformancePunchIn.tsx =====\n// cursor-performance-punch-in —— 光标拟人表演 + 点击跟随推近\n// 放大光标沿贝塞尔曲线从左下滑入（末端甩腕过冲）→ 悬停按钮微亮响应 →\n// 点击：按钮下陷 + 涟漪扩散 + 整画布以点击点为原点推近 1→1.4 → 停 → 缓退回。\n// 推近\"有去有回\"区别 crash-zoom。收尾真静止 ≥35f。全灰阶。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { FakeDashboard, G } from '../../_fixtures/Fixtures';\n\n// 时间轴（30fps，共 150f）\nconst T = {\n  cursorInEnd: 30, // 0–30f 光标贝塞尔滑入（f24 过冲峰值，f24–30 拐回落定）\n  click: 40,       // 30–40f 悬停响应 10f；f40 点击\n  punchEnd: 52,    // 40–52f 推近 1→1.4（out-cubic）\n  holdEnd: 72,     // 52–72f 停 20f\n  backEnd: 90,     // 72–90f 缓退回 1.0（inOut-cubic）\n  total: 150,      // 90–150f 真静止 60f\n};\n\n// 按钮与点击点（画布坐标）\nconst BTN = { x: 1560, y: 130, w: 200, h: 64 };\nconst CLICK = { x: 1665, y: 168 };\n\n// 光标贝塞尔路径：左下滑入，先沉后扬，曲线有性格\nconst P0 = { x: 180, y: 1000 };\nconst P1 = { x: 820, y: 1075 };\nconst P2 = { x: 1795, y: 560 };\nconst P3 = { x: CLICK.x, y: CLICK.y };\nconst bez = (t: number) => {\n  const u = 1 - t;\n  return {\n    x: u * u * u * P0.x + 3 * u * u * t * P1.x + 3 * u * t * t * P2.x + t * t * t * P3.x,\n    y: u * u * u * P0.y + 3 * u * u * t * P1.y + 3 * u * t * t * P2.y + t * t * t * P3.y,\n  };\n};\n\nconst Cursor: React.FC<{ x: number; y: number }> = ({ x, y }) => (\n  <svg\n    width={48}\n    height={48}\n    viewBox=\"0 0 28 28\"\n    style={{\n      position: 'absolute',\n      left: x - 3.4,\n      top: y - 1.7,\n      filter: 'drop-shadow(0 4px 7px rgba(0,0,0,0.4))',\n    }}\n  >\n    <path\n      d=\"M2 1 L2 23 L8 17.5 L11.5 25 L15.5 23.2 L12 15.8 L20 15 Z\"\n      fill=\"#ffffff\"\n      stroke=\"#2f2f2f\"\n      strokeWidth={1.6}\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n);\n\nexport const CursorPerformancePunchIn: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // —— 光标：路径参数 t，f24 过冲到 1.05（沿切线甩过按钮），f24–30 拐回 1.0 ——\n  const t =\n    frame < 24\n      ? interpolate(frame, [0, 24], [0, 1.05], {\n          extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n          easing: Easing.out(Easing.cubic),\n        })\n      : interpolate(frame, [24, T.cursorInEnd], [1.05, 1], {\n          extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n          easing: Easing.inOut(Easing.quad),\n        });\n  const cur = bez(t);\n  // 点击帧光标随按钮微沉 3px（f40–42 下，f42–46 回）\n  const dip = interpolate(frame, [T.click, T.click + 2, T.click + 6], [0, 3, 0], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  // —— 悬停响应：f30–34 按钮提亮（深底提亮可见）+ 微放大 ——\n  const lift = interpolate(frame, [T.cursorInEnd, T.cursorInEnd + 4], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  const c = Math.round(47 + 38 * lift); // #2f2f2f → #555553 近似\n  const hoverScale = 1 + 0.05 * lift;\n\n  // —— 点击下陷：f40–42 scale→0.94，f42–46 弹回 ——\n  const press = interpolate(frame, [T.click, T.click + 2, T.click + 6], [1, 0.94, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  // —— 推近有去有回：40–52f 1→1.4（out-cubic）；72–90f 1.4→1（inOut）——\n  const zoom =\n    frame < T.holdEnd\n      ? interpolate(frame, [T.click, T.punchEnd], [1, 1.4], {\n          extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n          easing: Easing.out(Easing.cubic),\n        })\n      : interpolate(frame, [T.holdEnd, T.backEnd], [1.4, 1], {\n          extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n          easing: Easing.inOut(Easing.cubic),\n        });\n\n  // —— 涟漪：扩散 out-cubic（40–62f，直径 60→380），消散线性（40–66f）帧时间解耦 ——\n  const rippleAlive = frame >= T.click && frame < T.click + 26;\n  const rippleD = interpolate(frame, [T.click, T.click + 22], [60, 380], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n  const rippleOp = interpolate(frame, [T.click, T.click + 26], [0.9, 0], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  const rippleBw = interpolate(frame, [T.click, T.click + 22], [9, 3], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, overflow: 'hidden', position: 'relative' }}>\n      {/* 整画布以点击点为原点推近 */}\n      <div style={{\n        position: 'absolute', inset: 0,\n        transform: `scale(${zoom})`,\n        transformOrigin: `${CLICK.x}px ${CLICK.y}px`,\n      }}>\n        <FakeDashboard variant=\"A\" />\n\n        {/* 右上自绘 Deploy 按钮（叠在页面上） */}\n        <div style={{\n          position: 'absolute', left: BTN.x, top: BTN.y, width: BTN.w, height: BTN.h,\n          borderRadius: 14, background: `rgb(${c},${c},${c - 2})`,\n          boxShadow: '0 6px 18px rgba(0,0,0,0.28)',\n          display: 'flex', alignItems: 'center', justifyContent: 'center',\n          transform: `scale(${hoverScale * press})`,\n          fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 700,\n          fontSize: 27, color: '#ffffff', letterSpacing: 0.5,\n        }}>\n          Deploy\n        </div>\n\n        {/* 涟漪圆环：条件挂载，f66 后摘除 */}\n        {rippleAlive && (\n          <div style={{\n            position: 'absolute',\n            left: CLICK.x - rippleD / 2, top: CLICK.y - rippleD / 2,\n            width: rippleD, height: rippleD, borderRadius: '50%',\n            border: `${rippleBw}px solid ${G.ink}`,\n            opacity: rippleOp,\n          }} />\n        )}\n\n        {/* 放大光标（随画布一起被推近，钉在按钮上） */}\n        <Cursor x={cur.x} y={cur.y + dip} />\n      </div>\n    </div>\n  );\n};\n\n\n// ===== demos/interaction/input-trigger-moves/KeycapSmashCut.tsx =====\n// keycap-smash-cut｜键帽引信+猛切句号（keycap-press-trigger × smash-cut）\n// 0–25f 浅底中央 3D 键帽（⌘⏎）悬浮呼吸 → 25–28f 砰按下（3f 下沉 14px +\n// 底座 12→3px 压扁 + 键面变暗）+ 底部亮环（扩散 out-cubic / 消散线性解耦）\n// → 按下即引爆轰鸣段 28–58f：5 张卡 + 1 面板从四面八方高速飞入冲镜\n// （ease-in 全程加速 + scale 1.5→3 冲脸 + 速度门控 blur），背景整体\n// ease-in 推近 1→1.3 + 滚动 1.5°——切点前一刻动势最猛 → 58f 一帧猛切：\n// FakeDashboard variant=\"A\" 整齐静止全景，键帽已缩成小元素稳稳嵌在\n// 顶栏角落（引信呼应），死寂 82f（>50f）。总 140f。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, Card, FakeDashboard } from '../../_fixtures/Fixtures';\n\nconst T = {\n  press: 25,      // 按下起始（呼吸周期 25f，此帧位移恰好归零）\n  pressEnd: 28,   // 3f 压到底，同帧引爆轰鸣段\n  ringGrowEnd: 46, // 亮环扩散结束（out-cubic）\n  ringFadeEnd: 52, // 亮环消散结束（线性，帧时间与扩散解耦）\n  cut: 58,        // 猛切帧：>=58 全静止\n  total: 140,     // 死寂 82f\n};\n\n// —— 键帽（复用 keycap 规格：多层 box-shadow 造厚度）——\nconst Keycap: React.FC<{ size: number; thick: number; bright: number; label?: string }> = ({\n  size, thick, bright, label = '⌘⏎',\n}) => (\n  <div style={{\n    width: size, height: size, borderRadius: size * 0.117,\n    background: 'linear-gradient(165deg, #ffffff 0%, #f2f2f0 45%, #e2e2e0 100%)',\n    border: `2px solid ${G.border}`, boxSizing: 'border-box',\n    boxShadow: [\n      `0 ${thick}px 0 #6e6e6c`,\n      `0 ${thick + 4}px 2px #565654`,\n      `0 ${thick * 2 + 10}px ${thick * 2 + 14}px rgba(0,0,0,0.28)`,\n      'inset 0 3px 0 rgba(255,255,255,0.9)',\n      'inset 0 -6px 10px rgba(0,0,0,0.07)',\n    ].join(', '),\n    filter: `brightness(${bright})`,\n    display: 'flex', alignItems: 'center', justifyContent: 'center',\n  }}>\n    <div style={{\n      fontSize: size * 0.3, fontWeight: 700, color: G.ink,\n      fontFamily: 'Helvetica, Arial, sans-serif',\n    }}>\n      {label}\n    </div>\n  </div>\n);\n\n// —— 轰鸣段飞入物：5 张卡 + 1 面板，四面八方冲镜 ——\ntype Fly = {\n  from: [number, number];\n  to: [number, number];\n  rot: [number, number];\n  seed: number;\n  w: number;\n  h: number;\n  panel?: boolean;\n};\n\nconst FLIES: Fly[] = [\n  { from: [-1400, -140], to: [1400, 80], rot: [-6, 5], seed: 1, w: 440, h: 290 },\n  { from: [1400, 200], to: [-1400, -120], rot: [7, -4], seed: 2, w: 400, h: 260 },\n  { from: [-260, -900], to: [180, 900], rot: [-3, 8], seed: 3, w: 460, h: 300 },\n  { from: [-1300, 820], to: [1300, -760], rot: [5, -7], seed: 4, w: 420, h: 280 },\n  { from: [1350, -800], to: [-1350, 840], rot: [-8, 4], seed: 5, w: 480, h: 310 },\n  // 第 6 位：面板（更大更重，斜穿）\n  { from: [1500, 700], to: [-1500, -600], rot: [4, -6], seed: 6, w: 700, h: 430, panel: true },\n];\n\n// 卡 i 的第 k 轮：start = 28 + i*3 + k*16，时长 15 / 11（第二轮更快=整体加速）。\n// i=3 k=1 的 53–64f 在切点 58f 被硬切截断——绝不减速迎接切点。\nconst passWindow = (i: number, k: number): [number, number] => {\n  const start = T.pressEnd + i * 3 + k * 16;\n  const dur = k === 0 ? 15 : 11;\n  return [start, start + dur];\n};\n\n// 假面板飞入物：命令面板式灰阶块\nconst FlyPanel: React.FC<{ w: number; h: number }> = ({ w, h }) => (\n  <div style={{\n    width: w, height: h, background: G.card, border: `2px solid ${G.border}`,\n    borderRadius: 20, padding: 26, boxSizing: 'border-box',\n    display: 'flex', flexDirection: 'column', gap: 16,\n    boxShadow: '0 16px 44px rgba(0,0,0,0.28)',\n  }}>\n    <div style={{ height: 52, border: `2px solid ${G.line}`, borderRadius: 12, background: G.panel }} />\n    {Array.from({ length: 3 }).map((_, i) => (\n      <div key={i} style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 16 }}>\n        <div style={{ width: 28, height: 28, borderRadius: 8, background: G.mid }} />\n        <div style={{ height: 12, width: `${50 + ((i * 27) % 30)}%`, background: G.line, borderRadius: 6 }} />\n      </div>\n    ))}\n  </div>\n);\n\nconst FlyItem: React.FC<{ fly: Fly; i: number; frame: number }> = ({ fly, i, frame }) => {\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; // 冲脸\n  const blur = 1 + 4 * p;      // 速度门控方向糊感（ease-in 下速度 ∝ 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      {fly.panel\n        ? <FlyPanel w={fly.w} h={fly.h} />\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 KeycapSmashCut: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // —— 死寂段：58f 起 variant A 整齐静止全景 + 键帽小元素嵌顶栏角落，\n  //    无任何随帧变化的属性 ——\n  if (frame >= T.cut) {\n    return (\n      <div style={{ width: 1920, height: 1080, position: 'relative' }}>\n        <FakeDashboard variant=\"A\" />\n        {/* 键帽句号：稳稳嵌在顶栏（标题条右侧空档），呼应引信 */}\n        <div style={{ position: 'absolute', left: 464, top: 12 }}>\n          <Keycap size={44} thick={4} bright={1} />\n        </div>\n      </div>\n    );\n  }\n\n  // —— 引信段键帽运动 ——\n  // 呼吸：0–25f 一个完整正弦周期（±5px），f25 位移归零接按下\n  const breathY = frame < T.press ? -5 * Math.sin((2 * Math.PI * frame) / T.press) : 0;\n  // 按下：3f 下沉 14px，之后保持压死\n  const pressY = interpolate(frame, [T.press, T.pressEnd], [0, 14], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  // 底座厚度 12→3px 压扁（灵魂），轰鸣段保持压扁\n  const thick = interpolate(frame, [T.press, T.pressEnd], [12, 3], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  const bright = interpolate(frame, [T.press, T.pressEnd], [1, 0.9], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  // —— 亮环：扩散 out-cubic 90→640px，消散线性，帧时间解耦，摘罩=条件挂载 ——\n  const ringOn = frame >= T.press && frame < T.ringFadeEnd;\n  const ringD = interpolate(frame, [T.press, T.ringGrowEnd], [90, 640], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n  const ringA = interpolate(frame, [T.press, T.ringFadeEnd], [0.9, 0], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  const ringBorder = interpolate(frame, [T.press, T.ringGrowEnd], [14, 3], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n\n  // —— 轰鸣段背景动势：整体 ease-in 推近 + 滚动，切点前仍在加速 ——\n  const bgScale = interpolate(frame, [T.pressEnd, T.cut], [1, 1.3], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.quad),\n  });\n  const bgRot = interpolate(frame, [T.pressEnd, T.cut], [0, 1.5], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.quad),\n  });\n\n  return (\n    <div style={{\n      width: 1920, height: 1080, background: G.bg,\n      position: 'relative', overflow: 'hidden',\n    }}>\n      {/* 背景层（浅底 + 键帽 + 亮环），轰鸣段整体加速推近 */}\n      <div style={{\n        width: 1920, height: 1080, position: 'absolute',\n        transform: `scale(${bgScale}) rotate(${bgRot}deg)`,\n        transformOrigin: '50% 50%', background: G.bg,\n      }}>\n        {ringOn && (\n          <div style={{\n            position: 'absolute', left: 960 - ringD / 2, top: 540 + 120 + pressY - ringD / 2,\n            width: ringD, height: ringD, borderRadius: '50%',\n            border: `${ringBorder}px solid #4a4a48`, opacity: ringA,\n            boxSizing: 'border-box',\n          }} />\n        )}\n        <div style={{\n          position: 'absolute', left: 960 - 120, top: 540 - 120,\n          transform: `translateY(${breathY + pressY}px)`,\n        }}>\n          <Keycap size={240} thick={thick} bright={bright} />\n        </div>\n      </div>\n\n      {/* 轰鸣段飞入物 */}\n      {frame >= T.pressEnd && FLIES.map((fly, i) => (\n        <FlyItem key={i} fly={fly} i={i} frame={frame} />\n      ))}\n    </div>\n  );\n};\n\n\n"
}