算法与数据结构 / 动画引擎原理 · 从插值到播放控制 / 色彩空间:沿什么路径混合颜色 待审核 2 / 47
color space · OKLCH · HSL hue

色彩空间:沿什么路径混合颜色

两种颜色之间做插值,得到的不是一个点,而是一条路径。起点与终点固定,中途经过哪些颜色则完全由「在哪个空间里取平均」决定。同一对红与绿,四种空间给出四条形状迥异的路径:sRGB 中段塌成灰,线性光中段偏亮,HSL 会途经黄,OKLCH 则一路保持饱和。

这四条路径没有一条是「错」的,但它们回答的是不同的问题。sRGB 通道平均回答「数值的中点」,线性光回答「光强的中点」,OKLCH 回答「人眼看上去的中点」。动画要的通常是最后一个。

1 · 四条路径并排

图 1-1 · 同一对端点在四个色彩空间里的插值路径,以及一个沿选中路径滑动的色块。可换端点对、切换路径与 HSL 的色相方向。

2 · gamma 编码与线性光

sRGB 的通道值是 gamma 编码过的:数值 128 对应的实际光强约为最大值的 21%,而不是 50%。直接对编码值取平均,等于在一把非线性的尺子上找中点,落点系统性偏暗。跨色相的过渡最能暴露这一点——红到绿的中段会塌成一片灰。

正确的换算是解码到光强、在光强上平均、再编码回来:

lin(c)={c/12.92c0.04045(c+0.0551.055)2.4否则\text{lin}(c) = \begin{cases} c/12.92 & c \le 0.04045 \\ \left(\dfrac{c + 0.055}{1.055}\right)^{2.4} & \text{否则} \end{cases}

引擎的 mixColor 没有跑完整的 gamma 2.4,而是用通道值的平方近似这条曲线(见插值)。这一页的线性光带走的是完整换算,两者并排看中段亮度略有差异,但方向一致——都比朴素平均亮。

3 · 色相环的方向

HSL 的色相是一个角度,两个角度之间有两条弧可走。0° 到 240°:沿色相递增经黄、绿、青走过 240°,沿色相递减经品红只走 120°。默认取较短的那一段,但「较短」并不总是想要的——从红到红旁边的一点点橙,最短弧是对的;而做一个转满整圈的色相动画,最短弧会让它原地不动。

CSS Color 4 把这几档记作 shorter / longer / increasing / decreasing,本页的方向选择器与前三者同义。

警示 · HSL 沿色相环插值时明度与饱和度也在同时线性变化,途经色未必落在两端连成的「视觉直线」上。黄到紫这类端点,HSL 路径会先经过一段明显发绿的区域——那不是实现的问题,是色相环本身的几何。要避免途经色跑偏,得换到感知均匀的空间。

4 · 感知均匀的坐标

OKLab 的坐标按人眼感知校准:坐标上等距的差对应视觉上等距的差。OKLCH 是它的极坐标形式,LL 是明度、CC 是彩度、hh 是色相角。在这套坐标里沿直线或沿弧走,明度与饱和度都均匀推进,不会在中段塌陷成灰,也不会在某一段突然过饱和。

代价是换算不便宜:sRGB 到 OKLab 要先解码到线性光,再过一个 3×33 \times 3 矩阵,对三个分量各取一次立方根,最后再过一个矩阵。逐帧重算四条路径时这一步是本页最贵的计算——不过对于一次只算一个色值的真实动画,这点开销可以忽略。

// sRGB 编码值 ↔ 线性光强 (gamma 2.4)
const lin = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
const gam = (c) => c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;

// 线性光混合: 解码 → 平均 → 编码回来, 中点不发灰
const atLinear = (a, b, p) =>
  a.map(lin).map((x, i) => gam(x + (lin(b[i]) - x) * p));

// 色相环按方向插值 (最短弧 / 顺时针 / 逆时针)
function hueLerp(h1, h2, p, dir) {
  let d = h2 - h1;
  if (dir === 'short') d = ((d % 360) + 540) % 360 - 180;
  else if (dir === 'cw' && d < 0) d += 360;
  else if (dir === 'ccw' && d > 0) d -= 360;
  return h1 + d * p;
}

从 from 到 to 逐点求中间色的机制见插值;把进度 pp 随时间推进的快慢塑形,交给缓动