如何實現一個顏色選擇器

在開發公司UI組件庫的過程當中,顏色組件ColorPicker因爲時間關係沒有去深刻研究,看着花花綠綠的色譜圖,覺得實現起來會很複雜,就直接將一個開源的顏色選擇器封裝了一下。這大概是半年之前的事了,這篇文章也在個人博客中以草稿形式(只有標題沒有內容,當時是作了一個記錄,想着後來有時間了去研究,目前這種草稿還有不少😂)存放了半年了。前段時間請教了公司UI同事一些顏色相關的概念,又去搜索了下這方面的知識,收穫仍是蠻大的,尤爲是知乎色彩空間中的 HSL、HSV、HSB 有什麼區別?這篇提問中,@Forrest近乎大白話的回答,超讚👍。javascript


在瞭解了相關概念以後,再去審視這個組件的話,就不會有那種陌生行業的抵觸感了。最終也實現了這個這個組件,DEMO,並將實現過程記錄以下:php

【解析Photoshop調色器】小章節是基於維基百科和知乎文章對一些顏色概念的總結。開發視角總結的可能並不太專業,瞭解便可。css

解析Photoshop調色器

PhotoShop拾色器

HSL、HSV、HSB的區別

做爲前端,咱們瞭解HEX,瞭解RGB,但對於HSL、HSV、HSB這幾個顏色概念每每是模糊不清的。如上圖Photoshop的調色器,其中下部分別有HSBRGB等幾個輸入框,RGB分別表明紅綠藍三原色的顏色通道,經過對這三個顏色通道的變化以及相互之間的疊加能夠獲得各類不一樣的顏色,這是咱們所熟知的。那麼HSB又是什麼呢?html

事實上,HSBRGB同樣,也是一種顏色模式,其中H(hue)表示色相(什麼顏色),S(saturation)表示飽和度(顏色的純度),B(brightness)表示明度亮度(顏色的明亮程度)。相較於RGB模式,HSB顏色模式更加人性化,它定義了顏色「是什麼顏色?顏色豔不豔?顏色亮不亮?」。前端

什麼是色相

色相就是在不一樣的光照下,人眼所感受不一樣的顏色。 」赤橙黃綠青藍紫,誰持彩練當空舞?「,偉人的詩詞早就描繪過了。 在HSV/HSL色彩模式下, 色相是以紅色爲0度(360度),黃色爲60度,綠色爲120度,青色爲180度,藍色爲240度,品紅色爲300度的表現,其值範圍爲0 - 360度,以下圖: java

色相

什麼是飽和度

飽和度是指顏色的強度或純度。飽和度表示色相中彩色成分所佔的比例,用從 0 (透明) ~ 100% (徹底飽和)的百分比來度量。純度是色彩感受強弱的標誌,純度高的顏色人眼看上去顯得比較鮮豔。git

什麼是明度

明度是顏色的相對明暗程度。一般是從 0 (黑) ~ 100% (白)的百分比來度量的。github


一樣的,HSLHSVHSB同樣,都是基於色相、飽和度、明度三方面對色彩的解釋。其中HSB 和 HSV 是同一個東西,只是叫法上不一樣。後面咱們統一使用HSV來表示。正則表達式

對三種顏色模式的區別,知乎上(文末附連接)有個很淺顯易懂的回答:算法

在原理和表現上,HSL 和 HSB 中的 H(色相) 徹底一致,但兩者的 S(飽和度)不同, L 和 B (明度 )也不同:

  • HSB 中的 S 控制純色中混入白色的量,值越大,白色越少,顏色越純;
  • HSB 中的 B 控制純色中混入黑色的量,值越大,黑色越少,明度越高
  • HSL 中的 S 和黑白沒有關係,飽和度不控制顏色中混入黑白的多寡;
  • HSL 中的 L 控制純色中的混入的黑白兩種顏色。

HSV的三個值表達範圍分別爲: H [0-360] float S [0-1] float V [0-1] float

上述概念,可能並不太專業,瞭解便可。 用程序思惟來解釋上述概念的話,就是說咱們在一個顏色中混入不一樣程度的黑和白就能變爲另一種顏色值,以下圖:

PhotoShop拾色器
也就是說,拋開 RGB三個顏色通道以外,還能夠經過色相 H、飽和度 S、明度 V另外三個通道來表達顏色。

實現流程

color-picker效果圖
如上效果圖,就是最終要實現的顏色選擇器的效果。整個佈局由飽和度/明度面板、色相面板、透明度面板三個元件組成,三個面板都是滑塊模型,其中飽和度/明度面板上的滑塊能夠在其面板上自由滑動,而色相和透明度面板上的滑塊僅容許左右滑動。滑塊滑動最終產生的座標值將是用來計算 HSV值的依據。 「夫君在手,天下我有」。基於上述概念和思路,咱們只須要經過座標計算出色相 H、飽和度 S、明度 V三個值,就能夠將其轉換爲指定格式的顏色。
流程圖

  • 飽和度/明度面板由飽和度和明度兩層組成,滑塊座標值的Left值用來計算飽和度,Top值用來計算明度;
  • 色相面板的滑塊座標值的Left值用來計算色相值;
  • 透明度面板的滑塊座標值的Left值用來計算透明度,透明度做爲顏色的一種補充,和顏色自己沒有任何關係;
  • 最終產生的HSVA四個值將經過不一樣算法轉換成不一樣的顏色格式;

飽和度/亮度面板的實現

通俗點來講,飽和度就是在一個純色中「混入」了百分之多少的白色,明度就是在一個純色中「混入」了百分之多少的黑色;這些量都用百分比來表示,其值範圍在0% ~ 100%

飽和度是從左往右,混入的白色越少,表示顏色的純度越高。也就是在面板最左邊混入的白色達到純白的峯值,而最右邊混入的白色是接近於無的透明,這就是一條從左往右,由白色到透明的徑向漸變;一樣的道理能夠得出,明度能夠由一條從下往上,由純黑到徹底透明的徑向漸變表示。因爲這兩條漸變大部分區域是透明的,所以,當在這兩層的下方設置赤橙黃綠青藍紫等不一樣色相時,整個區域就會造成一個色譜。以下圖所示:

飽和度/亮度面板的實現原理
爲了更方便理解,我錄製了一個小視頻,經過調整面板元素的背景色,能夠更清晰的表現出這兩條徑向漸變覆蓋在一個純色背景色上產生的效果。 在清楚了色譜的造成過程以後,飽和度/亮度面板的實現就變得容易多了,爲了簡化結構,直接使用 ::before/::after僞元素來實現飽和度層和明度層。

佈局
<div class="mo-color-sat-val">
  <!-- 飽和度漸變元素 -->
  ::before
  <!-- 滑塊 -->
  <div class="mo-color-thumb" role="slider" tabindex="0">
    <span></span>
  </div>
  <!-- 明度漸變元素 -->
  ::after
</div>
複製代碼
.mo-color-sat-val {
  position: relative;
  // 色相將直接做用於面板的背景色上
  background: transparent;

  &::before,
  &::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }

  // 飽和度 一條從左往右,由純白到透明的徑向漸變
  &::before {
    background: linear-gradient(to right, white, transparent);
  }

  // 明度 一條從下往上,由純黑到透明的徑向漸變
  &::after {
    background: linear-gradient(to top, black, transparent);
  }
}
複製代碼
經過座標計算S值和V值

當滑塊在面板上自由滑動時,滑塊所在的位置就是計算S值和V值所須要的座標值。如何滑動和計算座標不是本文的重點,能夠直接參考源碼,我已將其封裝爲一個Draggable類,該類會在滑動過程當中產生一個座標,而且經過一系列鉤子函數返回實例。

// 初始化$sat面板的Draggable類
states.satDragIns = new Draggable(states.$sat, {
  drag: satDrag,
  end: satDrag
})

function satDrag (coordinate: Coordinate) {
  // satWidth 面板的寬度
  // satHeight 面板的高度
  // 飽和度和明度的範圍都是從 0 - 100 的一個百分比值,爲了便於計算,這裏直接保存爲小數, 座標原點是左下角
  const saturation = Math.round(coordinate.left / satWidth * 100) / 100
  const value = Math.round((1 - coordinate.top / satHeight) * 100) / 100
}

複製代碼
色相改變時重置面板背景色

飽和度/亮度面板的最後一環就是當色相改變時,要去設置面板的背景色,以便造成最終的色譜(如上面視頻效果)。只須要將色相改變後生成的H值(參考色相的實現)生成一個hsl格式的顏色便可:

states.$sat.style.background = `hsl(${h}, 100%, 50%)`
複製代碼

色相的實現

色相
能夠看出,HSV色彩空間下,色相條由紅色、黃色、綠色、青色、藍色、品紅色六中顏色漸變過渡造成,最終再由平紅色過渡到紅色閉環。將其還原成環狀可能更容易理解(黑色小點表示角度):
色相環
那麼,只要計算出每一個顏色的角度就能夠實現這個漸變條,漸變從紅色開始(0度),從紅色閉環(360度);中間5個色值依次平分角度:

0 deg: 紅色
1/6 deg: 黃色
2/6 deg: 綠色
...
5/6 deg: 品紅色
360 deg: 紅色
複製代碼
佈局

轉換成最終的CSS代碼以下:

// 色相
.mo-color-hue>.mo-color-rail {
  background: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%);
}
複製代碼
<div class="mo-color-hue">
  <!-- 漸變條 -->
  <div class="mo-color-rail"></div>
  <!-- 滑塊 -->
  <div class="mo-color-thumb" role="slider" tabindex="0">
    <span></span>
  </div>
</div>
複製代碼
經過座標計算H值
// 經過left座標和hue元素寬度計算出比例值,而後乘以360度計算出當前位置所處的度數
hue = Math.round((left / hueWidth) * 360 * 100) / 100
複製代碼

透明度的實現

透明度面板的實現是最簡單的,格子背景一樣使用CSS漸變來生成,具體能夠參考CSS大神張鑫旭的這篇文章《CSS屆的繪圖板CSS Paint API簡介》。

佈局
<!-- 格子背景直接做用在容器上 -->
<div class="mo-color-alpha">
  <!-- 顏色背景層 -->
  <div class="mo-color-rail"></div>
  <div class="mo-color-thumb" role="slider" tabindex="0">
    <span></span>
  </div>
</div>
複製代碼
.mo-color-alpha {
  background-color: white;
  background-image: linear-gradient(45deg, #c5c5c5 25%, transparent 0, transparent 75%, #c5c5c5 0, #c5c5c5), linear-gradient(45deg, #c5c5c5 25%, transparent 0, transparent 75%, #c5c5c5 0, #c5c5c5);
  background-size: 10px 10px;
  background-position: 0 0, 5px 5px;
}
複製代碼
經過座標計算透明度值
alpha = Math.round(left / alpWidth * 100) / 100
複製代碼
色相改變時更改顏色層的背景色
const hsl = hsv2hsl(h, s, v)
states.$alpRail.style.background = `linear-gradient(to right, transparent, hsl(${hsl.h}, ${hsl.s * 100}%, ${hsl.l * 100}%))`
複製代碼

如上,咱們獲得了生成一個顏色所必需的HSV三個重要的參數。有了這三個參數,就能夠經過顏色轉換算法,如hsv2rgb等,將hsv三個參數轉換爲不一樣格式的顏色值,至於轉換算法,本文就再也不展開,這個網站詳細的羅列了各類色彩的轉換算法,能夠參考。

小技巧:如何驗證一個顏色是否有效

不可忽略的是,顏色選擇器可能會接收來自用戶的傳入值,而且ColorPicker組件自己也內置了setValue方法,咱們沒法保證用戶傳入的是不是一個有效的顏色值,那麼如何去驗證這個顏色是否有效呢?起初我考慮用正則表達式去校驗,但因爲又會涉及到如傳入rgb(260, 260, 260)這種某個通道超出範圍的值,校驗起來比較困難。而後我在Chrome瀏覽器中給某個元素設置了一個不規範的顏色,發現Chrome將這個顏色自動降級爲白色了,那麼,若是將一個錯誤顏色賦值給一個DOM的color樣式,而後再獲取這個color,再去驗證獲取到的color和傳入的color是否一致是否能夠作爲校驗的依據呢?Chrome瀏覽器經過了這個小測驗,其餘瀏覽器沒有測試。

測試用例較少,該技巧可能存在問題。

/** * 校驗顏色是否合法 * 測試用例較少,該技巧可能存在問題 * * 原理,若是顏色不合法,將會被轉換爲rgb(255,255,255) * 只需驗證設置後的顏色是否等於傳入的顏色 * @param color */
function checkColor(color: string) {
  // todo
  const style = new Option().style
  style.color = color
  return style.color === color 
}

// 只負責將傳入的值轉換爲 h, s, v 三個通道值,而不去校驗值是否超出範圍或有效
function parseColor(color) {
  // hex2hsv
  if (HEX_REG.test(color)) {}
  // rgb2hsv
  if (RGB_REG.test(color)) {}
  // ...

  return {
    h,
    s,
    v,
    a
  }
}

setValue (color) {
  const { h, s, v, a } = parseColor(color)
  const {r, g, b} = hsv2rgb(h, s, v,)
  if (checkColor(`rgb(${r},${g},${b})`)) {
    // todo
  }
}

複製代碼

源碼

本文首發於個人博客

參考文獻

相關文章
相關標籤/搜索