本身動手寫個顏色類庫:掌握JS中的位運算符

從最近寫的一個圖表庫中單獨抽象出來了顏色類庫,功能包括HEX、RGB/RGBA以及HSL/HSLA各類色值的轉換以及顏色明暗變化。
在編寫的過程當中,涉及到了JS中的各類位運算符,對16進制色值的處理再也不是循環遍歷了。只對位運算符感興趣的建議直接閱讀目錄中的「HEX色值的快速轉換」。css

先上兩張圖,循環了1600個div,分別設置顏色的漸變和隨機。雖然如今css中對顏色的處理方法愈來愈豐富,但在一些場景——例如可視化圖表中咱們仍是須要用JS來控制顏色。
clipboard.png
clipboard.pngvue

需求分析

  1. 將各類格式的色值進行統一,方便操做,也確保展現效果一致。git

  2. 對顏色進行明暗處理,最明時爲白色(#fff),最暗時爲黑色(#000)。github

其中顏色格式包括:正則表達式

  • 3位Hex值瀏覽器

  • 6位Hex值babel

  • 整數型RGBthis

  • 百分比型RGBspa

  • 整數型RGBAcode

  • 百分比型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中的一個值,即十一、2二、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...

相關文章
相關標籤/搜索