{
  "slug": "timeline-travel",
  "title": "Timeline travel pan",
  "tagline": "Timeline travel — the camera accelerates along a horizontal ruler axis past version tick marks, a card springs upright and pauses briefly at each tick, and an abrupt stop pushes in on the final tick.",
  "description": "Timeline travel — the camera accelerates along a horizontal ruler axis past version tick marks, a card springs upright and pauses briefly at each tick, and an",
  "category": "data",
  "tags": [
    "shotcraft",
    "remotion",
    "data",
    "camera"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.38,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/timeline-travel/poster-cfe6a86a3f68.jpg",
    "video": "https://gimgs.net/motion/media/timeline-travel/video-7a1220e6b1c3.mp4"
  },
  "variants": [
    {
      "key": "timeline-travel",
      "label": "timeline-travel",
      "description": "Timeline travel — the camera accelerates along a horizontal ruler axis past version tick marks, a card springs upright and pauses briefly at each tick, and an abrupt stop pushes in on the final tick",
      "use": "",
      "video": "https://gimgs.net/motion/media/timeline-travel/video-7a1220e6b1c3.mp4",
      "poster": "https://gimgs.net/motion/media/timeline-travel/poster-cfe6a86a3f68.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/timeline-travel.html",
  "json_url": "https://gimgs.net/motion/timeline-travel.json",
  "template_url": "https://gimgs.net/motion/timeline-travel/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/timeline-travel.md",
  "updated_at": "2026-09-24T07:40:28.382Z",
  "prompt": "## When to use\n\nChangelog/milestone/company-history passages (another way to shoot \"we've always been shipping\"); complements scroll-brake-moves: that card is a vertical-list hard-brake, this card is a horizontal time journey\n\n## Intention\n\nShoot the product's version history as a journey along a timeline (the Hurt Locker title-sequence precedent): the camera eases in → sprints → hard-brakes in three speed phases moving horizontally, v1.0/v2.0/v3.0 ticks passing in sequence, and each passed tick springs a matching card upright — history flashing by outside the window, hard-stopping at \"today\" and pushing in. The speed itself says \"development is fast,\" the hard-stop says \"now matters most.\"\n\n## Duration & energy\n\n- Duration: 4–5s\n- Energy: Medium-high (an accelerate-then-hard-brake pacing shot)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n把产品的版本史拍成一次沿时间轴的旅行（《反恐王国》片头判例）：\n镜头缓起→冲刺→急刹三段变速横移，v1.0/v2.0/v3.0 刻度依次掠过，\n每过一格对应卡片从刻度线上 spring 弹立——历史在窗外闪过，\n急停在\"今天\"并推近。速度本身讲\"发展快\"，急停讲\"现在最重要\"。\n\n## 动作阶段\n| 阶段 | 帧段参考 | 内容 |\n|------|------|------|\n| 1 静置 | f0–12 | 空轴 + 首刻度定住 |\n| 2 加速掠过 | ~f12–100 | 世界层 translateX 分段缓动：缓起→冲刺→末 12% 急刹 |\n| 3 弹卡 | 掠过中 | 相机扫过刻度前 6f 卡片起弹（spring damping ~11 明显过冲），底边为轴 scaleY 立起 |\n| 4 急停推近 | 急停帧起 10f | scale 1→1.28 推近末刻度（out cubic），接静止 ≥30f |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 变速曲线 | 输入断点 [0,0.15,0.88,1]→输出 [0,0.055,0.9,1] | 中段不够快就没有\"旅行感\"；全程匀速即廉价 PPT |\n| 弹立时机 | 相机到达刻度前 6f 起弹，由 camXAt 反查 | 到了才弹观众已经错过；早太多读作预告 |\n| 刻度密度 | 主刻度 3–5 个 + 次刻度小点若干 | 次刻度点是速度感的参照物，删掉就感觉不到快 |\n| 急停推近 | 1→1.28 / 10f | 不推近急停就只是\"停了\"，推近才是\"到了\" |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 中段冲刺快到卡片糊时可叠 CameraMotionBlur（只包快速段，轮 #8 判例）\n- 急停后静止预算前置：demo 首版静止只有 25f 不达标，急停帧前移才够\n  ——排时间线时先给末段留 ≥30f 再倒推行程\n- 每张弹立卡片是信息位：实战放真实版本亮点截图/一句话，\n  掠过速度要保证至少末两张能读（Q6 同源）\n- 声音：每次弹立一声轻打点，急停一声 impact（S2 同源）\n\n## 参考实现\ndemos/data/timeline-travel/\n（TimelineTravel.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/data/timeline-travel/TimelineTravel.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/timeline-travel/TimelineTravel.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/data/timeline-travel/TimelineTravel.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/data/timeline-travel/TimelineTravel.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/timeline-travel/TimelineTravel.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": 161,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/timeline-travel/TimelineTravel.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/data/timeline-travel/TimelineTravel.tsx =====\n// timeline-travel —— 时间轴横移（《反恐王国》式）\n// 镜头沿水平刻度轴加速横移，v1.0/v2.0/v3.0/Today 四个刻度依次掠过，\n// 每过刻度对应 Card 从刻度线 spring 过冲弹立 + 短停，镜头不停；\n// 末刻度 4f 急停 + 推近 1.28×。世界层只动 translateX/scale。\n// f0–12 初始静置；f118 起真静止 ≥42f（160f 总长）。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing, spring } from 'remotion';\nimport { G, Card, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst W = 1920;\nconst AXIS_Y = 700;\nconst TICK_GAP = 1400; // 刻度间距（世界坐标）\nconst TICKS = [\n  { label: 'v1.0', x: 960 },\n  { label: 'v2.0', x: 960 + TICK_GAP },\n  { label: 'v3.0', x: 960 + TICK_GAP * 2 },\n  { label: 'Today', x: 960 + TICK_GAP * 3 },\n];\nconst WORLD_W = 960 + TICK_GAP * 3 + 960;\n\nconst TRAVEL_START = 12;\nconst TRAVEL_END = 104; // 急停帧\nconst ZOOM_END = 114;\n\n// 相机 X：in-out 但前段慢后段快（poly(3) in 为主，末端 out 急收）\n// 用两段拼：0–0.82 加速段（Easing.in(poly(2.2))），0.82–1 急刹段\nconst camXAt = (f: number): number => {\n  const total = TICKS[3].x - 960; // 需要位移的世界距离\n  const t = interpolate(f, [TRAVEL_START, TRAVEL_END], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  // 加速→巡航→急刹：分段缓动，前 15% 缓起，中段近匀加速冲刺，末 12% 急收\n  const eased = interpolate(t, [0, 0.15, 0.88, 1], [0, 0.055, 0.9, 1], {\n    easing: Easing.inOut(Easing.quad),\n  });\n  return eased * total;\n};\n\n// 每张卡的弹立帧：相机中心扫过该刻度的时刻（数值上预先求好，避免逐帧求逆）\n// 通过 camXAt 反查：找到 camX == tick.x - 960 的帧\nconst popFrameOf = (tickX: number): number => {\n  for (let f = TRAVEL_START; f <= TRAVEL_END; f++) {\n    if (camXAt(f) >= tickX - 960) return f;\n  }\n  return TRAVEL_END;\n};\n\nconst CARD_W = 360;\nconst CARD_H = 240;\n\nconst TickStop: React.FC<{ i: number; frame: number }> = ({ i, frame }) => {\n  const tick = TICKS[i];\n  const pop = popFrameOf(tick.x) - 6; // 提前 6f 起弹，掠过时正好立起\n  const s = spring({\n    frame: frame - pop,\n    fps: 30,\n    config: { damping: 11, stiffness: 160, mass: 0.9 }, // 明显过冲\n    durationInFrames: 26,\n  });\n  const appeared = frame >= pop;\n\n  return (\n    <div style={{ position: 'absolute', left: tick.x, top: 0 }}>\n      {/* 刻度竖线 */}\n      <div style={{ position: 'absolute', left: -3, top: AXIS_Y - 28, width: 6, height: 56, background: G.ink, borderRadius: 3 }} />\n      {/* 刻度标签 */}\n      <div style={{ position: 'absolute', left: -80, top: AXIS_Y + 44, width: 160, textAlign: 'center', fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800, fontSize: 40, color: G.ink }}>\n        {tick.label}\n      </div>\n      {/* 卡片从刻度线弹立：以底边为轴 scaleY 0→1（带过冲），伴随上移 */}\n      {appeared && (\n        <div\n          style={{\n            position: 'absolute',\n            left: -CARD_W / 2,\n            top: AXIS_Y - 36 - CARD_H,\n            transform: `scaleY(${s}) scaleX(${0.6 + 0.4 * s})`,\n            transformOrigin: '50% 100%',\n            opacity: Math.min(1, s * 2),\n          }}\n        >\n          <Card w={CARD_W} h={CARD_H} seed={i + 2} />\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport const TimelineTravel: React.FC = () => {\n  const frame = useCurrentFrame();\n  const camX = camXAt(frame);\n\n  // 急停后推近末刻度：scale 1 → 1.28，中心对准 Today 刻度\n  const zoom = interpolate(frame, [TRAVEL_END, ZOOM_END], [1, 1.28], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  return (\n    <div style={{ width: W, height: 1080, background: G.bg, overflow: 'hidden', position: 'relative' }}>\n      {/* 推近层：以画面中央偏下（末刻度落点）为原点放大 */}\n      <div style={{ width: W, height: 1080, transform: `scale(${zoom})`, transformOrigin: '50% 62%' }}>\n        {/* 世界层：唯一横移的容器 */}\n        <div style={{ position: 'absolute', left: 0, top: 0, width: WORLD_W, height: 1080, transform: `translateX(${-camX}px)` }}>\n          {/* 主轴线 */}\n          <div style={{ position: 'absolute', left: 200, top: AXIS_Y - 3, width: WORLD_W - 400, height: 6, background: G.bar, borderRadius: 3 }} />\n          {/* 次刻度（小点，增强速度感） */}\n          {Array.from({ length: 22 }).map((_, i) => (\n            <div key={i} style={{ position: 'absolute', left: 960 + i * (TICK_GAP / 5) - 2, top: AXIS_Y - 12, width: 4, height: 24, background: G.bar, borderRadius: 2 }} />\n          ))}\n          {TICKS.map((_, i) => (\n            <TickStop key={i} i={i} frame={frame} />\n          ))}\n        </div>\n      </div>\n      <div style={{ position: 'absolute', top: 90, width: '100%', textAlign: 'center' }}>\n        <TitleBlock text=\"TIMELINE TRAVEL\" size={64} />\n      </div>\n    </div>\n  );\n};\n\n\n"
}