Motion · Dữ liệu
Chuyển động đồng hồ đo số liệu
Hai kiểu đọc số đồng hồ: needle-sweep-selftest — kim quét hết cung tự kiểm tra rồi mới về giá trị thật, và tape-scroll-fixed-pointer — kim đứng yên, dải vạch chia cuộn qua kèm tăng tốc rồi phanh gấp.
Nguồn: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composition Remotion (React/TSX) — có video preview, không có player trong trình duyệt
Biến thể
Kim quét ra ~12f ease-out hết toàn cung, quay về ~20f kèm lắc vượt ngưỡng 5-8° rồi ổn định ở giá trị thật; nhiều đồng hồ lệch nhịp 3-5f; số liệu bật ra bên dưới mặt đồng hồ cùng khung hình lúc ổn định.
Mở đầu dashboard / nghi thức hệ thống sẵn sàng
Dải vạch chia dài dịch chuyển: đoạn bò chậm → lao 45px/khung hình trong ~25f → phanh kiểu spring vượt ngưỡng rồi lắc dừng lại; số liệu trong khung cập nhật đồng bộ.
Một chỉ số nhảy vọt lớn (hiệu năng tăng gấp đôi/hạn mức tăng)
Khi nào dùng
Dùng cho nghi thức mở đầu dashboard/hé lộ chỉ số hiệu năng; A cho cảm giác khởi động nhiều đồng hồ, B cho một chỉ số nhảy vọt lớn.
Ý đồ
Biến "báo số liệu" thành hành động vật lý của đồng hồ đo: A là tự kiểm tra khi đề máy ô tô — kim trước tiên quét hết toàn cung (270°) rồi mới hạ về giá trị thật, nhiều đồng hồ lệch nhịp tạo thành làn sóng, cho thấy thang đo trước rồi mới báo số, tạo cảm giác tuyên bố khai mạc; B theo ngữ pháp dải tốc độ máy bay — kim/khung ngắm ghim chết ở giữa màn hình, dải vạch chia cuộn qua bên dưới; khi số nhảy lớn, dải vạch lao 45px/khung hình rồi phanh kiểu spring, vượt ngưỡng rồi lắc lại — "thế giới di chuyển, kim đứng yên", biến độ lớn của số liệu thành khoảng cách di chuyển nhìn thấy được. Phân công với odometer-digit-roll: đó là chữ số cuộn trong khung, B là cả dải vạch chia cùng trượt.
Thời lượng & năng lượng
- Thời lượng: A: 4–5s / B: 4–5s
- Năng lượng: Trung bình cao (kiểu nghi thức cơ khí)
Công thức cảnh quay (bản gốc tiếng Trung — timing, easing, bảng tham số, lỗi hay gặp)
意图
把"报数值"做成仪表的身体动作: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)
Mã nguồn tham chiếu (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
Component import fixture chung từ demos/_fixtures và helper từ assets/lib của repo; xem SKILL.md trong repo để biết quy trình render.
Mọi thứ trên trang này đều có bản máy đọc. Đưa agent link JSON, hoặc thêm MCP server gimgs rồi bảo nó tìm motion template.
- Tải JSON — trong đó có công thức cảnh quay (timing, easing, bảng tham số, lỗi hay gặp) và toàn bộ source TSX.
- Clone repo video-shotcraft; component import fixture chung từ demos/_fixtures và helper từ assets/lib.
- Đăng ký component thành <Composition> trong project Remotion rồi render bằng `npx remotion render`, hoặc dựng lại công thức bằng engine của bạn.
curl -s https://gimgs.net/motion/gauge-readout-moves.jsongit 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.mp4claude 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 "Chuyển động đồng hồ đo số liệu" (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).
// ===== 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>
);
};
- Read the recipePrompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
- Get the codetool: git
git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install - Register + rendertool: remotion
npx remotion render <CompositionId> out.mp4 # component: demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx, demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx - Or re-implementPort the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
| param | type | default | description |
|---|---|---|---|
| — | |||
Cùng danh mục

Lia dọc trục thời gian
Lia dọc trục thời gian — máy quay tăng tốc dọc theo trục thước ngang lướt qua các mốc phiên bản, mỗi mốc thẻ bật đứng dừng ngắn, mốc cuối phanh gấp đẩy máy vào.

Cuộn phanh gấp danh sách dài
Hai kiểu phanh cuộn dài: changelog-scroll-brake giảm tốc dừng chính xác; brake-reticle-lock thêm khóa tâm ngắm trên cùng khung hình.

Vòng đồng tâm hé lộ chú thích
Chủ thể toàn màn hình bị khung tròn thu lại thành sơ đồ vòng đồng tâm, rồi cả nhóm dịch trái thu nhỏ để lộ chú thích.

Va chạm ăn mừng bằng hạt
Hai kiểu hạt ăn mừng: confetti-crossfire bắn pháo hai bên, và counter-tick-sparks bắn tia lửa mỗi khi bộ đếm vượt mốc nghìn.