首页
/ Remotion 图表动画指南:用 spring 与 stroke 技法打造柱状图与饼图数据可视化

Remotion 图表动画指南:用 spring 与 stroke 技法打造柱状图与饼图数据可视化

2026-09-07 09:10:39作者:虞亚竹Luna

导读

本文基于 prompt-to-motion-graphics 模板内置的 charts 图表动画技能文档packages/template-prompt-to-motion-graphics/src/skills/charts.md),系统讲解在 Remotion 中为数据可视化添加高质量动画的实战方法:柱状图入场交错、坐标轴标注、柱内数值标签以及饼图分段动画。文中所有规则均在仓库内有对应的可运行完整示例(如 gold-price-charthistogram),读者读完即可直接把这些模式复用到自己的柱状图、饼图、直方图与进度条动画中,让数据画面同时具备清晰度与节奏感。

背景说明: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.tsconst frameClamped = Math.max(0, frame)),因此晚到的柱子会安静等待自己的出场时机,而先到的柱子已开始生长,形成自左向右(或按数据顺序)的涟漪效果。

该模式在仓库的完整示例中得到了直接印证:

  • gold-price-chart.ts 中 12 根月份柱子使用 STAGGER_DELAY = 5BARS_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.tsdefaultSpringConfig)。该实现基于阻尼振荡器模型,dampingstiffness 共同决定阻尼比 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>;

三个实现要点:

  1. 刻度容器与柱区等高:刻度列使用 flexDirection: column + justifyContent: space-between,让 5 档刻度均匀分布在整个绘图高度上;
  2. 翻转顺序yAxisSteps.reverse()(或复制后 slice().reverse(),避免原地修改)保证数值从上到下递减,即最大值在视觉顶部、最小值在底部;
  3. 竖轴落在柱区:给柱区容器加 borderLeft: "1px solid #333" 作为 Y 轴主线,柱子的起点被锚定在轴上。

在真实数据集上,刻度通常是“取整后的数据区间”而非固定的 0–100。以 gold-price-chart.ts 为例:数据范围约 2000–2750 美元,作者设置 minPrice = 1900、maxPrice = 2800yAxisSteps = [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(与柱状图同一套延迟思路);若要在展开后凸显某一段,也常把整组 progressinterpolate(progress, [0, 1], [0.6, 1]) 之类的小幅缩放做“装配完成后轻弹一下”的收尾。

interpolate() 本身还支持 extrapolateRight: "clamp" 等边界外推策略,可用在柱值标签、进度百分比等需要“停在终值不越界”的场景;spring() 的输出(常会短暂越过 1.0 再回落)也常与 interpolate() 配合映射到旋转角或位移区间。

完整落地:charts 技能的仓库示例闭环

charts.md 并非孤立规范,它背后有整套可运行的“参考答案”闭环:

  • 技能分类与加载src/skills/index.tscharts 注册为 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.tssrc/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.tshistogram.ts

把这几条规则内化为默认习惯,Remotion 产出的每一张数据图都会既准确又生动。

登录后查看全文
热门项目推荐
相关项目推荐