{
  "slug": "tension-camera-moves",
  "title": "Emotional camera moves",
  "tagline": "Four emotional camera moves — bullet-time freeze-orbit, dutch-roll to level, slow-push-in, and pull-back isolation; the camera makes the viewer \"feel\" rather than \"see.\"",
  "description": "Four emotional camera moves — bullet-time freeze-orbit, dutch-roll to level, slow-push-in, and pull-back isolation; the camera makes the viewer \"feel\" rather ",
  "category": "camera",
  "tags": [
    "shotcraft",
    "remotion",
    "camera"
  ],
  "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/tension-camera-moves/poster-24d84b99d66a.jpg",
    "video": "https://gimgs.net/motion/media/tension-camera-moves/video-f6328b0de935.mp4"
  },
  "variants": [
    {
      "key": "bullet-time-freeze-orbit",
      "label": "bullet-time-freeze-orbit · Freeze-orbit",
      "description": "A chart mid-growth freezes completely, the camera orbits the hovering UI plane with a 55° rotateY sweep and back, then time resumes and the growth finishes",
      "use": "The coronation moment for a core stat/chart; the beat that's \"worth stopping the clock for\"",
      "video": "https://gimgs.net/motion/media/tension-camera-moves/video-f6328b0de935.mp4",
      "poster": "https://gimgs.net/motion/media/tension-camera-moves/poster-24d84b99d66a.jpg"
    },
    {
      "key": "dutch-roll-to-level",
      "label": "dutch-roll-to-level · Roll to level",
      "description": "The pain-point passage holds a -10° dutch tilt for the whole frame (with a slight drift); the solution beat rolls back level in one motion with a single overshoot",
      "use": "The pain-point-to-solution flip beat; a course-correction meaning",
      "video": "https://gimgs.net/motion/media/tension-camera-moves/video-fda302fd4d63.mp4",
      "poster": "https://gimgs.net/motion/media/tension-camera-moves/poster-dutch-roll-to-level-5a287a6c4280.jpg"
    },
    {
      "key": "slow-push-in",
      "label": "slow-push-in · Slow push, building pressure",
      "description": "A 4s constant-acceleration push from 1.00 to 1.14 plus deepening vignette, releasing with an abrupt hard cut to a bright shot at the tension's peak",
      "use": "Building up before a big number/statement; a dark-to-bright chapter payoff",
      "video": "https://gimgs.net/motion/media/tension-camera-moves/video-b2071fc9f3e2.mp4",
      "poster": "https://gimgs.net/motion/media/tension-camera-moves/poster-slow-push-in-32b19a7642eb.jpg"
    },
    {
      "key": "pull-back-isolation",
      "label": "pull-back-isolation · Pull back, isolate",
      "description": "Pulling back from a close-up on the glowing hero card, sibling cards dim out in staggered order by distance, the background sinks to black, leaving the lone card suspended center-frame in the dark",
      "use": "Ending closure; the curtain call for \"the whole film was for this one number\"",
      "video": "https://gimgs.net/motion/media/tension-camera-moves/video-70498a75ccc0.mp4",
      "poster": "https://gimgs.net/motion/media/tension-camera-moves/poster-pull-back-isolation-9360b33a3409.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/tension-camera-moves.html",
  "json_url": "https://gimgs.net/motion/tension-camera-moves.json",
  "template_url": "https://gimgs.net/motion/tension-camera-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/tension-camera-moves.md",
  "updated_at": "2026-09-24T07:40:27.823Z",
  "prompt": "## When to use\n\nCamera language for emotional beats (awe/correction/pressure build/closure); complements space-camera-moves' \"flashy big moves\" — these four are small in motion, heavy in emotion\n\n## Intention\n\nspace-camera-moves' three styles are highlight flair — \"shooting the page as a 3D solid.\" These four are the opposite — the motion itself stays restrained, and all the force goes into emotional meaning: A says \"this moment deserves to have time stopped for it\" (an element mid-motion freezes, the camera orbits and inspects); B says \"the problem is solved, the world has been righted\" (a tilted, uneasy angle rolls back level on the beat); C says \"something is about to happen\" (an imperceptibly slow push-in builds tension, releasing with a hard cut at the peak); D says \"in the end only this one thing remains\" (pulling back as everything around dims layer by layer, one point left isolated to close the film). Each of the four occupies its own emotional slot — direct vocabulary for \"what should the viewer feel here\" at the storyboard stage.\n\n## Duration & energy\n\n- Duration: Single style 4–5s; at most 2 styles per film (each used ≤1 time)\n- Energy: A high / B medium / C low-building / D low-closing\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\nspace-camera-moves 三式是\"把页面当 3D 实体拍\"的高光炫技；这四式相反——\n动作本身克制，全部劲道在情绪语义上：A 说\"这一刻值得停下时间看\"\n（运动中的元素被冻住、相机绕行审视）；B 说\"问题解决了，世界被扶正\"\n（斜角悬着的难受感在节拍上滚回水平）；C 说\"有什么要发生了\"（慢到\n不自觉的推近积压张力，顶点硬切释放）；D 说\"最后只剩这一件事\"\n（后拉中周围逐层熄灭，孤悬一点收束全片）。四式各占一个情绪格，\n是分镜阶段\"这一段观众该感到什么\"的直接词汇。\n\n## 四式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A bullet-time-freeze-orbit 冻结环绕 | 图表生长到一半全冻住，相机绕悬停的 UI 平面 rotateY 扫 55° 再回，时间恢复接着长完 | 核心数据/图表的加冕时刻；\"值得停表看\"的一格 |\n| B dutch-roll-to-level 斜角滚正 | 痛点段整帧 -10° 斜角悬着（叠微漂移），解决方案一拍带单次过冲滚回水平 | 痛点→方案的翻转节拍；纠偏语义 |\n| C slow-push-in 慢推压迫 | 4s 匀加速推近 1.00→1.14 + 暗角渐深，张力顶点无过渡硬切亮场 | 大数字/宣言前的蓄力；暗→亮的章节爆点 |\n| D pull-back-isolation 拉远孤立 | 从发光主卡特写后拉，兄弟卡按距离错峰熄灭、背景沉黑，孤卡悬在暗场中央 | 结尾收束；\"全片只为这一个数字\"的谢幕 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A 双时钟 | 内容动画用 `effFrame = f<F1 ? f : f<F2 ? F1 : F1+(f-F2)` 驱动，相机用真实 f 驱动 | 命门：冻结期内容必须肉眼可见地完全静止，相机却在动——两个时钟各管各的 |\n| A 环绕 | perspective 1600 下 rotateY 0→55°→0（两段 inOut cubic，顶点悬停 ~10f）+ scale 1→1.12→1 | 55° 是\"绕到侧面看\"的下限；>70° 面板变纸片穿帮 |\n| B 斜置 | rotate -10° + ±0.8° 长周期正弦漂移 + 2px 纵漂；scale 1.15 防露边 | 漂移是\"悬着难受\"的活感，纯静止斜角读作构图错误 |\n| B 滚正 | 14f ease-out 冲过 0 到 +1.2° → 10f 收回 0（单次过冲不振荡）；scale 同步 1.15→1.08 | 过冲即\"扶正的手劲\"；振荡两次以上读作弹簧玩具 |\n| C 推近 | `scale [0,120f]→[1.00,1.14] Easing.in(quad)` + 暗角 opacity 0→0.5 同曲线 | 加速曲线是本体——匀速推读作普通 zoom；幅度 >1.2 变成普通推镜头 |\n| C 硬切 | 顶点帧零过渡直切下一景；暗景 A→亮景 B 反差要大 | 切的一拍才是释放；加任何过渡等于把弓拉满又慢慢松掉 |\n| D 后拉 | scale 2.2→0.62 / 110f Easing.out(cubic)，origin 锁主卡中心 | 前 20f 主卡仍占满画面，开场即特写不需要另加 hold |\n| D 熄灭 | 兄弟卡按 hypot 距离排序，帧 30 起每 8f 一张，16f 内 opacity→0 + brightness→0.3；背景 60–110f 沉入 #141414 | 由近到远熄灭 = \"世界向外塌暗\"；乱序熄灭读作故障 |\n| D 光晕 | 主卡双层白 box-shadow 60–100f 淡入 | 光晕是\"只剩它\"的视觉证词，与沉黑同步才成立 |\n| 收尾 | 四式动作完毕真静止 ≥30f | R1；D 式是全片句号，静止给足 40f+ |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- C 式\"前 2 秒不可察\"是设计意图不是缺陷——裁决/验收看完整播放，抽帧\n  diff 前段变化极小属正常（已确认成立）\n- A 式与 speed-ramp-freeze/impact-feedback 同为时间操纵，同一镜头别叠用\n  （帧 remap 互相打架）；A 的冻结是\"停给你看\"+相机动，speed-ramp 的\n  定格是\"停下来标注\"——选语义不选形似\n- B 式斜角只压\"痛点\"段——全片常态斜角是 MV 语言，产品片里读作镜头没扶稳\n- C 式硬切后的亮景必须信息完整可读（它承接全部积压的注意力）；\n  切进另一个铺垫段等于白蓄力\n- D 式放结尾时后接 outro-group-photo-launch 会打架（一收一放），二选一\n\n## 参考实现\ndemos/camera/tension-camera-moves/\n（BulletTimeFreezeOrbit.tsx / DutchRollToLevel.tsx / PullBackIsolation.tsx / SlowPushIn.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/camera/tension-camera-moves/BulletTimeFreezeOrbit.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/tension-camera-moves/BulletTimeFreezeOrbit.tsx\n- demos/camera/tension-camera-moves/DutchRollToLevel.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/tension-camera-moves/DutchRollToLevel.tsx\n- demos/camera/tension-camera-moves/PullBackIsolation.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/tension-camera-moves/PullBackIsolation.tsx\n- demos/camera/tension-camera-moves/SlowPushIn.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/tension-camera-moves/SlowPushIn.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/camera/tension-camera-moves/BulletTimeFreezeOrbit.tsx, demos/camera/tension-camera-moves/DutchRollToLevel.tsx, demos/camera/tension-camera-moves/PullBackIsolation.tsx, demos/camera/tension-camera-moves/SlowPushIn.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/camera/tension-camera-moves/BulletTimeFreezeOrbit.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/tension-camera-moves/BulletTimeFreezeOrbit.tsx"
    },
    {
      "path": "demos/camera/tension-camera-moves/DutchRollToLevel.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/tension-camera-moves/DutchRollToLevel.tsx"
    },
    {
      "path": "demos/camera/tension-camera-moves/PullBackIsolation.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/tension-camera-moves/PullBackIsolation.tsx"
    },
    {
      "path": "demos/camera/tension-camera-moves/SlowPushIn.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/tension-camera-moves/SlowPushIn.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/camera/tension-camera-moves/BulletTimeFreezeOrbit.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/tension-camera-moves/DutchRollToLevel.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/tension-camera-moves/PullBackIsolation.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/tension-camera-moves/SlowPushIn.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/camera/tension-camera-moves/BulletTimeFreezeOrbit.tsx =====\n// 子弹时间冻结环绕(bullet-time-freeze-orbit)——The Matrix bullet time。\n// 中央 900×560 面板内 5 根柱状图错峰生长(动画时钟 effFrame 驱动)。\n// 关键帧:0–20 hold 读布景;20–45 柱子正常生长;45–105 时钟咬死(柱子完全静止),\n// 相机 perspective(1600px) 下 rotateY 0→55°(45–72)→顶点悬停(72–82)→回 0(82–105),\n// 同步 scale 1→1.12→1 + translateX 摆动增强绕行感;105–120 时钟恢复柱子长完;\n// 118–128 数字标签浮现;128–150 全静止收尾。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst h = (n: number) => {\n  const s = Math.sin(n * 127.3) * 43758.5453;\n  return s - Math.floor(s);\n};\n\nconst PANEL_W = 900;\nconst PANEL_H = 560;\nconst BAR_COUNT = 5;\n\nexport const BulletTimeFreezeOrbit: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // ── 子弹时间时钟:0–45 正常走,45–105 冻结,105 起恢复 ──\n  const effFrame =\n    frame < 45 ? frame : frame < 105 ? 45 : 45 + (frame - 105);\n\n  // ── 冻结区间的相机环绕(用真实 frame 驱动) ──\n  const rotY =\n    frame < 72\n      ? interpolate(frame, [45, 72], [0, 55], {\n          extrapolateLeft: 'clamp',\n          extrapolateRight: 'clamp',\n          easing: Easing.inOut(Easing.cubic),\n        })\n      : frame < 82\n        ? 55 // 顶点悬停 10f\n        : interpolate(frame, [82, 105], [55, 0], {\n            extrapolateLeft: 'clamp',\n            extrapolateRight: 'clamp',\n            easing: Easing.inOut(Easing.cubic),\n          });\n  const orbitT = rotY / 55; // 0→1→0,复用做 scale / translateX\n  const scale = 1 + 0.12 * orbitT;\n  const tx = -170 * Math.sin(orbitT * Math.PI * 0.5) - 60 * orbitT; // 绕行横摆\n  const ty = -24 * orbitT;\n\n  // ── 柱子:错峰生长,全部由 effFrame 驱动(冻结即静止) ──\n  const chartW = PANEL_W - 140;\n  const chartH = PANEL_H - 190;\n  const barW = 92;\n  const gap = (chartW - BAR_COUNT * barW) / (BAR_COUNT - 1);\n  const bars = Array.from({ length: BAR_COUNT }).map((_, i) => {\n    const full = chartH * (0.42 + h(i + 1) * 0.55); // 目标高度\n    const start = 20 + i * 4;\n    const end = 48 + i * 3; // 20–60f 区间内错峰\n    const p = interpolate(effFrame, [start, end], [0, 1], {\n      extrapolateLeft: 'clamp',\n      extrapolateRight: 'clamp',\n      easing: Easing.out(Easing.cubic),\n    });\n    const value = Math.round((full / chartH) * 100); // 标签与柱高一致\n\n    return { hNow: full * p, full, value, done: p >= 1 };\n  });\n\n  // ── 恢复段:数字标签浮现(118–128),之后全静止 ──\n  const labelOp = interpolate(frame, [118, 128], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // 冻结提示条:冻结期间显示 FREEZE 徽标,帮助读出手法\n  const freezeOp = interpolate(frame, [45, 52, 98, 105], [0, 1, 1, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  return (\n    <div\n      style={{\n        width: 1920,\n        height: 1080,\n        background: G.bg,\n        position: 'relative',\n        overflow: 'hidden',\n        fontFamily: 'Helvetica, Arial, sans-serif',\n      }}\n    >\n      <div style={{ position: 'absolute', top: 56, left: 72 }}>\n        <TitleBlock text=\"BULLET TIME\" size={44} />\n      </div>\n\n      {/* 冻结徽标 */}\n      <div\n        style={{\n          position: 'absolute',\n          top: 62,\n          right: 84,\n          opacity: freezeOp,\n          background: G.ink,\n          color: G.card,\n          fontWeight: 800,\n          fontSize: 30,\n          letterSpacing: 4,\n          padding: '12px 26px',\n          borderRadius: 10,\n        }}\n      >\n        FREEZE\n      </div>\n\n      {/* 3D 舞台 */}\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          perspective: 1600,\n        }}\n      >\n        <div\n          style={{\n            width: PANEL_W,\n            height: PANEL_H,\n            background: G.card,\n            border: `2px solid ${G.border}`,\n            borderRadius: 18,\n            boxShadow: '0 16px 48px rgba(0,0,0,0.14)',\n            boxSizing: 'border-box',\n            padding: '44px 70px',\n            transform: `translateX(${tx}px) translateY(${ty}px) rotateY(${rotY}deg) scale(${scale})`,\n            transformStyle: 'preserve-3d',\n          }}\n        >\n          {/* 面板标题条 */}\n          <div\n            style={{\n              height: 20,\n              width: 260,\n              background: G.bar,\n              borderRadius: 10,\n              marginBottom: 14,\n            }}\n          />\n          <div\n            style={{\n              height: 12,\n              width: 170,\n              background: G.line,\n              borderRadius: 6,\n              marginBottom: 30,\n            }}\n          />\n\n          {/* 图表区:横向刻度线 + 柱子 */}\n          <div style={{ position: 'relative', width: chartW, height: chartH }}>\n            {[0, 1, 2, 3].map((i) => (\n              <div\n                key={i}\n                style={{\n                  position: 'absolute',\n                  left: 0,\n                  right: 0,\n                  top: (chartH / 4) * i,\n                  height: 2,\n                  background: G.line,\n                }}\n              />\n            ))}\n            {/* 基线 */}\n            <div\n              style={{\n                position: 'absolute',\n                left: 0,\n                right: 0,\n                bottom: 0,\n                height: 3,\n                background: G.mid,\n              }}\n            />\n            {bars.map((b, i) => (\n              <div key={i}>\n                <div\n                  style={{\n                    position: 'absolute',\n                    left: i * (barW + gap),\n                    bottom: 3,\n                    width: barW,\n                    height: b.hNow,\n                    background: i % 2 === 0 ? G.bar : G.mid,\n                    borderRadius: '8px 8px 0 0',\n                  }}\n                />\n                {/* 数字标签:恢复段浮现 */}\n                <div\n                  style={{\n                    position: 'absolute',\n                    left: i * (barW + gap),\n                    bottom: 3 + b.full + 14 - 10 * (1 - labelOp),\n                    width: barW,\n                    textAlign: 'center',\n                    fontWeight: 800,\n                    fontSize: 28,\n                    color: G.ink,\n                    opacity: labelOp,\n                  }}\n                >\n                  {b.value}\n                </div>\n              </div>\n            ))}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\n\n// ===== demos/camera/tension-camera-moves/DutchRollToLevel.tsx =====\nimport React from 'react';\nimport { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';\nimport { FakeDashboard, Card, G } from '../../_fixtures/Fixtures';\n\n// 斜角滚正（dutch-roll-to-level）：呈现痛点时整帧带 -10° 斜角悬着（叠极缓慢\n// 正弦漂移防止读作静态歪图），帧 70 解决方案的一拍整帧带单次过冲滚回水平\n// （-10° → +1.2° → 0），同时警示条淡出、干净卡浮现——\"世界被扶正\"打在节拍上。\n// 帧 0–70 斜置漂移 / 70–84 滚正冲过头 / 84–94 收回 0 / 94–140 真静止。\n\nconst ROLL = 70; // 滚正起拍\nconst LEVEL = 94; // 完全归位帧\n\nexport const DutchRollToLevel: React.FC = () => {\n  const f = useCurrentFrame();\n\n  // —— 斜置期的缓慢漂移（帧 70 前）：±0.8° 长周期正弦 + 2px 纵漂 ——\n  const driftT = Math.min(f, ROLL);\n  const driftRot = Math.sin(driftT * 0.035) * 0.8;\n  const driftY = Math.sin(driftT * 0.05) * 2;\n  // 滚正期间漂移随进度淡出\n  const driftFade = interpolate(f, [ROLL, ROLL + 6], [1, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // —— 滚正：-10° 用 14f 冲过 0 到 +1.2°，再 10f 收回 0（单次过冲不振荡） ——\n  const baseRot =\n    f < ROLL\n      ? -10\n      : f < ROLL + 14\n        ? interpolate(f, [ROLL, ROLL + 14], [-10, 1.2], {\n            easing: Easing.out(Easing.cubic),\n          })\n        : interpolate(f, [ROLL + 14, LEVEL], [1.2, 0], {\n            extrapolateRight: 'clamp',\n            easing: Easing.inOut(Easing.quad),\n          });\n\n  const rot = baseRot + driftRot * driftFade;\n  const y = driftY * driftFade;\n\n  // scale 1.15（防旋转露边）→ 滚正同步收到 1.08\n  const scale = interpolate(f, [ROLL, LEVEL], [1.15, 1.08], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.inOut(Easing.cubic),\n  });\n\n  // —— 警示条（痛点）：斜置期悬在上方，滚正一拍淡出；干净卡反向淡入 ——\n  const alertOpacity = interpolate(f, [ROLL, ROLL + 12], [1, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const cleanOpacity = interpolate(f, [ROLL + 8, ROLL + 22], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  return (\n    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          transform: `translateY(${y}px) rotate(${rot}deg) scale(${scale})`,\n          transformOrigin: '50% 50%',\n        }}\n      >\n        <FakeDashboard variant=\"B\" />\n\n        {/* 痛点警示条：深色横幅压在页面上方（斜着更显歪） */}\n        <div\n          style={{\n            position: 'absolute',\n            left: 560,\n            top: 120,\n            width: 800,\n            height: 88,\n            background: G.ink,\n            borderRadius: 14,\n            opacity: alertOpacity * 0.92,\n            display: 'flex',\n            alignItems: 'center',\n            gap: 18,\n            padding: '0 28px',\n            boxSizing: 'border-box',\n            boxShadow: '0 10px 30px rgba(0,0,0,0.25)',\n          }}\n        >\n          {/* 警示三角 */}\n          <div\n            style={{\n              width: 0,\n              height: 0,\n              borderLeft: '18px solid transparent',\n              borderRight: '18px solid transparent',\n              borderBottom: '32px solid #f7f7f6',\n            }}\n          />\n          <div style={{ height: 16, width: 420, background: G.mid, borderRadius: 8 }} />\n        </div>\n\n        {/* 解决方案：干净卡随滚正浮现在同一位置 */}\n        <div style={{ position: 'absolute', left: 560, top: 96, opacity: cleanOpacity }}>\n          <Card w={800} h={140} seed={2} style={{ boxShadow: '0 10px 30px rgba(0,0,0,0.12)' }} />\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n\n// ===== demos/camera/tension-camera-moves/PullBackIsolation.tsx =====\n// 拉远孤立收束（pull-back-isolation）——pull-back shot。\n// 相机容器 scale 2.2→0.62（0–110f，Easing.out(cubic)）：开场怼在主卡\n// \"99.9%\" 特写上，缓缓后拉露出周围 8 张兄弟卡。帧 30 起兄弟卡按离主卡\n// 距离由近到远错峰熄灭（每 8f 一张，opacity→0 + brightness 压暗）；\n// 背景 60–110f 从 #ececea 沉入 #141414；主卡白光晕 60–100f 淡入。\n// 帧 110–150 完全静止：暗场中央孤悬一张发光小卡——全片只为这一个数字。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, Card } from '../../_fixtures/Fixtures';\n\n// 8 张兄弟卡：相对主卡中心 (960,540) 的偏移 + 尺寸 + seed\nconst SIBS = [\n  { dx: -620, dy: -330, w: 360, h: 240, seed: 3 },\n  { dx: 10, dy: -390, w: 420, h: 220, seed: 4 },\n  { dx: 620, dy: -320, w: 380, h: 260, seed: 5 },\n  { dx: -680, dy: 20, w: 340, h: 230, seed: 6 },\n  { dx: 700, dy: 40, w: 360, h: 250, seed: 7 },\n  { dx: -600, dy: 360, w: 400, h: 240, seed: 8 },\n  { dx: 40, dy: 400, w: 440, h: 220, seed: 9 },\n  { dx: 640, dy: 350, w: 370, h: 250, seed: 10 },\n].map((s) => ({ ...s, dist: Math.hypot(s.dx, s.dy) }));\n\n// 按离主卡距离排名 → 错峰熄灭顺序（近的先灭）\nconst RANKED = SIBS.map((s, i) => i).sort((a, b) => SIBS[a].dist - SIBS[b].dist);\nconst FADE_START = RANKED.reduce<number[]>((acc, idx, rank) => {\n  acc[idx] = 30 + rank * 8;\n  return acc;\n}, []);\nconst FADE_DUR = 16;\n\nconst clamp = { extrapolateLeft: 'clamp' as const, extrapolateRight: 'clamp' as const };\n\nexport const PullBackIsolation: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 相机后拉：2.2（怼脸特写）→ 0.62（大远景孤悬）\n  const scale = interpolate(frame, [0, 110], [2.2, 0.62], {\n    easing: Easing.out(Easing.cubic),\n    ...clamp,\n  });\n\n  // 背景沉入黑暗：#ececea → #141414（60–110f）\n  const bgT = interpolate(frame, [60, 110], [0, 1], { easing: Easing.inOut(Easing.quad), ...clamp });\n  const bgC = Math.round(236 + (20 - 236) * bgT);\n  const bg = `rgb(${bgC},${bgC},${bgC})`;\n\n  // 主卡白光晕淡入（60–100f）\n  const glow = interpolate(frame, [60, 100], [0, 0.35], { ...clamp });\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: bg, overflow: 'hidden', position: 'relative' }}>\n      {/* 相机容器：以主卡中心（画面中心）为原点整体缩放 */}\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          transform: `scale(${scale})`,\n          transformOrigin: '960px 540px',\n        }}\n      >\n        {/* 兄弟卡：错峰熄灭 */}\n        {SIBS.map((s, i) => {\n          const t0 = FADE_START[i];\n          const op = interpolate(frame, [t0, t0 + FADE_DUR], [1, 0], {\n            easing: Easing.out(Easing.quad),\n            ...clamp,\n          });\n          const bright = interpolate(frame, [t0, t0 + FADE_DUR], [1, 0.3], { ...clamp });\n          return (\n            <div\n              key={i}\n              style={{\n                position: 'absolute',\n                left: 960 + s.dx - s.w / 2,\n                top: 540 + s.dy - s.h / 2,\n                opacity: op,\n                filter: `brightness(${bright})`,\n              }}\n            >\n              <Card w={s.w} h={s.h} seed={s.seed} />\n            </div>\n          );\n        })}\n\n        {/* 主卡：520×340 居中，叠大数字 + 白光晕 */}\n        <div\n          style={{\n            position: 'absolute',\n            left: 960 - 260,\n            top: 540 - 170,\n            width: 520,\n            height: 340,\n            borderRadius: 14,\n            boxShadow: `0 0 80px rgba(255,255,255,${glow}), 0 0 160px rgba(255,255,255,${glow * 0.6})`,\n          }}\n        >\n          <Card w={520} h={340} seed={2} />\n          <div\n            style={{\n              position: 'absolute',\n              inset: 0,\n              display: 'flex',\n              alignItems: 'center',\n              justifyContent: 'center',\n              fontFamily: 'Helvetica, Arial, sans-serif',\n              fontWeight: 800,\n              fontSize: 128,\n              letterSpacing: -3,\n              color: G.ink,\n              background: 'rgba(255,255,255,0.72)',\n              borderRadius: 14,\n            }}\n          >\n            99.9%\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\n\n// ===== demos/camera/tension-camera-moves/SlowPushIn.tsx =====\n// 慢推压迫（slow-push-in）——studiobinder camera movements。\n// 景 A（帧 0–120）：深色底 + 白色大数字 \"10x\"，scale 用 Easing.in(Easing.quad)\n// 从 1.00 匀加速推到 1.14——前 2 秒几乎不可察，后段明显可感；同时四角径向\n// 暗角 opacity 0→0.5 同步加深，构成压迫感的第二来源。\n// 帧 120 无任何过渡硬切景 B：满屏亮色 FakeDashboard(A) 真静止 30f——\n// 暗→亮的大反差让\"切\"这一拍才响。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, FakeDashboard } from '../../_fixtures/Fixtures';\n\nconst CUT = 120; // 硬切帧\n\nexport const SlowPushIn: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // ---- 景 B：帧 120 起，满屏亮面板，完全静止 ----\n  if (frame >= CUT) {\n    return <FakeDashboard variant=\"A\" />;\n  }\n\n  // ---- 景 A：0–120f 慢推 ----\n  // 匀加速推近：Easing.in(quad)——前段几乎不可察，后段可感\n  const scale = interpolate(frame, [0, CUT], [1.0, 1.14], {\n    easing: Easing.in(Easing.quad),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  // 暗角同步加深：压迫感的第二来源\n  const vignette = interpolate(frame, [0, CUT], [0, 0.5], {\n    easing: Easing.in(Easing.quad),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  return (\n    <div\n      style={{\n        width: 1920,\n        height: 1080,\n        background: G.side,\n        position: 'relative',\n        overflow: 'hidden',\n        fontFamily: 'Helvetica, Arial, sans-serif',\n      }}\n    >\n      {/* 被推近的内容层：整体 scale */}\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          transform: `scale(${scale})`,\n          transformOrigin: '50% 50%',\n        }}\n      >\n        <div\n          style={{\n            fontSize: 300,\n            fontWeight: 800,\n            color: '#ffffff',\n            letterSpacing: -6,\n            lineHeight: 1,\n          }}\n        >\n          10x\n        </div>\n        <div\n          style={{\n            marginTop: 36,\n            fontSize: 40,\n            fontWeight: 500,\n            color: '#b8b8b6',\n            letterSpacing: 6,\n          }}\n        >\n          FASTER THAN BASELINE\n        </div>\n      </div>\n\n      {/* 暗角层：四角径向渐变，随推近同步加深 */}\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          opacity: vignette,\n          background:\n            'radial-gradient(ellipse 62% 55% at 50% 50%, rgba(0,0,0,0) 45%, rgba(0,0,0,0.95) 100%)',\n          pointerEvents: 'none',\n        }}\n      />\n    </div>\n  );\n};\n\n\n"
}