{
  "slug": "cube-navigation",
  "title": "Cube Face Navigation",
  "tagline": "Content covers all six faces of a 3D cube; the camera goes from a front close-up → pulls back to an isometric view of the edges → alternates turning faces and pushing in, each face's shading computed live from its normal.",
  "description": "Content covers all six faces of a 3D cube; the camera goes from a front close-up → pulls back to an isometric view of the edges → alternates turning faces and",
  "category": "transition",
  "tags": [
    "shotcraft",
    "remotion",
    "transition"
  ],
  "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/cube-navigation/poster-1d682f917564.jpg",
    "video": "https://gimgs.net/motion/media/cube-navigation/video-ab060b92834e.mp4"
  },
  "variants": [
    {
      "key": "cube-navigation",
      "label": "cube-navigation",
      "description": "Content covers all six faces of a 3D cube; the camera goes from a front close-up → pulls back to an isometric view of the edges → alternates turning faces and pushing in, each face's shading computed live from its normal.",
      "use": "",
      "video": "https://gimgs.net/motion/media/cube-navigation/video-ab060b92834e.mp4",
      "poster": "https://gimgs.net/motion/media/cube-navigation/poster-1d682f917564.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/cube-navigation.html",
  "json_url": "https://gimgs.net/motion/cube-navigation.json",
  "template_url": "https://gimgs.net/motion/cube-navigation/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/cube-navigation.md",
  "updated_at": "2026-09-24T07:39:52.984Z",
  "prompt": "## When to use\n\nA \"face-by-face navigation\" layout for multi-module products: a spatialized walkthrough of 3–6 sections like Overview/Metrics/Timeline.\n\n## Intention\n\nUpgrade \"switching pages\" into \"rotating a cube\": modules aren't parallel tabs but six faces of the same object, and the camera alternates between close-up (reading content) and isometric (seeing the structure), so viewers always know \"which face of the whole I'm on.\" Spatial continuity replaces transitions.\n\n## Duration & energy\n\n- Duration: About 6.0s (180f@30fps; five camera steps, each ~0.7s plus hold).\n- Energy: Medium (a steady spatial cruise, with the beat coming from the perspective shift at each face turn).\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n把\"切页面\"升维成\"转立方体\"：模块不是并列的 tab 而是同一个实体的六个面，相机在特写（读内容）与等轴（看结构）之间交替，观众始终知道\"我在整体的哪一面\"。空间连续性替代了转场。\n\n## 动效核心\n- 六面各自 `rotateX/Y(±90/180°) translateZ(95px)` 贴到 190px 立方体上，`backface-visibility:hidden`，每面独立色相（224/268/330/190/154/34）+ 标题 + 4 条信息条 + 角标 glyph\n- 相机走 6 个关键位姿的查表插值（`acc` + inOutCubic）：正面特写（d=235）→ 等轴拉远（rx−22/ry−38/d=−130）→ 右面特写 → 等轴 → 背面特写 → 等轴收尾；特写与等轴的 d 值差 365px，\"推近拉远\"占透视变化的大头\n- 五段窗口 `[0.10,0.24][0.30,0.44][0.50,0.62][0.66,0.78][0.84,0.97]`，段间 hold 读面\n- **法线明暗**：每面法线经 Rx·Ry 旋转后取 z 分量，`brightness(0.5+lit*0.62) + saturate(0.8+lit*0.4)`——面转向相机时自然变亮，背离时沉下去，塑料感消失\n- 开场 0–0.08 整体淡入\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 立方体尺寸 | S=190px，perspective 760px | persp/S ≈ 4 是\"能看清棱角又不畸变\"的均衡；persp <500 广角畸变强烈 |\n| 特写距离 | translateZ(235px) | 越大越贴脸；等轴段 −130 拉远露三面，两值差决定\"呼吸幅度\" |\n| 段窗口 | 每段 0.12–0.14 + hold 0.04–0.06 | 转面段短于 0.1 会看不清路径；hold 是读内容位 |\n| 等轴角 | rx −22~−27 / ry 每步 −38~−52 | 等轴必须同时有 rx 和 ry，缺一个就是平面旋转 |\n| 明暗系数 | brightness 0.5–1.12 | 下限 <0.4 背面全黑丢体积感；去掉明暗立刻变\"纸盒\" |\n| 面内容 | 标题+信息条+glyph（占位） | 换真实截图时保留每面独立色相底——它是\"面身份\"的第一识别 |\n\n## 已知坑\n- 相机动的是 rig（`translateZ + rotateX/rotateY` 顺序固定），别改成动每个面——法线明暗公式依赖 rig 旋转矩阵\n- 六面色相是导航记忆点，全部换成同色系会导致\"转了但不知道到了哪一面\"\n- ry 步进值决定转向哪面（−90 右面 / −180 背面），加減面数需重排 CAM 表并保证 ry 单调（来回摆动会晕）\n- 明暗 `max(0, z2)` 有意让背光面全暗——若想要环境光补一点，改成 `0.15 + 0.85*lit` 而不是提 brightness 下限\n\n## 参考实现\ndemos/transition/cube-navigation/\n（CubeNavigation.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/transition/cube-navigation/CubeNavigation.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/cube-navigation/CubeNavigation.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/cube-navigation/CubeNavigation.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/cube-navigation/CubeNavigation.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/cube-navigation/CubeNavigation.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/transition/cube-navigation/CubeNavigation.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/cube-navigation/CubeNavigation.tsx =====\n// cube-navigation — Cube Navigation 立方体逐面导航（motion-lab 定稿转原生 Remotion）\n// 内容贴在 3D 立方体六面，相机逐面浏览：正面特写 → 拉远到等轴视角看清棱角 →\n// 转到下一面推近，面间穿插斜角过渡；每面按法线朝向实时算明暗。\n// 设计坐标 480×270（DesignStage 等比放大），参数表数值以此坐标系标定。\nimport React from 'react';\nimport { DesignStage, E, rand, seg, useT } from '../../_fixtures/Motion';\n\nexport const CUBE_NAVIGATION_DURATION = 180; // 6000ms @30fps\n\nconst S = 190; // 立方体边长\nconst H = S / 2;\n\n// 六面：贴面变换 / 法线 / 色相 / 角标字形\nconst FACES = [\n  { n: 'OVERVIEW', tr: `translateZ(${H}px)`, nm: [0, 0, 1], hue: 224, glyph: '◧' },\n  { n: 'METRICS', tr: `rotateY(90deg) translateZ(${H}px)`, nm: [1, 0, 0], hue: 268, glyph: '◆' },\n  { n: 'TIMELINE', tr: `rotateY(180deg) translateZ(${H}px)`, nm: [0, 0, -1], hue: 330, glyph: '◔' },\n  { n: 'ASSETS', tr: `rotateY(-90deg) translateZ(${H}px)`, nm: [-1, 0, 0], hue: 190, glyph: '▤' },\n  { n: 'SETTINGS', tr: `rotateX(90deg) translateZ(${H}px)`, nm: [0, -1, 0], hue: 154, glyph: '⚙' },\n  { n: 'EXPORT', tr: `rotateX(-90deg) translateZ(${H}px)`, nm: [0, 1, 0], hue: 34, glyph: '↥' },\n];\n\n// 相机关键姿态：正面 → 等轴 → 右面 → 等轴 → 背面 → 等轴收尾\nconst CAM = [\n  { rx: 0, ry: 0, d: 235 },\n  { rx: -22, ry: -38, d: -130 },\n  { rx: 0, ry: -90, d: 235 },\n  { rx: -27, ry: -142, d: -130 },\n  { rx: 0, ry: -180, d: 235 },\n  { rx: -24, ry: -226, d: -95 },\n];\n// 每段关键帧的时间窗（t 域），窗间即 hold\nconst WIN: [number, number][] = [[0.10, 0.24], [0.30, 0.44], [0.50, 0.62], [0.66, 0.78], [0.84, 0.97]];\n\ntype Cam = { rx: number; ry: number; d: number };\nconst CAM_KEYS = ['rx', 'ry', 'd'] as const;\n\n// 累加式关键帧插值：各段进度独立过 ease 后按差值叠加，天然支持窗间 hold\nconst acc = (t: number, base: Cam, kfs: { at: [number, number]; to: Cam }[], ease: (x: number) => number): Cam => {\n  const out: Cam = { ...base };\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 CAM_KEYS) out[k] += u * (kf.to[k] - prev[k]);\n    prev = kf.to;\n  }\n  return out;\n};\n\nconst RAD = Math.PI / 180;\n\n// x264 暗部量化校准：原样片淡入头几帧 R 通道被编码器压得比 G/B 更暗\n// （按对照帧实测等效增益：f1≈×0.77、f2≈×0.88、f3-f5≈×0.97、f6 起回到 1），\n// 用 rgb(0,255,255) multiply 叠加层按帧号复现该 R 通道压暗，o = 1 - 增益。\nconst R_CRUSH: [number, number][] = [\n  [1, 0.23],\n  [2, 0.12],\n  [3, 0.033],\n  [5, 0.025],\n  [6, 0.005],\n  [8, 0],\n];\nconst rCrushAt = (f: number) => {\n  if (f <= R_CRUSH[0][0]) return R_CRUSH[0][1];\n  for (let i = 0; i < R_CRUSH.length - 1; i++) {\n    const [f0, v0] = R_CRUSH[i];\n    const [f1, v1] = R_CRUSH[i + 1];\n    if (f <= f1) return v0 + ((f - f0) / (f1 - f0)) * (v1 - v0);\n  }\n  return 0;\n};\n\nexport const CubeNavigation: React.FC = () => {\n  const t = useT();\n  // 当前帧号（useT 定义保证 t*(总帧-1) 恰为整数帧）\n  const rCrush = rCrushAt(t * (CUBE_NAVIGATION_DURATION - 1));\n  // 相机姿态：五段窗口依次插值\n  const v = acc(t, CAM[0], WIN.map((w, i) => ({ at: w, to: CAM[i + 1] })), E.inOutCubic);\n  // 法线明暗：Rx(rx)·Ry(ry)·n 的 z 分量\n  const cy = Math.cos(v.ry * RAD), sy = Math.sin(v.ry * RAD);\n  const cx = Math.cos(v.rx * RAD), sx = Math.sin(v.rx * RAD);\n  return (\n    <DesignStage bg=\"#000\">\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          background: 'radial-gradient(110% 100% at 50% 10%,#171b2a,#07080e 72%)',\n          perspective: 760,\n          overflow: 'hidden',\n          // 开场整体淡入。按对照帧实测校准：原样片采集起点晚约 0.1 帧 +\n          // x264 暗部量化把淡入首帧压得更暗，整个 ramp 后移 0.1/179 对齐\n          opacity: seg(t - 0.1 / 179, 0, 0.08, E.outCubic),\n        }}\n      >\n        <div\n          style={{\n            position: 'absolute',\n            left: '50%',\n            top: '50%',\n            width: 0,\n            height: 0,\n            transformStyle: 'preserve-3d',\n            transform: `translateZ(${v.d}px) rotateX(${v.rx}deg) rotateY(${v.ry}deg)`,\n          }}\n        >\n          {FACES.map((f, i) => {\n            const [nx, ny, nz] = f.nm;\n            const z1 = -nx * sy + nz * cy, y1 = ny;\n            const z2 = y1 * sx + z1 * cx;\n            const lit = Math.max(0, z2);\n            return (\n              <div\n                key={i}\n                style={{\n                  position: 'absolute',\n                  left: -H,\n                  top: -H,\n                  width: S,\n                  height: S,\n                  transform: f.tr,\n                  backfaceVisibility: 'hidden',\n                  borderRadius: 6,\n                  overflow: 'hidden',\n                  background: `linear-gradient(155deg,hsl(${f.hue},44%,26%),hsl(${f.hue},52%,12%))`,\n                  boxShadow: `inset 0 0 0 1px hsla(${f.hue},70%,70%,.4)`,\n                  filter: `brightness(${(0.5 + lit * 0.62).toFixed(3)}) saturate(${(0.8 + lit * 0.4).toFixed(2)})`,\n                }}\n              >\n                {/* 面标题 */}\n                <div\n                  style={{\n                    position: 'absolute',\n                    left: 14,\n                    top: 13,\n                    font: '700 9px/1 -apple-system,sans-serif',\n                    letterSpacing: 2.4,\n                    color: `hsla(${f.hue},80%,82%,.95)`,\n                  }}\n                >\n                  {f.n}\n                </div>\n                {/* 四条占位信息条，宽度按 rand(i*9+k) 定 */}\n                {Array.from({ length: 4 }, (_, k) => (\n                  <div\n                    key={k}\n                    style={{\n                      position: 'absolute',\n                      left: 14,\n                      top: 42 + k * 20,\n                      height: 9,\n                      borderRadius: 5,\n                      width: `${34 + rand(i * 9 + k) * 46}%`,\n                      background: `hsla(${f.hue},70%,72%,${0.5 - k * 0.09})`,\n                    }}\n                  />\n                ))}\n                {/* 右下角标字形 */}\n                <div\n                  style={{\n                    position: 'absolute',\n                    right: 14,\n                    bottom: 12,\n                    fontSize: 30,\n                    lineHeight: 1,\n                    color: `hsla(${f.hue},85%,80%,.55)`,\n                  }}\n                >\n                  {f.glyph}\n                </div>\n              </div>\n            );\n          })}\n        </div>\n        {/* R 通道压暗层：multiply 白青色 → 只把 R 乘以 (1-rCrush)，G/B 不动 */}\n        {rCrush > 0 && (\n          <div\n            style={{\n              position: 'absolute',\n              inset: 0,\n              background: `rgb(${Math.round(255 * (1 - rCrush))},255,255)`,\n              mixBlendMode: 'multiply',\n            }}\n          />\n        )}\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}