在 canvas 或 shader 里做一条红到青的渐变,如果直接对 RGB 三个通道做线性插值,中间大概率会出现一段脏兮兮的灰棕色。前端圈子里「用 OKLCH」几乎成了标准答案——它确实能让渐变保持鲜艳。但我对 5 组互补色对跑了 100 步实测后,发现一个反直觉的结论:OKLCH 路径平均有 48.7% 的插值步会越出 sRGB 色域,直接截断就会产生硬边。灰廊是被赶走了,硬边又住进来了。
这篇文章不站队。我只把 sRGB、HSL、OKLCH 三条插值路径放在同一把尺子上量一量,看它们分别在「灰廊深度」「色相绕路」「色域裁剪」三个维度上表现如何。
背景:做渐变时我们到底在插什么
颜色插值不是把两个十六进制字符串平均一下那么简单。你选的色空间,本质上是在给中间色规定一条路径。
- sRGB:把两个颜色当成 RGB 三维空间里的点,直接拉直线。这条线很容易穿过色立方中心附近,于是红到青的中点会冲到
(128,128,128)这种灰褐色。 - HSL:先把端点转到色相-饱和度-亮度圆柱,再分别插值。它绕开了灰廊,但色相是按角度走的,最短弧经常要经过一个你根本没选的第三色。
- OKLCH:在感知均匀的 OKLab 柱坐标里插值。它保证路径最短、亮度均匀,代价是这条感知直线经常穿出 sRGB 这个小色域。
大多数开发者对 sRGB 的灰廊有痛感,对 HSL 的色相绕路不太敏感,对 OKLCH 的色域裁剪几乎没概念。下面我把三种路径都实现了一遍,用同一组数据说话。
三种插值路径的实现与观察
我挑了生成艺术和 UI 里最常见的 5 组互补/近互补色对:红→青、蓝→橙、品红→绿、紫→黄、粉→青蓝。每种色对分别用 sRGB、HSL、OKLCH 插 100 步,再统一转回 sRGB 显示。
图1:红→青在三种插值空间下的路径对比。sRGB 中点发灰;HSL 绕路穿过品红;OKLCH 走最短感知路径并保鲜艳。
只看图 1 就能读出三个关键现象:
- sRGB 红→青的中点明显发灰;
- HSL 虽然鲜艳,但路径从红拐到品红再到青蓝,完全不是「最短路径」;
- OKLCH 看起来最自然,红与青之间的过渡饱满均匀。
但这只是肉眼。我们要把「灰廊深度」「色相绕路」「色域裁剪」量化出来。
实证一:最小感知色度 C(灰廊深度)
我把每条路径上的中间颜色再统一转到 OKLCH,读取其色度C。C越低,说明路径上最灰的那一步越接近灰廊。结果如下:
| 色对 | sRGB 最小 C | HSL 最小 C | OKLCH 最小 C |
|---|---|---|---|
| 红→青 | 0.044 | 0.168 | 0.133 |
| 蓝→橙 | 0.023 | 0.215 | 0.156 |
| 品红→绿 | 0.061 | 0.152 | 0.189 |
| 紫→黄 | 0.088 | 0.170 | 0.125 |
| 粉→青蓝 | 0.068 | 0.174 | 0.122 |
平均最小 C:sRGB ≈ 0.057,HSL ≈ 0.176,OKLCH ≈ 0.145。
图2:五条色对的最小感知色度 C 对比。sRGB(红)几乎贴着 0,HSL(黄)与 OKLCH(绿)都保持在 0.12 以上。
sRGB 在互补色渐变里几乎必然制造灰廊,这是 RGB 立方几何决定的。HSL 和 OKLCH 都能把最小 C 拉到 0.12 以上,视觉上不再发灰。
实证二:色相绕路度数(HSL 的谎言)
HSL 保鲜艳的代价是色相绕路。我把每条路径上每一步的 OKLCH 色相累加,得到路径实际穿越的色相总跨度:
| 色对 | sRGB 色相跨度 | HSL 色相跨度 | OKLCH 色相跨度 |
|---|---|---|---|
| 红→青 | 154.8° | 134.8° | 0.1° |
| 蓝→橙 | 188.3° | 135.8° | 3.0° |
| 品红→绿 | 129.6° | 115.6° | 1.0° |
| 紫→黄 | 142.4° | 133.3° | 13.0° |
| 粉→青蓝 | 145.2° | 137.8° | 0.1° |
HSL 平均绕路约 131°,OKLCH 平均不到 4°。
这意味着用 HSL 做「红→青」渐变,浏览器会替你悄悄经过品红;做「蓝→橙」渐变,会经过紫和红。如果你的品牌或数据语义要求严格控制过渡色,HSL 这个自动绕路就是 bug,而不是特性。
实证三:OKLCH 的越界裁剪率
OKLCH 的麻烦不在灰廊,而在色域。它描述的是一个比 sRGB 大得多的感知空间,一条感知直线从 sRGB 内的一点走到另一点,很容易穿出 sRGB 的边界。我把每条 OKLCH 路径上越出[0,1]的 RGB 采样点比例算了出来:
| 色对 | OKLCH 越界比例 |
|---|---|
| 红→青 | 0% |
| 蓝→橙 | 35% |
| 品红→绿 | 45% |
| 紫→黄 | 66% |
| 粉→青蓝 | 0% |
平均越界比例 48.7%。
图3:OKLCH 路径越出 sRGB 色域的比例。紫→黄高达 66%,不做 gamut 映射直接截断会出现明显硬边。
越界本身不是错误,但你如果在 canvas 或 shader 里 naive 地把r/g/b截断到[0,1],那些越界采样点会被拍扁到色域边界上,形成肉眼可见的硬边。CSSlinear-gradient(in oklch, ...)浏览器会自动做 gamut 映射,所以网页渐变里不明显;可一旦你手写 OKLCH 插值并自己转回 RGB,这就是必须处理的坑。
局限与落地建议
这套实测只覆盖 sRGB 端点,没碰 P3、Rec.2020 等广色域;也没测试不同 gamut 映射算法(clip、chroma 缩减、L 保持)对视觉的影响。但它足够说明:选色空间不是「哪个更好」,而是「你愿意为哪种副作用买单」。
落地建议:
- 网页 CSS 渐变:直接用
linear-gradient(in oklch, ...),浏览器会帮你处理色域映射,风险最低。 - canvas / WebGL / 自写算法:如果手动做 OKLCH 插值,务必加 gamut 映射,比如迭代降低 chroma 直到 RGB 回退到
[0,1],而不是简单截断。 - 色相相近的端点(比如蓝→紫):HSL 绕路不严重,计算又比 OKLCH 轻,完全可以继续用。
- 互补/近互补端点:避开 sRGB 线性插值;如果性能敏感且不需要精确感知路径,HSL 是折中方案。
结论与下一步
三种插值路径都守恒,但守恒的不是同一个东西:sRGB 守恒的是 RGB 三个通道的算术平均,代价是灰廊;HSL 守恒的是色相弧长与饱和度,代价是绕路;OKLCH 守恒的是感知距离,代价是色域裁剪。没有银弹。
如果你在写生成艺术、数据可视化或设计系统,值得把这条判断写进项目规范:互补色渐变用 OKLCH + gamut 映射,邻近色用 HSL,sRGB 插值只作为兜底 fallback。
开源地址(结论段,指向同一组织即可,3 个):
- 矩阵门户:https://github.com/wangzifan396-wzf/WB
- 单文件工具聚合器:https://github.com/wangzifan396-wzf/nano-workbench
- GitHub 组织主页:wangzifan396-wzf (WangZi) · GitHub