{
  "slug": "transition-travel",
  "title": "Travel-through transitions",
  "tagline": "Two travel-through transitions — shared-element homecoming and letter-cavity pass-through; the camera drills through a real element inside the frame to complete the scene change.",
  "description": "Two travel-through transitions — shared-element homecoming and letter-cavity pass-through; the camera drills through a real element inside the frame to comple",
  "category": "transition",
  "tags": [
    "shotcraft",
    "remotion",
    "transition",
    "camera"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.39,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/transition-travel/poster-6dfd85714512.jpg",
    "video": "https://gimgs.net/motion/media/transition-travel/video-fd92c21f247f.mp4"
  },
  "variants": [
    {
      "key": "shared-element-morph",
      "label": "shared-element-morph · Shared-element homecoming",
      "description": "A full-screen close-up card shrinks, shifts, and grows rounded corners, flying flush into its slot in the dashboard grid with a 3% overshoot settle",
      "use": "The \"return to place\" meaning of detail-to-overview; used paired with style F's window pass-through (going in the window / coming back to place)",
      "video": "https://gimgs.net/motion/media/transition-travel/video-fd92c21f247f.mp4",
      "poster": "https://gimgs.net/motion/media/transition-travel/poster-6dfd85714512.jpg"
    },
    {
      "key": "letterform-zoom",
      "label": "letterform-zoom · Letter-cavity pass-through",
      "description": "A giant title's letter cavities (cut out with an SVG mask) reveal the new page through them; an exponential push drills through the cavity, and the instant it fills the frame the new page takes over, with the remaining strokes flung off-screen",
      "use": "A chapter title-to-body transition; a higher-energy alternative to style D's black-screen text card",
      "video": "https://gimgs.net/motion/media/transition-travel/video-b4a98bc27b13.mp4",
      "poster": "https://gimgs.net/motion/media/transition-travel/poster-letterform-zoom-855263a20614.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/transition-travel.html",
  "json_url": "https://gimgs.net/motion/transition-travel.json",
  "template_url": "https://gimgs.net/motion/transition-travel/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/transition-travel.md",
  "updated_at": "2026-09-24T07:40:29.839Z",
  "prompt": "## When to use\n\nJoins where the two shots share an element/container-level spatial relationship (a technique card, used complementarily with the six shot-transitions moves)\n\n## Intention\n\nThe six shot-transitions moves are \"handoff\" type — the outgoing shot closes, the incoming shot enters, and the join is covered by a white flash/black hold/focus pull/black screen/blur frame/window. This card's two styles are \"travel-through\" type — the background already exists as a real element inside the foreground frame (a grid slot / a letter cavity), and the camera makes one continuous spatial move drilling into it, so the viewer follows the same object the whole way with no handoff moment. The selection rule: if the background can already be \"present in advance\" inside the foreground, use travel-through; if the two shots are merely sequential with no spatial nesting, use handoff. The two styles further split by meaning — A is \"putting this card back where it belongs\" (detail-to-overview, the reverse of style F's window pass-through); C is \"the title itself is the door\" (a chapter text card and a transition rolled into one).\n\n## Duration & energy\n\n- Duration: n/a (a technique card; each style's action segment is 25–60f, holds before/after counted separately, frames drawn from the neighboring shots' budget)\n- Energy: n/a (a technique card, doesn't occupy an energy slot)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\nshot-transitions 六式是\"交棒\"型——前镜收尾、后镜入场，接缝靠白闪/暗场/\n焦点/黑场/糊帧/窗口盖住换页瞬间。本卡两式是\"穿越\"型——后景在前景画面里\n以真实元素存在（网格槽位/字形洞），镜头一条连续空间运动钻进去，\n观众从头到尾跟着同一个物体，没有交棒瞬间。选型口径：后景能在前景里\"预先\n在场\"→穿越；两景只是先后关系、无空间嵌套→交棒。两式内部再按语义分——\nA 是\"把这张卡放回去\"（详情→总览，F 式穿窗的逆向）；C 是\"标题即是门\"\n（章节字卡与转场二合一）。\n\n## 两式选型表\n\n| 式 | 做法 | 适用接缝 |\n|----|------|----------|\n| A 共享元素归位 shared-element-morph | 全屏特写卡收缩+位移+长出圆角，严丝合缝飞落进 dashboard 网格所属槽位，3% 过冲落座 | 详情→总览的\"归位\"语义；与 F 式穿窗成对使用（进窗/回位）|\n| C 字腔穿越 letterform-zoom | 巨型标题字腔（SVG mask 挖洞）透出新页，指数推进穿洞，洞撑满瞬间接管、残余笔画甩出画外 | 章节标题→正文；替代 D 式黑场字卡的高能量款 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A morph 主曲线 | 25f bezier(0.4,0,0.2,1) 冲到 1.03 过冲 + 10f Easing.out(cubic) 弹回 1；位置/尺寸/圆角共用同一条进度 p | 三属性各走各的曲线立刻读作\"变形\"而非\"同一物体\"；过冲 >5% 读作果冻 |\n| A 同一物体感 | 内容按槽位尺寸渲染、随容器等比缩放（scale 3.66→1，origin 左上）；投影随 p 从 0/36/110/0.32 收敛到 0/2/8/0.06 | 内容重排版=换了张卡，穿越感断；投影不收敛则卡永远悬空、落不了座 |\n| A 落座仪式 | 背景 dashboard 0.9 透明度待命，落座瞬间 5f 提到 1 | 背景全程满亮则飞落没有\"到家\"的一拍；提亮窗口跨骑落座帧前后 |\n| C 穿腔主推 | 盖板 SVG mask（白底+黑字 900 字重）挖洞，transform-origin 对准腔心（demo：\"DASH\" A 腔 ≈773,508）；scale=28^t，t 60f bezier(0.6,0,0.85,0.5) 慢起陡收 | ZOOM_MAX 按\"洞撑满画面\"配，字腔越小倍数越大；慢起段留给观众看清\"洞里有东西\" |\n| C 撤场与呼应 | scale∈[15,24] 盖板 opacity 1→0（残余笔画边飞边撤）；期望模糊量 0→16px 除以 scale 写进 blur 补偿；洞内 B 层微 dolly 1→1.1、接管后 25f 落回 1 | blur 不除 scale 会被 transform 放大成糊墙；B 不跟着 dolly 则洞里是张死图，\"穿进活页面\"的感知断掉 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验；灰阶测不出纹理级缺陷（轮 A 判例），真实素材\n  上先出静帧自检\n- C 式腔心坐标是字体度量估算（字宽 em 值手推），换字体/换词/渲染器字距\n  不同都会漂——渲后看第一帧推进方向，洞不居中就微调 origin，别信算出来的数\n- A 式槽位坐标必须精确到像素（demo 里从 fixtures 布局逐项推算 808,590,\n  524x454,r14）——差几像素落座后再对齐，观众读作\"飞到附近再吸附\"，\n  \"归位\"的严丝合缝感全无\n- 穿越两式与交棒六式同一接缝不叠加，一缝一式；穿越式已自带高能量，\n  再包白闪/震屏读作穿帮（规矩同 shot-transitions）\n\n## 参考实现\ndemos/transition/transition-travel/\n（LetterformZoom.tsx / SharedElementMorph.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/transition/transition-travel/LetterformZoom.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-travel/LetterformZoom.tsx\n- demos/transition/transition-travel/SharedElementMorph.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-travel/SharedElementMorph.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/transition-travel/LetterformZoom.tsx, demos/transition/transition-travel/SharedElementMorph.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/transition-travel/LetterformZoom.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-travel/LetterformZoom.tsx"
    },
    {
      "path": "demos/transition/transition-travel/SharedElementMorph.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-travel/SharedElementMorph.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": 132,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-travel/LetterformZoom.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/transition/transition-travel/SharedElementMorph.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/transition-travel/LetterformZoom.tsx =====\nimport React, { useId } from 'react';\nimport { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';\nimport { FakeDashboard, G } from '../../_fixtures/Fixtures';\n\n// letterform-zoom〔转场〕：巨型标题 \"DASH\" 字腔透出新页面，镜头急速推进\n// 字母 A 的三角字腔，洞被撑满全屏的瞬间新页面接管，残余笔画滑出画外。\n// 结构：底层 FakeDashboard B 静置全屏（微 dolly）；上层\"米灰盖板\"用\n// SVG <mask>（白底 + 黑字）在字形处挖洞——洞里即透出 B；对盖板整组做\n// transform-origin 对准 A 字腔中心的指数 scale 1→28（60f，前 20f 慢\n// 后 40f 陡）。scale 过临界值后盖板快速退场 = 撤掉 mask，B 全屏接管。\n\nconst FS = 560; // 标题字号\n// A 字腔（三角洞）中心：Helvetica Bold 度量估算——\n// \"DASH\" 总宽 ≈ 2.833em，居中起点 x≈167，A 占 571–975 → 腔心 x≈773；\n// 基线 y=741（大写居中），腔心约在基线上方 0.42em → y≈508。\nconst ORIGIN = { x: 773, y: 508 };\nconst BASELINE = 741;\nconst ZOOM_MAX = 28;\n\nconst titleFont: React.CSSProperties = {\n  fontFamily: 'Helvetica Neue, Helvetica, Arial, sans-serif',\n  fontWeight: 900,\n  fontSize: FS,\n};\n\nexport const LetterformZoom: React.FC = () => {\n  const frame = useCurrentFrame();\n  // mask ID 按实例生成，多实例同场不串引（useId 的 «:» 在 url() 里非法，需清洗）\n  const cutId = `lfz-cut-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;\n\n  // 0–25f 建立 hold；25–85f 推进。慢起 bezier 叠指数尺度 = 前段慢、后段陡\n  const t = interpolate(frame, [25, 85], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.bezier(0.6, 0, 0.85, 0.5),\n  });\n  const scale = Math.pow(ZOOM_MAX, t); // 指数推进：等比的\"穿越\"速度感\n\n  // scale 过临界值（洞已撑满画面中部）→ 盖板残余笔画边外飞边撤场（撤 mask）\n  const plateOpacity = interpolate(scale, [15, 24], [1, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // 速度模糊：期望视觉模糊量随推进升高；CSS filter 会被 transform 放大，\n  // 故除以 scale 补偿\n  const visBlur = interpolate(t, [0, 0.45, 1], [0, 1.5, 16], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const blurCss = visBlur / scale;\n\n  // 新页面微 dolly：推进期跟着轻推 1→1.1，接管后 25f 内落定回 1（收势）\n  const bScale =\n    frame < 85\n      ? interpolate(t, [0, 1], [1, 1.1])\n      : interpolate(frame, [85, 110], [1.1, 1], {\n          extrapolateLeft: 'clamp',\n          extrapolateRight: 'clamp',\n          easing: Easing.out(Easing.cubic),\n        });\n\n  return (\n    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>\n      {/* 新页面：先只在字腔里透出，接管后全屏（110–140f 静止收尾） */}\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          transform: `scale(${bScale})`,\n          transformOrigin: `${ORIGIN.x}px ${ORIGIN.y}px`,\n        }}\n      >\n        <FakeDashboard variant=\"B\" />\n      </div>\n\n      {/* 米灰盖板（字形挖洞）+ 字缘描边 + 副标灰条：整组指数推进后飞出画外 */}\n      {plateOpacity > 0 && (\n        <div\n          style={{\n            position: 'absolute',\n            inset: 0,\n            transform: `scale(${scale})`,\n            transformOrigin: `${ORIGIN.x}px ${ORIGIN.y}px`,\n            opacity: plateOpacity,\n            filter: blurCss > 0.02 ? `blur(${blurCss}px)` : undefined,\n          }}\n        >\n          <svg\n            width={1920}\n            height={1080}\n            viewBox=\"0 0 1920 1080\"\n            style={{ position: 'absolute', inset: 0, display: 'block' }}\n          >\n            <defs>\n              <mask id={cutId}>\n                <rect width={1920} height={1080} fill=\"#fff\" />\n                <text\n                  x={960}\n                  y={BASELINE}\n                  textAnchor=\"middle\"\n                  fill=\"#000\"\n                  style={titleFont}\n                >\n                  DASH\n                </text>\n              </mask>\n            </defs>\n            <rect width={1920} height={1080} fill={G.bg} mask={`url(#${cutId})`} />\n            {/* 字缘细描边：让\"洞\"的轮廓在米灰上读得清 */}\n            <text\n              x={960}\n              y={BASELINE}\n              textAnchor=\"middle\"\n              fill=\"none\"\n              stroke={G.ink}\n              strokeWidth={3}\n              opacity={0.3}\n              style={titleFont}\n            >\n              DASH\n            </text>\n          </svg>\n          {/* 副标灰条：随盖板一起被甩出画外，强化\"页面元素残余\"感 */}\n          <div\n            style={{\n              position: 'absolute',\n              left: 0,\n              right: 0,\n              top: 812,\n              display: 'flex',\n              justifyContent: 'center',\n              gap: 16,\n            }}\n          >\n            <div style={{ width: 240, height: 16, background: G.bar, borderRadius: 8 }} />\n            <div style={{ width: 130, height: 16, background: G.line, borderRadius: 8 }} />\n          </div>\n        </div>\n      )}\n    </AbsoluteFill>\n  );\n};\n\n\n// ===== demos/transition/transition-travel/SharedElementMorph.tsx =====\nimport React from 'react';\nimport { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion';\nimport { FakeDashboard, Card, G } from '../../_fixtures/Fixtures';\n\n// shared-element-morph〔转场〕：全屏特写面板收缩、位移、长出圆角，\n// 严丝合缝飞落进 dashboard 网格里它所属的卡片槽位，落座带 3% 过冲。\n// 观众感觉是同一个物体被镜头送回原位（FLIP 共享元素转场）。\n//\n// 目标槽位 = FakeDashboard A 中列第二行那格（seed 5），按 fixtures 布局精确推算：\n//   侧栏 220 + 网格 padding 36 → 列宽 (1920-220-72-56)/3 = 524\n//   头部 72 + padding 36     → 行高 (1008-72-28)/2   = 454\n//   中列 x = 220+36+524+28 = 808；第二行 y = 72+36+454+28 = 590\nconst SLOT = { x: 808, y: 590, w: 524, h: 454, r: 14, seed: 5 };\nconst FULL = { x: 0, y: 0, w: 1920, h: 1080, r: 0 };\n\n// 节拍：0–35 全屏特写 hold ｜ 35–60 morph 25f（bezier 0.4,0,0.2,1 → 1.03）\n//       60–70 过冲回弹落座 ｜ 70–130 静止收尾\nconst MORPH_START = 35;\nconst MORPH_END = 60;\nconst SETTLE_END = 70;\n\nconst lerp = (a: number, b: number, t: number) => a + (b - a) * t;\n\nexport const SharedElementMorph: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 位置/尺寸/圆角共用同一条进度曲线：25f 冲到 1.03，再 10f 弹回 1\n  const drive = interpolate(frame, [MORPH_START, MORPH_END], [0, 1.03], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.bezier(0.4, 0, 0.2, 1),\n  });\n  const settle = interpolate(frame, [MORPH_END, SETTLE_END], [1.03, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n  const p = frame <= MORPH_END ? drive : settle;\n\n  const x = lerp(FULL.x, SLOT.x, p);\n  const y = lerp(FULL.y, SLOT.y, p);\n  const w = lerp(FULL.w, SLOT.w, p);\n  const h = lerp(FULL.h, SLOT.h, p);\n  const r = lerp(FULL.r, SLOT.r, p);\n\n  // 内容是同一张卡：按目标尺寸渲染，等比放大铺满全屏（全屏 = 这张卡的特写），\n  // 收缩过程中内容随容器一起缩小，强化\"同一个物体\"的感知\n  const contentScale = w / SLOT.w;\n\n  // 投影随尺寸缩小：悬空时大而深，落座后收敛到 fixtures 卡片原生投影量级\n  const ps = Math.min(Math.max(p, 0), 1);\n  const shadowY = lerp(36, 2, ps);\n  const shadowBlur = lerp(110, 8, ps);\n  const shadowAlpha = lerp(0.32, 0.06, ps);\n\n  // 背景 dashboard 先 0.9 透明度待命，落座瞬间提到 1\n  const bgOpacity = interpolate(frame, [MORPH_END - 2, MORPH_END + 3], [0.9, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.quad),\n  });\n\n  return (\n    <AbsoluteFill style={{ background: G.bg, overflow: 'hidden' }}>\n      <div style={{ opacity: bgOpacity }}>\n        <FakeDashboard variant=\"A\" />\n      </div>\n      {/* 共享元素：全屏特写 → 精确飞落进槽位 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: x,\n          top: y,\n          width: w,\n          height: h,\n          borderRadius: r,\n          overflow: 'hidden',\n          background: G.card,\n          boxShadow: `0 ${shadowY}px ${shadowBlur}px rgba(0,0,0,${shadowAlpha})`,\n        }}\n      >\n        <div\n          style={{\n            position: 'absolute',\n            left: 0,\n            top: 0,\n            transform: `scale(${contentScale})`,\n            transformOrigin: 'top left',\n          }}\n        >\n          <Card\n            w={SLOT.w}\n            h={SLOT.h}\n            seed={SLOT.seed}\n            style={{ boxShadow: 'none', borderRadius: r / Math.max(contentScale, 0.0001) }}\n          />\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n\n"
}