Motion · Transition

Cube Face Navigation

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.

1920×108016:930 fps6.5s · 195 framesremotionLicense: Apache-2.0#shotcraft#remotion#transition

Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player

When to use

A "face-by-face navigation" layout for multi-module products: a spatialized walkthrough of 3–6 sections like Overview/Metrics/Timeline.

Intention

Upgrade "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.

Duration & energy

  • Duration: About 6.0s (180f@30fps; five camera steps, each ~0.7s plus hold).
  • Energy: Medium (a steady spatial cruise, with the beat coming from the perspective shift at each face turn).

Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)

意图

把"切页面"升维成"转立方体":模块不是并列的 tab 而是同一个实体的六个面,相机在特写(读内容)与等轴(看结构)之间交替,观众始终知道"我在整体的哪一面"。空间连续性替代了转场。

动效核心

  • 六面各自 rotateX/Y(±90/180°) translateZ(95px) 贴到 190px 立方体上,backface-visibility:hidden,每面独立色相(224/268/330/190/154/34)+ 标题 + 4 条信息条 + 角标 glyph
  • 相机走 6 个关键位姿的查表插值(acc + inOutCubic):正面特写(d=235)→ 等轴拉远(rx−22/ry−38/d=−130)→ 右面特写 → 等轴 → 背面特写 → 等轴收尾;特写与等轴的 d 值差 365px,"推近拉远"占透视变化的大头
  • 五段窗口 [0.10,0.24][0.30,0.44][0.50,0.62][0.66,0.78][0.84,0.97],段间 hold 读面
  • 法线明暗:每面法线经 Rx·Ry 旋转后取 z 分量,brightness(0.5+lit*0.62) + saturate(0.8+lit*0.4)——面转向相机时自然变亮,背离时沉下去,塑料感消失
  • 开场 0–0.08 整体淡入

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 立方体尺寸 | S=190px,perspective 760px | persp/S ≈ 4 是"能看清棱角又不畸变"的均衡;persp <500 广角畸变强烈 | | 特写距离 | translateZ(235px) | 越大越贴脸;等轴段 −130 拉远露三面,两值差决定"呼吸幅度" | | 段窗口 | 每段 0.12–0.14 + hold 0.04–0.06 | 转面段短于 0.1 会看不清路径;hold 是读内容位 | | 等轴角 | rx −22~−27 / ry 每步 −38~−52 | 等轴必须同时有 rx 和 ry,缺一个就是平面旋转 | | 明暗系数 | brightness 0.5–1.12 | 下限 <0.4 背面全黑丢体积感;去掉明暗立刻变"纸盒" | | 面内容 | 标题+信息条+glyph(占位) | 换真实截图时保留每面独立色相底——它是"面身份"的第一识别 |

已知坑

  • 相机动的是 rig(translateZ + rotateX/rotateY 顺序固定),别改成动每个面——法线明暗公式依赖 rig 旋转矩阵
  • 六面色相是导航记忆点,全部换成同色系会导致"转了但不知道到了哪一面"
  • ry 步进值决定转向哪面(−90 右面 / −180 背面),加減面数需重排 CAM 表并保证 ry 单调(来回摆动会晕)
  • 明暗 max(0, z2) 有意让背光面全暗——若想要环境光补一点,改成 0.15 + 0.85*lit 而不是提 brightness 下限

参考实现

demos/transition/cube-navigation/ (CubeNavigation.tsx)

Reference implementation (Remotion)

  • demos/transition/cube-navigation/CubeNavigation.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/cube-navigation/CubeNavigation.tsx

The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.

Everything on this page is also machine-readable. Point your agent at the JSON, or add the gimgs MCP server and ask it to search motion templates.

  1. Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
  2. Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
  3. Register the component as a <Composition> in a Remotion project and render with `npx remotion render`, or re-implement the recipe in your own engine.
curl -s https://gimgs.net/motion/cube-navigation.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/transition/cube-navigation/CubeNavigation.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("cube-navigation")

Prompt

Use the video-shotcraft shot recipe "Cube Face Navigation" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/cube-navigation.json
Raw TSX: https://gimgs.net/motion/cube-navigation/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/transition/cube-navigation/CubeNavigation.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 6.5s

1. Read the JSON and follow its "prompt" section (when to use, intention, parameter table, known pitfalls).
2. Adapt it for: <describe what you want>.
3. Either clone the repo, register the component as a <Composition> in a Remotion project and run npx remotion render <CompositionId> out.mp4 — or re-implement the recipe in your own engine (HTML/WAAPI works: keep the same timing and easing).

JSON · Markdown · llms.txt

6,520 chars · Apache-2.0Open raw template ↗
// ===== demos/transition/cube-navigation/CubeNavigation.tsx =====
// cube-navigation — Cube Navigation 立方体逐面导航(motion-lab 定稿转原生 Remotion)
// 内容贴在 3D 立方体六面,相机逐面浏览:正面特写 → 拉远到等轴视角看清棱角 →
// 转到下一面推近,面间穿插斜角过渡;每面按法线朝向实时算明暗。
// 设计坐标 480×270(DesignStage 等比放大),参数表数值以此坐标系标定。
import React from 'react';
import { DesignStage, E, rand, seg, useT } from '../../_fixtures/Motion';

export const CUBE_NAVIGATION_DURATION = 180; // 6000ms @30fps

const S = 190; // 立方体边长
const H = S / 2;

// 六面:贴面变换 / 法线 / 色相 / 角标字形
const FACES = [
  { n: 'OVERVIEW', tr: `translateZ(${H}px)`, nm: [0, 0, 1], hue: 224, glyph: '◧' },
  { n: 'METRICS', tr: `rotateY(90deg) translateZ(${H}px)`, nm: [1, 0, 0], hue: 268, glyph: '◆' },
  { n: 'TIMELINE', tr: `rotateY(180deg) translateZ(${H}px)`, nm: [0, 0, -1], hue: 330, glyph: '◔' },
  { n: 'ASSETS', tr: `rotateY(-90deg) translateZ(${H}px)`, nm: [-1, 0, 0], hue: 190, glyph: '▤' },
  { n: 'SETTINGS', tr: `rotateX(90deg) translateZ(${H}px)`, nm: [0, -1, 0], hue: 154, glyph: '⚙' },
  { n: 'EXPORT', tr: `rotateX(-90deg) translateZ(${H}px)`, nm: [0, 1, 0], hue: 34, glyph: '↥' },
];

// 相机关键姿态:正面 → 等轴 → 右面 → 等轴 → 背面 → 等轴收尾
const CAM = [
  { rx: 0, ry: 0, d: 235 },
  { rx: -22, ry: -38, d: -130 },
  { rx: 0, ry: -90, d: 235 },
  { rx: -27, ry: -142, d: -130 },
  { rx: 0, ry: -180, d: 235 },
  { rx: -24, ry: -226, d: -95 },
];
// 每段关键帧的时间窗(t 域),窗间即 hold
const WIN: [number, number][] = [[0.10, 0.24], [0.30, 0.44], [0.50, 0.62], [0.66, 0.78], [0.84, 0.97]];

type Cam = { rx: number; ry: number; d: number };
const CAM_KEYS = ['rx', 'ry', 'd'] as const;

// 累加式关键帧插值:各段进度独立过 ease 后按差值叠加,天然支持窗间 hold
const acc = (t: number, base: Cam, kfs: { at: [number, number]; to: Cam }[], ease: (x: number) => number): Cam => {
  const out: Cam = { ...base };
  let prev = base;
  for (const kf of kfs) {
    const u = seg(t, kf.at[0], kf.at[1], ease);
    for (const k of CAM_KEYS) out[k] += u * (kf.to[k] - prev[k]);
    prev = kf.to;
  }
  return out;
};

const RAD = Math.PI / 180;

// x264 暗部量化校准:原样片淡入头几帧 R 通道被编码器压得比 G/B 更暗
// (按对照帧实测等效增益:f1≈×0.77、f2≈×0.88、f3-f5≈×0.97、f6 起回到 1),
// 用 rgb(0,255,255) multiply 叠加层按帧号复现该 R 通道压暗,o = 1 - 增益。
const R_CRUSH: [number, number][] = [
  [1, 0.23],
  [2, 0.12],
  [3, 0.033],
  [5, 0.025],
  [6, 0.005],
  [8, 0],
];
const rCrushAt = (f: number) => {
  if (f <= R_CRUSH[0][0]) return R_CRUSH[0][1];
  for (let i = 0; i < R_CRUSH.length - 1; i++) {
    const [f0, v0] = R_CRUSH[i];
    const [f1, v1] = R_CRUSH[i + 1];
    if (f <= f1) return v0 + ((f - f0) / (f1 - f0)) * (v1 - v0);
  }
  return 0;
};

export const CubeNavigation: React.FC = () => {
  const t = useT();
  // 当前帧号(useT 定义保证 t*(总帧-1) 恰为整数帧)
  const rCrush = rCrushAt(t * (CUBE_NAVIGATION_DURATION - 1));
  // 相机姿态:五段窗口依次插值
  const v = acc(t, CAM[0], WIN.map((w, i) => ({ at: w, to: CAM[i + 1] })), E.inOutCubic);
  // 法线明暗:Rx(rx)·Ry(ry)·n 的 z 分量
  const cy = Math.cos(v.ry * RAD), sy = Math.sin(v.ry * RAD);
  const cx = Math.cos(v.rx * RAD), sx = Math.sin(v.rx * RAD);
  return (
    <DesignStage bg="#000">
      <div
        style={{
          position: 'absolute',
          inset: 0,
          background: 'radial-gradient(110% 100% at 50% 10%,#171b2a,#07080e 72%)',
          perspective: 760,
          overflow: 'hidden',
          // 开场整体淡入。按对照帧实测校准:原样片采集起点晚约 0.1 帧 +
          // x264 暗部量化把淡入首帧压得更暗,整个 ramp 后移 0.1/179 对齐
          opacity: seg(t - 0.1 / 179, 0, 0.08, E.outCubic),
        }}
      >
        <div
          style={{
            position: 'absolute',
            left: '50%',
            top: '50%',
            width: 0,
            height: 0,
            transformStyle: 'preserve-3d',
            transform: `translateZ(${v.d}px) rotateX(${v.rx}deg) rotateY(${v.ry}deg)`,
          }}
        >
          {FACES.map((f, i) => {
            const [nx, ny, nz] = f.nm;
            const z1 = -nx * sy + nz * cy, y1 = ny;
            const z2 = y1 * sx + z1 * cx;
            const lit = Math.max(0, z2);
            return (
              <div
                key={i}
                style={{
                  position: 'absolute',
                  left: -H,
                  top: -H,
                  width: S,
                  height: S,
                  transform: f.tr,
                  backfaceVisibility: 'hidden',
                  borderRadius: 6,
                  overflow: 'hidden',
                  background: `linear-gradient(155deg,hsl(${f.hue},44%,26%),hsl(${f.hue},52%,12%))`,
                  boxShadow: `inset 0 0 0 1px hsla(${f.hue},70%,70%,.4)`,
                  filter: `brightness(${(0.5 + lit * 0.62).toFixed(3)}) saturate(${(0.8 + lit * 0.4).toFixed(2)})`,
                }}
              >
                {/* 面标题 */}
                <div
                  style={{
                    position: 'absolute',
                    left: 14,
                    top: 13,
                    font: '700 9px/1 -apple-system,sans-serif',
                    letterSpacing: 2.4,
                    color: `hsla(${f.hue},80%,82%,.95)`,
                  }}
                >
                  {f.n}
                </div>
                {/* 四条占位信息条,宽度按 rand(i*9+k) 定 */}
                {Array.from({ length: 4 }, (_, k) => (
                  <div
                    key={k}
                    style={{
                      position: 'absolute',
                      left: 14,
                      top: 42 + k * 20,
                      height: 9,
                      borderRadius: 5,
                      width: `${34 + rand(i * 9 + k) * 46}%`,
                      background: `hsla(${f.hue},70%,72%,${0.5 - k * 0.09})`,
                    }}
                  />
                ))}
                {/* 右下角标字形 */}
                <div
                  style={{
                    position: 'absolute',
                    right: 14,
                    bottom: 12,
                    fontSize: 30,
                    lineHeight: 1,
                    color: `hsla(${f.hue},85%,80%,.55)`,
                  }}
                >
                  {f.glyph}
                </div>
              </div>
            );
          })}
        </div>
        {/* R 通道压暗层:multiply 白青色 → 只把 R 乘以 (1-rCrush),G/B 不动 */}
        {rCrush > 0 && (
          <div
            style={{
              position: 'absolute',
              inset: 0,
              background: `rgb(${Math.round(255 * (1 - rCrush))},255,255)`,
              mixBlendMode: 'multiply',
            }}
          />
        )}
      </div>
    </DesignStage>
  );
};


  1. Read the recipe
    Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
  2. Get the code
    tool: git
    git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install
  3. Register + render
    tool: remotion
    npx remotion render <CompositionId> out.mp4 # component: demos/transition/cube-navigation/CubeNavigation.tsx
  4. Or re-implement
    Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
paramtypedefaultdescription
—

More in this category