你知道 react-color 的實現原理嗎
阿新 • • 發佈:2021-01-10
## 一、前言
[`ReactColor`](https://github.com/casesandberg/react-color) 是一個優秀的 React 顏色選擇器元件,官方給了多種佈局供開發者選擇。 筆者常用的主題為 Sketch,這種主題涵蓋了**顏色面板**、**推薦色塊**、**RGB顏色**輸入等功能,比較完善。但是最近在寫一個富文字編輯器,編寫過程中遇到了一些問題,比如使用者在點選推薦色塊時,編輯器會失去焦點,無法對字型顏色進行更改。如果是編輯器自有的元件,可以使用以下程式碼 ```javascript event.preventDefault(); ``` 該程式碼可以禁止瀏覽器預設行為,比如點選推薦色塊之後只將色值向上傳遞,而不改變瀏覽器當前 `focus` 狀態。但是 `ReactColor` 並沒有暴露該事件,故 clone 了原始碼,在編輯器內集成了該元件,實現功能的同時也能夠減少打包體積。
## 二、實現原理
本章節主要介紹 `ReactColor` 的實現原理,以比較有代表性的 Sketch 主題為例。 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1308183/1610244626581-dc5e3b4d-2954-46ab-a80d-1afebb370c69.png#align=left&display=inline&height=392&margin=%5Bobject%20Object%5D&name=image.png&originHeight=784&originWidth=1396&size=233488&status=done&style=none&width=698) 由上圖可以看到,整個顏色選擇器面板由這六個部分組成,分別是**亮度與飽和度調節面板**、**色相 Hue 調節面板**、**透明度調節面板**、**當前顏色的 RGBA 與 Hex 值**、**推薦色塊以及顏色實時預覽**。下面的部分就來介紹其原理實現。
### 2.1 HSV 色彩模型
與顏色相關的幾個屬性分別為亮度、飽和度、色相與透明度,與我們平時用到的 RGB 色彩模型不同,`ReactColor` 中用的是 HSV 色彩模型,其具體含義如下: ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1308183/1610245443851-acbc5ee8-e7a2-4fdd-80d2-72902a0e0726.png#align=left&display=inline&height=132&margin=%5Bobject%20Object%5D&name=image.png&originHeight=264&originWidth=736&size=17362&status=done&style=none&width=368) 下面是維基百科對 HSV 色彩模型的介紹: > HSV即[色相](https://zh.wikipedia.org/wiki/%E8%89%B2%E7%9B%B8)、[飽和度](https://zh.wikipedia.org/wiki/%E8%89%B2%E5%BA%A6_(%E8%89%B2%E5%BD%A9%E5%AD%A6))、[明度](https://zh.wikipedia.org/wiki/%E6%98%8E%E5%BA%A6)(英語:Hue, Saturation, Value),又稱HSB,其中B即英語:Brightness。
> - [色相](https://zh.wikipedia.org/wiki/%E8%89%B2%E7%9B%B8)(H)是色彩的基本屬性,就是平常所說的[顏色](https://zh.wikipedia.org/wiki/%E9%A2%9C%E8%89%B2)名稱,如[紅色](https://zh.wikipedia.org/wiki/%E7%BA%A2%E8%89%B2)、[黃色](https://zh.wikipedia.org/wiki/%E9%BB%84%E8%89%B2)等。
> - [飽和度](https://zh.wikipedia.org/wiki/%E8%89%B2%E5%BA%A6_(%E8%89%B2%E5%BD%A9%E5%AD%A6))(S)是指色彩的純度,越高色彩越純,低則逐漸變灰,取0-100%的數值。
> - [明度](https://zh.wikipedia.org/wiki/%E6%98%8E%E5%BA%A6)(V),亮度(L),取0-100%。
至於為什麼選用 HSV 色彩模型而不是直接使用 RGB,大家在使用 ReactColor 的過程中應該會發現,只要在下方的 色相 Hue 調節面板上選中了顏色,亮度與飽和度調節面板就會呈現什麼顏色。舉個例子:你選擇了黃色,那麼最上方調節面板呈現的就是黃色,差別也只是飽和度與明度不同而已。這就是使用 HSV 色彩模型的優勢,讓使用者選擇的顏色變成可預知並且方便調節的。
RGB 顏色空間利用三個顏色分量的線性組合來表示顏色,任何顏色都與這三個分量有關,而且這三個分量是高度相關的,所以連續變換顏色時並不直觀,想對影象的顏色進行調整需要更改這三個分量才行。自然環境下獲取的影象容易受自然光照、遮擋和陰影等情況的影響,即對亮度比較敏感。而 RGB 顏色空間的三個分量都與亮度密切相關,**即只要亮度改變,三個分量都會隨之相應地改變**,而沒有一種更直觀的方式來表達,而這就是 HSV 色彩模型的優勢所在。
### 2.2 HSV 轉 RGB
上面提到,在日常的前端開發過程中還是普遍使用 RGB 色彩模型進行顏色表示,在使用者設定好 HSV 值後我們需要將其轉為 RGB 值,公式如下(該公式來自[維基百科](https://zh.wikipedia.org/wiki/HSL%E5%92%8CHSV%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4#%E4%BB%8EHSV%E5%88%B0RGB%E7%9A%84%E8%BD%AC%E6%8D%A2)) $h_i = \lfloor h/60 \rfloor$ $f = h/60 - h_i$ $p = v * (1-s)$ $q = v*(1-f*s)$ $t = v * (1 - (1-f)*s)$ $$rgb=\begin{cases} (v,t,p), & \text{if $h_i=0$} \\ (q,v,p), & \text{if $h_i=1$} \\ (p,v,t), & \text{if $h_i=2$} \\ (p,q,v), & \text{if $h_i=3$} \\ (t,p,v), & \text{if $h_i=4$} \\ (v,p,q), & \text{if $h_i=5$} \\ \end{cases}$$
這樣在使用者選擇完成後就可以對色彩空間實時轉換,通過 `onChange` 回撥返回給使用者。
### 2.3 HSV 色彩模型在 ReactColor 中的實現
既然使用了 HSV 色彩模型就要考慮一下如何表示這三個變數,下面我們分兩部分來講。 #### 2.3.1 Hue 色相 | 顏色名稱 | 紅綠藍含量 | 角度 | 代表物體 | | :---: | :---: | :---: | :---: | | 紅色 | R255,G0,B0 | 0° | [血液](https://zh.wikipedia.org/wiki/%E8%A1%80%E6%B6%B2)、[草莓](https://zh.wikipedia.org/wiki/%E8%8D%89%E8%8E%93) | | 橙色 | R255,G128,B0 | 30° | [火](https://zh.wikipedia.org/wiki/%E7%81%AB)、[橙子](https://zh.wikipedia.org/wiki/%E6%A9%99%E5%AD%90) | | 黃色 | R255,G255,B0 | 60° | [香蕉](https://zh.wikipedia.org/wiki/%E9%A6%99%E8%95%89)、[杧果](https://zh.wikipedia.org/wiki/%E6%9D%A7%E6%9E%9C) | | 黃綠 | R128,G255,B0 | 90° | [檸檬](https://zh.wikipedia.org/wiki/%E6%AA%B8%E6%AA%AC) | | 綠色 | R0,G255,B0 | 120° | [草](https://zh.wikipedia.org/wiki/%E8%8D%89)、[樹葉](https://zh.wikipedia.org/wiki/%E6%A8%B9%E8%91%89) | | 青綠 | R0,G255,B128 | 150° | [軍裝](https://zh.wikipedia.org/wiki/%E5%86%9B%E8%A3%85) | | 青色 | R0,G255,B255 | 180° | [水面](https://zh.wikipedia.org/wiki/%E6%B0%B4%E9%9D%A2)、[天空](https://zh.wikipedia.org/wiki/%E5%A4%A9%E7%A9%BA) | | 靛藍 | R0,G128,B255 | 210° | [水面](https://zh.wikipedia.org/wiki/%E6%B0%B4%E9%9D%A2)、[天空](https://zh.wikipedia.org/wiki/%E5%A4%A9%E7%A9%BA) | | 藍色 | R0,G0,B255 | 240° | [海](https://zh.wikipedia.org/wiki/%E6%B5%B7)、[墨水](https://zh.wikipedia.org/wiki/%E5%A2%A8%E6%B0%B4) | | 紫色 | R128,G0,B255 | 270° | [葡萄](https://zh.wikipedia.org/wiki/%E8%91%A1%E8%90%84)、[茄子](https://zh.wikipedia.org/wiki/%E8%8C%84%E5%AD%90) | | 品紅 | R255,G0,B255 | 300° | [火](https://zh.wikipedia.org/wiki/%E7%81%AB)、[桃子](https://zh.wikipedia.org/wiki/%E6%A1%83%E5%AD%90) | | 紫紅 | R255,G0,B128 | 330° | [墨水](https://zh.wikipedia.org/wiki/%E5%A2%A8%E6%B0%B4) |
如何橫向表示色相呢,只需要一行 CSS 程式碼: ```css background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); ``` 這樣即可大致表達出 0-360 度的色相值,效果如下:
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1308183/1610250341698-b1a3b4ee-965c-48c2-b68c-f3d7406c4a30.png#align=left&display=inline&height=17&margin=%5Bobject%20Object%5D&name=image.png&originHeight=34&originWidth=414&size=2912&status=done&style=none&width=207)
根據滑鼠拖動的位置距離左邊界的距離就可以計算出色相值。 ```javascript /** * 在顏色值發生變化時實時計算相應的色相值 * @param event */ const handleChange = (event: any) => { if (!ref.current) { return; } const clientRect = ref.current.getBoundingClientRect(); const { width: containerWidth } = clientRect; const x: number = typeof event.pageX === 'number' ? event.pageX : event.touches[0].pageX; const left = x - (clientRect.left + window.pageXOffset); let innerHue; // 處理邊界值 if (left < 0) { innerHue = 0; } else if (left > containerWidth) { innerHue = 359; } else { const percent = (left * 100) / containerWidth; innerHue = (360 * percent) / 100; } setHue(innerHue); props.onChange({ h: innerHue }); }; ``` #### 2.3.2 Saturation 飽和度與 Value 明度
**飽和度(S)**是指色彩的純度,越高色彩越純,低則逐漸變灰,取0-100%的數值。**明度(V)**指[顏色](https://zh.wikipedia.org/wiki/%E9%A2%9C%E8%89%B2)的亮度,不同的顏色具有不同的明度。 在 ReactColor 中按照如下方式來表示飽和度與明度。
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1308183/1610251248680-b0e02b70-e148-4163-a841-fd552c37ebd5.png#align=left&display=inline&height=408&margin=%5Bobject%20Object%5D&name=image.png&originHeight=816&originWidth=1076&size=208723&status=done&style=none&width=538)
其實用 CSS 表示也比較簡單,使用漸變色來表示就可以實現該效果。 ```css background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0)); background: linear-gradient(to top, #000, rgba(0, 0, 0, 0)); ``` 與色相的計算方式一樣,也是根據滑鼠拖動的位置距離左邊界和下邊界的距離來計算,計算方法可以參考[色相的思路](#SDLwT)。 ## 三、總結
大家看完這篇文章應該發現程式碼部分其實我介紹的不多,更多還是介紹 HSV 色彩模型,以及作者為什麼沒有使用 RGB 表示。
如果大家去看 react-color 原始碼就會發現程式碼其實不難理解,難點還是在 HSV 的應用方法上面,大家如果有需要自己在專案裡面定製化顏色選擇器的話也可以根據這個思路來,一天之內就可以寫出來。
## 四、參考資料 - [HSL和HSV色彩空間](https://zh.wikipedia.org/wiki/HSL%E5%92%8CHSV%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4) - [色相](https://zh.wikipedia.org/wiki/%E8%89%B2%E7%9B%B8) - [明度](https://zh.wikipedia.org/wiki/%E6%98%8E%E5%BA%A6) - [飽和度](https://zh.wikipedia.org/wiki/%E8%89%B2%E5%BA%A6_(%E8%89%B2%E5%BD%A9%E5%
[`ReactColor`](https://github.com/casesandberg/react-color) 是一個優秀的 React 顏色選擇器元件,官方給了多種佈局供開發者選擇。 筆者常用的主題為 Sketch,這種主題涵蓋了**顏色面板**、**推薦色塊**、**RGB顏色**輸入等功能,比較完善。但是最近在寫一個富文字編輯器,編寫過程中遇到了一些問題,比如使用者在點選推薦色塊時,編輯器會失去焦點,無法對字型顏色進行更改。如果是編輯器自有的元件,可以使用以下程式碼 ```javascript event.preventDefault(); ``` 該程式碼可以禁止瀏覽器預設行為,比如點選推薦色塊之後只將色值向上傳遞,而不改變瀏覽器當前 `focus` 狀態。但是 `ReactColor` 並沒有暴露該事件,故 clone 了原始碼,在編輯器內集成了該元件,實現功能的同時也能夠減少打包體積。
本章節主要介紹 `ReactColor` 的實現原理,以比較有代表性的 Sketch 主題為例。 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1308183/1610244626581-dc5e3b4d-2954-46ab-a80d-1afebb370c69.png#align=left&display=inline&height=392&margin=%5Bobject%20Object%5D&name=image.png&originHeight=784&originWidth=1396&size=233488&status=done&style=none&width=698) 由上圖可以看到,整個顏色選擇器面板由這六個部分組成,分別是**亮度與飽和度調節面板**、**色相 Hue 調節面板**、**透明度調節面板**、**當前顏色的 RGBA 與 Hex 值**、**推薦色塊以及顏色實時預覽**。下面的部分就來介紹其原理實現。
與顏色相關的幾個屬性分別為亮度、飽和度、色相與透明度,與我們平時用到的 RGB 色彩模型不同,`ReactColor` 中用的是 HSV 色彩模型,其具體含義如下: ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1308183/1610245443851-acbc5ee8-e7a2-4fdd-80d2-72902a0e0726.png#align=left&display=inline&height=132&margin=%5Bobject%20Object%5D&name=image.png&originHeight=264&originWidth=736&size=17362&status=done&style=none&width=368) 下面是維基百科對 HSV 色彩模型的介紹: >
至於為什麼選用 HSV 色彩模型而不是直接使用 RGB,大家在使用 ReactColor 的過程中應該會發現,只要在下方的 色相 Hue 調節面板上選中了顏色,亮度與飽和度調節面板就會呈現什麼顏色。舉個例子:你選擇了黃色,那麼最上方調節面板呈現的就是黃色,差別也只是飽和度與明度不同而已。這就是使用 HSV 色彩模型的優勢,讓使用者選擇的顏色變成可預知並且方便調節的。
RGB 顏色空間利用三個顏色分量的線性組合來表示顏色,任何顏色都與這三個分量有關,而且這三個分量是高度相關的,所以連續變換顏色時並不直觀,想對影象的顏色進行調整需要更改這三個分量才行。自然環境下獲取的影象容易受自然光照、遮擋和陰影等情況的影響,即對亮度比較敏感。而 RGB 顏色空間的三個分量都與亮度密切相關,**即只要亮度改變,三個分量都會隨之相應地改變**,而沒有一種更直觀的方式來表達,而這就是 HSV 色彩模型的優勢所在。
### 2.2 HSV 轉 RGB
上面提到,在日常的前端開發過程中還是普遍使用 RGB 色彩模型進行顏色表示,在使用者設定好 HSV 值後我們需要將其轉為 RGB 值,公式如下(該公式來自[維基百科](https://zh.wikipedia.org/wiki/HSL%E5%92%8CHSV%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4#%E4%BB%8EHSV%E5%88%B0RGB%E7%9A%84%E8%BD%AC%E6%8D%A2)) $h_i = \lfloor h/60 \rfloor$ $f = h/60 - h_i$ $p = v * (1-s)$ $q = v*(1-f*s)$ $t = v * (1 - (1-f)*s)$ $$rgb=\begin{cases} (v,t,p), & \text{if $h_i=0$} \\ (q,v,p), & \text{if $h_i=1$} \\ (p,v,t), & \text{if $h_i=2$} \\ (p,q,v), & \text{if $h_i=3$} \\ (t,p,v), & \text{if $h_i=4$} \\ (v,p,q), & \text{if $h_i=5$} \\ \end{cases}$$
這樣在使用者選擇完成後就可以對色彩空間實時轉換,通過 `onChange` 回撥返回給使用者。
### 2.3 HSV 色彩模型在 ReactColor 中的實現
既然使用了 HSV 色彩模型就要考慮一下如何表示這三個變數,下面我們分兩部分來講。 #### 2.3.1 Hue 色相 | 顏色名稱 | 紅綠藍含量 | 角度 | 代表物體 | | :---: | :---: | :---: | :---: | | 紅色 | R255,G0,B0 | 0° | [血液](https://zh.wikipedia.org/wiki/%E8%A1%80%E6%B6%B2)、[草莓](https://zh.wikipedia.org/wiki/%E8%8D%89%E8%8E%93) | | 橙色 | R255,G128,B0 | 30° | [火](https://zh.wikipedia.org/wiki/%E7%81%AB)、[橙子](https://zh.wikipedia.org/wiki/%E6%A9%99%E5%AD%90) | | 黃色 | R255,G255,B0 | 60° | [香蕉](https://zh.wikipedia.org/wiki/%E9%A6%99%E8%95%89)、[杧果](https://zh.wikipedia.org/wiki/%E6%9D%A7%E6%9E%9C) | | 黃綠 | R128,G255,B0 | 90° | [檸檬](https://zh.wikipedia.org/wiki/%E6%AA%B8%E6%AA%AC) | | 綠色 | R0,G255,B0 | 120° | [草](https://zh.wikipedia.org/wiki/%E8%8D%89)、[樹葉](https://zh.wikipedia.org/wiki/%E6%A8%B9%E8%91%89) | | 青綠 | R0,G255,B128 | 150° | [軍裝](https://zh.wikipedia.org/wiki/%E5%86%9B%E8%A3%85) | | 青色 | R0,G255,B255 | 180° | [水面](https://zh.wikipedia.org/wiki/%E6%B0%B4%E9%9D%A2)、[天空](https://zh.wikipedia.org/wiki/%E5%A4%A9%E7%A9%BA) | | 靛藍 | R0,G128,B255 | 210° | [水面](https://zh.wikipedia.org/wiki/%E6%B0%B4%E9%9D%A2)、[天空](https://zh.wikipedia.org/wiki/%E5%A4%A9%E7%A9%BA) | | 藍色 | R0,G0,B255 | 240° | [海](https://zh.wikipedia.org/wiki/%E6%B5%B7)、[墨水](https://zh.wikipedia.org/wiki/%E5%A2%A8%E6%B0%B4) | | 紫色 | R128,G0,B255 | 270° | [葡萄](https://zh.wikipedia.org/wiki/%E8%91%A1%E8%90%84)、[茄子](https://zh.wikipedia.org/wiki/%E8%8C%84%E5%AD%90) | | 品紅 | R255,G0,B255 | 300° | [火](https://zh.wikipedia.org/wiki/%E7%81%AB)、[桃子](https://zh.wikipedia.org/wiki/%E6%A1%83%E5%AD%90) | | 紫紅 | R255,G0,B128 | 330° | [墨水](https://zh.wikipedia.org/wiki/%E5%A2%A8%E6%B0%B4) |
如何橫向表示色相呢,只需要一行 CSS 程式碼: ```css background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); ``` 這樣即可大致表達出 0-360 度的色相值,效果如下:
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1308183/1610250341698-b1a3b4ee-965c-48c2-b68c-f3d7406c4a30.png#align=left&display=inline&height=17&margin=%5Bobject%20Object%5D&name=image.png&originHeight=34&originWidth=414&size=2912&status=done&style=none&width=207)
根據滑鼠拖動的位置距離左邊界的距離就可以計算出色相值。 ```javascript /** * 在顏色值發生變化時實時計算相應的色相值 * @param event */ const handleChange = (event: any) => { if (!ref.current) { return; } const clientRect = ref.current.getBoundingClientRect(); const { width: containerWidth } = clientRect; const x: number = typeof event.pageX === 'number' ? event.pageX : event.touches[0].pageX; const left = x - (clientRect.left + window.pageXOffset); let innerHue; // 處理邊界值 if (left < 0) { innerHue = 0; } else if (left > containerWidth) { innerHue = 359; } else { const percent = (left * 100) / containerWidth; innerHue = (360 * percent) / 100; } setHue(innerHue); props.onChange({ h: innerHue }); }; ``` #### 2.3.2 Saturation 飽和度與 Value 明度
**飽和度(S)**是指色彩的純度,越高色彩越純,低則逐漸變灰,取0-100%的數值。**明度(V)**指[顏色](https://zh.wikipedia.org/wiki/%E9%A2%9C%E8%89%B2)的亮度,不同的顏色具有不同的明度。 在 ReactColor 中按照如下方式來表示飽和度與明度。
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1308183/1610251248680-b0e02b70-e148-4163-a841-fd552c37ebd5.png#align=left&display=inline&height=408&margin=%5Bobject%20Object%5D&name=image.png&originHeight=816&originWidth=1076&size=208723&status=done&style=none&width=538)
其實用 CSS 表示也比較簡單,使用漸變色來表示就可以實現該效果。 ```css background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0)); background: linear-gradient(to top, #000, rgba(0, 0, 0, 0)); ``` 與色相的計算方式一樣,也是根據滑鼠拖動的位置距離左邊界和下邊界的距離來計算,計算方法可以參考[色相的思路](#SDLwT)。 ## 三、總結
大家看完這篇文章應該發現程式碼部分其實我介紹的不多,更多還是介紹 HSV 色彩模型,以及作者為什麼沒有使用 RGB 表示。
如果大家去看 react-color 原始碼就會發現程式碼其實不難理解,難點還是在 HSV 的應用方法上面,大家如果有需要自己在專案裡面定製化顏色選擇器的話也可以根據這個思路來,一天之內就可以寫出來。
## 四、參考資料 - [HSL和HSV色彩空間](https://zh.wikipedia.org/wiki/HSL%E5%92%8CHSV%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4) - [色相](https://zh.wikipedia.org/wiki/%E8%89%B2%E7%9B%B8) - [明度](https://zh.wikipedia.org/wiki/%E6%98%8E%E5%BA%A6) - [飽和度](https://zh.wikipedia.org/wiki/%E8%89%B2%E5%BA%A6_(%E8%89%B2%E5%BD%A9%E5%