{
  "slug": "basic-3d-scene",
  "title": "Basic 3D Scene Tour",
  "tagline": "An impress.js-style spatial deck: cards scattered through 3D space with distinct position/rotation/scale, the camera flies to align to the inverse of each step's pose, ending pulled back to an OVERVIEW.",
  "description": "An impress.js-style spatial deck: cards scattered through 3D space with distinct position/rotation/scale, the camera flies to align to the inverse of each ste",
  "category": "camera",
  "tags": [
    "shotcraft",
    "remotion",
    "camera"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 6.5,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/basic-3d-scene/poster-ec2c404e7813.jpg",
    "video": "https://gimgs.net/motion/media/basic-3d-scene/video-7c7324ceff61.mp4"
  },
  "variants": [
    {
      "key": "basic-3d-scene",
      "label": "basic-3d-scene",
      "description": "An impress.js-style spatial deck: cards scattered through 3D space with distinct position/rotation/scale, the camera flies to align to the inverse of each step's pose, ending pulled back to an OVERVIEW.",
      "use": "",
      "video": "https://gimgs.net/motion/media/basic-3d-scene/video-7c7324ceff61.mp4",
      "poster": "https://gimgs.net/motion/media/basic-3d-scene/poster-ec2c404e7813.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/basic-3d-scene.html",
  "json_url": "https://gimgs.net/motion/basic-3d-scene.json",
  "template_url": "https://gimgs.net/motion/basic-3d-scene/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/basic-3d-scene.md",
  "updated_at": "2026-09-24T07:39:43.222Z",
  "prompt": "## When to use\n\nSpatializing a concept/roadmap/three-step story; replacing flat slides with a \"different spatial viewpoint per step\" feel.\n\n## Intention\n\nTurn \"turning a page\" into \"traveling through the space of an idea\": each card has not just a position but a pose (step two tilts 40° sideways, step three rotates the whole frame 90°), and as the camera aligns to it, the viewer's world rotates too — the spatial change itself becomes the ritual of \"entering a new chapter.\"\n\n## Duration & energy\n\n- Duration: About 6.0s (180f@30fps; four stops, three flights of 0.96s each).\n- Energy: Medium (each transition is a spatial surprise; the dwell segments are quiet reading time).\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n把\"翻页\"变成\"在想法的空间里旅行\"：每张卡不仅有位置还有姿态（第二步侧转 40°、第三步整个画框转 90°），相机对齐它时观众的世界也跟着转——空间变化本身成为\"进入新章节\"的仪式感。\n\n## 动效核心\n- 核心配方一行：**camera = stepTransform.inverse()**。每步只写卡片自己的 pose（x/y/z/rx/ry/rz/s），相机变换按 `scale(1/s) rotateZ(-rz) rotateY(-ry) rotateX(-rx) translate3d(-x,-y,-z)` 取逆，顺序严格相反\n- 四站姿态各有戏剧点：STEP01 原点正视 → STEP02 右移 520px + ry−40°（世界左转）→ STEP03 rz90°（画框侧躺，相机跟着转正它）→ OVERVIEW s=3.1（相机 1/3.1 拉远总览）\n- 飞行链式插值：三段 `flyAt=[0.22,0.48,0.76]` 各 0.16（inOutCubic），cam 从当前值向下一站 lerp——中断任一段都是合法姿态\n- enter/exit 聚焦：`af`（累计飞行进度）与卡片序号距离决定 `opacity 0.28–1 + blur 0–3.5px`；OVERVIEW 段强制全员点亮（`max(1-d, over)`）\n- OVERVIEW 卡本身放大 3.1 倍但内容预除 2.2——总览时它是\"标题牌\"而非巨物\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 站点姿态 | 每站至少一个新变换维度 | 只挪 x/y 是平移幻灯片；rz 站（画框旋转）是记忆点，一支片用一次 |\n| 飞行时长 | 0.16（约 0.96s） | 有旋转的段别快于 0.12，观众需要时间理解\"世界在转\" |\n| 停留间隔 | 0.10–0.12 | 读卡位；卡内文字多时优先加停留而不是减飞行 |\n| OVERVIEW 缩放 | s=3.1（相机拉远 1/3.1） | 值越大总览越\"高空\"；保证所有卡都进画框再留 10% 边距 |\n| 聚焦衰减 | opacity 0.28 底 + blur 3.5px 峰 | 底值别低于 0.2——离焦卡要\"在场\"，纯黑会丢空间感 |\n| perspective | 1000px | 与最大 |z|（520）比值约 2:1；更深的 z 需同步加大 persp 防畸变 |\n\n## 已知坑\n- 逆变换的旋转顺序（Z→Y→X 反着正变换的 X→Y→Z）不能乱——乱序在单轴旋转时看不出来，多轴复合站会歪\n- 卡片 pose 写在卡自身 transform 和相机公式两处消费，加站只改 poses 表，两处 transform 字符串别动\n- rz90° 站的卡内文字会跟着躺倒，相机转正后才可读——这是设计（转正瞬间\"啊看懂了\"），但该站文案要短\n- OVERVIEW 后无返程，作段落收尾用；要循环回 STEP01 就在末尾加一段 fly back（cam 再 lerp 回 poses[0]）\n\n## 参考实现\ndemos/camera/basic-3d-scene/\n（Basic3DScene.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/camera/basic-3d-scene/Basic3DScene.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/basic-3d-scene/Basic3DScene.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/basic-3d-scene/Basic3DScene.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/basic-3d-scene/Basic3DScene.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/basic-3d-scene/Basic3DScene.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": 195,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/basic-3d-scene/Basic3DScene.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/basic-3d-scene/Basic3DScene.tsx =====\n// basic-3d-scene — Impress 3D Steps 空间步进（motion-lab 定稿转原生 Remotion）\n// impress.js 式演示：卡片散布 3D 空间各处（不同 xyz+旋转+缩放），相机依次飞到\n// 每个 Step 的姿态对齐观看；核心配方 camera = stepTransform.inverse()，\n// 非当前 step 半透明+模糊做 enter/exit。\n// 设计坐标 480×270（DesignStage 等比放大），参数表数值以此坐标系标定。\nimport React from 'react';\nimport { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';\n\nexport const BASIC_3D_SCENE_DURATION = 180; // 6000ms @30fps\n\n// 每个 step：位置 + 姿态 + 缩放（相机将取其逆）\nconst POSES = [\n  { x: 0,   y: 0,   z: 0,    rx: 0, ry: 0,   rz: 0,  s: 1,   hue: 215, tt: 'STEP 01',  sub: 'Position the idea' },\n  { x: 520, y: -60, z: -180, rx: 0, ry: -40, rz: 0,  s: 1,   hue: 265, tt: 'STEP 02',  sub: 'Rotate the view' },\n  { x: 160, y: 300, z: -520, rx: 0, ry: 0,   rz: 90, s: 1,   hue: 165, tt: 'STEP 03',  sub: 'Spin the frame' },\n  { x: 220, y: 90,  z: -260, rx: 0, ry: 0,   rz: 0,  s: 3.1, hue: 25,  tt: 'OVERVIEW', sub: 'See everything' },\n];\n\n// 飞行时刻表：0 停在 step0，之后三段飞行\nconst FLY_AT = [0.22, 0.48, 0.76];\nconst FLY = 0.16;\n\nexport const Basic3DScene: React.FC = () => {\n  const t = useT();\n\n  // activeFloat = 已完成的飞行进度之和 → 当前相机在哪个 step 之间\n  let af = 0;\n  const cam = { ...POSES[0] };\n  for (let i = 0; i < FLY_AT.length; i++) {\n    const f = seg(t, FLY_AT[i], FLY_AT[i] + FLY, E.inOutCubic);\n    af += f;\n    const p = POSES[i + 1];\n    cam.x = lerp(f, cam.x, p.x); cam.y = lerp(f, cam.y, p.y); cam.z = lerp(f, cam.z, p.z);\n    cam.rx = lerp(f, cam.rx, p.rx); cam.ry = lerp(f, cam.ry, p.ry); cam.rz = lerp(f, cam.rz, p.rz);\n    cam.s = lerp(f, cam.s, p.s);\n  }\n  // overview 段进度（此时全部卡片点亮）\n  const over = seg(t, FLY_AT[2], FLY_AT[2] + FLY);\n\n  return (\n    <DesignStage bg=\"#0a0b10\">\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          overflow: 'hidden',\n          background: 'radial-gradient(ellipse at 50% 40%,#141828 0%,#0a0b10 70%)',\n          perspective: '1000px',\n        }}\n      >\n        {/* world：相机 = 场景逆变换 —— 先反平移，再反旋转，再 1/scale */}\n        <div\n          style={{\n            position: 'absolute',\n            left: '50%',\n            top: '50%',\n            width: 0,\n            height: 0,\n            transformStyle: 'preserve-3d',\n            willChange: 'transform',\n            transform: `scale(${1 / cam.s})\n              rotateZ(${-cam.rz}deg) rotateY(${-cam.ry}deg) rotateX(${-cam.rx}deg)\n              translate3d(${-cam.x}px,${-cam.y}px,${-cam.z}px)`,\n          }}\n        >\n          {POSES.map((p, i) => {\n            const last = i === POSES.length - 1;\n            // enter/exit：距当前视点越远越暗越糊（overview 时全部点亮）\n            const d = Math.min(1, Math.abs(af - i));\n            const focus = Math.max(1 - d, over);\n            return (\n              <div\n                key={i}\n                style={{\n                  position: 'absolute',\n                  left: last ? -160 : -110,\n                  top: last ? -100 : -70,\n                  width: last ? 320 : 220,\n                  height: last ? 200 : 140,\n                  boxSizing: 'border-box',\n                  borderRadius: 10,\n                  padding: '18px 20px',\n                  willChange: 'opacity,filter',\n                  background: `linear-gradient(150deg,hsl(${p.hue},45%,16%),hsl(${p.hue},55%,9%))`,\n                  border: `1px solid hsl(${p.hue},60%,34%)`,\n                  boxShadow: `0 18px 50px rgba(0,0,0,.55), inset 0 1px 0 hsla(${p.hue},70%,70%,.25)`,\n                  transform: `translate3d(${p.x}px,${p.y}px,${p.z}px) rotateX(${p.rx}deg) rotateY(${p.ry}deg) rotateZ(${p.rz}deg) scale(${p.s / (last ? 2.2 : 1)})`,\n                  fontFamily: '-apple-system,system-ui,sans-serif',\n                  color: '#eef1f8',\n                  opacity: 0.28 + focus * 0.72,\n                  filter: `blur(${(1 - focus) * 3.5}px)`,\n                }}\n              >\n                <div style={{ fontSize: 11, letterSpacing: 3, color: `hsl(${p.hue},80%,68%)`, fontWeight: 700 }}>\n                  {p.tt}\n                </div>\n                <div style={{ fontSize: last ? 26 : 21, fontWeight: 800, marginTop: 8 }}>{p.sub}</div>\n                <div style={{ marginTop: 12, height: 5, width: '56%', borderRadius: 3, background: `hsl(${p.hue},70%,45%)` }} />\n                <div style={{ marginTop: 7, height: 5, width: '34%', borderRadius: 3, background: `hsla(${p.hue},50%,60%,.4)` }} />\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}