{
  "slug": "space-camera-moves",
  "title": "3D space camera moves",
  "tagline": "Two 3D spatial camera moves — exploded-view (parts blow apart along Z then reassemble) and drone-dive-landing (a drone dives and lands).",
  "description": "Two 3D spatial camera moves — exploded-view (parts blow apart along Z then reassemble) and drone-dive-landing (a drone dives and lands).",
  "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/space-camera-moves/poster-d98fc9d99e4f.jpg",
    "video": "https://gimgs.net/motion/media/space-camera-moves/video-c09f9cce7966.mp4"
  },
  "variants": [
    {
      "key": "exploded-view",
      "label": "exploded-view · Exploded teardown",
      "description": "The whole page tilts into 3D, then its parts blast apart along Z in staggered order and hover; after a beat they reassemble in reverse with a screen-shake close",
      "use": "Architecture/composition reveal: \"what's inside this product\"; a module-overview passage",
      "video": "https://gimgs.net/motion/media/space-camera-moves/video-c09f9cce7966.mp4",
      "poster": "https://gimgs.net/motion/media/space-camera-moves/poster-d98fc9d99e4f.jpg"
    },
    {
      "key": "drone-dive-landing",
      "label": "drone-dive-landing · Drone dive landing",
      "description": "A near-vertical overhead hover → a sharp dive → an air-cushion deceleration that lands on a close-up of the hero card",
      "use": "Opening establishing shot into the topic; a chapter opener that plunges from a global map into a single point",
      "video": "https://gimgs.net/motion/media/space-camera-moves/video-bc3c675380ca.mp4",
      "poster": "https://gimgs.net/motion/media/space-camera-moves/poster-drone-dive-landing-53642880dc72.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/space-camera-moves.html",
  "json_url": "https://gimgs.net/motion/space-camera-moves.json",
  "template_url": "https://gimgs.net/motion/space-camera-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/space-camera-moves.md",
  "updated_at": "2026-09-24T07:40:23.112Z",
  "prompt": "## When to use\n\nHighlight passages that shoot a flat page as a 3D object; both are \"big moves\" — at most 2 uses total per film\n\n## Intention\n\nThe depth-layer card adds depth to a pan — layers move, the page stays a page. These two go further: the whole page is treated as one 3D solid, and the camera (or the world) performs a live-action-grade maneuver around it. The two moves each cover distinct narrative ground: A says \"see what it's made of\" (a teardown reveal, Apple-keynote language); C says \"a god's-eye view plunging into the hero\" (wide shot to close-up focus, overview to focal point).\n\n## Duration & energy\n\n- Duration: A: 5s (full explode–hover–reassemble arc); C: 3–5s single-direction dive\n- Energy: High\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\ndepth-layer 卡是给平移加纵深——层在动、页面还是页面。这两式更进一步：\n整页被当成一个 3D 实体，相机（或世界）绕着它做实拍级机动。两式叙事\n语义各占一格：A 说\"看看它由什么组成\"（拆解展示，Apple 发布片语言）；\nC 说\"从全局俯瞰砸到主角\"（上帝视角一头扎进 hero 特写，全景→焦点）。\n\n## 两式选型\n\n| 式 | 做法 | 适用段落 |\n|----|------|----------|\n| A exploded-view | 整页 3D 倾斜后构件沿 Z 轴错峰炸开悬停，一拍后逆序合体震屏收口 | 架构/组成展示：\"这个产品里有什么\"；模块总览段 |\n| C drone-dive-landing | 近垂直俯角悬停 → 猛扎俯冲 → 气垫减速停在 hero 卡特写 | 开场定场→入题；全局地图砸进单点的章节启动 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A 空间基座 | perspective 1600 + 整组 scale 0.76 + rotateX 18° rotateY -12° + preserve-3d | 倾角是空间感来源；正视炸开读不出深度 |\n| A 炸开 | 各构件 translateZ 60–320px 错落分布，错峰 3f，每层 14f ease-out-back(1.7) | back 回弹给\"咔\"一声的机械感；深度全同读作整页浮起 |\n| A 合体 | 逆序 ease-in 12f，归位帧 2f 指数衰减震屏（幅 ~13px） | 震屏是落锤——没有它合体读作软趴趴的淡入 |\n| A 深度锚 | 投影随 z·p 下移变虚 + 底板压暗 22% + 远层 brightness ↓28% | 三个线索缺一层间关系就糊 |\n| C 行程 | 同一条 p 驱动 rotateX 72°→0 + scale 0.42→1.35 + translate 收拢；origin 钉死 hero 卡中心（如 518,335） | 一条 p = 一台相机一次机动；分开驱动读作三个动画打架 |\n| C 速度曲线 | 主俯冲 25f Easing.in(cubic) 吃 82% 行程 → 气垫 20f Easing.out(poly(5)) 走 18% | 切换帧速度骤降 = 气垫顶住的体感；82/18 比例比帧数更关键 |\n| C 氛围 | 全场包 CameraMotionBlur(shutterAngle 220, samples 9)；俯视期页面下方椭圆软影随落地收干 | 软影卖\"悬空高度\"；blur 卖速度，缺一维就假 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 两式都吃真实截图分层/高清纹理：A 需页面按构件切块截图（顶栏/侧栏/\n  卡各自独立成图，整页一张图炸不开）；C 终点是特写，素材先按\n  审美准则 Q2 的高分辨率栅格化技法处理\n- C 式易错：Remotion 无 Easing.quint，写 Easing.poly(5)（本批实渲踩坑）\n- A 式构件布局要绝对定位复刻原页面网格（demo 里 8 层复刻 FakeDashboard\n  A），流式布局做不了 per-构件 translateZ\n\n## 参考实现\ndemos/camera/space-camera-moves/\n（DroneDiveLanding.tsx / ExplodedView.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/camera/space-camera-moves/DroneDiveLanding.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/space-camera-moves/DroneDiveLanding.tsx\n- demos/camera/space-camera-moves/ExplodedView.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/space-camera-moves/ExplodedView.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/space-camera-moves/DroneDiveLanding.tsx, demos/camera/space-camera-moves/ExplodedView.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/space-camera-moves/DroneDiveLanding.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/space-camera-moves/DroneDiveLanding.tsx"
    },
    {
      "path": "demos/camera/space-camera-moves/ExplodedView.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/space-camera-moves/ExplodedView.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/space-camera-moves/DroneDiveLanding.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/space-camera-moves/ExplodedView.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/space-camera-moves/DroneDiveLanding.tsx =====\nimport React from 'react';\nimport { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';\nimport { CameraMotionBlur } from '@remotion/motion-blur';\nimport { FakeDashboard, G } from '../../_fixtures/Fixtures';\n\n// drone-dive-landing：上帝视角俯视整页平躺的 dashboard（近垂直俯角、缩小居中），\n// 相机猛扎下来——俯角抬平、页面放大立正，最后一段气垫式长尾减速，\n// 稳稳停在 hero 卡正前方特写。FPV 无人机俯冲降落的运镜翻译。\n//\n// hero 卡 = FakeDashboard A 网格左上格：\n// 侧栏 220 + padding 36 = x 256 起，列宽 (1628-56)/3 = 524；\n// 顶栏 72 + padding 36 = y 108 起，行高 (936-28)/2 = 454。\n// 卡中心 (256+262, 108+227) = (518, 335)，全程 transform-origin 钉在这里。\nconst HERO = { cx: 518, cy: 335 };\nconst DIVE_START = 20; // 开头 hold 20f：上帝视角建立\nconst DIVE_END = 45;   // 主俯冲段 25f，ease-in(cubic) 越冲越快\nconst LAND_END = 65;   // 气垫段 20f，ease-out(quint) 长尾减速，之后真静止\nconst DIVE_SHARE = 0.82; // 俯冲段吃掉 82% 行程，剩 18% 留给气垫\n\nconst Scene: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 两段速度曲线拼一条行程 p∈[0,1]：先猛加速扎下，切换帧速度骤降 = 气垫顶住的体感\n  const pDive = interpolate(frame, [DIVE_START, DIVE_END], [0, DIVE_SHARE], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.cubic),\n  });\n  const pLand = interpolate(frame, [DIVE_END, LAND_END], [0, 1 - DIVE_SHARE], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.poly(5)),\n  });\n  const p = frame < DIVE_END ? pDive : DIVE_SHARE + pLand;\n\n  // 三轴联动，全部由同一条 p 驱动（同一台\"相机\"的一次连续机动）\n  const rotX = interpolate(p, [0, 1], [72, 0]);      // 俯角抬平\n  const scale = interpolate(p, [0, 1], [0.42, 1.35]); // 缩小全景 → hero 特写\n  // 平移：起点让整页平躺在画面中央偏上；终点 hero 卡中心正对画面中心\n  const tx = interpolate(p, [0, 1], [256, 960 - HERO.cx]);\n  const ty = interpolate(p, [0, 1], [150, 540 - HERO.cy]);\n\n  // 地面投影：俯视时页面悬空、下方拖一团椭圆软影；落地立正后影子收干\n  const shadowOp = interpolate(p, [0, 0.8], [0.32, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const shadowW = 1300 * (0.6 + scale * 0.4);\n\n  return (\n    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>\n      {/* 地面软影（高度感线索） */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 960 - shadowW / 2,\n          top: 620,\n          width: shadowW,\n          height: 320,\n          borderRadius: '50%',\n          background: 'radial-gradient(ellipse at center, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0) 68%)',\n          opacity: shadowOp,\n          filter: 'blur(18px)',\n        }}\n      />\n      {/* 相机 = perspective 容器；页面绕 hero 卡中心做 rotateX + scale + translate */}\n      <AbsoluteFill style={{ perspective: 1400 }}>\n        <div\n          style={{\n            position: 'absolute',\n            width: 1920,\n            height: 1080,\n            transformOrigin: `${HERO.cx}px ${HERO.cy}px`,\n            transform: `translate(${tx}px, ${ty}px) rotateX(${rotX}deg) scale(${scale})`,\n            boxShadow: `0 ${6 + (1 - p) * 30}px ${20 + (1 - p) * 60}px rgba(0,0,0,${0.1 + (1 - p) * 0.14})`,\n            borderRadius: 6,\n            overflow: 'hidden',\n          }}\n        >\n          <FakeDashboard variant=\"A\" />\n        </div>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\nexport const DroneDiveLanding: React.FC = () => (\n  <CameraMotionBlur shutterAngle={220} samples={9}>\n    <Scene />\n  </CameraMotionBlur>\n);\n\n\n// ===== demos/camera/space-camera-moves/ExplodedView.tsx =====\nimport React from 'react';\nimport { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';\nimport { Card, G } from '../../_fixtures/Fixtures';\n\n// exploded-view：整页 dashboard 带 3D 倾斜，咔地沿 Z 轴炸开——顶栏/侧栏/六卡\n// 各自浮到不同深度悬停（近大而实、远略暗），层间透出投影；hold 一拍后\n// 逆序咔哒合体，2f 震屏收口。\n//\n// 节拍：0–24 静止建立（已倾斜）→ 24–59 错峰炸开（每层 14f ease-out-back）\n//      → 悬停 → 90–123 逆序合体（每层 12f ease-in）→ 123 震屏 → 静止到 150\n\nconst EXPLODE = 24; // 炸开起始帧\nconst ASSEMBLE = 90; // 合体起始帧\nconst STAGGER = 3; // 层间错峰\nconst N = 8; // 可动层数（顶栏 + 侧栏 + 6 卡）\nconst CLOSE = ASSEMBLE + (N - 1) * STAGGER + 12; // = 123，最后一层归位\n\n// FakeDashboard variant A 的布局常量（绝对定位复刻）\nconst SIDE_W = 220;\nconst TOP_H = 72;\nconst PAD = 36;\nconst GAP = 28;\nconst CARD_W = (1920 - SIDE_W - PAD * 2 - GAP * 2) / 3; // 524\nconst CARD_H = (1080 - TOP_H - PAD * 2 - GAP) / 2; // 454\n\ntype Layer = {\n  key: string;\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n  z: number; // 炸开深度 60–320\n  order: number; // 错峰序\n  radius: number;\n  node: React.ReactNode;\n};\n\nconst Sidebar: React.FC = () => (\n  <div style={{ width: SIDE_W, height: 1080, background: G.side, padding: '28px 22px', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 18 }}>\n    <div style={{ width: 40, height: 40, borderRadius: 10, background: '#777775' }} />\n    {Array.from({ length: 7 }).map((_, i) => (\n      <div key={i} style={{ height: 12, width: `${60 + ((i * 29) % 35)}%`, background: G.sideBar, borderRadius: 6 }} />\n    ))}\n  </div>\n);\n\nconst Topbar: React.FC = () => (\n  <div style={{ width: 1920 - SIDE_W, height: TOP_H, background: G.panel, borderBottom: `2px solid ${G.line}`, display: 'flex', alignItems: 'center', padding: '0 32px', gap: 20, boxSizing: 'border-box' }}>\n    <div style={{ height: 18, width: 180, background: G.bar, borderRadius: 9 }} />\n    <div style={{ marginLeft: 'auto', height: 36, width: 320, background: '#fff', border: `2px solid ${G.line}`, borderRadius: 18, boxSizing: 'border-box' }} />\n    <div style={{ width: 36, height: 36, borderRadius: 18, background: G.mid }} />\n  </div>\n);\n\n// 六卡深度：错落分布在 60–320，近的自然更大（perspective 缩放）\nconst CARD_Z = [150, 300, 80, 230, 320, 110];\n\nconst LAYERS: Layer[] = [\n  { key: 'top', x: SIDE_W, y: 0, w: 1920 - SIDE_W, h: TOP_H, z: 260, order: 0, radius: 0, node: <Topbar /> },\n  { key: 'side', x: 0, y: 0, w: SIDE_W, h: 1080, z: 190, order: 1, radius: 0, node: <Sidebar /> },\n  ...Array.from({ length: 6 }).map((_, i) => {\n    const col = i % 3;\n    const row = Math.floor(i / 3);\n    return {\n      key: `card${i}`,\n      x: SIDE_W + PAD + col * (CARD_W + GAP),\n      y: TOP_H + PAD + row * (CARD_H + GAP),\n      w: CARD_W,\n      h: CARD_H,\n      z: CARD_Z[i],\n      order: 2 + i,\n      radius: 14,\n      node: <Card w={CARD_W} h={CARD_H} seed={i + 1} />,\n    };\n  }),\n];\n\nexport const ExplodedView: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 每层进度：炸开 ease-out-back（带一点回弹的“咔”）× 合体逆序 ease-in\n  const layerP = (order: number) => {\n    const out = interpolate(\n      frame,\n      [EXPLODE + order * STAGGER, EXPLODE + order * STAGGER + 14],\n      [0, 1],\n      { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.back(1.7)) },\n    );\n    const back = interpolate(\n      frame,\n      [ASSEMBLE + (N - 1 - order) * STAGGER, ASSEMBLE + (N - 1 - order) * STAGGER + 12],\n      [0, 1],\n      { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.cubic) },\n    );\n    return out * (1 - back);\n  };\n\n  // 全局散开度（驱动底板变暗）\n  const g =\n    interpolate(frame, [EXPLODE, EXPLODE + 24], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic) }) *\n    (1 - interpolate(frame, [ASSEMBLE, CLOSE], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.cubic) }));\n\n  // 合体收口：2f 震屏，指数衰减\n  const since = frame - CLOSE;\n  const env = since >= 0 ? 13 * Math.exp(-since / 1.3) : 0;\n  const shakeX = env * Math.sin(since * 3.3);\n  const shakeY = env * 0.7 * Math.sin(since * 4.7 + 1.1);\n\n  return (\n    <AbsoluteFill style={{ background: '#dedddb', overflow: 'hidden' }}>\n      <AbsoluteFill style={{ perspective: 1600, transform: `translate(${shakeX}px, ${shakeY}px)` }}>\n        <div\n          style={{\n            position: 'absolute',\n            left: 0,\n            top: 0,\n            width: 1920,\n            height: 1080,\n            transform: 'scale(0.76) rotateX(18deg) rotateY(-12deg)',\n            transformOrigin: '50% 50%',\n            transformStyle: 'preserve-3d',\n          }}\n        >\n          {/* 底板：页面背景留在 Z=0，炸开时整体压暗，衬出层间深度 */}\n          <div style={{ position: 'absolute', inset: 0, background: G.bg, border: `2px solid ${G.border}`, boxSizing: 'border-box', filter: `brightness(${1 - g * 0.22})` }} />\n\n          {/* 投到底板上的假投影：随层浮起而下移/变虚 */}\n          {LAYERS.map((L) => {\n            const p = Math.min(1, Math.max(0, layerP(L.order)));\n            if (p <= 0.01) return null;\n            return (\n              <div\n                key={`sh-${L.key}`}\n                style={{\n                  position: 'absolute',\n                  left: L.x + L.z * 0.16 * p,\n                  top: L.y + L.z * 0.26 * p,\n                  width: L.w,\n                  height: L.h,\n                  borderRadius: L.radius,\n                  background: 'rgba(0,0,0,0.30)',\n                  filter: `blur(${8 + L.z * 0.09 * p}px)`,\n                  opacity: 0.5 * p,\n                  transform: 'translateZ(2px)',\n                }}\n              />\n            );\n          })}\n\n          {/* 可动层：沿 Z 浮起，近的更大更实、远的略暗 */}\n          {LAYERS.map((L) => {\n            const p = layerP(L.order);\n            const pc = Math.min(1, Math.max(0, p));\n            const bright = 1 - (1 - L.z / 320) * 0.28 * pc; // 深度越浅（z 小=离底板近=远离镜头）越暗\n            return (\n              <div\n                key={L.key}\n                style={{\n                  position: 'absolute',\n                  left: L.x,\n                  top: L.y,\n                  width: L.w,\n                  height: L.h,\n                  borderRadius: L.radius,\n                  transform: `translateZ(${L.z * p}px)`,\n                  filter: `brightness(${bright})`,\n                  boxShadow: pc > 0.02 ? `0 ${10 + L.z * 0.1 * pc}px ${16 + L.z * 0.14 * pc}px rgba(0,0,0,${0.12 + 0.1 * pc})` : 'none',\n                }}\n              >\n                {L.node}\n              </div>\n            );\n          })}\n        </div>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n\n"
}