现代 CSS

JavaScript中的颜色转换

特别声明:如果您喜欢小站的内容,可以点击申请会员进行全站阅读。如果您对付费阅读有任何建议或想法,欢迎发送邮件至: airenliao@gmail.com!或添加QQ:874472854(^_^)

图解CSS系列的《CSS 颜色》一文中介绍了CSS中颜色的使用。到目前为止,给Web元素设置颜色的方式有很多种,比如使用颜色名称定义颜色、使用#RGB(或#RRGGBB)、#RGBA(或#RRGGBBAA)、hsl()(或hsla())、hwb()lab()lch()等。在CSS的颜色规范的第四版本中的第十五节中,专门介绍了在JavaScript中如何实现颜色不同空间域的转换。在Web的开发中,有的时候难免离不开使用JavaScript来操作颜色,接下来在这篇文章中,我们主要和大家一起来聊聊JavaScript中颜色空间的转换应该如何实现。

原则上说,任意两种颜色模式之间的转换都可以通过JavaScript脚本来实现

颜色世界中的一些基本概念

从《CSS 颜色》中我们或多或少的了解到,在Web的颜色世界中和印刷世界(或者说现实生活)中,颜色是丰富多彩的,除了能点缀我们的生活之外,同样也能丰富Web的世界。在Web环境中,描述颜色有几个概念是缺少不了的,即 颜色模型颜色空间颜色配置文件。简单地说,编码的时候,这几个概念是非常重要的。

颜色模型

不知道你有没有安装打印机墨盒的经验。如果有的话,你应该知道大多数的打印机都有四种颜色的墨盒,对应的就是我们常说的*CMYK颜色模型:

而在设计软件或Web使用中,CMYK对于大家来说,使用的并不多,反而是RGB模型更多一些。事实上,不管是CMYK还是RGB都被称为颜色模型

简单地说,颜色模型是一种将色谱描述为多维模型的可视化方法。大多数现代的色彩(彩色)模型都有三个维度(比如我们熟悉的RGB模式),因此也被描述为 3D形状,但也有的色彩模型有更多的维度,比如CMYK。对于很多开发者而言,较为熟悉的是RGBHSVHSL颜色模型,它们在当前的数字设计工具和编程语言中都很流行。这些颜色模型都使用相同的RGB原色(即红、绿、蓝):

而时至今日,除了上面所说的几种颜色模型,还有一种新的颜色模型HCL,该模型的配色能更方便地达到高可用性(Accessibility),对人眼更加友好。接下来简单的了解来了解这几种颜色模型。

RGB颜色模型

色彩(或颜色)是人类大脑对事物的一种主观感觉,为了将这种感性进行理性描述,数学家们创建了 RGB 颜色模型。

RGB颜色模型是一种三维的颜色模型,即通过三个数的组合(颜色色值)来表述某一种特定的颜色。简单地说,就是使用红色绿色蓝色混合在一起产生一种特定的颜色。当在这些维度中定义颜色时,我们必须知道颜色光谱中的颜色序列,例如,100%的红色和绿色混合产生黄色。RGB颜色模型通常被描述为一个立方体,通过在三维空间中将红色、绿色和蓝色维度映射到xyz轴.

三维坐标系中的xyz轴分别对应的是RGB颜色模型中的RedGreenBlue。根据“空间直角坐标系”相关的知识,我们可以获知RGB(x, y, z)将构建的每个颜色点,都对应于立方体中的某个点,也就是说每一种颜色都被包含在我们的色彩空间之内。下面的录屏就是用来演示这一点,其中所有可能的颜色混合都表示在立方体的范围内。

RGB颜色模型在编码中创建的颜色并不是特别直观,比如rgb(230, 33, 120),对于这样的rgb()函数中的xyz坐标值,开发者并无法直观的知道它具体代表的是什么颜色。特别是,前景色(color)和背景色(background-color)要实现互补时,更难于直观描述。

HSL颜色模型

在介绍RGB颜色模型的时候提到过,使用rgb(x, y, x)(比如rgb(230, 33, 120))描述的颜色,更多的时候是适用于告诉计算机该如何做色彩的展示,但对于设计师或者开发者来说并不怎么友好,他们一般不会这样去设计自己想要的颜色。

不管是设计师还是开发者,在实际中也并不会这样去使用颜色(设置颜色),更多的是通过拾色器(比如设计师使用的设计软件中的拾色器,开发者在设计软件中的拾色器或者浏览器开发者工具中的拾色器)。比如:

既然RGB颜色模式不易于直观的描述(表达颜色),那么我们就可以考虑使用其他的颜色模式,比如说HSL颜色模式,即 Hue (色相)、Saturation(饱和度)和 Lightness(明度,也有人称亮度)。在众多的设计软件中,调色器也具有HSL颜色模型,往往是分为两个部分组成:一个二维的调色窗口,然后配合一个一维的调色轴,以此组合成为三维调色器。

事实上,HSL模型描述的颜色也可以用RGB颜色模型来描述,也就是说,HSL颜色模型中的每一个色彩都能和RGB颜色模型中的色彩相对应,换句话说,HSL颜色模型中的空间直角坐标系内的每一个坐标点,都会对应到RGB颜色模型中的空间直角坐标系中。比如下图所示:

你可能已经发现了,在RGB颜色模型的坐标系空间中(上图的立方体),超靠近顶点位置(xyz轴的交叉点o),颜色也就越深,o点是纯黑色的;反之,远离o坐标点的位置,颜色也就越浅(o点的对角点a位置是纯白色)。而oa连线中的每一个点,其rgb(x, y, z)的值都是x = y = z,而且该条线中的每一个点都是从纯黑到纯白不同程度的灰色。

如果调整RGB直角坐标系构成的立方体角度,让oa直线垂直于设备的屏幕,就会看到下图这样的形状:

如果将这个六边形拉伸变成一个圆形,它就成为我们常见的一个色盘。其中圆形中每个角度对应的颜色就是HSL中的 H(色相),从圆心到圆周的变化,代表的是 S(饱和度),从o点到a点的变化,代表的是 L(亮度)。

回到拾色器中,它就变成:

HSL颜色模型中,我们可以用一个立柱体来描述,会更清晰:

HSV颜色模型

HSV颜色模型和HSL颜色模型极其相似,也是一个圆柱形颜色模型。

其中H指的也是色相,其中0360度代表的是红色,60度代表黄色,120度代表绿色,180度代表青色、240代表蓝色,300度代表洋红。这几个颜色也常被称为色相环中的六大主色

S也是用来控制颜色的使用量。100%饱和度折颜色将是最纯的颜色,而0%饱和度将产生灰色。而V有点类似于HSL中的L,主要用来控制颜色的亮度。0%亮度的颜色是纯黑色,而100%亮度的颜色没有黑色混入到颜色中。

注意,HSV也常常被称为HSB

HSV颜色模型中,三个维度是相互依赖的。如果颜色的V维度设置为0%HS数量并不重要,因为颜色将是黑色。同样地,如果颜色的S设置为0%H也没有关系,因为没有使用颜色。HSV中的H(色相)展开的话也是一个圆环,每个角度对应的是不同的颜色。

HSL一样,下图可以很形象的描述颜色:

HWB颜色模型

HWB颜色模型和前面的HSLHSV(或HSB)类似,也可以用柱形来描述:

HWBHue Whiteness Blackness三个单词的首字母缩写,该颜色模型是由@Alvy Ray Smith引入的一种颜色模型。创建该颜色模型的初衷主要是用来作为HSL(或HSV)颜色模型的替代方案。这样做可以让设计师或开发者设计颜色更为简单,正如@Alvy Ray Smith所说:

使用HWB颜色模型,你所要做的就是选择一种色调(H),用白色让其更亮,用黑色让其变得更黑

也就是说,HWB中的H和前面提到的HSLHSV中的H是相同的,都是用色相环来描述颜色。而WB分别代表的是添加到颜色中白色和黑色所占程度,一般都是用百分比来表示。如果要让一个颜色(色相环中对应的某一颜色)变亮,就需要增加白色所占份量,反之,如果想让颜色变暗,就是相应的增加黑色所占份量;如果要颜色不饱和,就同时增加白色和黑色的份量。

如果WB两个百分比加起来超过100%,那么就是灰色的,并且这两个属性被规范化,加起来正好是100%。例如hwb(0deg 75% 75%)hwb(0deg 50% 50%)相同。而hwb(0deg 25% 100%)会调整为hwb(0deg 20% 80%)

最后要提到的一点是,HWB颜色模型可以很好的用于用户输入的颜色,但对于非专业人士的话,也较难使用,但对于专业人员来说,更易于理解,因为HWB的概念类似于涂料的混合:

Lab颜色模型

Lab颜色模型是由CIE(国际照明委员会)制定的一种色彩模型。自然界中任何一点色都可以在Lab空间中表达出来,它的色彩空间比RGB空间还要大。另外,这种模式是以数字化方式来描述人的视觉感应,与设备无关,所以它弥补了RGBCMYK模式必须依赖于设备色彩特性的不足。

由于Lab的色彩空要比RGB模式和CMYK模式的色彩空间大。这就意味着RGB以及CMYK所能描述的色彩信息在Lab空间中都能得以映射。Lab颜色模型取坐标Lab,其中L为亮度,a的正数代表红色,a的负数代表绿色;b的正数代表黄色,负数代表蓝色。

HCL颜色模型

我们熟悉用来指定颜色都是定义在sRGB颜色空间中,比如rgbhsl等。但随着科技技术不断的发展,sRGB颜色空间越来越不够使用了。比如现在大多数显示器的色域更接近P3P3的体积比sRGB50%

HCL颜色模型中,相同的坐标数值变化会产生相同的感知色差。这种颜色空间的属性被称为“感知一致性”。换句话说,使用HCL配色能更方便地达到高可用性,对人眼更加友好。

HCL中的H指的是色相,C(Chroma)指的是色距,可以理解为相对饱和度,其中LHSL中的L(高度Lightness)不同,这里的L指的是感知明度(Luminance),也被称为相对感知明度(Relative Luminance),为了和HSL中的L能道区分开,HCL中的LLuma来描述。另外,HCL中的L是基于人眼对亮度对感知而创造的。下图解释了人眼对两种色彩空间的色相环内颜色的明度是如何感知的:

简单地说,HCL中的明度实际上是有意义的

HCL 属于另一种色彩空间,两者不一定能完全匹配。具体表现为:转换后颜色但色相、感知明度均存在,但颜色的色距不一定在 HCL 色彩空间范围内

颜色空间

颜色空间(Color Space是用来组织颜色的方式,换句话说,是用来定义颜色的范围比较知名的颜色空间有sRGBAdobeRGB等。

需要注意的是,尽管颜色模型是抽象的数学概念,但是如果没有相应的颜色空间,就不可能将颜色模型可视化。上面的RGBHSVHSL颜色模型示例都显示在sRGB颜色空间中,因为这是Web的默认颜色空间。

颜色配置文件

数字图像可以通过在其元数据中嵌入颜色配置文件来指定特定的颜色空间。将会告诉任何想要读取图像的程序,像素值是根据特定的颜色空间来表示的,没有颜色配置文件的图像通常被假定为sRGB。为了正确地在多个设备上复制相同的颜色,颜色配置文件非常重要,而且你经常会看到专业的打印服务要求将图像文件设置为特定的颜色空间。如果你曾经将一张图像粘贴到现有的Photoshop项目中,结果发现颜色不对,那么你就是这个问题的受害者(颜色配置文件不匹配)。

如果数字图像使用其具有广泛色域的颜色配置文件,那么它在大多数屏幕上肯定会丢失颜色,因为大多数屏幕只能显示sRGB色域内的颜色。然而,许多新的屏幕支持更大的颜色范围。苹果iMac视网膜屏幕使用了一个称为DCI-P3RGB颜色空间,其色域范围与AdobeRGB(1998)大致相同,但它包含更多的红黄色,并排除了一些绿蓝色。为了空出颜色管理的复杂性,在视网膜屏幕上运行的一些浏览器可能会使没有颜色配置文件的sRGB图像的颜色过度饱和,而其他浏览器会正确地将sRGB像素值转换为DCI-P3

特别声明,对于颜色模型、颜色空间和颜色配置文件几个概念来说,在色彩领域是非常重要的概念,上面这点篇幅无法彻底的将这几个概念彻底介绍清楚。加上自己也是这方面的菜鸟,如果有不对之处,还请路过的大神多多指正

颜色的转换

在《CSS 颜色》一文中,我们了解和学习了在CSS中如何通过不同的方式来描述颜色。不同的方式有不同的优缺点,而且使用姿势也不一样,这里不做阐述。接下来,我们要重点聊的是,如何通过JavaScript实现颜色模式之间的转换,比如rgb()#RRGGBBhsl()rgb()等。如果你对这方面也感兴趣,欢迎继续往下阅读。

RGB和#RRGGBB互转

先来看RGB#RRGGBB两种模式互转。这两种模式都是用红绿蓝来描述一个颜色的,不同的是RGB是十进制数(或百分比),#RRGGBB是十六进制。比如#0798ff可以用rgb(7 152 255)(或者老的语法rgb(7, 152, 255)):

在聊两种模式之间如何通过JavaScript实现转换之前,有一些基础需要简单的了解一下。刚刚也提到过,运用于RGB颜色中的值是我们最熟悉的数字系统,即十进制数字系统

十进制数字系统是一个以10为基数的系统,有十个唯一的字符用于定义这些数字,这十个数字字符就是0 ~ 9

要十进制数字系统中,我们可以把每一个数字都建模成一个能被十的幂除尽:

这就意味着,十进制数字的每个部分都是1101001000或任何10n次方存储桶。我们想要表示的每个相得到的基数为10的数都将位于此模型指定的范围内。

RGB颜色模式中取值范围是0 ~ 255,放到十进制的模型中的话,大致如下:

事实上,十进制数对于我们来说最熟悉不过了,我们不需要这些额外的可视化层或将每个数字映射到适当的10次方的表达式。

接下来,我们来了解一下十六进制数,十六进制数相对于十进制数来说,要更复杂一点。

在十六进制的世界中,表示的数字是从0 ~ 15。需要注意的是,并不是所有数都是小数。从0 ~ 9都是基数为10的数字。一旦到了10,就需要改用字母来表示,10a(或A)、11b(或B)、12c(或C)、13d(或D),14e(或E),15f(或F)。即0 ~ 15的数字在十进制和十六进制对应的关系如下:

现在我们对十进制和十六进制数有了一个基础的了解。接着需要解决的是,十进制如何转换为十六进制(RGB#RRGGBB)或者十六进制如何转为十进制(#RRGGBBRGB)。我们先来看十进制转十六进制。

前面我们用视图层向大家演示了十进制数存储模型。除了用视图来表示之外,还可以用取余的计算模式来表示,比如42这个十进制数:

42 / 10 = 4 余 2
4 / 10  = 0 余 4

先用4210,得到商为4,余为2,接着用商4继续除10,得到商为0,余为4。当商为0时,结束计算过程。现在生成的结果是反过来走,把余数拼结在一起,比如上面的示例就是42,组合在一起就是42。其他的十进制数的计算过程也可以像这样,比如222

222 / 10 = 22 余 2
22  / 10 = 2  余 2
2   / 10 = 0  余 2

这样的原理同样可以用于十进制转十六进制中,同样拿42这个数为例,转成十六进制的计算过程:

42 / 16 = 2 余 10
2  / 16 = 0 余 2

当商为0时结束计算,并且把余数拼合起,即余数找拼合起来是210,在十六进制中10对应的是A,因此拼出来的结果是2A,也就是说,十进制的42转换为十六进制数的结果是2A。再来看一个示例,比如220

220 / 16 = 13 余 12 ❯❯❯ c
13  / 16 = 0  余 13 ❯❯❯ d

220十进制转为十六进制是dc

另外,十进制转十六进制还有一种更为简单的计算方式,比如255十进制要转换为十六进制,可以像下面这样:

255   / 16   = 15.9375 取整 15 ❯❯❯ f
.9375 * 16   = 15             ❯❯❯ f

255转换成十六进制是ff

那么一个RGB中对应的值转换成#RRGGBB就需要分三步完成,比如rgb(7, 152, 255)颜色值将分成三组,R:7G:152B:255。按照上面的计算过程来计算:

// 先计算R
7 / 16 = 0 余 7 ❯❯❯ 7(十六进制)

7转成的十六进制就为07,在十六进制中,当只有一位数时,在该数前面补0

// 再计算G
152 / 16 = 9 余 8 ❯❯❯ 8(十六进制)
9   / 16 = 0 余 9 ❯❯❯ 9(十六进制)

152转成的十六进制就是98

// 最后计算B
255 / 16 = 15 余 15 ❯❯❯ f(十六进制)
15  / 16 = 0  余 15 ❯❯❯ f(十六进制)

255转成的十六进制数就是ff。这样将计算出来的RGB的值拼接在一起,就得出最终的#RRGGBB,即rgb(0, 152, 255)转出来的十六进制值为#0798ff

如果你坚持阅读到现在,我想你已经了解到如何快速的将十进制数转换为十六制了。这样一来,就完成了RGB#RRGGBB的理论知识。但有的时候需要#RRGGBBRGB,这样就避免不了将十六进制转十进制。同样这是最基础的理论知识了。

十六进制转十进制相对来说要更为简单,只需要用到一些乘法知识就可以。比如说十六进制的2A要转换成十进制数。和十进制不同的是,转十六进制是以16为基数,如下所示:

这样一来,2A这个十六进制数转十进制数,它的计算过程是:

2A最终转成的十进制数是42

按这样的计算方式,我们就可以很快的将一个十六进制的#RRGGBB转换成一个十进制的RGB。比如我们有一个#0798ff的颜色,转换成一个十进制的RGB颜色。我们可以按下面这样的过程来计算:

也就说#0798ff转成rgb的话就是rgb(7, 152, 255)

我想到这里为止,你对十进制转十六进制和十六进制转十进制有了一定的了解。这样我们就可以进入到JavaScript的世界中了。在JavaScript中,我们可以使用toString()来做十进制和十六进制之间的转换,比如说:

const c = 255
c.toString(16) // ❯❯❯ ff

同样的,可以使用eval().toString()将一个十六进制转换成十进制,不同的是,在十六进制中需要前面补位0x,比如要将十六进制2a转换成十进制的话,就是0x2a转:

eval("0x2a").toString(10) // ❯❯❯ 42

也可以使用parseInt(),比如:

parseInt('0x2a', 16) // ❯❯❯ 42

这样一来,我们就可以构建两个函数RGBToHex()实现RGB#RRGGBBHexToRGB()实现#RRGGBBRGB。有了这两个函数,就可以很轻易的实现RGB#RRGGBB两种颜色模式的互转。

先来看RGBToHex()函数怎么构建。通过前面的学习,使用rgb()函数描述颜色有两种方式rgb(r, g, b)rgb(r g b)。它们的主要区别是新老语法书写方式的差异,老语法用逗号,做分隔,新语法是有空格符做分隔。换句话说,给RGBToHex()函数可以是两种传参的方式,其一是(r, g, b),其二是(rgb)。那么函数的代码就可以这样来构造:

/** 
 * RGB颜色转Hex颜色函数
 * @param {number} r - R通道值
 * @param {number} g - G通道值
 * @param {number} b - B通道值
*/
function RGBToHex(r, g, b) {
    r = r.toString(16)
    g = g.toString(16)
    b = b.toString(16)

    if (r.length == 1) {
        r = `0${r}`
    }

    if (g.length == 1) {
        g = `0${g}`
    }

    if (b.length == 1) {
        b = `0${b}`
    }

    return `#${r}${g}${b}`
}

RGBToHex(255, 0, 125)   // ❯❯❯ "#ff007d"
RGBToHex(255, 255, 255) // ❯❯❯ "#ffffff"

上面的代码我们可以继续做一个优化,即十进制转十六进制部分,我们可以单独构造一个函数:

function componentToHex(c) {
    const hex = c.toString(16)
    return hex.length == 1 ? `0${hex}` : hex
}

function RGBToHex(r, g, b) {
    return `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}`
}

RGBToHex(255, 255, 255) // ❯❯❯ "#ffffff"
RGBToHex(255, 55, 255)  // ❯❯❯ "#ff37ff"

再来看另外一种方式:

/**
 * RGB颜色转Hex颜色函数
 * @param {string} rgb - rgb()描述的颜色
*/
function RGBToHex(rgb) {
    // 选择分隔符方式 逗号或空格作为分隔符
    let sep = rgb.indexOf(',') > -1 ? ',' : ' '

    // 将`(rgb)`转换为[r, g, b]
    rgb = rgb.substr(4).split(')')[0].split(sep)

    let r = (+rgb[0]).toString(16)
    let g = (+rgb[1]).toString(16)
    let b = (+rgb[2]).toString(16)

    if (r.length == 1) {
        r = `0${r}`
    }

    if (g.length == 1) {
        g = `0${g}`
    }

    if (b.length == 1) {
        b == `0${b}`
    }

    return `#${r}${g}${b}`
}

RGBToHex('rgb(122, 122, 122)') // ❯❯❯ "#7a7a7a"
RGBToHex('rgb(122 122 122)')   // ❯❯❯ "#7a7a7a"
RGBToHex('rgb(22 122 122)')    // ❯❯❯ "#167a7a"
RGBToHex('rgb(22, 122, 122)')  // ❯❯❯ "#167a7a"

在使用rgb()函数来描述颜色时,除了可以使用十进制的数值0 ~ 255之外,还可以使用百分比值0% ~ 100%,比如rgb(10% 20% 30%)

rgb()使用百分比值时是基于255进行转换,比如上面的rgb(10% 20% 30%)

R: 10 x 255 / 100 = 25.5  ❯❯❯ 25
G: 20 x 255 / 100 = 51    ❯❯❯ 51
B: 30 x 255 / 100 = 76.5  ❯❯❯ 77

注意,不同的浏览器四舍五入方式不一样,上面是Chrome转换得到的值

如果要让RGBToHex()函数也能满足百分比值时候的转换,那么函数需要基于上面的做一点改造:

/**
 * RGB颜色转Hex颜色函数
 * @param {string} rgb - rgb()描述的颜色
*/
function RGBToHex(rgb) {
    let sep = rgb.indexOf(',') > -1 ? ',' : ' '
    rgb = rgb.substr(4).split(')')[0].split(sep)

    // s% ❯❯❯ 0 ~ 255
    for (let R in rgb) {
        let r = rgb[R]

        if (r.indexOf('%') > -1) {
            // eg: 7% ❯❯❯ 75 * 255 / 100 = 191.25 ❯❯❯ 191
            rgb[R] = Math.round(r.substr(0, r.length - 1) / 100 * 255)
        }
    }

    let r = (+rgb[0]).toString(16)
    let g = (+rgb[1]).toString(16)
    let b = (+rgb[2]).toString(16)

    if (r.length == 1) {
        r = `0${r}`
    }

    if (g.length == 1) {
        g = `0${g}`
    }

    if (b.length == 1) {
        b == `0${b}`
    }

    return `#${r}${g}${b}`
}

RGBToHex('rgb(122 122 22)')    // ❯❯❯ "#7a7a16"
RGBToHex('rgb(122, 122, 22)')  // ❯❯❯ "#7a7a16"
RGBToHex('rgb(10% 12% 22%)')   // ❯❯❯ "#1a1f38"
RGBToHex('rgb(10%, 12%, 22%)') // ❯❯❯ "#1a1f38"

在构建RGBToHex()函数时,使用了一些JavaScript的基础知识,比如indexOf()substr()split()toString()等。

完成RGBToHex()函数的构建后,我们再来看HexToRGB()函数的构建。

平时我们使用十六进制<hex-color>来描述颜色的话,一般是#RGB#RRGGBB,当RGB的两位值相同时,可以简写会一位,比如#ff00ff可以简写为#f0f。而转成的rgb()可以是rgb(r, g, b)rgb(r g b)。在十六进制转十进制的时候,需要在前面添加0x,这样一来的话,r对应的十六进制是0xrrg对应的十六进制是0xggb对应的十六进制是0xbb。为了获得最后的rgb()字符串的值,我们需要在变量前面加上+,将它们从字符串转换回数字。整个HexToRGB()函数代码像下面所示:

/**
 * Hex颜色转RGB颜色函数
 * @param {string} hex - <hex-color>
 * @param {string} seq - 分隔符,逗号或空格分隔符
*/
function HexToRGB(hex, sep) {
    // hex是十六进制颜色 可以是#RGB或#RRGGBB
    // sep是分隔符,rgb()函数支持两种分隔符,可以是逗号,也可以是空格符

    let r = 0, g = 0, b = 0

    // #RGB
    if (hex.length == 4) {
        r = `0x${hex[1]}${hex[1]}`
        g = `0x${hex[2]}${hex[2]}`
        b = `0x${hex[3]}${hex[3]}`
    } else if (hex.length == 7) { // #RRGGBB
        r = `0x${hex[1]}${hex[2]}`
        g = `0x${hex[3]}${hex[4]}`
        b = `0x${hex[5]}${hex[6]}`
    }

    return `rgb(${+r}${sep}${+g}${sep}${+b})`
}

HexToRGB('#000', ',')    // ❯❯❯ "rgb(0,0,0)"
HexToRGB('#000', ' ')    // ❯❯❯ "rgb(0 0 0)"
HexToRGB('#afde00', ' ') // ❯❯❯ "rgb(175 222 0)"
HexToRGB('#afde00', ',') // ❯❯❯ "rgb(175,222,0)"

如果希望<hex-color>转换出来的rgb()函数的值是百分比的话,可以在上面的HexToRGB()函数的基础上再增加一个参数,比如isPct,该值是一个布尔值,如果是true表示输出的值是百分比值,反之是数值。改造完的HexToRGB()函数,像下面这样:

/**
* @param {string} hex - hex是十六进制颜色 可以是#RGB或#RRGGBB
* @param {string} sep - sep是分隔符,rgb()函数支持两种分隔符,可以是逗号,也可以是空格符
* @param {boolean} isPct - isPct用来判断输出的值是不是百分比,true输出的是百分比,false输出的是数字值
*/
function HexToRGB(hex, sep, isPct) {
    let r = 0, g = 0, b = 0
    isPct = isPct === true

    // #RGB
    if (hex.length == 4) {
        r = `0x${hex[1]}${hex[1]}`
        g = `0x${hex[2]}${hex[2]}`
        b = `0x${hex[3]}${hex[3]}`
    } else if (hex.length == 7) { // #RRGGBB
        r = `0x${hex[1]}${hex[2]}`
        g = `0x${hex[3]}${hex[4]}`
        b = `0x${hex[5]}${hex[6]}`
    }

    if (isPct) {
        r = +(r / 255 * 100).toFixed(1)
        g = +(g / 255 * 100).toFixed(1)
        b = +(b / 255 * 100).toFixed(1)
    } else {
        r = +r
        g = +g
        b = +b
    }

    const rgbVal = isPct ? `${r}%${sep}${g}%${sep}${b}%` : `${r}${sep}${g}${sep}${b}`

    return `rgb(${rgbVal})`
}

HexToRGB('#0fe', ',', true)     // ❯❯❯ "rgb(0%,100%,93.3%)"
HexToRGB('#afe', ',', false)    // ❯❯❯ "rgb(170,255,238)"
HexToRGB('#0feafe', ',', true)  // ❯❯❯ "rgb(5.9%,91.8%,99.6%)"
HexToRGB('#0feafe', ',', false) // ❯❯❯ "rgb(15,234,254)"
HexToRGB('#afe', ' ', true)     // ❯❯❯ "rgb(66.7% 100% 93.3%)"
HexToRGB('#afe', ' ', false)    // ❯❯❯ "rgb(170 255 238)"
HexToRGB('#a0ffef', ' ', true)  // ❯❯❯ "rgb(62.7% 100% 93.7%)"
HexToRGB('#a0ffef', ' ', false) // ❯❯❯ "rgb(160 255 239)"

RGBA和#RRGGBBAA互转

RGBA#RRGGBBAA是在RGB#RRGGBB的基础上增加了颜色的透明通道A。由于A通道(alpha)的值是0 ~ 1(或0% ~ 100%)之间,如果我们不用百分比来描述A通道值的话,需要将其乘以255,将结果四舍五入,然后将其转为十六进制数。

和前面一样,我们可能通过构建两个不同的函数实现RGBA#RRGGBBAA两种颜色模式之间的互转。这里我们创建RGBAToHexA()实现RGBA#RRGGBBAA,创建HexAToRGB()实现#RRGGBBAARGBA。在CSS颜色模块的Level4版本中,表达带有透明通道的颜色有两种写法:rgba(r, g, b, a)rgb(r g b / a),对于十六进制的话是#rgba#rrggbbaa

我们先来看rgba(r, g, b, a)这种老式语法怎么转成#rrggbbaaRGBAToHexA()函数的代码如下:

/**
* @param {number} r - R通道值 0 ~ 255 或 0% ~ 100%
* @param {number} g - G通道值 0 ~ 255 或 0% ~ 100%
* @param {number} b - B通道值 0 ~ 255 或 0% ~ 100%
* @param {number} a - A通道值 0 ~ 1 或 0% ~ 100%
*/

function RGBAToHexA(r, g, b, a) {
    r = r.toString(16)
    g = g.toString(16)
    b = b.toString(16)
    a = Math.round(a * 255).toString(16)

    if (r.length == 1) {
        r = `0${r}`
    }

    if (g.length == 1) {
        g = `0${g}`
    }

    if (b.length == 1) {
        b = `0${b}`
    }

    if (a.length == 1) {
        a = `0${a}`
    }

    return `#${r}${g}${b}${a}`
}

RGBAToHexA(122, 234, 23, .5) // ❯❯❯ "#7aea1780"
RGBAToHexA(255, 255, 255, 1) // ❯❯❯ "#ffffffff"
RGBAToHexA(0, 0, 0, 0)       // ❯❯❯ "#00000000"

如果希望RGBAToHexA()函数更为灵活的话,上面的代码就有点缺陷,特别是针对于新语法规则,和取百分比值的时候。我们可以针对这些缺陷做一些改造:

/**
* @param {string} rgba - rgba()函数描述的颜色,eg, rgba(122 122 122 / .5) 或 rgba(122, 122, 122, .5) 或 rgba(20% 30% 40% / 50%) 在新版本语法中 rgba()和rgb()等效
*/

function RGBAToHexA(rgba) {
    let sep = rgba.indexOf(',') > -1 ? ',' : ' '
    rgba = rgba.substr(5).split(')')[0].split(sep)

    // 如果使用空格分隔符,需要去掉/分隔符
    if (rgba.indexOf('/') > -1) {
        rgba.splice(3, 1)
    }

    for (let R in rgba) {
        let r = rgba[R]

        if (r.indexOf('%') > -1) {
            let percent = r.substr(0, r.length - 1) / 100

            if (R < 3) {
                rgba[R] = Math.round(percent * 255)
            } else {
                rgba[R] = percent
            }
        }
    }

    let r = (+rgba[0]).toString(16)
    let g = (+rgba[1]).toString(16)
    let b = (+rgba[2]).toString(16)
    let a = Math.round(+rgba[3] * 255).toString(16)

    if (r.length == 1) {
        r = `0${r}`
    }

    if (g.length == 1) {
        g = `0${g}`
    }

    if (b.length == 1) {
        b = `0${b}`
    }

    if (a.length == 1) {
        a = `0${a}`
    }

    return `#${r}${g}${b}${a}`
}

RGBAToHexA('rgba(255,25,2,0.5)')       // ❯❯❯ "#ff190280"
RGBAToHexA('rgba(255 25 2 / 0.5)')     // ❯❯❯ "#ff190280"
RGBAToHexA('rgba(50%,30%,10%,0.5)')    // ❯❯❯ "#804d1a80"
RGBAToHexA('rgba(50%,30%,10%,50%)')    // ❯❯❯ "#804d1a80"
RGBAToHexA('rgba(50% 30% 10% / 0.5)')  // ❯❯❯ "#804d1a80"
RGBAToHexA('rgba(50% 30% 10% / 50%)')  // ❯❯❯ "#804d1a80"

HexAToRGBA()函数的构造和前面的HexToRGB()有点类似,只不在他的基础上增加了A通道。所以,我们同样给HexAToRGBA()函数传三个参数hex(表示#RRGGBBAA#RGBA颜色),sep(rgb()rgba()函数中的分隔符),isPct转换出来的值是否是百分比值。函数具体代码如下:

/**
* @param {string} hex - hex是十六进制颜色 可以是#RGBA或#RRGGBBAA
* @param {string} sep - sep是分隔符,rgb()函数支持两种分隔符,可以是逗号,也可以是空格符
* @param {boolean} isPct - isPct用来判断输出的值是不是百分比,true输出的是百分比,false输出的是数字值
*/

function HexAToRGBA(hex, sep, isPct) {
    let r = 0, g = 0, b = 0, a = 1

    isPct = isPct === true

    if (hex.length == 5) {
        r = `0x${hex[1]}${hex[1]}`
        g = `0x${hex[2]}${hex[2]}`
        b = `0x${hex[3]}${hex[3]}`
        a = `0x${hex[4]}${hex[4]}`
    } else if (hex.length == 9) {
        r = `0x${hex[1]}${hex[2]}`
        g = `0x${hex[3]}${hex[4]}`
        b = `0x${hex[5]}${hex[6]}`
        a = `0x${hex[7]}${hex[8]}`
    }



    if (isPct) {
        r = +(r / 255 * 100).toFixed(1)
        g = +(g / 255 * 100).toFixed(1)
        b = +(b / 255 * 100).toFixed(1)
        a = +(a / 255 * 100).toFixed(1)
    } else {
        r = +r
        g = +g
        b = +b
        a = +(a / 255).toFixed(1)
    }

    if (sep === ',') {
        return isPct ? `rgb(${r}%${sep}${g}%${sep}${b}%${sep}${a}%)` : `rgb(${r}${sep}${g}${sep}${b}${sep}${a})`

    } else if (sep === ' ') {
        return isPct ? `rgb(${r}%${sep}${g}%${sep}${b}%${sep} / ${a}%)` : `rgb(${r}${sep}${g}${sep}${b}${sep} / ${a})`
    }
}

HexAToRGBA('#09ff98ea', ',', true)     // ❯❯❯ "rgb(3.5%,100%,59.6%,91.8%)"
HexAToRGBA('#09ff98ea', ',', false)    // ❯❯❯ "rgb(9,255,152,0.9)"
HexAToRGBA('#09ff98ea', ' ', true)     // ❯❯❯ "rgb(3.5% 100% 59.6%  / 91.8%)"
HexAToRGBA('#09ff98ea', ' ', false)    // ❯❯❯ "rgb(9 255 152  / 0.9)"
HexAToRGBA('#09ff', ',', true)         // ❯❯❯ "rgb(0%,60%,100%,100%)"
HexAToRGBA('#09ff', ',', false)        // ❯❯❯ "rgb(0,153,255,1)"
HexAToRGBA('#09ff', ' ', true)         // ❯❯❯ "rgb(0% 60% 100%  / 100%)"
HexAToRGBA('#09ff', ' ', false)        // ❯❯❯ "rgb(0 153 255  / 1)"

基于上面的内容,我们可以构建一个简单颜色转换器,比如rgb<hex>

特别声明,该示例功能比较弱,仅做了rgb#rrggbb的互转,对于rgba#rrggbbaa无法互转。在检测input输入值是否是有效值时,未能较好的做相应检测!

RGB和HSL互转

在CSS中可以使用rgb()hsl()函数分别表述RGBHSL颜色。rgb()除了可以使用0 ~ 255还可以使用0% ~ 100%来赋值,在hsl()中除了H之外都可以使用百分比值。也就是说,RGBHSL相比,除了色相H之外,所有值都可以用百分比表示。

换句话说,从RGBHSL具有一定的挑战性,因为这会涉及到一些数学公式。首先,我们必须将红色R、绿色G和蓝色B的值除以255,才能使用0 ~ 1之间的值。然后我们根据一定的公式计算出HSL相应的值,即hsl

首先我们来看如何计算出HSL颜色模式中的L。计算L的时候可以根据下面的公式来计算:

其中MRGB值的最大值,m是最小值。用JavaScript的话,可以这样描述:

const RGBToLightness = (r, g, b) => {
    r = r / 255
    g = g / 255
    b = b / 255

    let M = Math.max(r, g, b)
    let m = Math.min(r, g, b)

    return `${Math.round((M + m) / 2 * 100)}%`
}

计算颜色的饱和度S相对于计算L要稍微复杂一点。如果亮度为01,则饱和度值为0,否则,它基于下面的数学公式计算出饱和度S:

同样的,我们可以使用JavaScript将计算颜色饱和度S转换成代码:

const RGBToSaturation = (r, g, b) => {
    r = r / 255
    g = g / 255
    b = b / 255
    let M = Math.max(r, g, b)
    let m = Math.min(r, g, b)
    let delta = M - m
    let l = (M + m) / 2

    return delta == 0 ? `0%` : `${Math.round(delta / (1 - Math.abs(2 * l - 1)) * 100)}%`
}

RGB颜色模型中计算色相H的公式要复杂很多:

用JavaScript代码来描述:

const RGBToHue = (r, g, b) => {
    let h = Math.round(Math.atan2(Math.sqrt(3) * (g - b), 2 * r - g - b) * 180 / Math.PI)
    if ( h < 0) {
        return h + 360
    } else {
        return h
    }
}

最后乘180 / Math.PI是将结果从弧度转换成度数。

有了RGBToLightness()RGBToSaturation()RGBToHue()三个基本函数之后,要将一个RGB颜色转换为HSL模式的颜色就要简单地说。我们可以构建一个新的函数RGBToHSL()

const RGBToHSL= (r, g, b) => {
    const h = RGBToHue(r, g, b)
    const s = RGBToSaturation(r, g, b)
    const l = RGBToLightness(r, g, b)
    return `hsl(${h}, ${s}, ${l})`
}

RGBToHSL(255, 255, 255) // ❯❯❯ "hsl(0, 0%, 100%)"
RGBToHSL(55, 55, 255)   // ❯❯❯ "hsl(240, 100%, 61%)"
RGBToHSL(180, 85, 155)  // ❯❯❯ "hsl(315, 39%, 52%)"

接下来使用RGBToHSL()函数就可以实现RGB颜色转HSL颜色:

上面创建的函数可以帮助我们实现RGBHSL,只不过@akinuri在StackOverflow上回答《RGB to HSL conversion》时介绍的更为详细,也更易于理解。@akinuri用RGB的立方体给我们剖析了整个计算过程。

大家都知道,RGB颜色模式,它是由RGB三个颜色通道构成的颜色,放到三维空间的话,其对应的就是xyz,也就构成了一个六面的立方体。但是在创建RGB立方体之前,我们先来看2D空间。RGB的两种组合是:RGRBGB。如果用图来描述的话,就像下面这样的:

这就是RGB立方体的前三面。如果我们把它们放到一个三维空间,它会产生一个半立方体:

正如上图所示,通过两种颜色混合,会得到一个新的颜色在(255, 255)点,即黄色品红青色。同样,这些的两种组合得到YMYCMC。就是六方体缺失的另外三个面。一旦我们把它们加起来,就会得到一个完整的RGB立方体:

比如,51, 153, 204就在这个立方体上:

现在我们有了RGB立方体,让我们把它投射到六边形上。首先,将立方体在x轴上倾斜45°,然后在y轴上倾斜35.264°。第二次倾斜后,黑色的角在底部,白色的角在顶部,它们都通过z轴。

正如你所看到的,当我们从顶部看立方体时,我们得到了我们想要的六边形的颜色顺序。但是我们需要把它投射到一个真正的六边形上。我们要做的是画一个与立方体顶视图大小相同的六边形。六边形的所有角都对应着立方体的角和颜色,立方体上的角是白色的,投射到六边形的中心。黑色是省略了。如果我们把所有颜色都映射到六边形上,我们就能得到正确的结果。

对应的51, 153, 204在六边形上对应的位置,如下图所示:

具体的转换原理可以用维基上提供的图来描述

接下来就是计算颜色了。在进行计算之前,先定义一下色调H

色调大致是矢量到投影中某一点的角度,红色为

色调是指点所在六边形边缘的距离。

这是来自HSLHSV维基页面提供的计算公式。我们将在这个解释中用到它。

检查六边形和在它上面51, 153, 204点的位置:

(r, g, b)分别是一个颜色的红、绿和蓝坐标,它们的值是在01之间的实数:

r = r / 255 ❯❯❯ r = 51  / 255 = 0.2
g = g / 255 ❯❯❯ g = 153 / 255 = 0.6
b = b / 255 ❯❯❯ b = 204 / 255 = 0.8

max等价于r, gb中的最大者。设min等于这些值中的最小者:

M = max(r, g, b) ❯❯❯ M = max(0.2, 0.6, 0.8) = 0.8
m = min(r, g, b) ❯❯❯ m = min(0.2, 0.6, 0.8) = 0.2

Mm之间有一个差值delta。这个差值在计算色相H和饱和度S是会用到。如果用代码来实现上面的描述的话,大致如下:

/**
* RGB 颜色转 HSL颜色函数
* @param {number} r - RGB颜色中R的值
* @param {number} g - RGB颜色中G的值
* @param {number} b - RGB颜色中B的值 
*/
function RGBToHSL(r, g, b) {
    // 颜色的红、绿和蓝坐标,它们的值是在0 ~ 1之间的实数
    r = r / 255
    g = g / 255
    b = b / 255

    // 找出最大值和最小值,并计算出它们之间的差值
    let M = Math.max(r, g, b) // 最大值
    let m = Math.min(r, g, b) // 最小值
    let delta = M - m         // 最大值 M 和最小值 m 之间的差值

    // 初始值HSL的通道h, s, l初始值为0
    let h = 0, s = 0, l = 0

    // ... 
}

然后计算色度C(Chroma),在HSL中又被称为饱和度SS大概是颜色点到原点的距离。

C = OP / OP'
C = M - m
C = 0.8 - 0.2 = 0.6

现在我们有了rgbC的值。如果我们检查条件,如果 M = b(最大值MRGB颜色B通道相等),则是true,可以用H'= (r - g) / C + 4表示。在六边形中,(r - g) / C对应的就是BP段的长度。

BP段长度 = (r - g)/ C = (0.2 - 0.6) / 0.6 = -0.6666666666666666

我们把这个线段放在内六边形上。六边形的起点是R(红色)在。如果线段长度是正的,它应该在RY上,如果是负的,它应该在RM上。在这种情况下,它是-0.6666666666666666,在RM的边缘上:

接下来,我们需要改变的位置,或者说P₁代为b(因为M=b)。蓝色是240°。六边形有六条边,每条边对应60°240 / 60 = 4)。我们需要转变P₁增量,在以前的BP段长度上加4(对应240°)。转移后,P₁就会到P,我们就会得到RYGCP的长度:

BP段长度 = (r - g) / C = (0.2 - 0.6) / 0.6 = -0.6666666666666666
RYGCP   = BP段长度 + 4 = 3.3333333333333335

六边形的周长是6,相当于360°53, 151, 204的距离是3.3333333333333335。如果将该值乘以60,就会得到它的度数:

H' = 3.3333333333333335
H  = H' * 60 = 200°

M = r的情况下,由于我们将线段的一端放在r()位置处,如果线段长度为正,我们不需要将线段移到r处。P₁的位置将是正的。但是如果线段的长度是负的,我们就需要移动6,因为是负值,意味着角的位置大于180°,我们需要做一个完整的旋转。

到这有点绕是吧,其实就是为了计算色相的角度值。我们只要记住上面的公式:

M = r ❯❯❯ H' = ((g - b) / C) % 6 ❯❯❯ H = H' x 60°
M = g ❯❯❯ H' = ((b - r) / C) + 2 ❯❯❯ H = H' x 60°
M = b ❯❯❯ H' = ((r - g) / C) + 4 ❯❯❯ H = H' x 60°
C = 0 ❯❯❯ H' = undefined

这样就可以上面未完全的函数中添加有关于色相H的计算:

/**
* RGB 颜色转 HSL颜色函数
* @param {number} r - RGB颜色中R的值
* @param {number} g - RGB颜色中G的值
* @param {number} b - RGB颜色中B的值 
*/
function RGBToHSL(r, g, b) {
    // 颜色的红、绿和蓝坐标,它们的值是在0 ~ 1之间的实数
    r = r / 255
    g = g / 255
    b = b / 255

    // 找出最大值和最小值,并计算出它们之间的差值
    let M = Math.max(r, g, b) // 最大值
    let m = Math.min(r, g, b) // 最小值
    let delta = M - m         // 最大值 M 和最小值 m 之间的差值

    // 初始值HSL的通道h, s, l初始值为0
    let h = 0, s = 0, l = 0

    // 计算色相h

    // 如果C = 0 即 M = m, 没有差别,这个时候h=0
    if (delta == 0) {
        h = 0
    } else if (M == r) {
        // r = M (红色通道r是最大值)
        // M = r ❯❯❯ H' = ((g - b) / C) % 6 
        h = ((g - b) / delta) % 6
    } else if (M == g) {
        // g = M (绿色通道g是最大值)
        // M = g ❯❯❯ H' = ((b - r) / C) + 2
        h = ((b - r) / delta) + 2
    } else {
        // b = M (蓝色通道b是最大值)
        // M = b ❯❯❯ H' = ((r - g) / C) + 4
        h = ((r - g) / delta) + 4
    }

    // 最终色相的值 H = H' x 60°
    h = Math.round(h * 60)

    // 计算出来的色相值h,如果小于0的话,需要做一下处理
    if (h < 0) {
        h += 360
    }

    // ...
}

剩下的就是计算亮度l和饱和度s了。在计算饱和度s之前,我们先来计算亮度l,因为饱和度s取决于亮度l。亮度的值是最大值M和最小值m两个值总和的一半,即 l = (M + m) / 2

前面提到过,最大值M和小值m有可能有差值(当他们不相等时,可能是正值,也可能是负值),前面将两者的差值赋值给了delta,这个差值很重要,因为它将决定颜色饱和度s的值。如果delta的值是0,则饱和度s的值为0,否则就是1减去2倍亮度l的绝对值减去1(数学公式就是1 - Math.abs(2 * l - 1))。因为HSL颜色模式中,sl的值都是百分比值,因此我们最后还需要将得到的sl值转换为百分比值。

将计算亮度和饱和度的计算放到函数中:

/**
* RGB 颜色转 HSL颜色函数
* @param {number} r - RGB颜色中R的值
* @param {number} g - RGB颜色中G的值
* @param {number} b - RGB颜色中B的值 
*/
function RGBToHSL(r, g, b) {
    // 颜色的红、绿和蓝坐标,它们的值是在0 ~ 1之间的实数
    r = r / 255
    g = g / 255
    b = b / 255

    // 找出最大值和最小值,并计算出它们之间的差值
    let M = Math.max(r, g, b) // 最大值
    let m = Math.min(r, g, b) // 最小值
    let delta = M - m         // 最大值 M 和最小值 m 之间的差值

    // 初始值HSL的通道h, s, l初始值为0
    let h = 0, s = 0, l = 0

    // 计算色相h

    // 如果C = 0 即 M = m, 没有差别,这个时候h=0
    if (delta == 0) {
        h = 0
    } else if (M == r) {
        // r = M (红色通道r是最大值)
        // M = r ❯❯❯ H' = ((g - b) / C) % 6 
        h = ((g - b) / delta) % 6
    } else if (M == g) {
        // g = M (绿色通道g是最大值)
        // M = g ❯❯❯ H' = ((b - r) / C) + 2
        h = ((b - r) / delta) + 2
    } else {
        // b = M (蓝色通道b是最大值)
        // M = b ❯❯❯ H' = ((r - g) / C) + 4
        h = ((r - g) / delta) + 4
    }

    // 最终色相的值 H = H' x 60°
    h = Math.round(h * 60)

    // 计算出来的色相值h,如果小于0的话,需要做一下处理
    if (h < 0) {
        h += 360
    }

    // 计算亮度l
    l = (M + m) / 2

    // 计算饱和度
    s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1))

    // 将s和l转换成百分比值
    s = +(s * 100).toFixed(1)
    l = +(l * 100).toFixed(1)

    // hsl(),h, s, l之间的分隔符可以是空格,也可以是逗号
    return `hsl(${h} ${s}% ${l}%)`
}

console.log(RGBToHSL(255, 22, 90))   // ❯❯❯ "hsl(342 100% 54.3%)"
console.log(RGBToHSL(55, 122, 190))  // ❯❯❯ "hsl(210 55.1% 48%)"
console.log(RGBToHSL(155, 122, 220)) // ❯❯❯ "hsl(260 58.3% 67.1%)"
console.log(RGBToHSL(5, 222, 2))     // ❯❯❯ "hsl(119 98.2% 43.9%)"

可以找个工具验证,上面转换出来的结果是否符合预期。另外,计算H还有另外一套公式:

即:

M = m           ❯❯❯ h = 0
M = r && g >= b ❯❯❯ h = ((g - b) / (M - m)) * 60 + 0
M = r && g < b  ❯❯❯ h = ((g - b) / (M - m)) * 60 + 360
M = g           ❯❯❯ h = ((b - r) / (M - m)) * 60 + 120
M = b           ❯❯❯ h = ((r - g) / (M - m)) * 60 + 240

按这个公式来计算h的话,RGBToHSL()函数代码中计算h的部分就可以换成:

if (delta == 0) {
    h = 0
} else if (M == r && g >= b) {
    h = (g - b) / delta * 60
} else if (M == r && g < b) {
    h = (g - b) / delta * 60 + 360
} else if (M == g) {
    h = (b - r) / delta * 60 + 120
} else {
    h = (r - g) / delta * 60 + 240
}

h = Math.round(h)

// 计算出来的色相值h,如果小于0的话,需要做一下处理
if (h < 0) {
    h += 360
}

最终代码是:

/**
* RGB 颜色转 HSL颜色函数
* @param {number} r - RGB颜色中R的值
* @param {number} g - RGB颜色中G的值
* @param {number} b - RGB颜色中B的值 
*/
function RGBToHSL(r, g, b) {
    // 颜色的红、绿和蓝坐标,它们的值是在0 ~ 1之间的实数
    r = r / 255
    g = g / 255
    b = b / 255

    // 找出最大值和最小值,并计算出它们之间的差值
    let M = Math.max(r, g, b) // 最大值
    let m = Math.min(r, g, b) // 最小值
    let delta = M - m         // 最大值 M 和最小值 m 之间的差值

    // 初始值HSL的通道h, s, l初始值为0
    let h = 0, s = 0, l = 0

    // 计算色相h
    if (delta == 0) {
        h = 0
    } else if (M == r && g >= b) {
        h = (g - b) / delta * 60
    } else if (M == r && g < b) {
        h = (g - b) / delta * 60 + 360
    } else if (M == g) {
        h = (b - r) / delta * 60 + 120
    } else {
        h = (r - g) / delta * 60 + 240
    }

    h = Math.round(h)

    // 计算出来的色相值h,如果小于0的话,需要做一下处理
    if (h < 0) {
        h += 360
    }

    // 计算出来的色相值h,如果小于0的话,需要做一下处理
    if (h < 0) {
        h += 360
    }

    // 计算亮度l
    l = (M + m) / 2

    // 计算饱和度
    s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1))

    // 将s和l转换成百分比值
    s = +(s * 100).toFixed(1)
    l = +(l * 100).toFixed(1)

    return `hsl(${h} ${s}% ${l}%)`
}

最后得到的结果是一样的:

console.log(RGBToHSL(255, 22, 90))   // ❯❯❯ "hsl(342 100% 54.3%)"
console.log(RGBToHSL(55, 122, 190))  // ❯❯❯ "hsl(210 55.1% 48%)"
console.log(RGBToHSL(155, 122, 220)) // ❯❯❯ "hsl(260 58.3% 67.1%)"
console.log(RGBToHSL(5, 222, 2))     // ❯❯❯ "hsl(119 98.2% 43.9%)"

上面RGBToHSL()函数中的传参都是按颜色rgb三个通道传值,有的时候可能会直接转一个rgb(r g b)以及带%的值,毕竟这些在CSS颜色语法规则中都是成立。为此,我们只需要在上面的函数基础上稍作修改:

/**
* RGB 颜色转 HSL颜色函数
* @param {number} rgb - <rgb-color> 
*/
function RGBToHSL(rgb) {
    let sep = rgb.indexOf(",") > -1 ? "," : " ";
    rgb = rgb.substr(4).split(")")[0].split(sep);

    for (let R in rgb) {
      let r = rgb[R];
      if (r.indexOf("%") > -1)
        rgb[R] = Math.round(r.substr(0,r.length - 1) / 100 * 255);
    }

    // 颜色的红、绿和蓝坐标,它们的值是在0 ~ 1之间的实数
    let r = rgb[0] / 255
    let g = rgb[1] / 255
    let b = rgb[2] / 255

    // 找出最大值和最小值,并计算出它们之间的差值
    let M = Math.max(r, g, b) // 最大值
    let m = Math.min(r, g, b) // 最小值
    let delta = M - m         // 最大值 M 和最小值 m 之间的差值

    // 初始值HSL的通道h, s, l初始值为0
    let h = 0, s = 0, l = 0

    // 计算色相h

    // 如果C = 0 即 M = m, 没有差别,这个时候h=0
    if (delta == 0) {
        h = 0
    } else if (M == r) {
        // r = M (红色通道r是最大值)
        // M = r ❯❯❯ H' = ((g - b) / C) % 6 
        h = ((g - b) / delta) % 6
    } else if (M == g) {
        // g = M (绿色通道g是最大值)
        // M = g ❯❯❯ H' = ((b - r) / C) + 2
        h = ((b - r) / delta) + 2
    } else {
        // b = M (蓝色通道b是最大值)
        // M = b ❯❯❯ H' = ((r - g) / C) + 4
        h = ((r - g) / delta) + 4
    }

    // 最终色相的值 H = H' x 60°
    h = Math.round(h * 60)

    // 计算出来的色相值h,如果小于0的话,需要做一下处理
    if (h < 0) {
        h += 360
    }

    // 计算亮度l
    l = (M + m) / 2

    // 计算饱和度
    s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1))

    // 将s和l转换成百分比值
    s = +(s * 100).toFixed(1)
    l = +(l * 100).toFixed(1)

    return `hsl(${h} ${s}% ${l}%)`
}

console.log(RGBToHSL('rgb(255, 122, 90)'))   // ❯❯❯ "hsl(12 100% 67.6%)"
console.log(RGBToHSL('rgb(55, 122, 190)'))   // ❯❯❯ "hsl(210 55.1% 48%)"
console.log(RGBToHSL('rgb(155, 122, 220)'))  // ❯❯❯ "hsl(260 58.3% 67.1%)"
console.log(RGBToHSL('rgb(50, 222, 102)'))   // ❯❯❯ "hsl(138 72.3% 53.3%)"

接下来看HSLRGB。相对而言,HSLRGB要简单地多。

HSL颜色模式中通过hsl三个通道来定义一个颜色值,其中代表色相的h是一个角度值,在0 ~ 360范围,代表饱和度的s和亮度的l的值在0 ~ 1范围。同样的,在开始编码之前有一些数学公式需要去了解。

计算颜色色度的值C

C = (1 - | 2l - 1 |) * s

用JavaScript代表来表示的话:

C = (1 - Math.abs(2 * l - 1)) * s

还有一个临时的色调值H',将用它来确定我们所属的色调圈的“”:

H' = H / 60

接下来,我们设置一个X值,它将用作中间(第二大)组件值:

X = C * (1 - | H' mod 2 - 1 |) = C * (1 - | (H / 60) mod 2 - 1 |)

再设置一个m值,用于调整各个亮度值:

m = L - C / 2

根据色调区间值,rgb值将映射到CX0

上图中的公式是两种方式计算rgb值,但原理是一样的。在色相圆环上,每隔60°对应一个主色区,从色相盘中可以得知每60°扇区对应的主色:

对应的关系是:

0   <= H && H < 60  ❯❯❯ R' = C, G' = X, B' = 0
60  <= H && H < 120 ❯❯❯ R' = X, G' = C, B' = 0
120 <= H && H < 180 ❯❯❯ R' = 0, G' = C, B' = X
180 <= H && H < 240 ❯❯❯ R' = 0, G' = X, B' = C
240 <= H && H < 300 ❯❯❯ R' = X, G' = 0, B' = C
300 <= H && H < 360 ❯❯❯ R' = C, G' = 0, B' = X

R = (R' + m) * 255
G = (G' + m) * 255
B = (B' + m) * 255

对于上面的公式,如果用JavaScript来转述的话,大致如下:

if (0 <= h && h < 60) {
    // ❯❯❯ R' = C, G' = X, B' = 0
    r = c
    g = x
    b = 0
} else if (60 <= h && h < 120) {
    // ❯❯❯ R' = X, G' = C, B' = 0
    r = x
    g = c
    b = 0
} else if (120 <= h && h < 180) {
    // ❯❯❯ R' = 0, G' = C, B' = X
    r = 0
    g = c
    b = x
} else if (180 <= h && h < 240) {
    // ❯❯❯ R' = 0, G' = X, B' = C
    r = 0
    g = x
    b = c
} else if (240 <= h && h < 300) {
    // ❯❯❯ R' = X, G' = 0, B' = C
    r = x
    g = 0
    b = c
} else if (300 <= h && h < 360) {
    // ❯❯❯ R' = C, G' = 0, B' = X
    r = c
    g = 0
    b = x
}

r = Math.round((r + m) * 255)
g = Math.round((g + m) * 255)
b = Math.round((b + m) * 255)

第二种描述方式将色相盘用1 ~ 6来表示,其中1 ❯ 60°2 ❯ 120°3 ❯ 180°4 ❯ 240°5 ❯ 300°6 ❯ 360°

注意,360°对应于色盘上的同一主色区

H' < 1 ❯❯❯ R₁ = C, G₁ = X, B₁ = 0
H' < 2 ❯❯❯ R₁ = X, G₁ = C, B₁ = 0
H' < 3 ❯❯❯ R₁ = 0, G₁ = C, B₁ = X
H' < 4 ❯❯❯ R₁ = 0, G₁ = X, B₁ = C
H' < 5 ❯❯❯ R₁ = X, G₁ = 0, B₁ = C
H' < 6 ❯❯❯ R₁ = C, G₁ = 0, B₁ = X

R = R₁ + m
G = G₁ + m
B = B₁ + m

对应的JavaScript代码:

if (H' <= 1) {
    r = c
    g = x
    b = 0
} else if (H' <= 2) {
    r = x
    g = c
    b = 0
} else if (H' <= 3) {
    r = 0
    g = c
    b = x
} else if (H' <= 4) {
    r = 0
    g = x
    b = c
} else if (H' <= 5) {
    r = x
    g = 0
    b = c
} else if (H' <= 6) {
    r = c
    g = 0
    b = x
}

r = Math.round(r + m)
g = Math.round(g + m)
b = Math.round(b + m)

相比较而言,我更喜欢第一种方式,能更为直观的和色相盘上的度数匹配起来。现在可以把HSLToRGB()函数的代码完善起来:

/**
* HSL 颜色转换成RGB颜色 函数
* @param {number} h - H通道值
* @param {string} s - S通道值
* @param {string} l - L通道值
*/
function HSLToRGB(h, s, l) {
    // s, l ∈ [0,1]
    // 移除%符,将值转换为0~1之间
    s = s.replace('%','') / 100
    l = l.replace('%','') / 100

    // 计算色度c的值
    // ❯❯❯ C = (1 - | 2l - 1 |) * s
    let c = (1 - Math.abs(2 * l - 1)) * s

    // 计算x值,用作中间值
    // ❯❯❯ X = C * (1 - | H' mod 2 - 1 |)
    // ❯❯❯ H' = H / 60 ❯❯❯ C * (1 - | (H / 60) mod 2 - 1 |)
    let x = c * (1 - Math.abs((h / 60) % 2 - 1))

    // 计算m值,用于调整亮度值
    // ❯❯❯ m = L - C / 2
    let m = l - c / 2

    // 初始化r, g, b的值为0
    let r = 0, g = 0, b = 0

    // 根据色相h的颜色区域,计算出r, g, b的值
    if (0 <= h && h < 60) {
        // ❯❯❯ R' = C, G' = X, B' = 0
        r = c
        g = x
        b = 0
    } else if (60 <= h && h < 120) {
        // ❯❯❯ R' = X, G' = C, B' = 0
        r = x
        g = c
        b = 0
    } else if (120 <= h && h < 180) {
        // ❯❯❯ R' = 0, G' = C, B' = X
        r = 0
        g = c
        b = x
    } else if (180 <= h && h < 240) {
        // ❯❯❯ R' = 0, G' = X, B' = C
        r = 0
        g = x
        b = c
    } else if (240 <= h && h < 300) {
        // ❯❯❯ R' = X, G' = 0, B' = C
        r = x
        g = 0
        b = c
    } else if (300 <= h && h < 360) {
        // ❯❯❯ R' = C, G' = 0, B' = X
        r = c
        g = 0
        b = x
    }

    // 计算出最终的R、G、B通道的值
    // 每个通道加上m值,然后乘以255,再将值作四舍五入
    // ❯❯❯ R = (R' + m) * 255
    // ❯❯❯ G = (G' + m) * 255
    // ❯❯❯ B = (B' + m) * 255
    r = Math.round((r + m) * 255)
    g = Math.round((g + m) * 255)
    b = Math.round((b + m) * 255)

    // 输出RGB颜色:rgb(r g b) || rgb(r, g, b)
    return `rgb(${r} ${g} ${b})`
}

HSLToRGB(60, '50%', '100%')  // ❯❯❯ "rgb(255 255 255)"
HSLToRGB(120, '85%', '50%')  // ❯❯❯ "rgb(19 236 19)"
HSLToRGB(120, '100%', '20%') // ❯❯❯ "rgb(0 102 0)"

和前面的一样,如果传给HSLToRGB()函数的参数值是个hsl(h, s, l)字符串的话,需要对传入的值做一下相关的处理;

/**
* HSL 颜色转换成RGB颜色 函数
* @param {string} hsl - <hsl-color>
*/
function HSLToRGB(hsl) {
    let sep = hsl.indexOf(',') > -1 ? ',' : ' '
    hsl = hsl.substr(4).split(')')[0].split(sep)

    let h = hsl[0]
    let s = hsl[1].replace('%', '') / 100
    let l = hsl[2].replace('%', '') / 100

    // 计算色度c的值
    // ❯❯❯ C = (1 - | 2l - 1 |) * s
    let c = (1 - Math.abs(2 * l - 1)) * s

    // 计算x值,用作中间值
    // ❯❯❯ X = C * (1 - | H' mod 2 - 1 |)
    // ❯❯❯ H' = H / 60 ❯❯❯ C * (1 - | (H / 60) mod 2 - 1 |)
    let x = c * (1 - Math.abs((h / 60) % 2 - 1))

    // 计算m值,用于调整亮度值
    // ❯❯❯ m = L - C / 2
    let m = l - c / 2

    // 初始化r, g, b的值为0
    let r = 0, g = 0, b = 0

    // 根据色相h的颜色区域,计算出r, g, b的值
    if (0 <= h && h < 60) {
        // ❯❯❯ R' = C, G' = X, B' = 0
        r = c
        g = x
        b = 0
    } else if (60 <= h && h < 120) {
        // ❯❯❯ R' = X, G' = C, B' = 0
        r = x
        g = c
        b = 0
    } else if (120 <= h && h < 180) {
        // ❯❯❯ R' = 0, G' = C, B' = X
        r = 0
        g = c
        b = x
    } else if (180 <= h && h < 240) {
        // ❯❯❯ R' = 0, G' = X, B' = C
        r = 0
        g = x
        b = c
    } else if (240 <= h && h < 300) {
        // ❯❯❯ R' = X, G' = 0, B' = C
        r = x
        g = 0
        b = c
    } else if (300 <= h && h < 360) {
        // ❯❯❯ R' = C, G' = 0, B' = X
        r = c
        g = 0
        b = x
    }

    // 计算出最终的R、G、B通道的值
    // 每个通道加上m值,然后乘以255,再将值作四舍五入
    // ❯❯❯ R = (R' + m) * 255
    // ❯❯❯ G = (G' + m) * 255
    // ❯❯❯ B = (B' + m) * 255
    r = Math.round((r + m) * 255)
    g = Math.round((g + m) * 255)
    b = Math.round((b + m) * 255)

    // 输出RGB颜色:rgb(r g b) || rgb(r, g, b)
    return `rgb(${r} ${g} ${b})`
}

console.log(HSLToRGB("hsl(60, 60%, 100%)"));  // ❯❯❯ "rgb(255 255 255)"
console.log(HSLToRGB("hsl(120, 85%, 50%)"));  // ❯❯❯ "rgb(19 236 19)"
console.log(HSLToRGB("hsl(120, 100%, 20%)")); // ❯❯❯ "rgb(0 102 0)"

HSL颜色模式中的H,除了可以是deg单位值之外,还可以是radturn等单位。如果有必要的话,可以在HSLToRGB()函数中对H色相中的单位做相关处理:

/**
* HSL 颜色转换成RGB颜色 函数
* @param {string} hsl - <hsl-color>
*/
function HSLToRGB(hsl) {
    let sep = hsl.indexOf(',') > -1 ? ',' : ' '
    hsl = hsl.substr(4).split(')')[0].split(sep)

    let h = hsl[0]
    let s = hsl[1].replace('%', '') / 100
    let l = hsl[2].replace('%', '') / 100

    // 对h中的单位做转换处理
    if (h.indexOf('deg') > -1) {
        h = h.replace('deg', '')
    } else if (h.indexOf('rad') > -1) {
        h = Math.round(h.replace('rad', '') * (180 / Math.PI))
    } else if (h.indexOf('turn') > -1) {
        h = Math.round(h.replace('turn', '') * 360)
    }

    if (h > 360) {
        h %= 360
    }

    // 计算色度c的值
    // ❯❯❯ C = (1 - | 2l - 1 |) * s
    let c = (1 - Math.abs(2 * l - 1)) * s

    // 计算x值,用作中间值
    // ❯❯❯ X = C * (1 - | H' mod 2 - 1 |)
    // ❯❯❯ H' = H / 60 ❯❯❯ C * (1 - | (H / 60) mod 2 - 1 |)
    let x = c * (1 - Math.abs((h / 60) % 2 - 1))

    // 计算m值,用于调整亮度值
    // ❯❯❯ m = L - C / 2
    let m = l - c / 2

    // 初始化r, g, b的值为0
    let r = 0, g = 0, b = 0

    // 根据色相h的颜色区域,计算出r, g, b的值
    if (0 <= h && h < 60) {
        // ❯❯❯ R' = C, G' = X, B' = 0
        r = c
        g = x
        b = 0
    } else if (60 <= h && h < 120) {
        // ❯❯❯ R' = X, G' = C, B' = 0
        r = x
        g = c
        b = 0
    } else if (120 <= h && h < 180) {
        // ❯❯❯ R' = 0, G' = C, B' = X
        r = 0
        g = c
        b = x
    } else if (180 <= h && h < 240) {
        // ❯❯❯ R' = 0, G' = X, B' = C
        r = 0
        g = x
        b = c
    } else if (240 <= h && h < 300) {
        // ❯❯❯ R' = X, G' = 0, B' = C
        r = x
        g = 0
        b = c
    } else if (300 <= h && h < 360) {
        // ❯❯❯ R' = C, G' = 0, B' = X
        r = c
        g = 0
        b = x
    }

    // 计算出最终的R、G、B通道的值
    // 每个通道加上m值,然后乘以255,再将值作四舍五入
    // ❯❯❯ R = (R' + m) * 255
    // ❯❯❯ G = (G' + m) * 255
    // ❯❯❯ B = (B' + m) * 255
    r = Math.round((r + m) * 255)
    g = Math.round((g + m) * 255)
    b = Math.round((b + m) * 255)

    // 输出RGB颜色:rgb(r g b) || rgb(r, g, b)
    return `rgb(${r} ${g} ${b})`
}

console.log(HSLToRGB('hsl(180 100% 50%)'))     // ❯❯❯ "rgb(0 255 255)"
console.log(HSLToRGB('hsl(180deg,100%,50%)'))  // ❯❯❯ "rgb(0 255 255)"
console.log(HSLToRGB('hsl(180deg 100% 50%)'))  // ❯❯❯ "rgb(0 255 255)"
console.log(HSLToRGB('hsl(3.14rad,100%,50%)')) // ❯❯❯ "rgb(0 255 255)"
console.log(HSLToRGB('hsl(3.14rad 100% 50%)')) // ❯❯❯ "rgb(0 255 255)"
console.log(HSLToRGB('hsl(0.5turn,100%,50%)')) // ❯❯❯ "rgb(0 255 255)"

RGBA和HSLA互转

有了RGBToHSL(rgb)HSLToRGB(hsl)两个函数之后,要实现RGBAHSLA两种颜色的互转不是件难事,基本可以直接使用RGBToHSL(rgb)HSLToRGB(hsl)两个函数的代码。只不过是在处理rgba字符串和hsla两个字符串参数做一些修改。

这里新创建两个函数RGBAToHSLA(rgba)HSLAToRGBA(hsla)。为了节约篇幅,直接在下面的示例中查阅最终代码:

RGB和HSV互转

HSLHSV较为相似。在RGBHSV的时候,其中H的计算和RGBHSL中一样,不同的只时SV的转换有所差异。即计算公式不同:

V = M
C = 0 ❯❯❯ S = 0
C != 0 ❯❯❯ S = C / V ❯❯❯ C / M

我们创建一个RGBToHSV()函数,并且给这个函数传一个rgb()颜色值的字符串:

/**
* RGB 颜色转 HSV颜色函数
* @param {number} rgb - <rgb-color> 
*/
function RGBToHSV(rgb) {
    let sep = rgb.indexOf(",") > -1 ? "," : " ";
    rgb = rgb.substr(4).split(")")[0].split(sep);

    for (let R in rgb) {
    let r = rgb[R];
    if (r.indexOf("%") > -1)
        rgb[R] = Math.round(r.substr(0,r.length - 1) / 100 * 255);
    }

    // 颜色的红、绿和蓝坐标,它们的值是在0 ~ 1之间的实数
    let r = rgb[0] / 255
    let g = rgb[1] / 255
    let b = rgb[2] / 255

    // 找出最大值和最小值,并计算出它们之间的差值
    let M = Math.max(r, g, b) // 最大值
    let m = Math.min(r, g, b) // 最小值
    let delta = M - m         // 最大值 M 和最小值 m 之间的差值

    // 初始值HSL的通道h, s, v初始值为0
    let h = 0, s = 0, v = 0

    // 计算色相h

    // 如果C = 0 即 M = m, 没有差别,这个时候h=0
    if (delta == 0) {
        h = 0
    } else if (M == r) {
        // r = M (红色通道r是最大值)
        // M = r ❯❯❯ H' = ((g - b) / C) % 6 
        h = ((g - b) / delta) % 6
    } else if (M == g) {
        // g = M (绿色通道g是最大值)
        // M = g ❯❯❯ H' = ((b - r) / C) + 2
        h = ((b - r) / delta) + 2
    } else {
        // b = M (蓝色通道b是最大值)
        // M = b ❯❯❯ H' = ((r - g) / C) + 4
        h = ((r - g) / delta) + 4
    }

    // 最终色相的值 H = H' x 60°
    h = Math.round(h * 60)

    // 计算出来的色相值h,如果小于0的话,需要做一下处理
    if (h < 0) {
        h += 360
    }

    // 计算亮度V
    v = M

    // 计算饱和度s
    s = delta == 0 ? 0 : delta / M

    // 将s和l转换成百分比值
    s = +(s * 100).toFixed(1)
    v = +(v * 100).toFixed(1)

    return `hsv(${h} ${s}% ${v}%)`
}

console.log(RGBToHSV('rgb(255, 122, 90)'))   // ❯❯❯ "hsv(12 65% 100%)"
console.log(RGBToHSV('rgb(55, 122, 190)'))   // ❯❯❯ "hsv(210 71% 75%)"
console.log(RGBToHSV('rgb(155, 122, 220)'))  // ❯❯❯ "hsv(260 45% 86%)"
console.log(RGBToHSV('rgb(50, 222, 102)'))   // ❯❯❯ "hsv(138 77% 87%)"

HSV又称为HSB,目前还没有主流浏览器支持,但在设计软件中可以看到HSB颜色模式

我们可以通过像Sketch这样的设计软件来验证,RGBToHSV(rgb)函数是否成功:

HSV转成RGB时,在计算C公式不同:

C = V x S

对应的m也有相应的影响:

m = V - C

其他的公式和RGBHSL是相同的:

/**
* HSV 颜色转换成RGB颜色 函数
* @param {string} hsv - <hsv-color>
*/
function HSVToRGB(hsv) {
    let sep = hsv.indexOf(',') > -1 ? ',' : ' '
    hsv = hsl.substr(4).split(')')[0].split(sep)

    let h = hsv[0]
    let s = hsv[1].replace('%', '') / 100
    let v = hsv[2].replace('%', '') / 100

    // 对h中的单位做转换处理
    if (h.indexOf('deg') > -1) {
        h = h.replace('deg', '')
    } else if (h.indexOf('rad') > -1) {
        h = Math.round(h.replace('rad', '') * (180 / Math.PI))
    } else if (h.indexOf('turn') > -1) {
        h = Math.round(h.replace('turn', '') * 360)
    }

    if (h > 360) {
        h %= 360
    }

    // 计算色度c的值
    // ❯❯❯ C = V * S
    let c = v * s

    // 计算x值,用作中间值
    // ❯❯❯ X = C * (1 - | H' mod 2 - 1 |)
    // ❯❯❯ H' = H / 60 ❯❯❯ C * (1 - | (H / 60) mod 2 - 1 |)
    let x = c * (1 - Math.abs((h / 60) % 2 - 1))

    // 计算m值,用于调整亮度值
    // ❯❯❯ m = v - c
    let m = v - c

    // 初始化r, g, b的值为0
    let r = 0, g = 0, b = 0

    // 根据色相h的颜色区域,计算出r, g, b的值
    if (0 <= h && h < 60) {
        // ❯❯❯ R' = C, G' = X, B' = 0
        r = c
        g = x
        b = 0
    } else if (60 <= h && h < 120) {
        // ❯❯❯ R' = X, G' = C, B' = 0
        r = x
        g = c
        b = 0
    } else if (120 <= h && h < 180) {
        // ❯❯❯ R' = 0, G' = C, B' = X
        r = 0
        g = c
        b = x
    } else if (180 <= h && h < 240) {
        // ❯❯❯ R' = 0, G' = X, B' = C
        r = 0
        g = x
        b = c
    } else if (240 <= h && h < 300) {
        // ❯❯❯ R' = X, G' = 0, B' = C
        r = x
        g = 0
        b = c
    } else if (300 <= h && h < 360) {
        // ❯❯❯ R' = C, G' = 0, B' = X
        r = c
        g = 0
        b = x
    }

    // 计算出最终的R、G、B通道的值
    // 每个通道加上m值,然后乘以255,再将值作四舍五入
    // ❯❯❯ R = (R' + m) * 255
    // ❯❯❯ G = (G' + m) * 255
    // ❯❯❯ B = (B' + m) * 255
    r = Math.round((r + m) * 255)
    g = Math.round((g + m) * 255)
    b = Math.round((b + m) * 255)

    // 输出RGB颜色:rgb(r g b) || rgb(r, g, b)
    return `rgb(${r} ${g} ${b})`
}

console.log(HSVToRGB('hsv(180 100% 50%)'))     // ❯❯❯ "rgb(0 128 128)"
console.log(HSVToRGB('hsv(180deg,100%,50%)'))  // ❯❯❯ "rgb(0 128 128)"
console.log(HSVToRGB('hsv(3.14rad,100%,50%)')) // ❯❯❯ "rgb(0 128 128)"
console.log(HSVToRGB('hsv(3.14rad 100% 50%)')) // ❯❯❯ "rgb(0 128 128)"
console.log(HSVToRGB('hsv(0.5turn,100%,50%)')) // ❯❯❯ "rgb(0 128 128)"

#RRGGBB和HSL/HSV互转

有了前面的基础,我们要实现#RRGGBBHSL(或HSV)的互转就轻易地多了(并没有我们想象的那么复杂)。为什么这么说呢?就拿#RRGGBBHSL来说吧,只需要先将#RRGGBB转成RGB,然后将RGB转成HSL。就是这么的简单。比如,我们有一个#f908ac这样的十六进制颜色:

const color = '#f908ac'

// 使用HexToRGB(hex)函数将其值转为rgb颜色
const rgbColor = HexToRGB(color)

console.log(rgbColor) // ❯❯❯ "rgb(249 8 172)"

// 再使用RGBToHSL(rgb)函数将其转换为hsl颜色
const hslColor = RGBToHSL(rgbColor)

console.log(hslColor) // ❯❯❯ "hsl(319 95.3% 50.4%)"

同样的,HSL#RRGGBB也类似,不同的是先用HSLToRGB(hsl)HSL颜色转换成RGB,然后再用RGBToHex(rgb)函数将RGB颜色转换成#RRGGBB

RGB和HCL互转

HCLRGBHSL或者HSV/B都有所不同,因为他属于另一个色彩空间。在RGB转成HCL有可能两者不一定能完全匹配。具体的表现是:虽然转换后颜色的色相H和感知明度Luma都存在,但颜色的色距不一定在HCL色彩空间范围内

将一个RGB颜色转换成HCL颜色(在CSS中用lch()描述的颜色)需要经历好几个过程:

sRGB ❯❯❯ linearRGB ❯❯❯ CIEXyz ❯❯❯ CIELab ❯❯❯ CIELch

这里会涉及到很多转换公式,具体的可以查阅 CSS Color Module Level 4第15小节

对于这一部分,感兴趣的同学可以阅读相应的代码,这里不做过多阐述。

小结

在《CSS 颜色》一文主要介绍了CSS中颜色表示方式,在新的规范中提供新的语法规则。但在实际中,我们有的时候需要通过JavaScript对颜色格式进行转换。那么在这篇文章中,主要介绍了不同格式之间是如何转换的。

在使用JavaScript对颜色模式进行转换时,会涉及到很多数学公式,这是比较烧脑的部分,得多费点时间。时至今日,虽然描述同一种颜色有很多种格式,其中RGB模式可以很好的和其他颜色模式互通,也就是说,通过RGB做为桥梁,你可以对任意之间的颜色模式做为互转,比如文章中提到的#RRGGBBHSL互转,就是这么操作的。

最后希望这篇文章对大家有所帮助,如果你在这方面有更多的经验,欢迎在下面的评论中与我们共享。

返回顶部