乐趣区

JavaScript中的颜色空间转换

我在做 webapp 的顶部导航栏时,碰到了一个挑战,导航栏的字体与图标要根据背景的颜色深浅来显示不同白色和黑色,但是导航栏的颜色是支持多种配色的,我不可能根据每一个配色去定义这个颜色的深浅,于是我开始研究起了颜色空间的转换……

对于 CSS,我们最常见的就是 16 进制的 RGB。从黑色到白色依次是 #000000 … #FFFFFF

RGB

RGB 也称三原色光模式(RGB color mode),原理是将红绿蓝三种颜色的色光已不同的比例相加,已形成多种多样的色光(三原色不可能用其他灯光的颜色合成)— 维基百科

当前计算机硬件采取每一个像素用 24Bit 来表示不同的颜色,每 8 位表示一个原色的强度,最高值为 2^8,也就是 256 个值,组合起来可以表示 16777216(256^3) 种颜色。之所以 24 位,这是因为人眼最高只能分辨出 1000 万种颜色,因此足矣。

当然也有 32Bit 的模式,但是实际上也是 24Bit,余下的 8Bit 不分配到像素中,主要是为了提高数据输送的速度(一般而言 1word 为 16Bit,32Bit === 1 double word, 处理器不需要做多余的换算),同样在一些特殊情况下,余下的 8Bit 用来表示像素的透明度

因此 #FFFFFF 同样可以表示为 rgb(255, 255, 255)

很自然地就可以采用三维空间来描述 RGB 的全值域,如图所示 x 轴为红色,y 轴为蓝色,z 轴为绿色。黑色藏在了立方体的背面。MAX=255,MIN=0。用这种方式表示可以很简单得通过计算两个点的距离远近来判断颜色是否相近。

几个极点的坐标分别表示的颜色

r g b name
0 0 0 黑 (black)
255 255 255 白 (white)
255 0 0 红 (red)
255 255 0 黄 (yellow)
0 255 0 绿 (green)
0 255 255 青 (cyan-blue)
0 0 255 蓝 (blue)
255 0 255 品红 (magenta)

但是这不足以解决文章开头需要解决的问题,因为从当前的颜色空间中,我们无法要直观地去辨别那些颜色是亮色,哪些颜色是暗色,很难,我们只知道一个颜色的红绿蓝混合比例。我们需要找出一种规律,去分类颜色的明暗。

HSL/HSV

出于我们感性的角度,颜色混合并不直观,我们判别一种颜色的思维首先会看看这是什么颜色,然后再确认颜色深浅如何、明暗度如何。事实上大部分艺术家在创作的时候也更倾向于这种思维。

所以我们很多软件上的调色工具都会基于这样的思路去设计,首先会有一个色板、然后会有饱和度、亮度这样的调整。

然而一早出现 HSL 的时候却不是为了此目的,记录最早显示的是 1938 年 Georges Valensi 为了解决彩色电视信号兼容单色电视信号的问题发明了 HSL 色彩空间(单色电视信号仅包括 L 信号)。往后 1978 年 Alvy Ray Smith 在编写 SuperPaint(SuperPaint 是第一个计算机光栅图形编辑器或“绘画”程序之一)的时候发明了 HSV(HSB)模型。经过实践反映了这两种模型给使用者可以带来更直观的感受。

HSL 三种维度分辨为 Hue(色相)、Saturation(饱和度)、Lightness(亮度),几何表示为一个圆柱坐标系。色相是这个圆柱的偏角,饱和度为圆柱水平切面的半径,亮度以圆柱的高度表示。

HSV 的三种维度分别是 Hue(色相)、Saturation(饱和度)、Value(明度),在色相的定义上与 HSL 保持一致,但是在饱和度上的定义是有区别的。

这两者之间应该使用哪种模型,目前是非常有争议的。支持 HSL 的人认为他更好的反映了饱和度和亮度作为两个独立参数的概念直觉(HSV 最低的饱和度为白色是非常反直觉的),而另一部分的人认为,HSL 的饱和度的定义容易给人造成迷惑,比如亮度极高时,白色被认为是高饱和度的。这意味着

  • HSL 中,饱和度总是从完全饱和变化到等价的灰色,而在 HSV 中是从完全饱和变化为白色。
  • HSL 中亮度的变化跨域从黑色到选择的色相再到白色的过程,HSV 中明度的定义只从黑色过渡到选择的色相。

因此通常在绘制坐标时,饱和度会被替换为 色度(Chroma)表示,用以过滤一些不符合直觉的坐标,HSL 对应呈双锥型的 HCL,而 HSV 则对应锥形的 HCV 模型。

如今 HSL 与 HSV 在软件上已经有了大量的运用。比如

  • Adobe 套件(Photoshop,Illustrator…)– HSV
  • APPLE Mac OS X 系统颜色选择器 –HSV
  • CSS3 — HSL
  • Windows 系统颜色选择器 — HSL

当然随着系统的升级与支持,也不乏有两者都支持的软件。

没有孰优孰劣,在做选择的时候只考虑使用的场景哪种更适合,当然在 WEB 的范畴中,由于 CSS3 的标准规定,HSL 更有利于颜色的换算。而了解到这里,我已经对开头的需求有了一个明确的实现方案。

RGB 到 HSL 的换算

在数学上定义为 RGB 空间的 r,g,b 坐标到 HSL 空间的 h,s,l 坐标的换算。

  • r,g,b ∈ [0, 1] ,max = max(r, g, b), min = min(r, g, b)
  • h ∈ [0, 360], s,l ∈ [0, 1]

首先会先计算色相值,对应是在圆柱横切面角度六等分的不同夹角下的值有不同的换算公式,从上往下 1 到 5 对应的区域

实际上 max = min 时是的灰色,h = undefined,上图中的第一条公式表示有误。当 h = 0° 一般表示计算为红色,也就是包含在了第二条计算公式中。这点需要特别注意

其次是亮度的计算,其实亮度的定义这方面是有争议的,并不是真正意义上明确的,而是基于不同的模型做不同的定义,这里就不做具体的讨论,HSL 中亮度的定义取 RGB 中最大值与最小值相加的二分之一

最后是饱和度的计算公式,首先定义色度 Chroma = max – min,从生理角度理解三种视锥细胞中,刺激最大与刺激最小之间的差异,
让人产生了颜色的鲜艳感,而与刺激中等的细胞关系不大。

我们一开始介绍 HSL 的时候有提到过,HSL 模型中有些值实际上已经超出了 RGB 定义的范畴,超出的部分实际上是没有意义的,所以圈定了另外一个范围为 HCL,是呈现一种双锥形的几何表示。当为亮度为极值时,饱和度恒等于 0。
中间分成两节对应不同的计算公式。下半截对应公式 2,上半截对应公式 1, 紧接着饱和度是受到亮度的制约,因此配合亮度进行计算。

代码实现

function RGB2HSL(r, g, b) {
  r = r / 255
  g = g / 255
  b = b / 255

  const max = Math.max(r, g, b)
  const min = Math.min(r, g, b)
  const delta = max - min
  let h, s, l

  if (max ==== min) {h = 0} else if (max === r) {h = ((g - b) / delta) % 6
  } else if (max === g) {h = (b - r) / delta + 2
  } else {h = (r - g) / delta + 4
  }
  h = Math.round(h * 60)
  if (h < 0) h += 360

  l = (max + min) / 2,
  s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));

  // 切换为百分比模式
  s = +(s * 100).toFixed(1);
  l = +(l * 100).toFixed(1);

  return {h, s, l}
}

RGB 到 HSV(HSB)

色相的换算跟 HSL 是一致的。饱和度和明度定义分别是

代码实现

function RGB2HSV(r, g, b) {
  r = r / 255
  g = g / 255
  b = b / 255

  const max = Math.max(r, g, b)
  const min = Math.min(r, g, b)
  const delta = max - min
  let h, s, l

  if (max ==== min) {h = 0} else if (max === r) {h = ((g - b) / delta) % 6
  } else if (max === g) {h = (b - r) / delta + 2
  } else {h = (r - g) / delta + 4
  }
  h = Math.round(h * 60)
  if (h < 0) h += 360

  // 基于 HSL 函数简单的变化即可适用
  l = max,
  s = delta === 0 ? 0 : delta / max;

  s = +(s * 100).toFixed(1);
  l = +(l * 100).toFixed(1);

  return {h, s, l}
}

HSL 到 RGB

???? 施工中 …
(理解起来有点难度,待续)

应用

  • 首先解决文章开头的问题。我们只需要知道背景颜色,比如输入 ”#1388F5″、简单地切换为十进制后,套用 RGB2HSL 从而获取到亮度值 L。只需要设定一个亮度阈值,判断 L 是否大于这个值,来加载响应的样式。
const HSL = RGB2HSL(hex2RGB("#1388F5"))
// 假定阈值是 55,这个可以按需要调整
const className = HSL.l > 55 ? "light" : "dark"

// 如果是亮色则加载黑字和黑色图标、如果是暗色则加载白色字体和白图标
// ps: 黑夜模式????
render(className)
  • 同样的通过 HSL 模型,通过设置图片中某个色相范围的颜色饱和度为 0,我们可以很简单地帮一张图片去色,又或者是指保留图片只的某个颜色达到局部彩色效果(RGB 通道开关)


  • 如果要制作一个 web 的调色板,不可避免地一定会应用到 HSL 模型
退出移动版