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。用这种方式表示可以很简单得通过计算两个点的距离远近来判断颜色是否相近。

由SharkD - 自己的作品. Download source code.,CC BY-SA 3.0,https://commons.wikimedia.org/w/index.php?curid=9803283

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

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(亮度),几何表示为一个圆柱座标系。色相是这个圆柱的偏角,饱和度为圆柱水平切面的半径,亮度以圆柱的高度表示。

By SharkD - Own work. Source-code available at the POV-Ray Object Collection., CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=8421400

HSV 的三种维度分别是 Hue(色相)、Saturation(饱和度)、Value(明度),在色相的定义上与HSL保持一致,但是在饱和度上的定义是有区别的。
By HSV_color_solid_cylinder.png: SharkDderivative work: SharkD  Talk - HSV_color_solid_cylinder.png, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=9801673

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

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

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

By Hcl-hcv_models.svg: Jacob RusHSL_color_solid_dblcone.png: SharkDderivative work: SharkD  Talk - Hcl-hcv_models.svgHSL_color_solid_dblcone.png, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=9802536

By Hcl-hcv_models.svg: Jacob RusHSV_color_solid_cone.png: SharkDderivative work: SharkD  Talk - Hcl-hcv_models.svgHSV_color_solid_cone.png, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=9802544

如今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模型
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章