Fork me on GitHub

颜色的世界

原创文章,未经允许,请勿转载

颜色表示方法

自然界的颜色有无数种,我们用数字来表示有限的颜色种类,根据三基色原理,人眼对红、绿、蓝最为敏感,大多数的颜色可以通过红、绿、蓝三色按照不同的比例合成产生,比如:红色+绿色=黄色绿色+蓝色=青色红色+青色=白色(二次叠加)

R、G、B 三个字节表示颜色,可以表示 256*256*256 = 16777216 种颜色,这种颜色我们称之为真彩色,是目前电脑能表示的最高色彩,也是普遍认为人眼对颜色的最大分辨能力。

我们平常所说的 RGBA 里的 A(alpha)指的是透明度,透明度不是颜色的一部分,而是用于图层/颜色混合时的叠加度量值

由于早期电脑内存宝贵,一张图片如果使用真彩色存储比较浪费,所以早期电脑程序使用了减配的字节来表示 rgb,比如:

  • 用一个字节表示:RGB332(3+3+2=8 位色):(2^3)(2^3)(2^2)=256(256 Color) 俗称 256 色

  • 用两个字节表示:RGB555(5+5+5=15 位色):(2^5)(2^5)(2^5)=32768(32768/1024=32, 32K Color) 有 1 位未使用

  • 另外一种用两个字节表示:RGB565(5+6+5=16 位色):(2^5)(2^6)(2^5)=65536(65536/1024=32, 64K Color) 由于人眼对绿色最敏感,所有绿色分量占比会多一些

我们常用的RGBRGBA格式属于RGB888,就是 RGB 各个分量分别占用 8 位,除了 RGB,在印刷工业领域还会使用HSL表示法,HSL 分别为 hue(色相)、saturation(饱和度)、lightness(亮度)

他们都是可以通过计算公式互相转换的,在内存存储上有区别之外,本质上没有区别

我们来看一个具体的例子,比如下面这个颜色

#5697ff = rgb(86, 151, 255) = hsl(217, 100%, 67%)

首先 #5697ff = rgb(86, 151, 255) 这两种表示方法很容易理解,只是 16 进制和 10 进制的表现形式, 0x56 = 86。重点看下 HSL,这个三个值不是那么直观的能计算出来

这里有一个js版本的RGB和HSL互转代码
function hslToRgb(h, s, l){
    var r, g, b;

    if(s == 0){
        r = g = b = l; // achromatic
    }else{
        var hue2rgb = function hue2rgb(p, q, t){
            if(t < 0) t += 1;
            if(t > 1) t -= 1;
            if(t < 1/6) return p + (q - p) * 6 * t;
            if(t < 1/2) return q;
            if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
            return p;
        }

        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;
        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);
    }

    return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}

function rgbToHsl(r, g, b){
    r /= 255, g /= 255, b /= 255;
    var max = Math.max(r, g, b), min = Math.min(r, g, b);
    var h, s, l = (max + min) / 2;

    if(max == min){
        h = s = 0; // achromatic
    }else{
        var d = max - min;
        s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
        switch(max){
            case r: h = (g - b) / d + (g < b ? 6 : 0); break;
            case g: h = (b - r) / d + 2; break;
            case b: h = (r - g) / d + 4; break;
        }
        h /= 6;
    }

    return [h, s, l];
}

色相、饱和度、亮度

那么色相、饱和度、亮度到底代表什么呢?

  • hue 色相,和人眼直观所见的颜色相关,赤橙黄绿青蓝紫
  • saturation 饱和度,颜色的鲜艳程度,饱和度越小,图像越趋于灰度图像。饱和度越大,图像越鲜艳,给人的感觉是彩色的,而不是黑白灰的图像
  • lightness 亮度,亮度仅与图像的最多颜色成分和最少的颜色成分的总量有关。亮度越小,图像越趋于黑色。亮度越高图像越趋于明亮的白色

用过fireworks或者photoshop的同学应该对上面几个概念比较熟悉,因为直接操作 RGB 是比较困难的,比如我们想让图像变亮一些,本质上可以通过改变 RGB 分量来达到目的,但是没那么直观,如果操作 HSL,那么问题就变得简单多了,直接改变 L 分量就可以了。

我们来改一下上面的颜色,把亮度提高 10%,hsl(217, 100%, 67%) 改为 hsl(217, 100%, 77%)

把饱和度设置为 0,则会变为灰度图像,hsl(217, 100%, 67%) 改为 hsl(217, 0%, 67%)

把亮度设置为 0,则会变为黑色图像,hsl(217, 100%, 67%) 改为 hsl(217, 100%, 0%)

色相的值通常在 0 ~ 360 之间,我写了一个 css 渐变来直观感受一下色相从 0 到 360 的分布

饱和度 100% 亮度 50%

饱和度 50% 亮度 50%

饱和度 100% 亮度 90%

颜色的混合

一张半透明的蓝色图片覆盖在一张红色图片上,会呈现什么颜色?早期的计算机程序里面颜色是没有 alpha 透明度的,所以没有漂亮的异形窗口,不是因为做不到,而是颜色混合算法比较费性能,需要按图片分层,然后一个像素一个像素的计算混合后的颜色,实际上颜色混合算法是一个挺大的范畴,各种各样的算法,包括使用汇编指令优化等一些高级玩法

图层普通alpha混合算法如下:

pixel = alpha * image + background * (1 - alpha)

我们实现一个 js 版本的看看

let backdrop = { r: 255, g: 0, b: 0, a: 1 } //红色
let foreground = { r: 0, g: 0, b: 255, a: 0.5 } //半透明蓝色

let result = {
  r: Math.round(foreground.r * foreground.a + backdrop.r * (1.0 - foreground.a)),
  g: Math.round(foreground.g * foreground.a + backdrop.g * (1.0 - foreground.a)),
  b: Math.round(foreground.b * foreground.a + backdrop.b * (1.0 - foreground.a)),
}

console.log(`rgb(${result.r},${result.g},${result.b})`) //output rgb(128,0,128)

得到的结果是 rgb(128,0,128),我们用 svg 来演示一下

rgb(255,0,0) rgb(255,0,0) rgb(128,0,128)

其他颜色编码格式

另外一种在视频处理领域比较常见的是YUV格式,摄像头输出、彩色电视信号、相机存储(JPG/JPEG文件格式)都使用YUV格式,Y存储了亮度,而UV存储了调色盘/颜色偏移量数据

YUV的发明是由于彩色电视与黑白电视的过渡时期。黑白视频只有Y(Luma,Luminance)视频,也就是灰阶值。到了彩色电视规格的制定,是以YUV/YIQ的格式来处理彩色电视图像,把UV视作表示彩度的C(Chrominance或Chroma),如果忽略C信号,那么剩下的Y(Luma)信号就跟之前的黑白电视频号相同,这样一来便解决彩色电视机与黑白电视机的兼容问题。YUV最大的优点在于只需占用极少的带宽

网页渲染过程

渲染的过程就是把网页元素栅格化、平面化的过程,把元素按照渲染算法,合并到一张图片上,这张图片称之为帧缓冲(framebuffer),是存在于内存中的一个二维数组,每个数组元素是一个4字节的结构,栅格化可以通过CPU也可以通过GPU,通过GPU渲染就是所谓的硬件加速

一般把一棵dom树混合到一张平面图上,最常见的算法是画家算法,所谓画家算法就是从后向前绘制,像画油画一样,后面刷的颜色把之前的颜色覆盖,以下面这棵dom树为例,先画body(比如body上有设置背景色)再画 header,然后是content,sesion1、secrion2,然后是footer

body
├── header
├── content
│   ├── section1
│   └── section2
└── footer

使用z-index可以改变渲染顺序,这样就改变了显示的前后层次

栅格化之后,这个缓冲区会再次作为子组件与浏览器标签页、浏览器、操作系统等通过各层API传递,最终显示到显示器上

sequenceDiagram 元素->>缓冲区: 渲染 缓冲区->>浏览器:标签页 浏览器->>操作系统:应用窗口 操作系统->显示器:显卡

相关链接

https://www.w3.org/TR/compositing-1/#compositemode

来源:悠游悠游,2019-06-04,原文地址:https://yymmss.com/p/rgb-colors.html