Motion · Dados

Gauge readout moves

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.

1920×108016:930 fps4.72s · 142 quadrosremotionLicença: Apache-2.0#shotcraft#remotion#data

Fonte: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composição Remotion (React/TSX) — vídeo de prévia, sem player no navegador

Variantes

needle-sweep-selftest
指针去程 ~12f ease-out 甩满弧,回程 ~20f 带 5-8° 过冲回摆落真值;多表错峰 3-5f;落定同帧盘下数值弹出
dashboard 开场/系统就绪仪式
tape-scroll-fixed-pointer
长刻度带 translate:慢爬段→45px/f 冲刺 ~25f→spring 刹车过冲回摆停位;窗内读数同步刷新
单指标大跳变(性能翻倍/配额提升)

When to use

For a dashboard opening ritual / performance-metric reveal; use A for a multi-gauge power-on feel, B for a single metric's big jump.

Intention

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

Duration & energy

  • Duration: A: 4–5s / B: 4–5s
  • Energy: Medium-high (mechanical-ritual type)

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

意图

把"报数值"做成仪表的身体动作:A 是汽车点火自检——表盘指针先"唰"地 甩满全弧(270°)再回落定格真实值,多表错峰成波浪,先亮量程再报数, 开场宣誓感;B 是飞机空速带语法——指针/取景窗钉死画面正中,带刻度的 标尺整体从底下滚过,大数值跳变时刻度带 45px/f 冲刺、末尾 spring 刹车 带回摆过冲,"世界动、针不动",数值大小变成看得见的位移距离。 与 odometer-digit-roll 分工:那是数字在窗里滚,B 是连刻度格线全在平移。

两式选型

| 式 | 做法 | 适用 | |----|------|------| | A needle-sweep-selftest | 指针去程 ~12f ease-out 甩满弧,回程 ~20f 带 5–8° 过冲回摆落真值;多表错峰 3–5f;落定同帧盘下数值弹出 | dashboard 开场/系统就绪仪式 | | B tape-scroll-fixed-pointer | 长刻度带 translate:慢爬段→45px/f 冲刺 ~25f→spring 刹车过冲回摆停位;窗内读数同步刷新 | 单指标大跳变(性能翻倍/配额提升) |

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A 甩弧幅度 | 满量程 270° | 只甩到真值=没有自检仪式感;满弧再回落才是"先亮量程" | | A 过冲 | 回落带 5–8° 回摆 | 无过冲读作电子表;过冲=机械指针的质量 | | A 错峰 | 3–5f/表 | 齐甩读作复制粘贴;错峰成波浪 | | B 冲刺速度 | 45px/f(大跳变段) | 慢了读作匀速爬升,"世界动"不成立 | | B 刹车 | spring 过冲(冲过头再弹回停位) | 词汇原案的回摆是"哐"的来源,直停可惜 | | 真静止 | 停位后 ≥30f 全锁死 | 指针类最忌尾漂 |

已知坑

  • demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,

首次实战须以真实素材回验

  • 同批 ring-close-overfill(合环过盈)改改再看无附言——环形进度类

待重做,别急着当第三式并入

  • A 的表盘是自绘 SVG(弧刻度+指针),实战贴真 dashboard 时优先找

页面里现成的 gauge 组件特写,没有再造

  • 声音:A 甩针一声"唰"+落定"嗒"(多表错峰各一声);B 冲刺段

低频滚动声、刹车一声闷"哐"

参考实现

demos/data/gauge-readout-moves/ (NeedleSweepSelftest.tsx / TapeScrollFixedPointer.tsx)

Reference implementation (Remotion)

  • demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx
  • demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx

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

Tudo nesta página também é legível por máquina. Aponte seu agente para o JSON ou adicione o servidor MCP do gimgs e peça para buscar templates de motion.

  1. Baixe o JSON — contém a receita do plano (timing, easing, tabela de parâmetros, armadilhas) e o código TSX completo.
  2. Clone o repositório video-shotcraft; o componente importa fixtures de demos/_fixtures e helpers de assets/lib.
  3. Registre o componente como <Composition> em um projeto Remotion e renderize com `npx remotion render`, ou reimplemente a receita no seu próprio engine.
curl -s https://gimgs.net/motion/gauge-readout-moves.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx, demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("gauge-readout-moves")

Prompt

Use the video-shotcraft shot recipe "Gauge readout moves" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/gauge-readout-moves.json
Raw TSX: https://gimgs.net/motion/gauge-readout-moves/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx, demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 4.72s

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

9,582 chars · Apache-2.0Abrir template bruto ↗
// ===== demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx =====
// needle-sweep-selftest —— 满弧扫针自检
// 卡片内三个并排 270° 表盘。f12 "点火"后指针错峰 4f 依次从 0 甩满全弧
// (去程 12f ease-out),再回落到各自真实值(回程 20f:先落过头 8° 再回摆),
// 落定同帧盘下数值文字弹出。三表错峰形成波浪。f60 后真静止 80f。
// 帧确定性:纯 interpolate 分段,全部 clamp,settle 后每帧输出常数。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, TitleBlock } from '../../_fixtures/Fixtures';

const AMBER = '#b45309';
const RED = '#7c2d12';

const CARD_W = 1500;
const CARD_H = 640;
const CARD_X = (1920 - CARD_W) / 2;
const CARD_Y = 300;

const GA_W = CARD_W / 3; // 每表盘占位宽
const R = 148;
const CX = GA_W / 2;
const CY = 240;

// 表盘角 d∈[0,270] → SVG 角 a = 135 + d(0=右,顺时针,y 向下)
const polar = (a: number, r: number): [number, number] => [
  CX + r * Math.cos((a * Math.PI) / 180),
  CY + r * Math.sin((a * Math.PI) / 180),
];

const arcPath = (d0: number, d1: number, r: number): string => {
  const [x0, y0] = polar(135 + d0, r);
  const [x1, y1] = polar(135 + d1, r);
  const large = d1 - d0 > 180 ? 1 : 0;
  return `M ${x0.toFixed(2)} ${y0.toFixed(2)} A ${r} ${r} 0 ${large} 1 ${x1.toFixed(2)} ${y1.toFixed(2)}`;
};

// 三表:点火 f12,错峰 4f
const GAUGES = [
  { start: 12, target: 190 },
  { start: 16, target: 120 },
  { start: 20, target: 235 },
];

const needleAngle = (frame: number, s: number, target: number): number => {
  if (frame <= s) return 0;
  if (frame <= s + 12) {
    // 去程:甩满全弧,ease-out(起步猛)
    return interpolate(frame, [s, s + 12], [0, 270], {
      easing: Easing.out(Easing.cubic),
    });
  }
  if (frame <= s + 25) {
    // 回程主段:落过真实值 8°
    return interpolate(frame, [s + 12, s + 25], [270, target - 8], {
      easing: Easing.inOut(Easing.cubic),
    });
  }
  // 回摆:从过冲位弹回真实值,之后 clamp 为常数
  return interpolate(frame, [s + 25, s + 32], [target - 8, target], {
    easing: Easing.out(Easing.cubic),
    extrapolateRight: 'clamp',
  });
};

const Gauge: React.FC<{ start: number; target: number }> = ({ start, target }) => {
  const frame = useCurrentFrame();
  const d = needleAngle(frame, start, target);
  const settle = start + 32;
  const value = Math.round((target / 270) * 100);

  const popScale = interpolate(frame, [settle, settle + 4, settle + 8], [0.3, 1.18, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const popOp = interpolate(frame, [settle, settle + 3], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  const [tipX, tipY] = polar(135, R - 26);
  const [tailX, tailY] = polar(315, 36);

  // 大刻度 11 根(每 27°),小刻度每 9°
  const ticks: React.ReactNode[] = [];
  for (let k = 0; k <= 30; k++) {
    const dd = k * 9;
    const major = k % 3 === 0;
    const a = 135 + dd;
    const [x0, y0] = polar(a, R - 8);
    const [x1, y1] = polar(a, major ? R - 30 : R - 19);
    ticks.push(
      <line key={k} x1={x0} y1={y0} x2={x1} y2={y1}
        stroke={dd >= 225 ? RED : G.mid} strokeWidth={major ? 4 : 2} />
    );
  }

  return (
    <div style={{ width: GA_W, height: 480, position: 'relative' }}>
      <svg width={GA_W} height={430}>
        {/* 弧形轨道 + 红区 */}
        <path d={arcPath(0, 270, R)} fill="none" stroke={G.line} strokeWidth={10} strokeLinecap="round" />
        <path d={arcPath(225, 270, R)} fill="none" stroke={RED} strokeWidth={10} strokeLinecap="round" opacity={0.85} />
        {ticks}
        {/* 指针:琥珀刚体,rotate 全量程往返 */}
        <g transform={`rotate(${d.toFixed(3)} ${CX} ${CY})`}>
          <line x1={tailX} y1={tailY} x2={tipX} y2={tipY} stroke={AMBER} strokeWidth={9} strokeLinecap="round" />
        </g>
        <circle cx={CX} cy={CY} r={15} fill={G.ink} />
        <circle cx={CX} cy={CY} r={6} fill={AMBER} />
      </svg>
      {/* 落定同帧弹出的数值 */}
      <div style={{
        position: 'absolute', left: 0, right: 0, top: 396, textAlign: 'center',
        opacity: popOp, transform: `scale(${popScale.toFixed(4)})`,
      }}>
        <span style={{ fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800, fontSize: 62, color: G.ink }}>
          {value}
        </span>
        <span style={{ fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 700, fontSize: 30, color: G.mid, marginLeft: 8 }}>
          %
        </span>
      </div>
    </div>
  );
};

export const NeedleSweepSelftest: React.FC = () => {
  return (
    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>
      <div style={{ position: 'absolute', top: 110, width: '100%', textAlign: 'center' }}>
        <TitleBlock text="NEEDLE SWEEP SELF-TEST" size={72} />
      </div>
      <div style={{
        position: 'absolute', left: CARD_X, top: CARD_Y, width: CARD_W, height: CARD_H,
        background: G.card, border: `2px solid ${G.border}`, borderRadius: 14,
        boxSizing: 'border-box', boxShadow: '0 2px 8px rgba(0,0,0,0.06)', padding: 32,
      }}>
        <div style={{ height: 16, width: 320, background: G.bar, borderRadius: 8 }} />
        <div style={{ position: 'absolute', left: 0, top: 96, display: 'flex' }}>
          {GAUGES.map((g, i) => (
            <Gauge key={i} start={g.start} target={g.target} />
          ))}
        </div>
      </div>
    </div>
  );
};


// ===== demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx =====
// tape-scroll-fixed-pointer —— 滚带定针
// 取景窗+琥珀三角指针钉死在画面正中,竖向长刻度带(0–500,每 50 一大格)
// 从窗后滚过:"世界动、针不动"。先慢速爬(f12–55 读数 60→140 缓涨),
// 然后一次大跳变:刻度带冲刺 ~23f(峰值 ~50px/f)冲过 420 到 442,
// spring 刹车回摆一次(442→415→420)落定。窗内读数同步。f96 后真静止 44f。
// 帧确定性:value(frame) 纯分段 interpolate 全 clamp,带偏移 = value 线性映射。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, TitleBlock } from '../../_fixtures/Fixtures';

const AMBER = '#b45309';

const PXU = 3; // px per unit:500 量程 → 1500px 长带
const CENTER_Y = 590; // 取景窗中线(屏幕坐标)
const TAPE_X = 830; // 刻度带左缘
const TAPE_W = 260;

// 读数时间线:静置→慢爬→冲刺(过冲到442)→回摆(415)→落定420→真静止
const valueAt = (frame: number): number => {
  if (frame <= 12) return 60;
  if (frame <= 55) {
    return interpolate(frame, [12, 55], [60, 140]); // 慢爬 ~5.6px/f
  }
  if (frame <= 78) {
    return interpolate(frame, [55, 78], [140, 442], {
      easing: Easing.inOut(Easing.cubic), // 冲刺段,峰值 ~50px/f
    });
  }
  if (frame <= 88) {
    return interpolate(frame, [78, 88], [442, 415], {
      easing: Easing.out(Easing.cubic), // 刹车回摆:甩过头再拉回
    });
  }
  return interpolate(frame, [88, 96], [415, 420], {
    easing: Easing.out(Easing.cubic),
    extrapolateRight: 'clamp',
  });
};

export const TapeScrollFixedPointer: React.FC = () => {
  const frame = useCurrentFrame();
  const v = valueAt(frame);

  // 刻度带:数值大在上。单位 u 的屏幕 y = CENTER_Y + (v - u) * PXU
  const yOf = (u: number): number => CENTER_Y + (v - u) * PXU;

  const ticks: React.ReactNode[] = [];
  for (let u = 0; u <= 500; u += 10) {
    const y = yOf(u);
    if (y < 180 || y > 1010) continue; // 视野外裁剪
    const major = u % 50 === 0;
    ticks.push(
      <div key={u} style={{
        position: 'absolute', top: y - 2, right: 0,
        width: major ? 92 : 46, height: major ? 5 : 3,
        background: major ? G.ink : G.mid, borderRadius: 2,
      }} />
    );
    if (major) {
      ticks.push(
        <div key={`n${u}`} style={{
          position: 'absolute', top: y - 22, right: 110,
          fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 700,
          fontSize: 38, color: G.mid, textAlign: 'right', width: 90,
        }}>
          {u}
        </div>
      );
    }
  }

  return (
    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>
      <div style={{ position: 'absolute', top: 90, width: '100%', textAlign: 'center' }}>
        <TitleBlock text="TAPE SCROLL · FIXED POINTER" size={68} />
      </div>

      {/* 刻度带容器(世界层:整体在动) */}
      <div style={{
        position: 'absolute', left: TAPE_X, top: 180, width: TAPE_W, height: 830,
        background: G.panel, border: `2px solid ${G.border}`, borderRadius: 12,
        overflow: 'hidden', boxSizing: 'border-box',
      }}>
        <div style={{ position: 'absolute', inset: 0 }}>{
          // 内层坐标 = 屏幕 y - 180
          <div style={{ position: 'absolute', left: 0, right: 14, top: -180, height: 1080 + 400 }}>
            {ticks}
          </div>
        }</div>
      </div>

      {/* 固定层:取景窗 + 琥珀三角指针 + 读数(针不动) */}
      <div style={{
        position: 'absolute', left: TAPE_X - 10, top: CENTER_Y - 46,
        width: TAPE_W + 20, height: 92,
        border: `5px solid ${AMBER}`, borderRadius: 10,
        boxShadow: '0 4px 14px rgba(0,0,0,0.14)', boxSizing: 'border-box',
        background: 'rgba(255,255,255,0.14)',
      }} />
      <svg width={60} height={64} style={{ position: 'absolute', left: TAPE_X - 66, top: CENTER_Y - 32 }}>
        <polygon points="4,4 56,32 4,60" fill={AMBER} />
      </svg>

      {/* 同步读数窗 */}
      <div style={{
        position: 'absolute', left: TAPE_X + TAPE_W + 70, top: CENTER_Y - 84,
        width: 380, height: 168, background: G.card,
        border: `2px solid ${G.border}`, borderRadius: 14,
        boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
        display: 'flex', flexDirection: 'column', justifyContent: 'center',
        padding: '0 36px', boxSizing: 'border-box',
      }}>
        <div style={{ height: 12, width: 150, background: G.line, borderRadius: 6, marginBottom: 12 }} />
        <div style={{ fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800, fontSize: 96, color: AMBER, letterSpacing: -2, lineHeight: 1 }}>
          {Math.round(v)}
        </div>
      </div>
    </div>
  );
};


  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/data/gauge-readout-moves/NeedleSweepSelftest.tsx, demos/data/gauge-readout-moves/TapeScrollFixedPointer.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
—

Mais nesta categoria