从最近写的一个图表库中单独抽象出来了颜色类库,功能包括HEX、RGB/RGBA以及HSL/HSLA各种色值的转换以及颜色明暗变化。
在编写的过程中,涉及到了JS中的各种位运算符,对16进制色值的处理不再是循环遍历了。只对位运算符感兴趣的建议直接阅读目录中的“HEX色值的快速转换”。
先上两张图,循环了1600个div,分别设置颜色的渐变和随机。虽然现在css中对颜色的处理方法越来越丰富,但在一些场景——例如可视化图表中我们还是需要用JS来控制颜色。
需求分析
将各种格式的色值进行统一,方便操作,也确保展示效果一致。
对颜色进行明暗处理,最明时为白色(#fff),最暗时为黑色(#000)。
其中颜色格式包括:
3位Hex值
6位Hex值
整数型RGB
百分比型RGB
整数型RGBA
百分比型RGBA
HSL
HSLA
常见的颜色命名,如black
流程及接口
要实现以上的功能,流程上应该包括:
通过正则表达式检测颜色格式。
将颜色统一为一种最易操作的格式。由于我们的操作主要为明暗操作,那么RGB/RGBA格式显然是最方便的,因此将各种格式统一为RGB/RGBA。
为每个格式化后的颜色添加“变明”、“变暗”两个方法,并返回一个新的标准格式颜色对象,以便链式调用。
颜色对象还需要有一个输出颜色字符串的方法,以便在所有操作完成后输出最终的色值添加给对应的Dom。
检测颜色格式
注意,此类库使用了部分ES6语法,如需转化为浏览器可直接使用的版本,可用babel进行转换。
检测格式时,主要依靠的是正则表达式,具体如下:
const reHex3 = /^#([0-9a-f]{3})$/
const reHex6 = /^#([0-9a-f]{6})$/
const reRgbInteger = /^rgb\(\s*([-+]?\d+)\s*,\s*([-+]?\d+)\s*,\s*([-+]?\d+)\s*\)$/
const reRgbPercent = /^rgb\(\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*\)$/
const reRgbaInteger = /^rgba\(\s*([-+]?\d+)\s*,\s*([-+]?\d+)\s*,\s*([-+]?\d+)\s*,\s*([-+]?\d+(?:\.\d+)?)\s*\)$/
const reRgbaPercent = /^rgba\(\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)\s*\)$/
const reHslPercent = /^hsl\(\s*([-+]?\d+(?:\.\d+)?)\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*\)$/
const reHslaPercent = /^hsla\(\s*([-+]?\d+(?:\.\d+)?)\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)\s*\)$/
对于已命名的颜色,则构建了一个named对象,key为颜色名称,value则为16进制色值,例如:
const named = {
aliceblue: 0xf0f8ff,
antiquewhite: 0xfaebd7,
...
yellowgreen: 0x9acd32
}
通过named.hasOwnProperty方法来检测输入的字符串是否是已命名的颜色,如果是,则用其16进制色值替换。
实际上,我创建了3个class,分别为Color、Rgb和Hsl。以上的颜色检测均放在Color的format方法中,将格式化之后的颜色放入Color的f属性里,代码如下:
class Color {
constructor () {
this.f = {}
}
format (str) {
let m
str = (str + '').trim().toLowerCase()
if (reHex3.exec(str)) {
m = parseInt(reHex3.exec(str)[1], 16)
this.f = new Rgb((m >> 8 & 0xf) | (m >> 4 & 0x0f0), (m >> 4 & 0xf) | (m & 0xf0), ((m & 0xf) << 4) | (m & 0xf), 1)
} else if (reHex6.exec(str)) {
m = parseInt(reHex6.exec(str)[1], 16)
this.f = this.rgbn(m)
} else if (reRgbInteger.exec(str)) {
m = reRgbInteger.exec(str)
this.f = new Rgb(m[1], m[2], m[3], 1)
} else if (reRgbPercent.exec(str)) {
m = reRgbPercent.exec(str)
const r = 255 / 100
this.f = new Rgb(m[1] * r, m[2] * r, m[3] * r, 1)
} else if (reRgbaInteger.exec(str)) {
m = reRgbaInteger.exec(str)
this.f = this.rgba(m[1], m[2], m[3], m[4])
} else if (reRgbaPercent.exec(str)) {
m = reRgbaPercent.exec(str)
const r = 255 / 100
this.f = this.rgba(m[1] * r, m[2] * r, m[3] * r, m[4])
} else if (reHslPercent.exec(str)) {
m = reHslPercent.exec(str)
this.f = this.hsla(m[1], m[2] / 100, m[3] / 100, 1)
} else if (reHslaPercent.exec(str)) {
m = reHslaPercent.exec(str)
this.f = this.hsla(m[1], m[2] / 100, m[3] / 100, m[4])
} else if (named.hasOwnProperty(str)) {
this.f = this.rgbn(named[str])
} else if (str === 'transparent') {
this.f = new Rgb(NaN, NaN, NaN, 0)
} else {
this.f = null
throw new Error('Invalid color format.')
}
return this.f
}
rgbn (n) {
return new Rgb(n >> 16 & 0xff, n >> 8 & 0xff, n & 0xff, 1)
}
rgba (r, g, b, a) {
if (a <= 0) r = g = b = NaN
return new Rgb(r, g, b, a)
}
hsla (h, s, l, a) {
if (a <= 0) {
h = s = l = NaN
} else if (l <= 0 || l >= 1) {
h = s = NaN
} else if (s <= 0) {
h = NaN
}
return new Hsl(h, s, l, a).rgb()
}
}
为了方便读者快速理解代码,用了大量的if / else if,实际可以用三元表达式替代,让代码更优雅紧凑。
通过阅读Color类,可以知道最终f属性均为一个 new Rgb
或 new Hsl
构造出来的对象,接下来就具体说说Color类中的这些位运算符起到了什么作用。
HEX色值的快速转换
HSL和RGB的转换没有什么黑魔法,都是查Wiki之后写的方法,大同小异,所以重点讲讲16进制色值是怎样处理的。
网上资料中,大部分的HEX转Rgb都是通过遍历字符串,将HEX色值分隔,再转化为10进制数字。但在阅读d3.js的源码后,发现还有更巧妙的处理方法。
首先补充一下HEX色值的基本概念。HEX色值可以为3位或者6位,3位可以理解为一种简写,如#123
,实际等于#112233
。
而对于一个6位的HEX色值,如#112233
,在转换为RGB时,实际是每两位对应RGB中的一个值,即11、22、33分别对应R、G、B。
>>
和&
首先以6位HEX色值为例,我们通过正则表达式取出其值后,parseInt(str, 16)转化为16进制数字,也可以通过在前面加上'0x'来达到这一效果,目的都是告诉解析器,它是一个16进制的数。依然以#112233
为例,具体看看代码:
const m = parseInt('112233', 16) // 0x112233
// 分别获取R、G、B的值
const r = m >> 16 & 0xff // 17
const g = m >> 8 & 0xff // 34
const b = m & 0xff // 51
那么>>
和&
分别起什么作用,为什么这样一操作就能直接取出对应数值呢?
>>
是JS中的右移运算符,用于将数字的二进制右移n位。对于一个16进制的数字而言,每一位数字都对应4位2进制数字,如0x112233
的二进制就是0001 0001 0010 0010 0011 0011
。
因此要取出最左端11对应的10进制数字,只需要将其右移16位,剩下左起的8位即可。
那么当我们需要取中间的22和最右端的33时该怎么办呢?这就需要用到&
。&
是JS中位的与运算,说起来有点绕口,实际就是将两端的值的二进制按位一一取与运算。
所以我们实际看看取22和33时发生了什么:
// 0x112233的二进制为0001 0001 0010 0010 0011 0011
let n = 0x112233 >> 8 // 0001 0001 0010 0010
// 将n和0xff按位与运算,0xff的二进制为1111 1111
n & 0xff // 0010 0010 也就是 0x22
n = 0x112233 & 0xff // 0011 0011 也就是 0x33
简单的说,就是通过与0xff这个二进制最右端8均为1的数与运算,从而取出目标数最右端的八位,并舍弃其余所有位数。
总的来说,就是先用>>
调整位置,再用&
筛选。
<<
和|
我们接着处理3位HEX值,以#123
为例,取出对应的R、G、B。
const m = parseInt('123', 16) // 0x123
const r = (m >> 8 & 0xf) | (m >> 4 & 0x0f0) // 17
const g = (m >> 4 & 0xf) | (m & 0xf0) // 34
const b = ((m & 0xf) << 4) | (m & 0xf) // 51
代码中出现的|
是位的或运算符,机制和&
相类似。<<
则是和>>
对应的左移运算符。
同样一步一步看看|
是怎么起到作用的:
// 0x123的二进制为0001 0010 0011
0x123 >> 8 & 0xf // 0001
0x123 >> 4 & 0x0f0 // 0001 0000
0001 | 0001 0000 // 0001 0001 也就是 0x11
0x123 >> 4 & 0xf // 0010
0x123 & 0xf0 // 0010 0000
0010 | 0010 0000 // 0010 0010 也就是 0x22
(0x123 & 0xf) << 4 // 0011 0000
0x123 & 0xf // 0011
0011 0000 | 0011 // 0011 0011 也就是 0x33
思路和6位时一样,只是增加了<<
和|
,更灵活的操作各种位运算。
剩余工作
之后要做的主要就是一些HSL转换、明暗变化以及各种错误处理,都是比较常规的做法,这里不多做赘述,有兴趣的可以看看代码:https://github.com/Yuyz0112/v...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。