{
  "slug": "gauge-readout-moves",
  "title": "Gauge readout moves",
  "tagline": "Two gauge-readout styles: needle-sweep-selftest, a full-arc self-test sweep before settling to the true value, and tape-scroll-fixed-pointer, a fixed pointer with a scrolling tape that sprints then brakes.",
  "description": "Two gauge-readout styles: needle-sweep-selftest, a full-arc self-test sweep before settling to the true value, and tape-scroll-fixed-pointer, a fixed pointer ",
  "category": "data",
  "tags": [
    "shotcraft",
    "remotion",
    "data"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.72,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/gauge-readout-moves/poster-f5ae50208e65.jpg",
    "video": "https://gimgs.net/motion/media/gauge-readout-moves/video-b2225c1d995d.mp4"
  },
  "variants": [
    {
      "key": "needle-sweep-selftest",
      "label": "needle-sweep-selftest · Needle sweep self-test",
      "description": "Needle swings out ~12f ease-out to the full arc, returns ~20f with a 5–8° overshoot rock to settle on the true value; multiple gauges staggered by 3–5f; the value pops in below the dial on the same frame it settles.",
      "use": "Dashboard opening / system-ready ritual",
      "video": "https://gimgs.net/motion/media/gauge-readout-moves/video-b2225c1d995d.mp4",
      "poster": "https://gimgs.net/motion/media/gauge-readout-moves/poster-f5ae50208e65.jpg"
    },
    {
      "key": "tape-scroll-fixed-pointer",
      "label": "tape-scroll-fixed-pointer · Fixed-pointer tape scroll",
      "description": "Long scale tape translates: a slow-crawl segment → sprints at 45px/f for ~25f → spring brake with overshoot and rock to a stop; the readout in the window refreshes in sync.",
      "use": "A single metric's big jump (performance doubling / quota increase)",
      "video": "https://gimgs.net/motion/media/gauge-readout-moves/video-0a01c4e56ed1.mp4",
      "poster": "https://gimgs.net/motion/media/gauge-readout-moves/poster-tape-scroll-fixed-pointer-1619e240daea.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/gauge-readout-moves.html",
  "json_url": "https://gimgs.net/motion/gauge-readout-moves.json",
  "template_url": "https://gimgs.net/motion/gauge-readout-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/gauge-readout-moves.md",
  "updated_at": "2026-09-24T07:39:58.601Z",
  "prompt": "## When to use\n\nFor a dashboard opening ritual / performance-metric reveal; use A for a multi-gauge power-on feel, B for a single metric's big jump.\n\n## Intention\n\nTurn \"reporting a value\" into a gauge's physical action: A is a car's ignition self-test — the needle first sweeps across the full arc (270°) then settles back to the real value, multiple gauges staggered into a wave, showing the range before the number, a launch-declaration feel; B follows an airspeed-tape grammar — the needle/viewport stays pinned dead center while the ruled tape scrolls past underneath; on a big jump the tape sprints at 45px/f then brakes with a spring, overshooting and rocking back — \"the world moves, the needle doesn't\", turning the size of a number into a visible distance traveled. Division of labor with odometer-digit-roll: that one rolls digits inside a window, B is the whole ruled scale line panning.\n\n## Duration & energy\n\n- Duration: A: 4–5s / B: 4–5s\n- Energy: Medium-high (mechanical-ritual type)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n把\"报数值\"做成仪表的身体动作：A 是汽车点火自检——表盘指针先\"唰\"地\n甩满全弧（270°）再回落定格真实值，多表错峰成波浪，先亮量程再报数，\n开场宣誓感；B 是飞机空速带语法——指针/取景窗钉死画面正中，带刻度的\n标尺整体从底下滚过，大数值跳变时刻度带 45px/f 冲刺、末尾 spring 刹车\n带回摆过冲，\"世界动、针不动\"，数值大小变成看得见的位移距离。\n与 odometer-digit-roll 分工：那是数字在窗里滚，B 是连刻度格线全在平移。\n\n## 两式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A needle-sweep-selftest | 指针去程 ~12f ease-out 甩满弧，回程 ~20f 带 5–8° 过冲回摆落真值；多表错峰 3–5f；落定同帧盘下数值弹出 | dashboard 开场/系统就绪仪式 |\n| B tape-scroll-fixed-pointer | 长刻度带 translate：慢爬段→45px/f 冲刺 ~25f→spring 刹车过冲回摆停位；窗内读数同步刷新 | 单指标大跳变（性能翻倍/配额提升） |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A 甩弧幅度 | 满量程 270° | 只甩到真值=没有自检仪式感；满弧再回落才是\"先亮量程\" |\n| A 过冲 | 回落带 5–8° 回摆 | 无过冲读作电子表；过冲=机械指针的质量 |\n| A 错峰 | 3–5f/表 | 齐甩读作复制粘贴；错峰成波浪 |\n| B 冲刺速度 | 45px/f（大跳变段） | 慢了读作匀速爬升，\"世界动\"不成立 |\n| B 刹车 | spring 过冲（冲过头再弹回停位） | 词汇原案的回摆是\"哐\"的来源，直停可惜 |\n| 真静止 | 停位后 ≥30f 全锁死 | 指针类最忌尾漂 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 同批 ring-close-overfill（合环过盈）改改再看无附言——环形进度类\n  待重做，别急着当第三式并入\n- A 的表盘是自绘 SVG（弧刻度+指针），实战贴真 dashboard 时优先找\n  页面里现成的 gauge 组件特写，没有再造\n- 声音：A 甩针一声\"唰\"+落定\"嗒\"（多表错峰各一声）；B 冲刺段\n  低频滚动声、刹车一声闷\"哐\"\n\n## 参考实现\ndemos/data/gauge-readout-moves/\n（NeedleSweepSelftest.tsx / TapeScrollFixedPointer.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx\n- demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/gauge-readout-moves/TapeScrollFixedPointer.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/gauge-readout-moves/NeedleSweepSelftest.tsx, demos/data/gauge-readout-moves/TapeScrollFixedPointer.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/gauge-readout-moves/NeedleSweepSelftest.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx"
    },
    {
      "path": "demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/gauge-readout-moves/TapeScrollFixedPointer.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": 142,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/gauge-readout-moves/TapeScrollFixedPointer.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/gauge-readout-moves/NeedleSweepSelftest.tsx =====\n// needle-sweep-selftest —— 满弧扫针自检\n// 卡片内三个并排 270° 表盘。f12 \"点火\"后指针错峰 4f 依次从 0 甩满全弧\n// （去程 12f ease-out），再回落到各自真实值（回程 20f：先落过头 8° 再回摆），\n// 落定同帧盘下数值文字弹出。三表错峰形成波浪。f60 后真静止 80f。\n// 帧确定性：纯 interpolate 分段，全部 clamp，settle 后每帧输出常数。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst AMBER = '#b45309';\nconst RED = '#7c2d12';\n\nconst CARD_W = 1500;\nconst CARD_H = 640;\nconst CARD_X = (1920 - CARD_W) / 2;\nconst CARD_Y = 300;\n\nconst GA_W = CARD_W / 3; // 每表盘占位宽\nconst R = 148;\nconst CX = GA_W / 2;\nconst CY = 240;\n\n// 表盘角 d∈[0,270] → SVG 角 a = 135 + d（0=右，顺时针，y 向下）\nconst polar = (a: number, r: number): [number, number] => [\n  CX + r * Math.cos((a * Math.PI) / 180),\n  CY + r * Math.sin((a * Math.PI) / 180),\n];\n\nconst arcPath = (d0: number, d1: number, r: number): string => {\n  const [x0, y0] = polar(135 + d0, r);\n  const [x1, y1] = polar(135 + d1, r);\n  const large = d1 - d0 > 180 ? 1 : 0;\n  return `M ${x0.toFixed(2)} ${y0.toFixed(2)} A ${r} ${r} 0 ${large} 1 ${x1.toFixed(2)} ${y1.toFixed(2)}`;\n};\n\n// 三表：点火 f12，错峰 4f\nconst GAUGES = [\n  { start: 12, target: 190 },\n  { start: 16, target: 120 },\n  { start: 20, target: 235 },\n];\n\nconst needleAngle = (frame: number, s: number, target: number): number => {\n  if (frame <= s) return 0;\n  if (frame <= s + 12) {\n    // 去程：甩满全弧，ease-out（起步猛）\n    return interpolate(frame, [s, s + 12], [0, 270], {\n      easing: Easing.out(Easing.cubic),\n    });\n  }\n  if (frame <= s + 25) {\n    // 回程主段：落过真实值 8°\n    return interpolate(frame, [s + 12, s + 25], [270, target - 8], {\n      easing: Easing.inOut(Easing.cubic),\n    });\n  }\n  // 回摆：从过冲位弹回真实值，之后 clamp 为常数\n  return interpolate(frame, [s + 25, s + 32], [target - 8, target], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateRight: 'clamp',\n  });\n};\n\nconst Gauge: React.FC<{ start: number; target: number }> = ({ start, target }) => {\n  const frame = useCurrentFrame();\n  const d = needleAngle(frame, start, target);\n  const settle = start + 32;\n  const value = Math.round((target / 270) * 100);\n\n  const popScale = interpolate(frame, [settle, settle + 4, settle + 8], [0.3, 1.18, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const popOp = interpolate(frame, [settle, settle + 3], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  const [tipX, tipY] = polar(135, R - 26);\n  const [tailX, tailY] = polar(315, 36);\n\n  // 大刻度 11 根（每 27°），小刻度每 9°\n  const ticks: React.ReactNode[] = [];\n  for (let k = 0; k <= 30; k++) {\n    const dd = k * 9;\n    const major = k % 3 === 0;\n    const a = 135 + dd;\n    const [x0, y0] = polar(a, R - 8);\n    const [x1, y1] = polar(a, major ? R - 30 : R - 19);\n    ticks.push(\n      <line key={k} x1={x0} y1={y0} x2={x1} y2={y1}\n        stroke={dd >= 225 ? RED : G.mid} strokeWidth={major ? 4 : 2} />\n    );\n  }\n\n  return (\n    <div style={{ width: GA_W, height: 480, position: 'relative' }}>\n      <svg width={GA_W} height={430}>\n        {/* 弧形轨道 + 红区 */}\n        <path d={arcPath(0, 270, R)} fill=\"none\" stroke={G.line} strokeWidth={10} strokeLinecap=\"round\" />\n        <path d={arcPath(225, 270, R)} fill=\"none\" stroke={RED} strokeWidth={10} strokeLinecap=\"round\" opacity={0.85} />\n        {ticks}\n        {/* 指针：琥珀刚体，rotate 全量程往返 */}\n        <g transform={`rotate(${d.toFixed(3)} ${CX} ${CY})`}>\n          <line x1={tailX} y1={tailY} x2={tipX} y2={tipY} stroke={AMBER} strokeWidth={9} strokeLinecap=\"round\" />\n        </g>\n        <circle cx={CX} cy={CY} r={15} fill={G.ink} />\n        <circle cx={CX} cy={CY} r={6} fill={AMBER} />\n      </svg>\n      {/* 落定同帧弹出的数值 */}\n      <div style={{\n        position: 'absolute', left: 0, right: 0, top: 396, textAlign: 'center',\n        opacity: popOp, transform: `scale(${popScale.toFixed(4)})`,\n      }}>\n        <span style={{ fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800, fontSize: 62, color: G.ink }}>\n          {value}\n        </span>\n        <span style={{ fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 700, fontSize: 30, color: G.mid, marginLeft: 8 }}>\n          %\n        </span>\n      </div>\n    </div>\n  );\n};\n\nexport const NeedleSweepSelftest: React.FC = () => {\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', top: 110, width: '100%', textAlign: 'center' }}>\n        <TitleBlock text=\"NEEDLE SWEEP SELF-TEST\" size={72} />\n      </div>\n      <div style={{\n        position: 'absolute', left: CARD_X, top: CARD_Y, width: CARD_W, height: CARD_H,\n        background: G.card, border: `2px solid ${G.border}`, borderRadius: 14,\n        boxSizing: 'border-box', boxShadow: '0 2px 8px rgba(0,0,0,0.06)', padding: 32,\n      }}>\n        <div style={{ height: 16, width: 320, background: G.bar, borderRadius: 8 }} />\n        <div style={{ position: 'absolute', left: 0, top: 96, display: 'flex' }}>\n          {GAUGES.map((g, i) => (\n            <Gauge key={i} start={g.start} target={g.target} />\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n};\n\n\n// ===== demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx =====\n// tape-scroll-fixed-pointer —— 滚带定针\n// 取景窗+琥珀三角指针钉死在画面正中，竖向长刻度带（0–500，每 50 一大格）\n// 从窗后滚过：\"世界动、针不动\"。先慢速爬（f12–55 读数 60→140 缓涨），\n// 然后一次大跳变：刻度带冲刺 ~23f（峰值 ~50px/f）冲过 420 到 442，\n// spring 刹车回摆一次（442→415→420）落定。窗内读数同步。f96 后真静止 44f。\n// 帧确定性：value(frame) 纯分段 interpolate 全 clamp，带偏移 = value 线性映射。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst AMBER = '#b45309';\n\nconst PXU = 3; // px per unit：500 量程 → 1500px 长带\nconst CENTER_Y = 590; // 取景窗中线（屏幕坐标）\nconst TAPE_X = 830; // 刻度带左缘\nconst TAPE_W = 260;\n\n// 读数时间线：静置→慢爬→冲刺(过冲到442)→回摆(415)→落定420→真静止\nconst valueAt = (frame: number): number => {\n  if (frame <= 12) return 60;\n  if (frame <= 55) {\n    return interpolate(frame, [12, 55], [60, 140]); // 慢爬 ~5.6px/f\n  }\n  if (frame <= 78) {\n    return interpolate(frame, [55, 78], [140, 442], {\n      easing: Easing.inOut(Easing.cubic), // 冲刺段，峰值 ~50px/f\n    });\n  }\n  if (frame <= 88) {\n    return interpolate(frame, [78, 88], [442, 415], {\n      easing: Easing.out(Easing.cubic), // 刹车回摆：甩过头再拉回\n    });\n  }\n  return interpolate(frame, [88, 96], [415, 420], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateRight: 'clamp',\n  });\n};\n\nexport const TapeScrollFixedPointer: React.FC = () => {\n  const frame = useCurrentFrame();\n  const v = valueAt(frame);\n\n  // 刻度带：数值大在上。单位 u 的屏幕 y = CENTER_Y + (v - u) * PXU\n  const yOf = (u: number): number => CENTER_Y + (v - u) * PXU;\n\n  const ticks: React.ReactNode[] = [];\n  for (let u = 0; u <= 500; u += 10) {\n    const y = yOf(u);\n    if (y < 180 || y > 1010) continue; // 视野外裁剪\n    const major = u % 50 === 0;\n    ticks.push(\n      <div key={u} style={{\n        position: 'absolute', top: y - 2, right: 0,\n        width: major ? 92 : 46, height: major ? 5 : 3,\n        background: major ? G.ink : G.mid, borderRadius: 2,\n      }} />\n    );\n    if (major) {\n      ticks.push(\n        <div key={`n${u}`} style={{\n          position: 'absolute', top: y - 22, right: 110,\n          fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 700,\n          fontSize: 38, color: G.mid, textAlign: 'right', width: 90,\n        }}>\n          {u}\n        </div>\n      );\n    }\n  }\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', top: 90, width: '100%', textAlign: 'center' }}>\n        <TitleBlock text=\"TAPE SCROLL · FIXED POINTER\" size={68} />\n      </div>\n\n      {/* 刻度带容器（世界层：整体在动） */}\n      <div style={{\n        position: 'absolute', left: TAPE_X, top: 180, width: TAPE_W, height: 830,\n        background: G.panel, border: `2px solid ${G.border}`, borderRadius: 12,\n        overflow: 'hidden', boxSizing: 'border-box',\n      }}>\n        <div style={{ position: 'absolute', inset: 0 }}>{\n          // 内层坐标 = 屏幕 y - 180\n          <div style={{ position: 'absolute', left: 0, right: 14, top: -180, height: 1080 + 400 }}>\n            {ticks}\n          </div>\n        }</div>\n      </div>\n\n      {/* 固定层：取景窗 + 琥珀三角指针 + 读数（针不动） */}\n      <div style={{\n        position: 'absolute', left: TAPE_X - 10, top: CENTER_Y - 46,\n        width: TAPE_W + 20, height: 92,\n        border: `5px solid ${AMBER}`, borderRadius: 10,\n        boxShadow: '0 4px 14px rgba(0,0,0,0.14)', boxSizing: 'border-box',\n        background: 'rgba(255,255,255,0.14)',\n      }} />\n      <svg width={60} height={64} style={{ position: 'absolute', left: TAPE_X - 66, top: CENTER_Y - 32 }}>\n        <polygon points=\"4,4 56,32 4,60\" fill={AMBER} />\n      </svg>\n\n      {/* 同步读数窗 */}\n      <div style={{\n        position: 'absolute', left: TAPE_X + TAPE_W + 70, top: CENTER_Y - 84,\n        width: 380, height: 168, background: G.card,\n        border: `2px solid ${G.border}`, borderRadius: 14,\n        boxShadow: '0 2px 8px rgba(0,0,0,0.06)',\n        display: 'flex', flexDirection: 'column', justifyContent: 'center',\n        padding: '0 36px', boxSizing: 'border-box',\n      }}>\n        <div style={{ height: 12, width: 150, background: G.line, borderRadius: 6, marginBottom: 12 }} />\n        <div style={{ fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800, fontSize: 96, color: AMBER, letterSpacing: -2, lineHeight: 1 }}>\n          {Math.round(v)}\n        </div>\n      </div>\n    </div>\n  );\n};\n\n\n"
}