渐变刚甩掉「灰廊」,就被 OKLCH 裁出硬边:sRGB / HSL / OKLCH 三条插值路径的实测复盘

在 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,读取其色度 CC 越低,说明路径上最灰的那一步越接近灰廊。结果如下:

色对 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],那些越界采样点会被拍扁到色域边界上,形成肉眼可见的硬边。CSS linear-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 个):

相关推荐
幸福摩天轮2 小时前
function Calling工具系统怎么实现
前端
Jul1en_3 小时前
【Claude Code Compact】源码级别的学习上下文压缩
java·前端·学习·github·ai编程
windliang3 小时前
Claude Code 源码分析(五):一次 Tool 调用怎样通过权限检查
前端·人工智能·面试
fengci.3 小时前
Microweber CMS 未授权路径穿越漏洞(CVE-2026-65694)
android·开发语言·前端·学习·php
Deryck_德瑞克3 小时前
【Nginx】配置差异分析
服务器·前端·nginx
前端 贾公子3 小时前
第06章:结构化输出 (上)
java·服务器·前端
北斗落凡尘3 小时前
React面试题
前端
一棵白菜4 小时前
mac 部署 n8n
前端
学高数就犯困4 小时前
React:常见的性能优化手段
前端·react.js
天天码行空4 小时前
vkeyboardhand:零依赖虚拟键盘指法组件
前端·javascript·vue.js