资讯专栏INFORMATION COLUMN

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

yvonne / 776人阅读

摘要:在编写的过程中,涉及到了中的各种位运算符,对进制色值的处理不再是循环遍历了。只对位运算符感兴趣的建议直接阅读目录中的色值的快速转换。通过阅读类,可以知道最终属性均为一个或构造出来的对象,接下来就具体说说类中的这些位运算符起到了什么作用。

从最近写的一个图表库中多带带抽象出来了颜色类库,功能包括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 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://www.ucloud.cn/yun/80133.html

相关文章

  • 【译】 JavaScript中按位操作符的有趣应用

    摘要:检查设定位操作符还有一些其他有用的位屏蔽应用。请注意,位掩码中的位将有效地关闭十进制数中的相应位,因为。 原文标题:Interesting use cases for JavaScript bitwise operators原文地址:https://blog.logrocket.com/in... 本文首发于公众号:符合预期的CoyPan JavaScript提供了几种运算符,可以对...

    oneasp 评论0 收藏0
  • JS魔法堂:彻底理解0.1 + 0.2 === 0.30000000000000004的背后

    摘要:也就是说不仅是会产生这种问题,只要是采用的浮点数编码方式来表示浮点数时,则会产生这类问题。到这里我们都理解只要采取的浮点数编码的语言均会出现上述问题,只是它们的标准类库已经为我们提供了解决方案而已。 Brief 一天有个朋友问我JS中计算0.7 * 180怎么会等于125.99999999998,坑也太多了吧!那时我猜测是二进制表示数值时发生round-off error所导致,但并不...

    JerryWangSAP 评论0 收藏0
  • Express 实战(二):Node.js 基础

    摘要:而通过实现名为的标准模块,完美的解决了模块导入问题。通常都被称为包管理器,而这也是它最大的特色。例如,接受请求发送响应。该模块主要处理文件相关内容,其中大多数都是文件读写功能。 在上一篇文章中,我们简单的介绍了 Node.js 。了解到它基于 JavaScript、天生异步、拥有大量的第三方类库。本文将会在之前的基础上,对 Node.js 进行更深入的介绍。其中主要内容包括: Nod...

    soasme 评论0 收藏0
  • Java 征途:行者的地图

    摘要:老实说,当时一进入世界的大门就晕了,各种规范概念和英文缩写词能把人整的晕晕乎乎。等新的英文缩写又出现了,一口老血还没来得及喷出,又重新振作开始新的学习征程。 showImg(http://upload-images.jianshu.io/upload_images/1131767-1c5d16e39435df10.jpg?imageMogr2/auto-orient/strip%7Ci...

    dkzwm 评论0 收藏0
  • 复习js 2

    摘要:复习表达式和运算符运算符拥有如下类型的运算符。例如十进制数字用二进制表示为,位运算符就是在这个二进制表示上执行运算,但是返回结果是标准的数值。因此,用于布尔值时,当任何一个操作数为则返回如果操作数都是则返回。 复习js day2 表达式和运算符 运算符 JavaScript 拥有如下类型的运算符。本节描述了运算符和运算符的优先级。 赋值运算符(Assignment operators...

    yhaolpz 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<