{
  "slug": "terminal-3d",
  "title": "3D terminal flythrough",
  "tagline": "Three terminal windows scattered in 3D space; the camera flies between them with a sinusoidal pull-back en route, typing out a command at each stop as results slide out line by line.",
  "description": "Three terminal windows scattered in 3D space; the camera flies between them with a sinusoidal pull-back en route, typing out a command at each stop as results",
  "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/terminal-3d/poster-8b8e4305fc98.jpg",
    "video": "https://gimgs.net/motion/media/terminal-3d/video-56c9de78df2a.mp4"
  },
  "variants": [
    {
      "key": "terminal-3d",
      "label": "terminal-3d",
      "description": "Three terminal windows scattered in 3D space; the camera flies between them with a sinusoidal pull-back en route, typing out a command at each stop as results slide out line by line",
      "use": "",
      "video": "https://gimgs.net/motion/media/terminal-3d/video-56c9de78df2a.mp4",
      "poster": "https://gimgs.net/motion/media/terminal-3d/poster-8b8e4305fc98.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/terminal-3d.html",
  "json_url": "https://gimgs.net/motion/terminal-3d.json",
  "template_url": "https://gimgs.net/motion/terminal-3d/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/terminal-3d.md",
  "updated_at": "2026-09-24T07:40:26.840Z",
  "prompt": "## When to use\n\nCLI/workflow demos for developer products: shooting \"three commands\" as a three-stop journey through space\n\n## Intention\n\nTerminal output is naturally \"flat scrolling,\" an anti-cinematic language; this card spatializes it: each command occupies a real window in space, and the camera visits window by window as if moving through a server room. The pull-back during flight lets the viewer glimpse \"there are more windows\" — a sense of the whole workflow.\n\n## Duration & energy\n\n- Duration: ~6.0s (180f@30fps; each of the 3 stops ~1.6s dwell + 0.85s flight)\n- Energy: Medium (the flight gives motion, the typing gives rhythm; overall a steady, technical narrative)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n终端输出天生是\"平面滚动\"的反电影语言，这张卡把它空间化：每条命令占据空间中一个实体窗口，相机像穿过机房一样逐窗拜访。飞行途中的拉远让观众瞥见\"还有更多窗口\"——工作流的全貌感。\n\n## 动效核心\n- 三窗各持 pose（x/y/z/ry），相机走**逆变换**：`translateZ(300-pull) rotateY(-ry) translate3d(-x,-y,-z)`——写窗口姿态表即得相机路径\n- 两段飞行窗口 STEP `[0.30,0.44][0.64,0.78]`（inOutCubic），途中 `sin(π)*210px` 正弦拉远鼓包——起飞时后撤、到站时贴近，\"呼吸式\"过渡\n- 距离聚焦：每窗按与相机 x 距离算 focus，`opacity 0.34–1 + blur 0–2.2px + brightness 0.7–1`——离焦窗虚化成背景，不抢戏\n- 打字机三要素：命令逐字符（0.09 窗口）、光标两种闪烁频率（打字中 40Hz 基快闪 / 打完 26Hz 慢闪）、输出行按 `0.022/行` 错峰从左滑入（−7px→0）\n- 打字起点 TYPE `[0.05,0.47,0.81]` 均在到站后——飞行中不打字，注意力不分裂\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 拉远鼓包 | sin(π)×210px | 砍掉后飞行是\"平移\"没有\"起降\"；>300 会穿出背景渐变边界 |\n| 飞行/停留比 | 0.14 / 0.20 | 停留必须容下打字 0.09 + 输出 0.145 + 读秒余量 |\n| 聚焦衰减 | /420px，blur 峰值 2.2px | 分母越小离焦越狠；三窗 x 间距（约 640px）应 >1.5 倍分母 |\n| 打字速度 | 命令 0.09 归一化（约 16 字符） | 换长命令保持字符数近似，或按字符数反算窗口宽度 |\n| 输出错峰 | 0.022/行，各 0.035 时长 | 4 行输出合计约 0.15；行多时压缩间隔保证停留段内吐完 |\n| 窗口姿态 | ry ±16–32°，z −110~+90 | 角度差是\"空间散布感\"来源；全部朝正前会退化成平面轮播 |\n\n## 已知坑\n- 相机是逆变换实现，**窗口自身 transform 与相机公式必须严格互逆**——改姿态表只改 pose 数据，别动两处 transform 字符串\n- 命令/输出文案是通用工具链占位（git/npm/log），换项目真实命令时保持\"命令短、输出 3–4 行\"的信息结构\n- 光标闪烁走 `floor(t*26/40)`，dur 改变后闪烁频率跟着变，需目检\"打字中快闪、待机慢闪\"的对比还在\n- 第三窗打字 0.81 起步，输出到 0.955 + settle——TYPE 表后移时留 ≥0.04 尾量，否则末行没进完就切黑\n\n## 参考实现\ndemos/camera/terminal-3d/\n（Terminal3D.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/camera/terminal-3d/Terminal3D.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/terminal-3d/Terminal3D.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/terminal-3d/Terminal3D.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/terminal-3d/Terminal3D.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/camera/terminal-3d/Terminal3D.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/terminal-3d/Terminal3D.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/terminal-3d/Terminal3D.tsx =====\n// terminal-3d — Terminal 3D 命令执行叙事流（motion-lab 定稿转原生 Remotion）\n// 多个通用桌面风格终端窗散布 3D 空间不同位置与角度，相机在窗间飞行（途中略微\n// 拉远）；每到一窗，窗内打字机敲出命令并逐行吐出结果，形成命令执行叙事流。\n// 设计坐标 480×270（DesignStage 等比放大），参数表数值以此坐标系标定。\nimport React from 'react';\nimport { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';\n\nexport const TERMINAL_3D_DURATION = 180; // 6000ms @30fps\n\n// 光栅化补偿系数：3D 子树里 Chrome 按布局尺寸取样文字，经 DesignStage 放大 4x\n// 后小号等宽字会糊。把整个 3D 场景数值 ×K 建模、再用外层 scale(1/K) 抵消——\n// 透视投影对整体缩放自相似（p·x/(p-z) 等比），画面几何与 effect.js 逐像素一致，\n// 仅文字光栅化分辨率提升到与原样片（4x device scale 截帧）相同。\nconst K = 4;\n\n// 关键帧累加器：base 起步，每段 [at0,at1] 内朝 to 推进（效果源文件级共享 helper）\ntype Vec = Record<string, number>;\nconst acc = (t: number, base: Vec, kfs: { at: number[]; to: Vec }[], keys: string[], ease: (x: number) => number) => {\n  const out: Vec = {};\n  for (const k of keys) out[k] = base[k];\n  let prev = base;\n  for (const kf of kfs) {\n    const u = seg(t, kf.at[0], kf.at[1], ease);\n    for (const k of keys) out[k] += u * (kf.to[k] - prev[k]);\n    prev = kf.to;\n  }\n  return out;\n};\n\n// 三个终端窗：3D 位姿 + 标题 + 命令 + 输出行（数值为 480×270 设计坐标）\nconst DATA = [\n  {\n    pose: { x: -300, y: -34, z: -110, ry: 24 },\n    title: '~/workspace — zsh',\n    cmd: '$ git status -sb',\n    out: ['## main...origin/main', ' M src/timeline.ts', ' M src/camera.ts', '?? fx/b01.js'],\n  },\n  {\n    pose: { x: 96, y: 62, z: 90, ry: -16 },\n    title: 'dev server',\n    cmd: '$ npm run dev',\n    out: ['vite v5.2.0  ready in 312 ms', '➜  local:   http://localhost:3000', '➜  network: 192.0.2.10:3000', 'watching 148 modules'],\n  },\n  {\n    pose: { x: 402, y: -74, z: -60, ry: -32 },\n    title: 'logs',\n    cmd: '$ tail -f server.log',\n    out: ['12:04:11 GET /api/render 200 41ms', '12:04:12 POST /api/queue 201 88ms', '12:04:14 worker#3 frame 240/270', '12:04:15 done → out/final.mp4'],\n  },\n];\n\n// Step 时序：每窗 [飞行起, 飞行止]；TYPE 为各窗打字起点\nconst STEP = [[0, 0.02], [0.30, 0.44], [0.64, 0.78]];\nconst TYPE = [0.05, 0.47, 0.81];\nconst PK = ['x', 'y', 'z', 'ry'];\n\nexport const Terminal3D: React.FC = () => {\n  const t = useT();\n\n  // 相机姿态 = 关键帧累加（两段飞行，inOutCubic）\n  const v = acc(t, DATA[0].pose, [\n    { at: STEP[1], to: DATA[1].pose },\n    { at: STEP[2], to: DATA[2].pose },\n  ], PK, E.inOutCubic);\n  // 飞行途中拉远：两段过渡各一个正弦鼓包\n  let pull = 0;\n  for (let i = 1; i < 3; i++) {\n    const u = seg(t, STEP[i][0], STEP[i][1]);\n    pull += Math.sin(u * Math.PI) * 210;\n  }\n\n  return (\n    <DesignStage bg=\"#07080e\">\n      {/* 反缩放包裹层：把 ×K 建模的场景缩回设计坐标（见 K 注释） */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 0,\n          top: 0,\n          width: 480 * K,\n          height: 270 * K,\n          transform: `scale(${1 / K})`,\n          transformOrigin: 'top left',\n        }}\n      >\n        {/* scene：透视容器 + 深空渐变背景 */}\n        <div\n          style={{\n            position: 'absolute',\n            inset: 0,\n            background: 'radial-gradient(120% 90% at 50% 0%,#141826,#07080e 70%)',\n            perspective: `${900 * K}px`,\n            overflow: 'hidden',\n            WebkitFontSmoothing: 'antialiased', // 原样片截帧为灰度平滑，避免笔画偏粗\n          }}\n        >\n          {/* world：相机逆变换载体 —— 先拉远/推近，再反转角，再反平移 */}\n          <div\n            style={{\n              position: 'absolute',\n              inset: 0,\n              transformStyle: 'preserve-3d',\n              transform: `translateZ(${(300 - pull) * K}px) rotateY(${-v.ry}deg) translate3d(${-v.x * K}px,${-v.y * K}px,${-v.z * K}px)`,\n            }}\n          >\n            {DATA.map((d, i) => {\n              const p = d.pose;\n              // 离焦：与当前相机 x 距离越远越暗越糊\n              const focus = 1 - Math.min(1, Math.abs(v.x - p.x) / 420);\n              // 打字机：命令逐字点亮，光标闪烁，输出逐行滑入\n              const ty = seg(t, TYPE[i], TYPE[i] + 0.09);\n              const n = Math.floor(ty * d.cmd.length + 0.0001);\n              const caretOp = ty >= 1\n                ? (Math.floor(t * 26) % 2 ? 0.15 : 0.9)\n                : (Math.floor(t * 40) % 2 ? 0.35 : 1);\n              return (\n                <div\n                  key={i}\n                  style={{\n                    position: 'absolute',\n                    left: '50%',\n                    top: '50%',\n                    width: 300 * K,\n                    height: 176 * K,\n                    margin: `${-88 * K}px 0 0 ${-150 * K}px`,\n                    borderRadius: 9 * K,\n                    background: '#0e1017',\n                    overflow: 'hidden',\n                    boxShadow: `0 ${24 * K}px ${60 * K}px rgba(0,0,0,.7),inset 0 0 0 ${K}px #2a3040`,\n                    transform: `translate3d(${p.x * K}px,${p.y * K}px,${p.z * K}px) rotateY(${p.ry}deg)`,\n                    opacity: 0.34 + focus * 0.66,\n                    filter: `blur(${(1 - focus) * 2.2 * K}px) brightness(${0.7 + focus * 0.3})`,\n                  }}\n                >\n                  {/* 标题栏：红黄绿灯 + 居中标题 */}\n                  <div\n                    style={{\n                      position: 'absolute', left: 0, top: 0, width: '100%', height: 22 * K,\n                      background: 'linear-gradient(180deg,#242a38,#1b202b)', borderBottom: `${K}px solid #2c3242`,\n                    }}\n                  >\n                    {['#ff6058', '#ffbd2e', '#28ca42'].map((c, k) => (\n                      <div\n                        key={c}\n                        style={{\n                          position: 'absolute', left: (9 + k * 13) * K, top: 8 * K, width: 7 * K, height: 7 * K,\n                          borderRadius: '50%', background: c,\n                        }}\n                      />\n                    ))}\n                    <div\n                      style={{\n                        position: 'absolute', left: 0, top: 0, width: '100%', height: 22 * K, textAlign: 'center',\n                        font: `600 ${8 * K}px/${22 * K}px Courier,monospace`, color: '#77809b',\n                      }}\n                    >\n                      {d.title}\n                    </div>\n                  </div>\n                  {/* 命令行：逐字 opacity + 尾随光标 */}\n                  <div\n                    style={{\n                      position: 'absolute', left: 12 * K, top: 32 * K,\n                      font: `600 ${9.5 * K}px/1 Courier,monospace`, color: '#9dffcf', whiteSpace: 'pre',\n                    }}\n                  >\n                    {Array.from(d.cmd, (ch, c) => (\n                      // 空格换成 nbsp，对齐 effect.js 的 textContent 处理\n                      <span key={c} style={{ opacity: c < n ? 1 : 0 }}>{ch === ' ' ? ' ' : ch}</span>\n                    ))}\n                    <span\n                      style={{\n                        color: '#9dffcf',\n                        opacity: caretOp,\n                        display: 'inline-block',\n                        transform: `translateX(${(d.cmd.length - n) * -0.1 * K}px)`,\n                      }}\n                    >\n                      ▌\n                    </span>\n                  </div>\n                  {/* 输出行：命令敲完后逐行 stagger 淡入 + 左滑归位 */}\n                  {d.out.map((o, k) => {\n                    const ou = seg(t, TYPE[i] + 0.10 + k * 0.022, TYPE[i] + 0.145 + k * 0.022, E.outCubic);\n                    return (\n                      <div\n                        key={k}\n                        style={{\n                          position: 'absolute', left: 12 * K, top: (52 + k * 16) * K,\n                          font: `500 ${9 * K}px/1 Courier,monospace`,\n                          color: k === 0 ? '#c9d3ea' : '#7f8aa6',\n                          whiteSpace: 'pre',\n                          opacity: ou,\n                          transform: `translateX(${lerp(ou, -7, 0) * K}px)`,\n                        }}\n                      >\n                        {o}\n                      </div>\n                    );\n                  })}\n                </div>\n              );\n            })}\n          </div>\n        </div>\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}