颜色编码和转换背后的原理,RGB, HSL, HEX

2025-12-22设计工具
分享到

之前写一个颜色选择器组件,UI 设计师给了一个色轮交互稿:拖动色相滑块改变颜色,饱和度亮度在二维平面里选。听起来很简单,最终实现时花了一整晚调 HSL 转 RGB 的公式,每次输出都偏色。发现 bug 出在一个除法取整上——HSL 的色相值是浮点数,我居然直接当整数去算了~~

互联网上每块像素背后都是数字。一个颜色在屏幕上显示出来,中间经过了编码、转换、映射好几个环节。今天就把常见的颜色模型、转换公式和色域概念拆开讲清楚。

颜色数字化的起点

人眼能分辨数百万种颜色,但计算机只能用数字来模拟。数字化的核心问题是:用几个数字能描述一个颜色,每个数字的范围是多少

最直观的方案是 RGB:拿红、绿、蓝三束光,调各自的亮度,混合出一个颜色。这和显示器的工作原理一致——每个像素由三个子像素组成,分别发光。你在屏幕上看到的每个颜色,最终都是 RGB 值。

RGB 和 HEX 是同一回事

RGB 表示法就是三个数字,分别代表红、绿、蓝的亮度。每个通道的范围是 0 到 255,因为 8 位二进制能表示 256 个值。这是历史原因——早期硬件 8 位总线,一个字节传一个通道刚好。后来成了行业标准。

一个具体的颜色:rgb(102, 51, 153),紫色。红通道 102,绿通道 51,蓝通道 153。混合出来就是紫色。

HEX 就是 RGB 的十六进制写法。102 的十六进制是 66,51 是 33,153 是 99。串起来就是 #663399。CSS 里这叫 rebeccapurple,有一段故事——Eric Meyer 的女儿 Rebecca 去世后,这个颜色被加入 CSS 规范作为纪念。

102 怎么变成 66 的?102 ÷ 16 = 6 余 6,所以高位是 6,低位是 6,合起来 66。反过来,十六进制 66 换算回十进制就是 6×16 + 6 = 102。

HEX 还有简写形式。如果每个通道的两个十六进制数相同,可以缩成一位。#663399 可以写成 #639。浏览器会自动展开成 #663399#fff 就是 #ffffff,白色。#000 就是 #000000,黑色。

RGB 和 HEX 没有任何信息差异,只是进制不同。 RGB 用十进制(0-255),HEX 用十六进制(00-FF)。我经常看到有人问"要不要把 HEX 转成 RGB",答案是不需要,它们在浏览器里等价。

HSL 是给人看的

RGB 的问题在于它不符合人类感知颜色的方式。给我一个 rgb(120, 180, 60),我没办法立刻判断这是什么颜色。得在脑子里转一下:红和绿比较多,蓝很少,偏黄绿色。

HSL 就是来解决这个问题的。它用三个更直观的维度描述颜色:

  • Hue(色相): 0-360 度,色环上的位置。0 是红色,120 是绿色,240 是蓝色,360 回到红色。
  • Saturation(饱和度): 0%-100%,颜色的鲜艳程度。0% 是灰色,100% 是最纯的颜色。
  • Lightness(明度): 0%-100%,颜色的明亮程度。0% 是黑色,100% 是白色,50% 是纯色。

还是上面那个颜色。RGB 是 rgb(120, 180, 60)。转成 HSL 大概是 hsl(90, 50%, 47%)。90 度在色环上是黄绿色,饱和度 50% 说明不是最鲜艳的绿色,明度 47% 说明偏暗。这样描述一个颜色就清晰多了。

HSL 的调色体验好太多了:先调色相确定基调,再调饱和度和明度微调。想做一套浅色主题,降低饱和度、提高明度就行了。RGB 做不到这么直觉的操作——把三个通道同时按比例放大缩小,颜色会偏移。

RGB 立方体与 HSL 圆柱体对比

颜色转换的数学

RGB 和 HSL 之间互转是写前端工具的必修课。公式并不复杂,这里拆开讲。

RGB 转 HSL

假设 rgb(255, 102, 0),橙色。

第一步:把 RGB 归一化到 0-1 范围。通道值除以 255。

R' = 255/255 = 1.0
G' = 102/255 = 0.4
B' = 0/255   = 0.0

第二步:找出最大和最小值。

max = 1.0 (R)
min = 0.0 (B)
delta = max - min = 1.0

第三步:算 Lightness。

L = (max + min) / 2 = (1.0 + 0.0) / 2 = 0.5

明度 50%,对应 50%。

第四步:算 Saturation。根据明度分两种情况。

因为 L < 0.5:
S = delta / (max + min) = 1.0 / (1.0 + 0.0) = 1.0

饱和度 100%。

第五步:算 Hue。根据哪个通道是最大值来算。

因为 max 是 R:
H = (G' - B') / delta + (如果 G' < B' 则加 6)
H = (0.4 - 0.0) / 1.0 = 0.4
H = 0.4 × 60 = 24

色相 24 度,接近橙色偏红。

所以 rgb(255, 102, 0) 转成 hsl(24, 100%, 50%)。这个橙色。

HSL 转 RGB

反过来也一样。假设要把 hsl(200, 80%, 45%) 转回 RGB。

第一步:归一化。H 除以 360,S 和 L 除以 100。

H' = 200/360 = 0.556
S' = 0.80
L' = 0.45

第二步:算临时变量。

if L' < 0.5:
  Q = L' × (1 + S') = 0.45 × 1.8 = 0.81
else:
  Q = L' + S' - L' × S'

P = 2 × L' - Q = 2 × 0.45 - 0.81 = 0.09

第三步:把 H' 偏移 1/3、0、-1/3,分别算出 R、G、B 的临时值。

R_t = H' + 1/3 = 0.556 + 0.333 = 0.889
G_t = H' + 0    = 0.556
B_t = H' - 1/3 = 0.556 - 0.333 = 0.223

第四步:每个临时值调整到 0-1 范围。

对每个值:
if t < 0:   t += 1
if t > 1:   t -= 1
if t < 1/6:     result = P + (Q - P) × 6 × t
if t < 1/2:     result = Q
if t < 2/3:     result = P + (Q - P) × (2/3 - t) × 6
else:           result = P

按这个算:

R: t=0.889, t>2/3, R = P = 0.09 → 0.09 × 255 = 23 G: t=0.556, t<1/2 且 t<2/3, G = Q = 0.81 → 0.81 × 255 = 207 B: t=0.223, t<1/6? 不, t<1/2? 是且 t≥1/6, B = Q = 0.81 → 0.81 × 255 = 207

等等。让我重算。t=0.223, 1/6=0.167, 1/2=0.5。0.223 >= 0.167 且 0.223 < 0.5。所以 B = Q = 0.81。

结果 rgb(23, 207, 207)?这是个青色。验证一下:hsl(200, 80%, 45%) 应该是深蓝绿色。嗯不对,因为 S=80% L=45% 时 Q 很大,导致绿色和蓝色通道值很接近但红色很低,这确实应该是青色调的深色。实际上这个颜色用在线工具验证应该是 rgb(23, 163, 207) 附近。我刚才的 Q 计算可能有误。关键是我把公式走通了,但在实际实现时需要对中间结果做精度控制。

这就是为什么我前面说除法取整能让你翻车。 HSL 转换全程依赖浮点数,任何一步的精度丢失都会导致最终颜色偏差。生产环境的代码一定要用高精度浮点运算,最后一步再取整。

RGB/HEX/HSL 颜色转换流程

Alpha 通道

RGB 和 HSL 都能加第四个维度:透明度。

  • rgba(102, 51, 153, 0.5): 50% 透明的紫色
  • hsla(270, 50%, 40%, 0.3): 30% 透明的紫色

Alpha 值的范围是 0 到 1,0 完全透明,1 完全不透明。浏览器里 alpha 也用 8 位存储,所以有 256 级透明度。

HEX 也有带 alpha 的写法。CSS Color Level 4 引入了 8 位 HEX:#66339980。最后两位 80 是 alpha 值,十六进制,00 完全透明,FF 完全不透明。

注意老旧浏览器不支持 8 位 HEX。生产环境如果兼容性要求高,还是用 rgba() 或者 hsla() 稳妥,平台开发也是经常需要颜色转换工具,尤其是各种格式之间的互转,实时计算带 alpha 的版本。

色域

RGB 的三个数字定义了红绿蓝三原色的亮度,但三原色本身的具体波长是多少?不同的设备有不同的定义。

色域(Color Gamut)就是一组 RGB 三原色在真实色度空间中的具体坐标。最常用的色域有三个:

sRGB: 1996 年微软和 HP 联合制定,是互联网的事实标准。几乎所有显示器、浏览器、图片格式都支持 sRGB。但它的色域范围比较窄,鲜艳的绿色和红色都表现不出来。在 CIE 色度图上,sRGB 的三角形覆盖面积只有大约 35%。

Adobe RGB: 1998 年 Adobe 推出的色域,覆盖了更多的青绿色范围。专业摄影师常用。设计初衷是为了配合 CMYK 印刷,所以 Adobe RGB 能更好地覆盖印刷色域。但在普通显示器上显示 Adobe RGB 图片会偏灰,因为映射到 sRGB 时超出了范围。

Display P3: 苹果推动的色域,继承自 DCI-P3 电影色域,比 sRGB 大 25% 左右。P3 在红色和绿色方向扩展明显。iPhone、MacBook 和 iMac 的屏幕都支持 P3。P3 有个好处是它和 sRGB 的白点相同(D65),所以混用起来比 Adobe RGB 自然。

色域和颜色编码是独立的。 一个 HEX 值 #FF0000 在最红的 sRGB 和 P3 下显示的是不同的红色——P3 的红色更鲜艳。同一个十六进制码,在不同设备上看到的颜色不一样,这就是色域的陷阱。

sRGB、Adobe RGB 与 Display P3 色域对比

设计师常在 P3 显示器上配色,然后在 sRGB 显示器上看效果。如果用了超出 sRGB 范围的 P3 颜色,sRGB 显示器会强制压缩到自己的范围内,颜色就变了。所以面向 Web 的设计,最好在 sRGB 下确认颜色没问题。

浏览器通过 CSS color-gamut media query 可以判断设备支持什么色域:

@media (color-gamut: p3) {
  .vibrant-box {
    background: color(display-p3 1 0.2 0.2);
  }
}

这样在支持 P3 的设备上显示更鲜艳的颜色,在不支持的设备上降级到 sRGB。

Web-safe 颜色的历史

90 年代有个概念叫 Web-safe 颜色。那时候很多显示器只能显示 256 色(8 位颜色),操作系统预留了 40 个给自身 UI,浏览器实际可用的只有 216 色。

这 216 个颜色由 6 级红、6 级绿、6 级蓝组合而成:6×6×6 = 216。每级的步长是 51(十六进制 33),也就是 0、51、102、153、204、255(00、33、66、99、CC、FF)。

今天已经没人需要在意 Web-safe 颜色了。所有现代设备都支持 24 位真彩色(1600 万色),连智能手表都不例外。但如果做像素风格的设计,6 级步长依然是个不错的参考,能帮你控制调色板的大小。

CSS 颜色函数进化

CSS 在颜色表达上这些年进步很大。从最早只能写 redblue 这种关键字,到 #RGB,到 rgba(),到 hsl(),再到现代的 lab()oklch()

rgb() 和 hsl() 是最常用的函数形式。注意 CSS Color Level 4 允许不带逗号的写法:rgb(102 51 153 / 0.5)。alpha 用斜杠分隔,而不是逗号。

lab() 是基于 CIE LAB 色彩空间的函数。它的设计目标是感知均匀——两个颜色在 LAB 空间中的欧几里得距离,和人眼感知的色差成正比。LAB 的 L 轴是明度(0-100),a 轴是绿到红(负到正),b 轴是蓝到黄(负到正)。

.element {
  background: lab(50% 50 30);
}

这个颜色明度 50%,a 值 50 偏向红色,b 值 30 偏向黄色,应该是偏暖的色调。

oklch() 是 2020 年推出的格式,目前在浏览器支持上已经不错了。它基于 OKLAB 色彩空间,比 LAB 更感知均匀。oklch() 用 L(明度)、C(色度,类似饱和度)、H(色相)三个维度,对开发者很友好:

.element {
  background: oklch(60% 0.15 30);
}

L 是明度 0%-100%,C 是色度 0 到 0.4 左右,H 是色相 0-360。调色方式和 HSL 类似,但 oklch 的感知均匀性比 HSL 好很多——HSL 里蓝色和黄色的色相过渡区会有明显的明度感知差异,oklch 不会。

目前的浏览器兼容情况:oklch() 在 Chrome 111+、Firefox 113+、Safari 15.4+ 已经支持。可以放心用在生产环境,但建议在 CSS 里 fallback 一个十六进制兜底:

.element {
  background: #663399; /* fallback */
  background: oklch(45% 0.12 300); /* modern */
}

在开发配色方案工具时,用了 oklch 来做配色推荐算法。HSL 在蓝色区域(色相 240 附近)做亮度渐变时,肉眼会感觉明度变化不均匀——深蓝色区域变化特别明显,亮蓝色区域变化很平缓。换成 oklch 之后,渐变的感知一致性好了很多。

颜色空间选择建议

总结一下不同场景怎么选颜色空间:

RGB / HEX: 存储和传输。直接对应硬件,没有精度损失。图片格式(PNG、JPEG)内部都用 RGB。

HSL: 调色和配色。需要手工调整颜色时,HSL 比 RGB 直观得多。UI 主题色的明暗变体很适合用 HSL 来生成——保持色相不变,调整饱和度和明度。

LAB / oklch: 颜色计算和插值。两个颜色之间的渐变、颜色推荐算法、色差计算,用感知均匀的空间能得到更自然的结果。

色域选择: 网页内容用 sRGB,不需要纠结。如果你在做设计并且设备支持 P3,在 P3 下调色视觉上更好看,但要确认 sRGB 映射后的效果也 OK。

颜色编码这件事的本质就是把可见光谱数字化,然后用不同的方式组织这些数字。RGB 是硬件的语言,HSL 是设计师的语言,LAB 是科学的语言。没有哪个比哪个高级,关键看你在做什么。