图形系统的颜色

1,062 阅读6分钟

RGB 与 RGBA

RGB

RGB 颜色的十六进制由六位十六进制数字组成的,形式如 #RRGGBB,其中 RR、GG、BB 分别是两位十六进制数字,分别表示红、绿、蓝三原色通道的色阶

色阶可以表示某个通道的强弱。

两位十六进制数字可以表示 256 个数字(0 ~ 255),也就是 256 个色阶。所以理论上一共能表示 2 的 24次方,也就是一共 16777216 种不同的颜色。

我们可以用一个三维立方体,把 RGB 能表示的所有颜色形象地描述出来。效果如下图:

rgb2.webp

不过 RGB 并不能表示人眼所能看见的所有颜色,如下图所示:

rgb.webp

灰色区域是人眼所能见到的全部颜色,中间的三角形是 RGB 能表示的所有颜色。不过 RGB 已经足够丰富了,一般的显示器、彩色打印机、扫描仪等都支持。

RGBA

RGBA 实际上就是在 RGB 的基础上增加了一个 Alpha 通道,也就是透明度,Alpha 的取值范围为 0 ~ 1。

WebGL 的 shader 默认支持 RGBA,不过其 R、G、B、A 的取值范围都是 0 ~ 1 之间的浮点数,0 ~ 1 之间的数字按照 0 ~ 255 依次去对应,比如 0.5 就对应 80(十六进制,十进制 128)。

RGB(A) 颜色表示法的局限性

RGB 和 RGBA 的颜色表示法非常简单,但使用起来也有局限性。就是无法很好的知道要以什么样的规则来配置颜色,才能让不同数据对应的图形之间的对比尽可能鲜明。我们只能大致直观地判断出它偏向于红色、绿色还是蓝色,或者在颜色立方体的大致位置。

例如我们在 WebGL 中随机生成一些颜色:

function randomRGB() {
  return [
    0.5 * Math.random(),
    0.5 * Math.random(),
    0.5 * Math.random(),
  ];
}

如果你想要随机生成一组颜色,那么你可能生成出来的颜色差别很大,也可能很小。因此在动态构建视觉颜色效果时 RGB 很不好用。

HSL 和 HSV

HSL 和 HSV 都是一种将 RGB 色阶中的点在圆柱坐标系中的表示法。这两种表示法试图做到比基于笛卡尔坐标系的几何结构 RGB 更加直观。

HSL 即色相、饱和度、亮度(Hue, Saturation, Lightness)。

HSV 即色相、饱和度、明度(Hue, Saturation, Value),又称 HSB,其中 B 即 Brightness。

  • 色相(H)是色彩的基本属性,就是平常所说的颜色名称,如红色、黄色等,取值为 0 ~ 360°。
  • 饱和度(S)是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取 0 ~ 100% 的数值。
  • 明度(V),亮度(L),取 0 ~ 100%。

1920px-Hsl-hsv_models.svg.png

HSL(a ~ d)和HSV(e ~ h)。上半部分(a、e):两者的 3D 模型截面。下半部分:将模型中三个参数的其中之一固定为常量,其它两个参数的图像。

Canvas2D 和 CSS 都是支持 HSL 颜色的,但是 WebGL 需要做转换,下面是一段 RGB 与 HSV 互转的代码:

vec3 rgb2hsv(vec3 c){
  vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
  vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
  vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
  float d = q.x - min(q.w, q.y);
  float e = 1.0e-10;
  return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

vec3 hsv2rgb(vec3 c){
  vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0), 6.0)-3.0)-1.0, 0.0, 1.0);
  rgb = rgb * rgb * (3.0 - 2.0 * rgb);
  return c.z * mix(vec3(1.0), rgb, c.y);
}

更多公式及代码可以参考 JS实现RGB,HSL,HSB相互转换

HSL 和 HSV 的局限性

  1. 色相相差相同的情况下,各个颜色直接的颜色变化并不是均匀的;
  2. 不同色相相同亮度的颜色看起来亮度却不一样,这是由于人眼对不同频率的光的敏感度不同造成的。

因此,HSL 依然不是最完美的颜色方法,我们还需要建立一套针对人类知觉的标准,这个标准在描述颜色的时候要尽可能地满足以下 2 个原则:

  1. 人眼看到的色差 = 颜色向量间的欧氏距离
  2. 相同的亮度,能让人感觉亮度相同

于是,一个针对人类感觉的颜色描述方式就产生了,它就是比较新的颜色表示技术 CIE Lab。

CIE Lab 和 CIE Lch

CIE L*a*b*(CIELAB)是惯常用来描述人眼可见的所有颜色的最完备的色彩模型,简称 Lab。它是为这个特殊目的而由国际照明委员会(Commission Internationale d'Eclairage 的首字母是 CIE)提出的。L、a 和 b 后面的星号(*)是全名的一部分,因为它们表示L*, a* 和 b*,不同于 L, a 和 b。因为红/绿和黄/蓝对立通道被计算为(假定的)锥状细胞响应的类似孟塞尔值的变换的差异,CIELAB 是 Adams 色彩值(Chromatic Value)空间。

L* 表示亮度, L* = 0 生成黑色而 L* = 100 指示白色,

a* 表示在红色/品红色和绿色之间的位置(a* 负值指示绿色而正值指示品红)

b* 表示在黄色和蓝色之间的位置(b* 负值指示蓝色而正值指示黄色)。

RGB 值也可以 Lab 转换,但是转换规则比较复杂,CIE Lab 比较特殊的一点是,目前还没有能支持 CIE Lab 的图形系统,但是css-color level4 规范已经给出了 Lab 颜色值的定义。而且,一些 JavaScript 库也已经可以直接处理 Lab 颜色空间了,如 d3-color

CIE Lch 和 CIE Lab 的对应方式类似于 RGB 和 HSL 和 HSV 的对应方式,也是将坐标从立方体的直角坐标系变换为圆柱体的极坐标系,这里就不再多说了。

Cubehelix 色盘

Cubehelix 色盘(立方螺旋色盘)是一种特殊的颜色表示法。简单来说,它的原理就是在 RGB 的立方中构建一段螺旋线,让色相随着亮度增加螺旋变换,如下图所示:

cubehelix.webp

如果我们要呈现颜色随数据动态改变的效果,那 Cubehelix 色盘就是一种非常更合适的选择。

我们直接用 NPM 上的 cubehelix 包写一个颜色随着长度变化的柱状图,你可以通过它来看看 Cubehelix 是怎么应用的。

codesandbox

import "./styles.css";
import { cubehelix } from "cubehelix";
import { useEffect } from "react";

export default function App() {
  useEffect(() => {
    const canvas = document.querySelector("canvas");
    const ctx = canvas.getContext("2d");
    ctx.translate(0, 256);
    ctx.scale(1, -1);

    const color = cubehelix(); // 构造cubehelix色盘颜色映射函数
    const T = 2000;

    function update(t) {
      const p = 0.5 + 0.5 * Math.sin(t / T);
      ctx.clearRect(0, -256, 512, 512);
      const { r, g, b } = color(p);
      ctx.fillStyle = `rgb(${255 * r},${255 * g},${255 * b})`;
      ctx.beginPath();
      ctx.rect(20, -20, 480 * p, 40);
      ctx.fill();
      window.ctx = ctx;
      requestAnimationFrame(update);
    }

    update(0);
  });

  return (
    <div className="App">
      <canvas width="500" height="300"></canvas>
    </div>
  );
}

效果如下:

cubehelix1.gif