12月02, 2018

给你点颜色看看

万事有不平,尔何空自苦;长将一寸身,衔木到终古?我愿平东海,身沉心不改;大海无平期,我心无绝时。呜呼!君不见,西山衔木众鸟多,鹊来燕去自成窠。 —— 清.顾炎武 《精卫》

引子

五颜六色的世界,是大自然对人类的一种馈赠。在计算机领域,显示器是颜色显示的主要介质。笔者接触过的最早的单色显示器到目前的广色域显示器,不得不说是技术的一个又一个飞跃。

在 Web 领域,我们不可避免的要和颜色打交道。其实除了我们最常见的十六进制表示方法,W3C 色彩标准还支持许多不同的颜色表示方法。它们各有所长。本文就带读者了解这些表示方法的原理、标准写法、浏览器支持程度以及相互转换方法。

表示方法及 CSS 色彩函数

rgb/rgba

这应该是读者最为熟悉的表示方法,其基本原理来自于三原色叠加理论。在数学上,有更为严谨的定义,读者可以参考这篇文章来了解更为本质的阐述。

使用 RGB 方式描述色彩时,经常采用以#引导的六位 16 进制数字来表示一个颜色。它的本质意义是:把六位数字按顺序分为 3 组,这时假设有 3 盏标准的红、绿、蓝色光源。那么,亮度从 00(最暗)到 FF(最亮)均分为 256 等分。上面的三组数字即为三个光源的亮度。如此,上述的 6 位 16 进制代码即为三个光源叠加出来的颜色。所有的 RGB 色域的颜色,可以用这个立方体近似表示。

实际上,这也是电脑显示器显示彩色的主要实现原理。

这种表示法有个简写方式,颜色#112233可简写为:#123,颜色#AACC00可简写为:#AC0

CSS3 中引入了颜色透明度的参量。可以使用 8 位表示,最后两位表示从 0-255 的透明度。类比 6 位表示的 3 位简写,有 4 位的简写方法。

需要说明的是,对于加入透明度的写法,IE 系列,包括 Edge 不支持。下面这张图表显示了目前的主流浏览器支持程度。

对应于 6 位和 8 位表示法,css 本身提供两组函数 rgb/rgba,下面给出一个用例:

.foo{
    width: 100px;
    height: 100px;
    color: rgb(100, 150, 30);
    background-color: rgba(100, 200, 200, .5);
}
.foo{
    width: 100px;
    height: 100px;
    background-color: rgba(20%, 25%, 30%, .5);
}

以 rgba 为例说明,对于前 3 个参数,每个参数为十进制或百分数,百分数还是十进制数,要求一致。最后一个数字为 0-1 间的小数或百分数,用以说明透明度。

如果是十进制数,则表示是对应光源的亮度。如果是百分数,可以表示是 0-255 空间上的百分比。

这组函数的意义,除了简化了 16 进制的计算,更重要的是,理论上,把原来的离散值域变成了连续的空间。不过受限于显示器制作工艺以及麦克亚当椭圆内同圆不可分辨的原理,完全连续是不可能,最多是提高了一些精度。

当然,这一串数字表示的颜色是不太具有可读性的。为此,CSS 定义了一组具名的颜色。并且发布了这些具名颜色的对应数值。如果你需要展示的颜色恰好是这些数值,不妨使用之,使得代码可读性增强。读者可以从 这里查询到这些具名颜色。

hsl/hsla

从前文我们知道,RGB 是对机器实现友好的。但是对于人类阅读来讲就是比较难以理解的了。比如你跟一个人说他的手机壳的颜色是#778899,这就比较难于理解了。有的时候,HSL 是一种更加人性化的颜色表示法。它更加符合人类对于颜色认知规律:“这是什么颜色?深浅如何?明暗如何?”。hsl 可以通过 CSS 的 hsl 函数设置。hsl 形如hsl(hue, saturation, lightness)

来看看 HSL 色彩模型的概念。

第一个参数:色相。代表的是人眼所能感知的颜色范围。参数如下定义:首先,定义一个圆环,以正上方为 0 度,顺时针为正方向。取值为 0 ~ 360 度的角度值,每个角度值代表一个颜色。几个特殊的角度值为:360°/0° 红、60° 黄、120° 绿、180° 青、240° 蓝、300° 洋红。完整的色环见下图:

第二个参数:饱和度。这个值接受的为 0 ~ 100%的百分数。一般地,色彩的饱和度为 100%。此时颜色最为鲜艳。当降低颜色饱和度,则颜色的表现就不那么鲜艳了,直到 0%,接近灰色。下图是在色相圆上,色彩饱和度变化的方向。

第三个参数:亮度。一般来说,HSL 色彩中的 L 预设值是 50%,增加这个数值则变亮,增亮到最后就变成纯白;减小这个数值则变暗,亮度缩减到最后就变成了黑色;若要变暗一点就把数值往 0%调整,若要变亮变白一点就把数值往 100%调整。

我们可以固定几组特殊的色相参数,看一下不同的饱和度、亮度下,色彩的表现。

这篇文章中,这张图片展示了 hsl 三个参数的变化的柱体图。这里需要注意一点,由于作图手法的原因,色相的旋转正方向与我们所述是相反的,不过颜色的对应关系是一致的。

举例而言,下面的代码的渲染结果就是黄色的色块,文字颜色为绿色,边框为 5px 的红色,边框的透明度为 20%:

.foo{
    width: 100px;
    height: 100px;
    color: hsl(120deg, 100% , 50%);
    background-color: hsl(60, 100% , 50%);
    border: 5px solid hsl(0, 100%, 50%, 20%);
}

这里第一个参数的单位为角度,deg 可以省略。hsl 可以接收第四个参数,含义为透明度。根据逻辑对应,hsla 函数依旧存在。与 hsl 函数作用一致。

hsl 表示法相对于 rgb 的好处主要是更加直观,读到颜色代码,可以大致了解色彩的表现。

现代浏览器对于 hsl 函数的支持比较完善,下图是主流浏览器的支持情况:

hwb

除了 HSL 色彩模型,CSS 标准还支持 HWB 色彩模型。HWB 其实可以约等于 HSV 色彩模型,在 CSS 中用 hwb 函数描述。HSV 即色相、饱和度、明度(Hue, Saturation, Value 或 Brightness)。无论是 HSV 还是 HSL 都是对三原色光模式的一种非线性变换。这些变换的目标都是创建对人类友好的色彩标记系统。HSL 和 HWB/HSV 之间存在直接的对应关系,如下:

HSL 和 HSV 究竟哪种更适合人类识别目前仍有争议。

目前浏览器对于 hwb 暂时没有支持,仅在标准建议阶段。

Lab 和 Lch

色彩的物理测量一般使用 Lab 色彩空间表示。这个色彩空间由 CIE(国际照明协会)在 1976 年创立。

Lab 色彩空间是基于一种颜色不能同时既是蓝又是黄这个理论而建立。所以 CIE Lab,单一数值可用于描述红/绿色及黄/蓝色特征。当一种颜色用 CIE Lab 时,L 表示标准光源的明度值;a 表示红/绿值 b 表示黄/蓝值。a、b 的取值可正可负。a 的数值越大越发红,越小越发绿。b 的值越大越发黄,越小越发蓝。

Lch 和 Lab 本质是一样的,在 Lab/Lch 色相图上,c 表示极径长度,h 表示逆时针转过的角度,约定水平向右为 0 度。下图为 CIE 的 Lab/Lch 色彩空间坐标图:

目前此标准暂时没有浏览器支持。仍处于标准建议阶段。

gray

这是 Lab 中 a=0,b=0 的特殊情况。支持一个或两个参数,即 Lab 中的 L 参数。如果提供第二个参数,则表示透明度。

目前此标准暂时没有浏览器支持。仍处于标准建议阶段。

指定色彩空间

为了充分的扩展性,标准也支持指定颜色空间。

所谓色彩空间,在表现上是一种颜色的组织描述方式。它描述了一组实际的颜色样本和数字的对应关系。

色彩模型是一种抽象数学模型,通过一组数字来描述颜色。

如果在色彩模型和一个特定的参照色彩空间之间创建特定的映射函数,那么就会在这个参照色彩空间中出现有限的“覆盖区”,成为色域。

从数学上讲,色彩空间由色彩模型和色域共同定义。

下面是一个指定色彩空间并为其指定颜色的代码例子:

color: color(swopc 0 206 190 77);
color: color(indigo 24 160 86 42 0 18 31);
color: color(prophoto 233 150 122);
color: color(p3 97 253 36);

@color-profile swopc {
  src: url('http://example.org/swop-coated.icc');}
@color-profile indigo {
  src: url('http://example.org/indigo-seven.icc');}
profile prophoto {
  src: url('http://example.org/prophoto.icc');}

color函数实现了这个功能。第一个参数是一个由@color-profile指定的空间名。后面的参数根据@color-profile的描述进行指定。由此,理论上可以支持所有的以及新开发的色彩空间的描述。

目前此标准暂时没有浏览器支持。仍处于标准建议阶段。

device-cmyk

上面的门探讨的主要是面向显示器上面的色彩空间。其他的诸如印刷品等,由于本身不发光,由反射光展现颜色。故实现有些许不同。常用 CMYK 模型来反映颜色。从印刷上来讲,简单说就是 cyan 青色、magenta 品红色、yellow 黄色、black 黑色颜料进行配比。为了扩展性,标准亦加入了对此的支持。

下面是使用device-cmyk定义的颜色

color: device-cmyk(0 81% 81% 30%);

理论上与下面的颜色是等价的:

color: rgb(178 34 34);

目前此标准暂时没有浏览器支持。仍处于标准建议阶段。

相互转化

上面我们看到了很多颜色的表示方法。对于浏览器支持较好的标准我们可以根据需求,放心的使用;对于支持不佳的标准,一方面,我们可以通过 PostCSS 以及各种 CSS 预编译器得到兼容的代码。另一方面,我们可以根据其定义,制作 polyfill。这里面主要是指各种表示法以及色彩空间的转化。在标准中,也提到了几种常用的转化算法。

hsl 转 rgb

function hslToRgb(hue, sat, light) {
  if( light <= .5 ) {
    var t2 = light * (sat + 1);
  } else {
    var t2 = light + sat - (light * sat);
  }
  var t1 = light * 2 - t2;
  var r = hueToRgb(t1, t2, hue + 2);
  var g = hueToRgb(t1, t2, hue);
  var b = hueToRgb(t1, t2, hue - 2);
  return [r,g,b];
}

function hueToRgb(t1, t2, hue) {
  if(hue < 0) hue += 6;
  if(hue >= 6) hue -= 6;

  if(hue < 1) return (t2 - t1) * hue + t1;
  else if(hue < 3) return t2;
  else if(hue < 4) return (t2 - t1) * (4 - hue) + t1;
  else return t1;
}

hwb 转 rgb

function hwbToRgb(hue, white, black) {
  var rgb = hslToRgb(hue, 1, .5);
  for(var i = 0; i < 3; i++) {
    rgb[i] *= (1 - white - black);
    rgb[i] += white;
  }
  return rgb;
}

sRGB 相关转换方法

function lin_sRGB(RGB) {
  // convert an array of sRGB values in the range 0.0 - 1.0
  // to linear light (un-companded) form.
  // https://en.wikipedia.org/wiki/SRGB
  return RGB.map(function (val) {
    if (val < 0.04045) {
      return val / 12.92;
    }

    return Math.pow((val + 0.055) / 1.055, 2.4);
  });
}

function gam_sRGB(RGB) {
  // convert an array of linear-light sRGB values in the range 0.0-1.0
  // to gamma corrected form
  // https://en.wikipedia.org/wiki/SRGB
  return RGB.map(function (val) {
    if (val > 0.0031308) {
      return 1.055 * Math.pow(val, 1/2.4) - 0.055;
    }

    return 12.92 * val;
  });
}

function lin_sRGB_to_XYZ(rgb) {
  // convert an array of linear-light sRGB values to CIE XYZ
  // using sRGB’s own white, D65 (no chromatic adaptation)
  // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
  var M = Math.matrix([
    [0.4124564,  0.3575761,  0.1804375],
    [0.2126729,  0.7151522,  0.0721750],
    [0.0193339,  0.1191920,  0.9503041]
  ]);

  return Math.multiply(M, rgb).valueOf();
}

function XYZ_to_lin_sRGB(XYZ) {
  // convert XYZ to linear-light sRGB
  var M = Math.matrix([
    [ 3.2404542, -1.5371385, -0.4985314],
    [-0.9692660,  1.8760108,  0.0415560],
    [ 0.0556434, -0.2040259,  1.0572252]
  ]);

  return Math.multiply(M, XYZ).valueOf();
}

// image-p3-related functions


function lin_P3(RGB) {
  // convert an array of image-p3 RGB values in the range 0.0 - 1.0
  // to linear light (un-companded) form.

  return RGB.map(function (val) {
    if (val < 0.04045) {
      return val / 12.92;
    }

    return Math.pow((val + 0.055) / 1.055, 2.4);
  });
}

function gam_P3(RGB) {
  // convert an array of linear-light P3 RGB  in the range 0.0-1.0
  // to gamma corrected form

  return RGB.map(function (val) {
    if (val > 0.0031308) {
      return 1.055 * Math.pow(val, 1/2.4) - 0.055;
    }

    return 12.92 * val;
  });
}

function lin_P3_to_XYZ(rgb) {
  // convert an array of linear-light P3 values to CIE XYZ
  // using  D65 (no chromatic adaptation)
  // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
  var M = Math.matrix([
    [0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
    [0.2289745640697488, 0.6917385218365064,  0.079286914093745],
    [0.0000000000000000, 0.04511338185890264, 1.043944368900976]
  ]);
  // 0 was computed as -3.972075516933488e-17

  return Math.multiply(M, rgb).valueOf();
}

function XYZ_to_lin_P3(XYZ) {
  // convert XYZ to linear-light P3
  var M = Math.matrix([
    [ 2.493496911941425,   -0.9313836179191239, -0.40271078445071684],
    [-0.8294889695615747,   1.7626640603183463,  0.023624685841943577],
    [ 0.03584583024378447, -0.07617238926804182, 0.9568845240076872]
  ]);

  return Math.multiply(M, XYZ).valueOf();
}

//Rec. 2020-related functions

function lin_2020(RGB) {
  // convert an array of Rec. 2020 RGB values in the range 0.0 - 1.0
  // to linear light (un-companded) form.
  return RGB.map(function (val) {
    return Math.pow(val, 2.4);
  });
}
//check with standard this really is 2.4 and 1/2.4, not 0.45 was wikipedia claims

function gam_2020(RGB) {
  // convert an array of linear-light Rec. 2020 RGB  in the range 0.0-1.0
  // to gamma corrected form
  return RGB.map(function (val) {
      return Math.pow(val, 1/2.4);
  });
}

function lin_2020_to_XYZ(rgb) {
  // convert an array of linear-light Rec. 2020 values to CIE XYZ
  // using  D65 (no chromatic adaptation)
  // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
  var M = Math.matrix([
    [0.6369580483012914, 0.14461690358620832,  0.1688809751641721],
    [0.2627002120112671, 0.6779980715188708,   0.05930171646986196],
    [0.000000000000000,  0.028072693049087428, 1.060985057710791]
  ]);
  // 0 is actually calculated as  4.994106574466076e-17

  return Math.multiply(M, rgb).valueOf();
}

function XYZ_to_lin_2020(XYZ) {
  // convert XYZ to linear-light Rec. 2020
  var M = Math.matrix([
    [1.7166511879712674,   -0.35567078377639233, -0.25336628137365974],
    [-0.6666843518324892,   1.6164812366349395,   0.01576854581391113],
    [0.017639857445310783, -0.042770613257808524, 0.9421031212354738]
  ]);

  return Math.multiply(M, XYZ).valueOf();
}

// Chromatic adaptation

function D65_to_D50(XYZ) {
  // Bradford chromatic adaptation from D65 to D50
  // The matrix below is the result of three operations:
  // - convert from XYZ to retinal cone domain
  // - scale components from one reference white to another
  // - convert back to XYZ
  // http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
  var M = Math.matrix([
    [ 1.0478112,  0.0228866, -0.0501270],
    [ 0.0295424,  0.9904844, -0.0170491],
    [-0.0092345,  0.0150436,  0.7521316]
   ]);

  return Math.multiply(M, XYZ).valueOf();
}

function D50_to_D65(XYZ) {
  // Bradford chromatic adaptation from D50 to D65
  var M = Math.matrix([
    [ 0.9555766, -0.0230393,  0.0631636],
    [-0.0282895,  1.0099416,  0.0210077],
    [ 0.0122982, -0.0204830,  1.3299098]
   ]);

  return Math.multiply(M, XYZ).valueOf();
}

// Lab and LCH

function XYZ_to_Lab(XYZ) {
  // Assuming XYZ is relative to D50, convert to CIE Lab
  // from CIE standard, which now defines these as a rational fraction
  var ε = 216/24389;  // 6^3/29^3
  var κ = 24389/27;   // 29^3/3^3
  var white = [[0.96422, 1.00000, 0.82521]; // D50 reference white

  // compute xyz, which is XYZ scaled relative to reference white
  var xyz = XYZ.map((value, i) => value / white[i]);

  // now compute f
  var f = xyz.map(value => value > ε ? Math.cbrt(value) : (κ * value + 16)/116);

  return [
    (116 * f[1]) - 16, // L
    500 * (f[0] - f[1]), // a
    200 * (f[1] - f[2]) // b
  ];
}

function Lab_to_XYZ(Lab) {
  // Convert Lab to D50-adapted XYZ
  var κ = 24389/27;   // 29^3/3^3
  var ε = 216/24389;  // 6^3/29^3
  var white = [[0.96422, 1.00000, 0.82521]; // D50 reference white
  var f = [];

  // compute f, starting with the luminance-related term
  f[1] = (Lab[0] + 16)/116;
  f[0] = Lab[1]/500 + f[1];
  f[2] = f[1] - Lab[2]/200;

  // compute xyz
  var xyz = [
    Math.pow(f[0],3) > ε ?   Math.pow(f[0],3)            : (116*f[0]-16)/κ,
    Lab[0] > κ * ε ?         Math.pow((Lab[0]+16)/116,3) : Lab[0]/κ,
    Math.pow(f[2],3)  > ε ?  Math.pow(f[2],3)            : (116*f[2]-16)/κ
  ];

  // Compute XYZ by scaling xyz by reference white
  return xyz.map((value, i) => value * white[i]);
}

function Lab_to_LCH(Lab) {
  // Convert to polar form
  return [
    Lab[0], // L is still L
    Math.sqrt(Math.pow(Lab[1], 2) + Math.pow(Lab[2], 2)), // Chroma
    Math.atan2(Lab[2], Lab[1]) * 180 / Math.PI // Hue, in degrees
  ];
}

function LCH_to_Lab(LCH) {
  // Convert from polar form
  return [
    LCH[0], // L is still L
    LCH[1] * Math.cos(LCH[2] * Math.PI / 180), // a
    LCH[1] * Math.sin(LCH[2] * Math.PI / 180) // b
  ];
}

总结

本文详细解释了 CSS 色彩标准的相关内容、语法、相关函数、各种表示方法的原理以及浏览器支持情况。同时对目前不支持的函数给出了一些转换方法,为 polyfill 的撰写提供一定的依据。

本文链接:https://www.leon82.com/post/color-in-web.html

-- EOF --

Comments