算法与数据结构 / 动画引擎原理 · 从插值到播放控制 / 插值:中间值怎么求 待审核 1 / 47
interpolate · mix · 线性光

插值:中间值怎么求

动画看上去是元素在动,落到实处是一个反复求值的问题:给定起点 from、终点 to 与进度 p[0,1]p \in [0,1],中间值是多少。插值(interpolation)就是这个问题的答案。它位于整套引擎的最底层——上面的缓动、弹簧、关键帧都只负责把 pp 算出来,真正把 pp 变成一个可以写进样式的值,是插值这一步。

麻烦在于「中间」并非只有一种含义。数字有唯一的线性中点;10px 这类带单位的字符串要先拆出数字部分;而颜色的中点取决于在哪个空间里求平均。@vega/animgetMixer(from, to) 按值的形态分派到对应的混合器,调用方不必自己判断类型。

1 · 三类值的中间值

数字的插值是 from + (to − from) · p,一条直线。带单位的字符串按同样的规则处理数字部分,再把单位原样贴回去,于是 10px200pxp = 0.5 处得到 105px。颜色是三类里唯一不能照搬这条规则的:sRGB 的通道值经过 gamma 编码,直接取平均得到的并不是「一半的光」。

图 1-1 · 同一个进度 p 下,数字、带单位字符串与颜色三类值各自的中间结果。可拖动 p 与两端颜色,对比线性光混合与朴素 sRGB 平均两条渐变带。

2 · 颜色通道的 gamma 编码

sRGB 的 0–255 通道值不是光强的线性刻度:数值 128 对应的实际光强约为最大值的 21%,而非 50%。直接对编码值取平均,等于在一把非线性的尺子上求中点,结果落在偏暗的一侧。跨色相的过渡最容易暴露这一点——红到绿、橙到蓝,朴素平均的中段会塌成一片灰。

正确的做法是先把通道值解码回线性光(linear-light)、在光强上取平均、再编码回 sRGB。引擎的 mixColor 用平方近似这套换算:中点取 from2+(to2from2)p\sqrt{\text{from}^2 + (\text{to}^2 - \text{from}^2) \cdot p},比完整的 gamma 2.4 解码便宜得多,而在动画这种逐帧场景下肉眼分辨不出差别。

警示 · 平方近似只在 sRGB 内部成立,换到别的色彩空间要另算。真正按感知均匀混合、或需要沿色相环走一条指定方向的弧,得换一整套换算,见 色彩空间

3 · 引擎的分派入口

getMixer 在建立动画时调用一次,返回一个只吃 pp 的闭包,逐帧路径上不再有类型判断。这也是为什么关键帧数组里可以混放数字、字符串与颜色:每一段各自持有自己的混合器。

import { mixNumber, getMixer, mixColor } from '@vega/anim';

mixNumber(0, 100, 0.5);              // 50 —— 线性
getMixer('10px', '200px')(0.5);      // '105px' —— 拆数字, 回贴单位
mixColor('#f76707', '#1864ab', 0.5); // 线性光混色, 中点不发灰

插值只回答「中间值是多少」,不回答「什么时候取哪个 pp」。把均匀流逝的时间重新塑形成有快慢的 pp,是缓动的职责;让一个物理系统自己演化出 pp,是弹簧的做法;一串途经值之间的分段插值见关键帧。引擎里的 interpolate(input, output) 还能把任意标量区间映射到任意值域,是滚动与拖拽驱动的基础。