Remotion 图表动画指南:用 spring 与 stroke 技法打造柱状图与饼图数据可视化
导读
本文基于 prompt-to-motion-graphics 模板内置的 charts 图表动画技能文档(packages/template-prompt-to-motion-graphics/src/skills/charts.md),系统讲解在 Remotion 中为数据可视化添加高质量动画的实战方法:柱状图入场交错、坐标轴标注、柱内数值标签以及饼图分段动画。文中所有规则均在仓库内有对应的可运行完整示例(如 gold-price-chart、histogram),读者读完即可直接把这些模式复用到自己的柱状图、饼图、直方图与进度条动画中,让数据画面同时具备清晰度与节奏感。
背景说明:charts.md 是 template-prompt-to-motion-graphics(提示词生成动态图形模板)中的一份“技能指引”。该模板把这份 Markdown 作为图表类生成任务的规则注入给 LLM(加载与组合逻辑见 src/skills/index.ts,注入入口见 src/app/api/generate/route.ts)。不过其全部要点都是通用的 Remotion 图表编码范式,可直接在任意 Remotion 项目中落地。
柱状图入场:用帧偏移实现错落交错
同一张图里的多根柱子如果同时从 0 长到满,会显得机械、缺乏重点。推荐做法是给每一根柱子一个递增的入场延迟(stagger),并用 spring() 产生有质感的生长曲线。
错误示范——所有柱子一起生长:
const bars = data.map((item, i) => {
const height = spring({ frame, fps, config: { damping: 18 } });
return <div style={{ height: height * item.value }} />;
});
正确示范——逐根错落入场:
const STAGGER_DELAY = 5;
const bars = data.map((item, i) => {
const delay = i * STAGGER_DELAY;
const height = spring({
frame: frame - delay,
fps,
config: { damping: 18, stiffness: 80 },
});
return <div style={{ height: height * item.value }} />;
});
关键点在于 frame - i * STAGGER_DELAY:每根柱子比前一根晚 5 帧启动。Remotion 的 spring() 会对负帧做钳制处理,返回 0(源码见 packages/core/src/spring/spring-utils.ts 的 const frameClamped = Math.max(0, frame)),因此晚到的柱子会安静等待自己的出场时机,而先到的柱子已开始生长,形成自左向右(或按数据顺序)的涟漪效果。
该模式在仓库的完整示例中得到了直接印证:
- gold-price-chart.ts 中 12 根月份柱子使用
STAGGER_DELAY = 5、BARS_START_FRAME = 10,即frame - i * 5 - 10; - histogram.ts 使用更慢的节奏
delay = i * 10。
延迟步长(5 帧或 10 帧)取决于数据量和总时长:柱子多、节奏紧时选 3–5 帧,追求从容气质时可放宽到 10 帧。
深入:图表场景下的 spring 参数含义
图表动画是“缓进”场景,一般不追求夸张弹跳,常用以下两组(charts.md 推荐的 damping: 18 / stiffness: 80 即落在该区间):
| 参数 | 作用 | 示例值区间 |
|---|---|---|
damping |
阻尼,值越大回弹越少、越“收敛” | 10–200(柱状图常用 15–20) |
stiffness |
刚度,值越大运动越快、越“干脆” | 50–200(柱状图常用 80–100) |
mass |
质量,值越大惯性越强、越“沉重” | 0.5–3(默认 1) |
仓库中 spring() 的默认配置为 damping: 10、mass: 1、stiffness: 100(见 spring-utils.ts 的 defaultSpringConfig)。该实现基于阻尼振荡器模型,damping 与 stiffness 共同决定阻尼比 zeta = damping / (2 * sqrt(stiffness * mass)),只有 zeta < 1(欠阻尼)时才会产生越过目标值的轻微过冲——这正是“数据柱子带一点灵性”的物理来源。
务必保留 Y 轴刻度标签
没有坐标轴的图表让人无法判断数值量级。图表动画必须画出带刻度的坐标轴,用浅色小字号(示例为 fontSize: 12, color: "#888")标注关键档位。
错误示范——只有光秃秃的柱区:
<div style={{ display: "flex", alignItems: "flex-end", gap: 8 }}>{bars}</div>
正确示范——左侧 Y 轴刻度 + 左侧竖边线:
const yAxisSteps = [0, 25, 50, 75, 100];
<div style={{ display: "flex" }}>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}
>
{yAxisSteps.reverse().map((step) => (
<span style={{ fontSize: 12, color: "#888" }}>{step}</span>
))}
</div>
<div
style={{
display: "flex",
alignItems: "flex-end",
gap: 8,
borderLeft: "1px solid #333",
}}
>
{bars}
</div>
</div>;
三个实现要点:
- 刻度容器与柱区等高:刻度列使用
flexDirection: column+justifyContent: space-between,让 5 档刻度均匀分布在整个绘图高度上; - 翻转顺序:
yAxisSteps.reverse()(或复制后slice().reverse(),避免原地修改)保证数值从上到下递减,即最大值在视觉顶部、最小值在底部; - 竖轴落在柱区:给柱区容器加
borderLeft: "1px solid #333"作为 Y 轴主线,柱子的起点被锚定在轴上。
在真实数据集上,刻度通常是“取整后的数据区间”而非固定的 0–100。以 gold-price-chart.ts 为例:数据范围约 2000–2750 美元,作者设置 minPrice = 1900、maxPrice = 2800,yAxisSteps = [1900, 2100, 2300, 2500, 2700],再用 normalizedHeight = ((item.price - minPrice) / range) * chartHeight 把真实值映射到像素高度——这样既保证刻度整齐,又不会因从 0 开始而压缩掉数据差异。轴线下沿还可用 borderBottom 补齐横向基线,形成完整的坐标框架。
数值标签:柱内放置、随生长淡入
当柱子足够高时,把数值标签放进柱子内部,并用柱子当前的生长进度控制标签透明度,让数字随柱子一起浮现;柱子过矮时(放不下文字)则应隐藏标签,避免文字溢出柱外造成视觉噪声。
const barHeight = normalizedHeight * progress;
<div style={{ height: barHeight, backgroundColor: COLOR_BAR }}>
{barHeight > 30 && (
<span style={{ opacity: progress, fontSize: 11 }}>
{item.value.toLocaleString()}
</span>
)}
</div>;
barHeight > 30:高度不足 30px 时不渲染标签(阈值可按字号与柱宽微调);opacity: progress:因为height = normalizedHeight * progress,标签透明度与柱高增长严格同步,不会出现“柱子还没长出来、数字却先显示完整”的穿帮;item.value.toLocaleString():给大数补千分位分隔符(如2,039),信息更易读。
histogram 示例对这一点做了变体强化:它把标签放在柱顶内侧,并用 {Math.round(item.value * progress)} 让数字本身也随动画从 0 逐步累加到真实值(见 histogram.ts),配合 drop-shadow 光晕提升视觉层次。实际项目中可按“信息密度优先”或“视觉干净优先”在两种做法间取舍。
饼图动画:stroke-dashoffset 逐段展开
饼图分段不适合用整体旋转扫入,推荐用描边技法:每段弧是一个 circle,通过 stroke-dasharray 控制可见弧长,再用 stroke-dashoffset 从“整段不可见”动画到“整段可见”,同时把整圈旋转 -90°,让第一段从 12 点钟方向起笔。
const circumference = 2 * Math.PI * radius;
const segmentLength = (value / total) * circumference;
const offset = interpolate(progress, [0, 1], [segmentLength, 0]);
<circle
r={radius}
cx={center}
cy={center}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={`${segmentLength} ${circumference}`}
strokeDashoffset={offset}
transform={`rotate(-90 ${center} ${center})`}
/>;
其中四个量的含义:
| 量 | 作用 |
|---|---|
circumference |
整圆周长 2 * Math.PI * radius,作为 dash 数组中的“空隙”参照 |
segmentLength |
该分段弧长 = 周长 × value / total,决定每段占圆的比例 |
strokeDasharray |
${segmentLength} ${circumference}:先画本段弧长,再跳过足够大的空隙,保证每根 circle 只显示自己那一段 |
strokeDashoffset |
从 segmentLength 动画到 0,让圆弧像“被画出来”一样逐段展开 |
interpolate() 把 progress(0→1)映射到 offset(segmentLength→0):初始时 offset 等于弧长,整段被推离可视区,随后 offset 递减,弧线逐渐显现。若希望各段依次入场而非同时画出,可把第 i 段写为 frame - i * STAGGER(与柱状图同一套延迟思路);若要在展开后凸显某一段,也常把整组 progress 用 interpolate(progress, [0, 1], [0.6, 1]) 之类的小幅缩放做“装配完成后轻弹一下”的收尾。
interpolate() 本身还支持 extrapolateRight: "clamp" 等边界外推策略,可用在柱值标签、进度百分比等需要“停在终值不越界”的场景;spring() 的输出(常会短暂越过 1.0 再回落)也常与 interpolate() 配合映射到旋转角或位移区间。
完整落地:charts 技能的仓库示例闭环
charts.md 并非孤立规范,它背后有整套可运行的“参考答案”闭环:
- 技能分类与加载:src/skills/index.ts 将
charts注册为GUIDANCE_SKILLS之一(另有 typography、sequencing、spring-physics 等),并在SKILL_DETECTION_PROMPT中定义触发词:charts, data, graphs, histograms, bar charts, pie charts, progress bars, statistics, metrics; - 运行时注入:src/app/api/generate/route.ts 在用户请求命中图表类需求时,调用
getCombinedSkillContent把 charts.md 拼入系统提示词,作为生成图表动画代码时的硬性规则; - 示例代码库:src/examples/code/gold-price-chart.ts 与 src/examples/code/histogram.ts 是本节四条规则的完整演示(标题渐显 → 逐柱交错生长 → Y 轴刻度 → 柱内数值淡入),可在模板的 Code 示例页中直接播放并改写。
对于非提示词应用场景的普通 Remotion 项目,建议的开发落地顺序为:先搭数据与坐标系(Y 轴刻度 + 柱区),再引入 useCurrentFrame / useVideoConfig 计算 fps,随后用 frame - delay 的错落 spring() 驱动高度,最后叠加柱内数值与标签——这正是 charts.md 从“入场节奏”到“坐标标注”再到“标签/饼图”的行文逻辑,也是生成结果质量最高的执行顺序。
小结
高质量图表动画的本质是“信息可读性 + 节奏感”的组合:
- 柱状图:每根柱子延迟 3–5 帧入场,配合
spring()(参考damping: 18 / stiffness: 80)获得自然生长曲线; - 可读性:永远画出带刻度的 Y 轴;柱内数值标签只在高度足够(>30px)时显示,并随生长进度淡入;
- 饼图:用
stroke-dasharray+stroke-dashoffset从 12 点方向逐段展开弧线; - 实现依据:上述规则均有仓库源码可查——spring 物理模型与默认参数见 packages/core/src/spring/spring-utils.ts,完整可运行范例见 gold-price-chart.ts 与 histogram.ts。
把这几条规则内化为默认习惯,Remotion 产出的每一张数据图都会既准确又生动。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00