{
  "slug": "odometer-digit-roll",
  "title": "Odometer digit roll",
  "tagline": "Full-screen digits spin like slot-machine reels, locking in one by one left to right with an overshoot, then pulse once locked.",
  "description": "Full-screen digits spin like slot-machine reels, locking in one by one left to right with an overshoot, then pulse once locked.",
  "category": "data",
  "tags": [
    "shotcraft",
    "remotion",
    "data",
    "typography"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.06,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/odometer-digit-roll/poster-e58667dcfa36.jpg",
    "video": "https://gimgs.net/motion/media/odometer-digit-roll/video-a8ff62f483f1.mp4"
  },
  "variants": [
    {
      "key": "odometer-digit-roll",
      "label": "odometer-digit-roll",
      "description": "Odometer digit-roll headline — every digit of a full-screen hero metric spins independently on a vertical slot-machine reel with motion trails, decelerating and locking left to right with overshoot, and the whole thing pulses deeper the instant it locks.",
      "use": "",
      "video": "https://gimgs.net/motion/media/odometer-digit-roll/video-a8ff62f483f1.mp4",
      "poster": "https://gimgs.net/motion/media/odometer-digit-roll/poster-e58667dcfa36.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/odometer-digit-roll.html",
  "json_url": "https://gimgs.net/motion/odometer-digit-roll.json",
  "template_url": "https://gimgs.net/motion/odometer-digit-roll/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/odometer-digit-roll.md",
  "updated_at": "2026-09-24T07:40:09.157Z",
  "prompt": "## When to use\n\nFull-screen reveal of a single hero metric (\"10x\"/\"99.98%\"-tier); division of labor with impact-feedback style B (damage-number pop): that's an element-level side dish, this is a full-screen main course\n\n## Intention\n\nThe library already has \"slam\" (score-slam) and \"pop\" (damage-number) for hero-number entrances, both displacement-based. This card is **mechanical**: the digits aren't flown in, they're \"computed\" — each digit is a 0–9 reel spinning fast, decelerating left to right, overshooting half a slot, and clicking into lock, the standard grammar of Vercel Ship/Stripe Sessions metric sections. The rolling carries built-in suspense (what will it land on?), and the per-digit lock carries built-in rhythm (clack, clack, clack, clack) — more anticipation than just showing the final value. Difference from VerticalTicker (backlog candidate): this has a final value and settles digit by digit, it isn't an infinite scroll wall.\n\n## Duration & energy\n\n- Duration: Roll + per-digit lock ~63f + pulse 8f + hold ≥45f, about 5s\n- Energy: Medium-high\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n王牌数字的登场库内有\"砸\"（score-slam）和\"弹\"（damage-number），\n都是位移系。本卡是**机械系**：数字不是飞进来的，是\"算出来的\"——\n每位一条 0–9 滚轮高速转动，从左到右逐位减速、过冲半格、咔哒锁定，\nVercel Ship/Stripe Sessions 指标段的标准语法。滚动过程自带悬念\n（它会停在几？），逐位锁定自带节奏（哒、哒、哒、哒），比直接显示\n终值多一整拍的期待感。与 VerticalTicker（backlog 待选）的区别：\n有终值、逐位停稳，不是无限滚动墙。\n\n## 动效核心\n- 每位一个 overflow:hidden 数位盒 + 0–9×2 纵列 strip，\n  `translateY = −(pos % 10) × 行高`，posAt(f, i) 纯帧函数\n- 位 i 于 20+i×7f 开始 16f Easing.out(cubic) 减速至目标 +0.5 行\n  **过冲半格**，再 6f 弹回整数锁定——锁定帧即各位的\"哒\"\n- 滚动期每位叠 2 个错帧残影副本（translateY ±行高×0.5，\n  opacity 0.25/0.12），按相邻帧速度门控，停稳自动摘——纵向拖影\n  与 smear-multiples 同理（可数残影不是模糊）\n- 全体锁定帧：整体 ink→#000→ink 加深脉冲 8f + 1.035 微缩放 +\n  下方标签条淡入——\"合计确认\"的句号\n- 小数点与 % 等非数字位全程驻场不滚\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 字号/字体 | 190px fw800 + fontVariantNumeric: tabular-nums | 非等宽数字每滚一格整行抖动（同 scramble 等宽判例） |\n| 错峰 | 20+i×7f 起停，从左到右 | 从右往左停读作倒着算；同时停失去\"哒哒哒\" |\n| 过冲 | +0.5 行再 6f 弹回 | 无过冲读作滑到即停，机械感减半 |\n| 残影 | 2 副本 0.25/0.12，速度门控 | 3+ 副本高速下糊成一条（行高小于卡片场景） |\n| 终值脉冲 | 加深 + 1.035 缩放 8f | 只加深不缩放在 190px 黑字上不可感（实渲加码） |\n| 收尾 | 脉冲归零后真静止 ≥45f | R1 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 数位 ≤6（含小数点前后）——每多一位多 7f 错峰，8 位滚完 >2.5s\n  观众等不起；大数换缩写（12.4M 不是 12,400,000）\n- 与 impact-feedback B（伤害数字）同片可共存但分工要清：全屏主菜\n  一次（本卡），卡片配菜多次（B 式）——两处都全屏就是两次主菜\n- 滚动的数字必须是**真值的各位**——终值 99.98 就滚 9/9/9/8，\n  中途乱数会被暂停党抓到\"假滚动\"\n- 声音强依赖候选位：逐位锁定\"哒\"×N + 终值确认一声低音\n  （sound-design §4.5），无声版成立但少一半爽感\n\n## 参考实现\ndemos/data/odometer-digit-roll/\n（OdometerDigitRoll.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/data/odometer-digit-roll/OdometerDigitRoll.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/odometer-digit-roll/OdometerDigitRoll.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/odometer-digit-roll/OdometerDigitRoll.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/odometer-digit-roll/OdometerDigitRoll.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/odometer-digit-roll/OdometerDigitRoll.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": 152,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/odometer-digit-roll/OdometerDigitRoll.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/odometer-digit-roll/OdometerDigitRoll.tsx =====\n// 里程表数字滚动大字报（odometer-digit-roll）——声画钩子 / Vercel Ship 指标段。\n// 全屏巨号 \"99.98%\"（190px fw800 tabular-nums），四个数位各是一条 0–9 纵向\n// strip（overflow:hidden 数位盒），高速滚动后逐位停稳：位 i 在 20+i*7f 开始\n// 减速（Easing.out(cubic)），过冲半格再 6f 弹回锁定。滚动期每位叠 2 个错帧\n// 残影副本（translateY ±行高*0.5，opacity 0.25/0.12），按帧速度门控，停稳即摘。\n// 小数点与 % 不滚动，一直驻场。\n// 关键帧：0–20 四位全速滚（0.85 行/帧）→ 20/27/34/41 逐位开始减速（各 16f\n// 减速 + 6f 回弹，锁定于 42/49/56/63）→ 63–71 整体加深脉冲（ink→#000→ink，\n// 附 1.035 微缩放加码）→ 66–84 下方标签条淡入 → 84–150 全静止（66f ≥45f）。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, interpolateColors, Easing } from 'remotion';\nimport { G, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst ROW = 210; // 数位行高（overflow 盒高）\nconst DW = 126; // 数位盒宽\nconst FS = 190; // 字号\nconst SPIN = 0.85; // 高速滚动速度：行/帧\nconst DIGITS = [9, 9, 9, 8]; // 目标数位（\"99.98\" 中可滚的四位）\n\n// 位 i 的 strip 位置（单位：行，连续值）。纯帧函数，天然确定性。\nconst posAt = (f: number, i: number): number => {\n  const d = DIGITS[i];\n  const s = 20 + i * 7; // 开始减速帧\n  const p0 = SPIN * s;\n  // 最小再走 6 行后，落在个位 = d 的最近整数位置\n  const T = Math.ceil((p0 + 6 - d) / 10) * 10 + d;\n  if (f < s) return SPIN * Math.max(f, 0);\n  if (f < s + 16)\n    return interpolate(f, [s, s + 16], [p0, T + 0.5], {\n      extrapolateLeft: 'clamp',\n      extrapolateRight: 'clamp',\n      easing: Easing.out(Easing.cubic),\n    });\n  if (f < s + 22)\n    return interpolate(f, [s + 16, s + 22], [T + 0.5, T], {\n      extrapolateLeft: 'clamp',\n      extrapolateRight: 'clamp',\n      easing: Easing.out(Easing.cubic),\n    });\n  return T;\n};\n\n// 一条 0–9 纵列 strip（两轮 20 格，容过冲跨界）\nconst Strip: React.FC<{ pos: number; color: string; opacity?: number; dy?: number }> = ({\n  pos,\n  color,\n  opacity = 1,\n  dy = 0,\n}) => (\n  <div\n    style={{\n      position: 'absolute',\n      left: 0,\n      top: 0,\n      width: DW,\n      transform: `translateY(${-(pos % 10) * ROW + dy}px)`,\n      opacity,\n    }}\n  >\n    {Array.from({ length: 20 }).map((_, k) => (\n      <div\n        key={k}\n        style={{\n          width: DW,\n          height: ROW,\n          lineHeight: `${ROW}px`,\n          textAlign: 'center',\n          fontSize: FS,\n          fontWeight: 800,\n          fontVariantNumeric: 'tabular-nums',\n          color,\n        }}\n      >\n        {k % 10}\n      </div>\n    ))}\n  </div>\n);\n\n// 单个数位盒：本体 strip + 滚动期 2 个错帧残影（速度门控，停稳即摘）\nconst DigitReel: React.FC<{ frame: number; i: number; color: string }> = ({ frame, i, color }) => {\n  const pos = posAt(frame, i);\n  const speed = Math.abs(pos - posAt(frame - 1, i));\n  const gate = interpolate(speed, [0.06, 0.5], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  return (\n    <div style={{ position: 'relative', width: DW, height: ROW, overflow: 'hidden' }}>\n      {gate > 0.001 && (\n        <>\n          <Strip pos={pos} color={color} opacity={0.25 * gate} dy={ROW * 0.5} />\n          <Strip pos={pos} color={color} opacity={0.12 * gate} dy={-ROW * 0.5} />\n        </>\n      )}\n      <Strip pos={pos} color={color} />\n    </div>\n  );\n};\n\n// 不滚动的静态字符（小数点 / %），一直驻场\nconst StaticGlyph: React.FC<{ ch: string; color: string; w?: number }> = ({ ch, color, w }) => (\n  <div\n    style={{\n      width: w,\n      height: ROW,\n      lineHeight: `${ROW}px`,\n      textAlign: 'center',\n      fontSize: FS,\n      fontWeight: 800,\n      fontVariantNumeric: 'tabular-nums',\n      color,\n    }}\n  >\n    {ch}\n  </div>\n);\n\nexport const OdometerDigitRoll: React.FC = () => {\n  const frame = useCurrentFrame();\n  // 全位锁定于 63f：整体加深脉冲 ink→#000→ink（8f），附微缩放加码可感性\n  const inkNow = interpolateColors(frame, [63, 67, 71], [G.ink, '#000000', G.ink]);\n  const pulseScale = interpolate(frame, [63, 67, 71], [1, 1.035, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.inOut(Easing.quad),\n  });\n  const labelOp = interpolate(frame, [66, 84], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.quad),\n  });\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', left: 120, top: 96 }}>\n        <TitleBlock text=\"ODOMETER DIGIT ROLL\" size={54} />\n      </div>\n      <div\n        style={{\n          position: 'absolute',\n          left: 0,\n          top: 400,\n          width: 1920,\n          display: 'flex',\n          justifyContent: 'center',\n          fontFamily: 'Helvetica, Arial, sans-serif',\n          transform: `scale(${pulseScale})`,\n          transformOrigin: '960px 105px',\n        }}\n      >\n        <DigitReel frame={frame} i={0} color={inkNow} />\n        <DigitReel frame={frame} i={1} color={inkNow} />\n        <StaticGlyph ch=\".\" color={inkNow} w={70} />\n        <DigitReel frame={frame} i={2} color={inkNow} />\n        <DigitReel frame={frame} i={3} color={inkNow} />\n        <StaticGlyph ch=\"%\" color={inkNow} />\n      </div>\n      {/* 下方标签条：全部锁定后淡入 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 0,\n          top: 680,\n          width: 1920,\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          gap: 16,\n          opacity: labelOp,\n        }}\n      >\n        <div style={{ width: 520, height: 22, background: G.bar, borderRadius: 11 }} />\n        <div style={{ width: 320, height: 14, background: G.line, borderRadius: 7 }} />\n      </div>\n    </div>\n  );\n};\n\n\n"
}