自己动手写个颜色类库:掌握JS中的位运算符

从最近写的一个图表库中单独抽象出来了颜色类库,功能包括HEX、RGB/RGBA以及HSL/HSLA各种色值的转换以及颜色明暗变化。
在编写的过程中,涉及到了JS中的各种位运算符,对16进制色值的处理不再是循环遍历了。只对位运算符感兴趣的建议直接阅读目录中的“HEX色值的快速转换”。

先上两张图,循环了1600个div,分别设置颜色的渐变和随机。虽然现在css中对颜色的处理方法越来越丰富,但在一些场景——例如可视化图表中我们还是需要用JS来控制颜色。
《自己动手写个颜色类库:掌握JS中的位运算符》
《自己动手写个颜色类库:掌握JS中的位运算符》

需求分析

  1. 将各种格式的色值进行统一,方便操作,也确保展示效果一致。

  2. 对颜色进行明暗处理,最明时为白色(#fff),最暗时为黑色(#000)。

其中颜色格式包括:

  • 3位Hex值

  • 6位Hex值

  • 整数型RGB

  • 百分比型RGB

  • 整数型RGBA

  • 百分比型RGBA

  • HSL

  • HSLA

  • 常见的颜色命名,如black

流程及接口

要实现以上的功能,流程上应该包括:

  1. 通过正则表达式检测颜色格式。

  2. 将颜色统一为一种最易操作的格式。由于我们的操作主要为明暗操作,那么RGB/RGBA格式显然是最方便的,因此将各种格式统一为RGB/RGBA。

  3. 为每个格式化后的颜色添加“变明”、“变暗”两个方法,并返回一个新的标准格式颜色对象,以便链式调用。

  4. 颜色对象还需要有一个输出颜色字符串的方法,以便在所有操作完成后输出最终的色值添加给对应的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 Rgbnew 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…

    原文作者:位运算
    原文地址: https://segmentfault.com/a/1190000006677741
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞