{
  "slug": "transition-hidden-cut",
  "title": "Hidden-cut transitions",
  "tagline": "Three hidden-cut transitions — foreground occlusion invisible cut, versus-slam opener, and warm light-leak burn; the hard cut hides inside 1-3 frames of occlusion/impact/light peak, so the viewer never sees the scissors.",
  "description": "Three hidden-cut transitions — foreground occlusion invisible cut, versus-slam opener, and warm light-leak burn; the hard cut hides inside 1-3 frames of occlu",
  "category": "transition",
  "tags": [
    "shotcraft",
    "remotion",
    "transition"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.05,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/transition-hidden-cut/poster-2dee76e55f4e.jpg",
    "video": "https://gimgs.net/motion/media/transition-hidden-cut/video-2451252d4c20.mp4"
  },
  "variants": [
    {
      "key": "invisible-cut",
      "label": "invisible-cut · Foreground occlusion invisible cut",
      "description": "An oversized card with heavy motion blur sweeps across the frame close to the lens; within the occluding frame that fills the whole screen, background A hard-cuts to B, then the card flies off and the viewer thinks it was still one continuous shot",
      "use": "For a page-to-page scene change meant to feel completely seamless; the main workhorse for a fake \"one continuous take\"",
      "video": "https://gimgs.net/motion/media/transition-hidden-cut/video-2451252d4c20.mp4",
      "poster": "https://gimgs.net/motion/media/transition-hidden-cut/poster-2dee76e55f4e.jpg"
    },
    {
      "key": "versus-slam",
      "label": "versus-slam · Versus slam opener",
      "description": "The left and right half-screens, each with a diagonal cut edge, accelerate in from off-screen and slam together; the impact frame carries a white flash, screen shake, and a VS stamp — the cut point is the impact itself",
      "use": "A comparison/showdown-meaning opener or chapter head (old vs. new approach, two products, before/after)",
      "video": "https://gimgs.net/motion/media/transition-hidden-cut/video-dbefa884a195.mp4",
      "poster": "https://gimgs.net/motion/media/transition-hidden-cut/poster-versus-slam-834ea1ca5896.jpg"
    },
    {
      "key": "light-leak-burn",
      "label": "light-leak-burn · Amber light-leak burn",
      "description": "Three amber soft-light blooms sweep diagonally across the frame; at the light's peak frame, when it's swallowed about 70% of the old page, it hard-cuts to the new page, which is already in place by the time the light fades",
      "use": "A warm-toned/film-grain-flavored chapter transition; softer, more directional, and warmer than a white flash",
      "video": "https://gimgs.net/motion/media/transition-hidden-cut/video-0c391e004eef.mp4",
      "poster": "https://gimgs.net/motion/media/transition-hidden-cut/poster-light-leak-burn-8bc1d6fd2f05.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/transition-hidden-cut.html",
  "json_url": "https://gimgs.net/motion/transition-hidden-cut.json",
  "template_url": "https://gimgs.net/motion/transition-hidden-cut/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/transition-hidden-cut.md",
  "updated_at": "2026-09-24T07:40:30.129Z",
  "prompt": "## When to use\n\nWhen two shots need a \"seamless scene change\" or a \"ceremonial reveal\" at the join (a technique card, chosen from the same tier as the six shot-transitions moves)\n\n## Intention\n\nSame family as shot-transitions style A, flash-cut: flash-cut covers a hard cut with one layer of white flash; these three swap in three different \"decoys\" — a sweeping foreground card (A), a collision-impact frame (B), a light leak climbing to its peak (C). The shared principle is magician's misdirection: the viewer's eyes get pulled away by a big motion/bright flash for those 1-3 frames while the hard cut happens, and when they refocus the scene has already changed, all \"without ever seeing the scissors.\" flash-cut is the plainest version; pick A for a sense of direction, B for ceremony, C for warmth.\n\n## Duration & energy\n\n- Duration: n/a (a technique card; each style's frame count is in its parameter table, drawn from the neighboring shots' budget)\n- Energy: n/a (a technique card, doesn't occupy an energy slot)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n与 shot-transitions A 式 flash-cut 同族：flash-cut 用一层白闪盖住硬切，\n这三式换了三种\"障眼物\"——扫过的前景卡片（A）、对撞的撞击帧（B）、\n爬到顶峰的漏光（C）。共同原理是魔术误导：观众的眼睛被大动作/强光\n吸走的那 1-3 帧里完成硬切，回过神来景已经换了，全程\"看不见剪刀\"。\nflash-cut 是最素的一款；要方向感选 A，要仪式感选 B，要温度选 C。\n\n## 三式选型\n| 式 | 做法 | 适用接缝 |\n|----|------|----------|\n| A invisible-cut 前景遮挡隐形切 | 一张超画幅卡片带重运动模糊贴脸横扫，糊满全屏的遮挡帧内背景 A→B 硬切，卡片飞出观众以为还是同一镜 | 想让换景完全无痕的页面→页面；伪\"一条 take\"的主力款 |\n| B versus-slam 对撞开屏 | 左右两半屏带斜切边从画外加速对冲撞合，撞击帧白闪+震屏+VS 盖章，切点就是撞击本身 | 对比/对阵语义的开屏或章节头（新旧方案、双产品、before/after）|\n| C light-leak-burn 暖色漏光 | 三团暖色柔光沿对角线斜扫，光峰帧吞掉旧页约七成时硬切新页，光退散时新页已就位 | 想要暖调/胶片味的章节过渡；比白闪柔、有方向、有温度 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A 横扫 | 卡片 1600x1000 再 scale(1.6)，x 从 -2200→2600 共 14f，bezier(0.3,0,0.7,1)；切点在扫掠中点全遮帧（demo 里 f47） | 中点必须覆盖 -280..2280 糊满 1920 画幅；两端完全出画 |\n| A 糊感 | 全场包 `<CameraMotionBlur shutterAngle={300} samples={12}>` + 卡片自身 blur(8px)/skewX(-v·0.018°) + 4 层手动残影（opacity 0.35→0.07、blur 14px、间隔 0.55f） | 三层糊叠出\"贴脸呼啸\"；只靠 CameraMotionBlur 糊不满遮挡窗口 |\n| A 带风推挤 | 背景 ±40px：切前 A 被拖向扫掠方向（ease-in），切后 B 从反侧 13f 回稳（ease-out） | 卖\"同一镜被风带了一下\"的错觉，是无痕感的一半功劳 |\n| B 斜缝几何 | clip-path polygon 斜边约 78°（缝顶 x=1075→缝底 x=845）；两半屏从 ±1200px 以 ease-in(cubic) 10f 对冲到位 | ease-in 加速才有\"砸\"感；垂直缝读作 PPT 分屏 |\n| B 撞击三件套 | 撞击帧同发：白闪 0.9→0 共 3f + 整机 shake 12px·e^(−t/1.6) 约 5f 收干 + VS 字块 scale 1.6→1 back(2.6) overshoot 6f 压出 | 三件必须同帧起跑，错开一帧就散；震屏幅度须过肉眼阈值（可感性判例）|\n| C 光层 | 3 团径向渐变（#f6c878/#e8a44a/#d98a2b，直径 1500/1950/2400px，blur 90px，mixBlendMode screen）沿右上→左下扫约 70f | 光团偏移沿扫掠方向拖尾排布，避免完美同心的\"手电筒感\" |\n| C 光强包络 | ease-in 爬升 27f（蓄力）→ 峰值帧藏切 → ease-out 收敛 43f（余温）；峰值叠 8% 全屏暖罩 + 近白热核（opacity=intensity²） | 爬升慢收敛快会读作故障闪光；热核只在临近峰值烧起来 |\n| C 冲淡 | 峰值时页面 filter：contrast 降 45%、brightness 抬 35% | 这是\"烧穿\"感的来源——光只叠不冲淡，读作贴了张贴纸 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- C 式是 Q4 光效判例解封后首个入库光效手法——严守 Q4 三约束：\n  单点使用（一支片一处，不群发）、正常速度回放自检好看才算过、\n  光效服务切点而非装饰；与 flash-cut 同片混用算两次光效藏切\n- A 式遮挡物必须真的全遮：demo 里 1600x1000 的卡要 scale(1.6) 才盖满\n  1920 画幅——换素材尺寸后先逐帧检查切点帧有没有露出背景边缘，\n  露一条缝整个魔术就穿帮\n- B 式 VS 字块语义仅适配\"对比/对阵\"内容——没有两方对峙关系时\n  别当通用转场用；去掉 VS 只留对撞则退化为一款斜缝 wipe，语义要求同降\n- 转场不叠加（规矩同 shot-transitions）：一个接缝只用一式；\n  这三式与六式同层选型，同片同类接缝别既用 flash-cut 又用 C 式\n- 分镜表阶段就逐接缝标注（写进\"关键动效\"列），帧预算从相邻镜头划（R3）\n\n## 参考实现\ndemos/transition/transition-hidden-cut/\n（InvisibleCut.tsx / LightLeakBurn.tsx / VersusSlam.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/transition/transition-hidden-cut/InvisibleCut.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-hidden-cut/InvisibleCut.tsx\n- demos/transition/transition-hidden-cut/LightLeakBurn.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-hidden-cut/LightLeakBurn.tsx\n- demos/transition/transition-hidden-cut/VersusSlam.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-hidden-cut/VersusSlam.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/transition/transition-hidden-cut/InvisibleCut.tsx, demos/transition/transition-hidden-cut/LightLeakBurn.tsx, demos/transition/transition-hidden-cut/VersusSlam.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/transition/transition-hidden-cut/InvisibleCut.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-hidden-cut/InvisibleCut.tsx"
    },
    {
      "path": "demos/transition/transition-hidden-cut/LightLeakBurn.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-hidden-cut/LightLeakBurn.tsx"
    },
    {
      "path": "demos/transition/transition-hidden-cut/VersusSlam.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-hidden-cut/VersusSlam.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": 122,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-hidden-cut/InvisibleCut.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-hidden-cut/LightLeakBurn.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-hidden-cut/VersusSlam.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/transition/transition-hidden-cut/InvisibleCut.tsx =====\nimport React from 'react';\nimport { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';\nimport { CameraMotionBlur } from '@remotion/motion-blur';\nimport { FakeDashboard, Card, G } from '../../_fixtures/Fixtures';\n\n// invisible-cut：前景遮挡隐形切——一张放大到超出画幅的卡片带重运动模糊\n// 从左侧贴脸扫过，糊满屏幕的瞬间背景从 A 无痕换成 B，卡片飞出右侧时\n// 观众以为还是同一镜。（invisible-cut + foreground-occlusion-swipe）\nconst SW_START = 40; // 卡片入场\nconst SW_END = 54; // 卡片出场（14f 横扫）\nconst CUT = 47; // 硬切点：扫掠中点，卡片完全糊满画面的那一帧\n\n// 卡片中扫掠曲线上的水平位置（容器 left，未缩放坐标）\nconst xAt = (f: number) =>\n  // 卡片 scale(1.6) 后半宽 1280：起点右缘 -120 / 终点左缘 2120，均完全出画；\n  // 中点 f47 覆盖 -280..2280，糊满整个 1920 画幅\n  interpolate(f, [SW_START, SW_END], [-2200, 2600], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.bezier(0.3, 0, 0.7, 1),\n  });\n\nconst Scene: React.FC = () => {\n  const frame = useCurrentFrame();\n  const x = xAt(frame);\n  // 瞬时速度（px/帧），驱动斜切与残影强度\n  const v = xAt(frame + 0.5) - xAt(frame - 0.5);\n  const sweeping = frame > SW_START - 2 && frame < SW_END + 3;\n  // 背景被\"带风\"轻推：A 被拖向左，切成 B 后从右侧回稳——卖同一镜错觉\n  const shove =\n    frame < CUT\n      ? interpolate(frame, [SW_START, CUT], [0, -40], {\n          extrapolateLeft: 'clamp',\n          extrapolateRight: 'clamp',\n          easing: Easing.in(Easing.quad),\n        })\n      : interpolate(frame, [CUT, CUT + 13], [40, 0], {\n          extrapolateLeft: 'clamp',\n          extrapolateRight: 'clamp',\n          easing: Easing.out(Easing.cubic),\n        });\n  return (\n    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>\n      {/* 背景层：硬切藏在遮挡帧内 */}\n      <div style={{ position: 'absolute', inset: 0, transform: `translateX(${shove}px)` }}>\n        {frame < CUT ? <FakeDashboard variant=\"A\" /> : <FakeDashboard variant=\"B\" />}\n      </div>\n      {/* 手动残影：4 层拖尾（在主卡身后），保证遮挡窗口糊满全屏 */}\n      {sweeping &&\n        [4, 3, 2, 1].map((i) => (\n          <div\n            key={i}\n            style={{\n              position: 'absolute',\n              left: xAt(frame - i * 0.55),\n              top: 40,\n              width: 1600,\n              height: 1000,\n              transform: 'scale(1.6)',\n              opacity: [0, 0.35, 0.22, 0.13, 0.07][i],\n              filter: 'blur(14px)',\n            }}\n          >\n            <Card w={1600} h={1000} seed={9} style={{ width: '100%', height: '100%' }} />\n          </div>\n        ))}\n      {/* 主卡：1600x1000 放大 1.6 倍（2560x1600 超出画幅），自带 blur 加强糊感 */}\n      {sweeping && (\n        <div\n          style={{\n            position: 'absolute',\n            left: x,\n            top: 40,\n            width: 1600,\n            height: 1000,\n            transform: `scale(1.6) skewX(${-v * 0.018}deg)`,\n            filter: 'blur(8px)',\n            boxShadow: '0 24px 80px rgba(0,0,0,0.3)',\n            borderRadius: 20,\n          }}\n        >\n          <Card w={1600} h={1000} seed={9} style={{ width: '100%', height: '100%' }} />\n        </div>\n      )}\n    </AbsoluteFill>\n  );\n};\n\nexport const InvisibleCut: React.FC = () => (\n  <CameraMotionBlur shutterAngle={300} samples={12}>\n    <Scene />\n  </CameraMotionBlur>\n);\n\n\n// ===== demos/transition/transition-hidden-cut/LightLeakBurn.tsx =====\nimport React from 'react';\nimport { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';\nimport { FakeDashboard, TitleBlock, G } from '../../_fixtures/Fixtures';\n\n// light-leak-burn〔转场〕：一团琥珀橙柔光从右上角斜扫入画，亮度顶峰时\n// 吞掉旧画面约七成（高光溢出、对比度被冲淡），光峰帧后切新页藏切点，\n// 光退散时新页已在光下就位——比白闪柔、有方向、有温度。\n// 节拍：0–25 建立旧页 hold；25–52 光斜扫入、爬向峰值；52 峰值帧切页；\n// 52–95 光沿对角线退出、强度衰减；95–130 新页静止 hold。\n\nconst PEAK = 52; // 光峰帧 = 藏切点\n\nexport const LightLeakBurn: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 光团沿对角线的位移进度：右上外 → 左下外，贯穿 25–95\n  const sweep = interpolate(frame, [25, 95], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.bezier(0.4, 0, 0.3, 1),\n  });\n\n  // 光强包络：25–PEAK 爬升（ease-in 有蓄力感），PEAK–95 收敛（ease-out 余温）\n  const rise = interpolate(frame, [25, PEAK], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.quad),\n  });\n  const fall = interpolate(frame, [PEAK, 95], [1, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n  const intensity = frame <= PEAK ? rise : fall;\n\n  // 光团中心：从画面右上外侧斜扫到左下外侧（有方向的漏光）\n  const cx = interpolate(sweep, [0, 1], [2350, -650]);\n  const cy = interpolate(sweep, [0, 1], [-500, 1500]);\n\n  // 三团琥珀光的偏移与直径（seed 正弦哈希做微差，避免完美同心）\n  const blobs = [0, 1, 2].map((i) => {\n    const j1 = (Math.sin(i * 127.3) * 43758) % 1; // -1..1 小数\n    const j2 = (Math.sin(i * 311.7) * 27183) % 1;\n    return {\n      dx: i * 260 - 260 + j1 * 90, // 沿扫掠方向拖尾排布\n      dy: i * 200 - 200 + j2 * 70,\n      d: 1500 + i * 450, // 直径 1500 / 1950 / 2400\n      color: ['#f6c878', '#e8a44a', '#d98a2b'][i],\n      alpha: [0.95, 0.8, 0.55][i],\n    };\n  });\n\n  // 峰值时页面被光冲淡：对比度降、亮度抬（旧页被\"烧穿\"的感觉）\n  const pageFilter = `contrast(${1 - intensity * 0.45}) brightness(${1 + intensity * 0.35})`;\n\n  // 全屏暖色罩：峰值时约 8%\n  const warmWash = intensity * 0.08;\n\n  return (\n    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>\n      {/* 页面层：光峰帧前是旧页（网格），之后是新页（列表+标题）——切点藏在最亮处 */}\n      <AbsoluteFill style={{ filter: pageFilter }}>\n        {frame <= PEAK ? (\n          <FakeDashboard variant=\"A\" />\n        ) : (\n          <>\n            <FakeDashboard variant=\"B\" />\n            <div style={{ position: 'absolute', left: 288, top: 132 }}>\n              <TitleBlock text=\"Next Page\" size={64} />\n            </div>\n          </>\n        )}\n      </AbsoluteFill>\n\n      {/* 全屏暖色罩：峰值 8%，让高光溢出带温度 */}\n      <AbsoluteFill\n        style={{\n          background: '#e8a44a',\n          opacity: warmWash,\n          mixBlendMode: 'screen',\n          pointerEvents: 'none',\n        }}\n      />\n\n      {/* 琥珀漏光层：3 团大直径径向渐变，blur 90px，screen 叠加，沿对角线扫过 */}\n      <AbsoluteFill style={{ pointerEvents: 'none' }}>\n        {blobs.map((b, i) => (\n          <div\n            key={i}\n            style={{\n              position: 'absolute',\n              left: cx + b.dx - b.d / 2,\n              top: cy + b.dy - b.d / 2,\n              width: b.d,\n              height: b.d,\n              borderRadius: '50%',\n              background: `radial-gradient(circle, ${b.color} 0%, ${b.color}cc 30%, transparent 68%)`,\n              filter: 'blur(90px)',\n              mixBlendMode: 'screen',\n              opacity: b.alpha * intensity,\n            }}\n          />\n        ))}\n        {/* 峰值核心过曝：光心一小团接近白的热核，峰值帧吞掉约七成画面 */}\n        <div\n          style={{\n            position: 'absolute',\n            left: cx - 550,\n            top: cy - 550,\n            width: 1100,\n            height: 1100,\n            borderRadius: '50%',\n            background: 'radial-gradient(circle, #fff3dd 0%, #f6c878aa 45%, transparent 70%)',\n            filter: 'blur(80px)',\n            mixBlendMode: 'screen',\n            opacity: intensity * intensity, // 只在临近峰值时才烧起来\n          }}\n        />\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n\n// ===== demos/transition/transition-hidden-cut/VersusSlam.tsx =====\nimport React from 'react';\nimport { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';\nimport { FakeDashboard, TitleBlock, G } from '../../_fixtures/Fixtures';\n\n// versus-slam 对撞开屏：左右两个半屏画面（带 78° 斜切边）从画外加速对冲，\n// 沿斜缝砰地撞合；撞击帧白闪 + 整机震屏指数衰减 + \"VS\" 字块盖章压出，结尾静止 hold。\nconst IMPACT = 30; // 撞击帧（前 20f 建立 hold + 10f ease-in 对冲）\n\n// 斜缝几何：78° 斜边 → 1080 高度上水平偏移 1080/tan(78°) ≈ 230px，中线 x=960 ±115\nconst SEAM_TOP_X = 1075; // 缝顶端 x\nconst SEAM_BOT_X = 845; // 缝底端 x\n// CSS 旋转顺时针为正：缝顶端偏右（1075 > 845）→ 正角度 ≈ +12°\nconst SEAM_DEG = (Math.atan2(SEAM_TOP_X - SEAM_BOT_X, 1080) * 180) / Math.PI;\n\nexport const VersusSlam: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 两半屏对冲：ease-in 加速，10f 从 ±1200px 冲到位\n  const leftX = interpolate(frame, [20, IMPACT], [-1200, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.cubic),\n  });\n  const rightX = -leftX;\n\n  // 撞击帧起：整机震屏 12px 指数衰减（约 5f 收干）\n  const since = frame - IMPACT;\n  const env = since >= 0 ? 12 * Math.exp(-since / 1.6) : 0;\n  const shakeX = env * Math.sin(since * 3.4);\n  const shakeY = env * 0.6 * Math.sin(since * 4.1 + 0.7);\n\n  // 白闪：撞击帧 0.9 → 0，3f 收掉\n  const flash = interpolate(frame, [IMPACT, IMPACT + 3], [0.9, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // \"VS\" 盖章：scale 1.6 → 1 带 back overshoot，6f 压出\n  const vsScale = interpolate(frame, [IMPACT, IMPACT + 6], [1.6, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.back(2.6)),\n  });\n  const vsOpacity = interpolate(frame, [IMPACT, IMPACT + 2], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  const impacted = frame >= IMPACT;\n\n  return (\n    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', inset: 0, transform: `translate(${shakeX}px, ${shakeY}px)` }}>\n        {/* 建立段的斜缝虚线预示（撞合后被实缝替代） */}\n        {!impacted && (\n          <svg width={1920} height={1080} style={{ position: 'absolute', inset: 0 }}>\n            <line\n              x1={SEAM_TOP_X} y1={0} x2={SEAM_BOT_X} y2={1080}\n              stroke={G.bar} strokeWidth={4} strokeDasharray=\"18 16\"\n            />\n          </svg>\n        )}\n        {/* 左半屏：FakeDashboard A 裁左半，斜边 78° */}\n        <div style={{\n          position: 'absolute', inset: 0,\n          transform: `translateX(${leftX}px)`,\n          clipPath: `polygon(0px 0px, ${SEAM_TOP_X}px 0px, ${SEAM_BOT_X}px 1080px, 0px 1080px)`,\n        }}>\n          <FakeDashboard variant=\"A\" />\n        </div>\n        {/* 右半屏：FakeDashboard B 裁右半 */}\n        <div style={{\n          position: 'absolute', inset: 0,\n          transform: `translateX(${rightX}px)`,\n          clipPath: `polygon(${SEAM_TOP_X}px 0px, 1920px 0px, 1920px 1080px, ${SEAM_BOT_X}px 1080px)`,\n        }}>\n          <FakeDashboard variant=\"B\" />\n        </div>\n        {/* 撞合后的实体斜缝条 */}\n        {impacted && (\n          <div style={{\n            position: 'absolute', left: 960 - 6, top: 540 - 700,\n            width: 12, height: 1400, background: G.ink,\n            transform: `rotate(${SEAM_DEG}deg)`,\n          }} />\n        )}\n        {/* \"VS\" 字块盖章：贴缝、随缝倾斜 */}\n        {impacted && (\n          <div style={{\n            position: 'absolute', left: 960, top: 540,\n            transform: `translate(-50%, -50%) rotate(${SEAM_DEG}deg) scale(${vsScale})`,\n            opacity: vsOpacity,\n            background: G.card, border: `6px solid ${G.ink}`, borderRadius: 20,\n            padding: '18px 46px', boxShadow: '0 18px 60px rgba(0,0,0,0.35)',\n          }}>\n            <TitleBlock text=\"VS\" size={140} />\n          </div>\n        )}\n      </div>\n      {/* 撞击白闪（不随震屏位移） */}\n      <AbsoluteFill style={{ background: '#ffffff', opacity: flash, pointerEvents: 'none' }} />\n    </AbsoluteFill>\n  );\n};\n\n\n"
}