你知道 react-color 的實現原理嗎

一、前言

ReactColor 是一個優秀的 React 顏色選擇器組件,官方給了多種布局供開發者選擇。

筆者常用的主題為 Sketch,這種主題涵蓋了顏色面板推薦色塊RGB顏色輸入等功能,比較完善。但是最近在寫一個富文本編輯器,編寫過程中遇到了一些問題,比如用戶在點擊推薦色塊時,編輯器會失去焦點,無法對字體顏色進行更改。如果是編輯器自有的組件,可以使用以下代碼

event.preventDefault();

該代碼可以禁止瀏覽器默認行為,比如點擊推薦色塊之後只將色值向上傳遞,而不改變瀏覽器當前 focus 狀態。但是 ReactColor 並沒有暴露該事件,故 clone 了源碼,在編輯器內集成了該組件,實現功能的同時也能夠減少打包體積。

二、實現原理

本章節主要介紹 ReactColor 的實現原理,以比較有代表性的 Sketch 主題為例。

image.png

由上圖可以看到,整個顏色選擇器面板由這六個部分組成,分別是亮度與飽和度調節面板色相 Hue 調節面板透明度調節面板當前顏色的 RGBA 與 Hex 值推薦色塊以及顏色實時預覽。下面的部分就來介紹其原理實現。

2.1 HSV 色彩模型

與顏色相關的幾個屬性分別為亮度、飽和度、色相與透明度,與我們平時用到的 RGB 色彩模型不同,ReactColor 中用的是 HSV 色彩模型,其具體含義如下:

image.png

下面是維基百科對 HSV 色彩模型的介紹:

HSV即色相飽和度明度(英語:Hue, Saturation, Value),又稱HSB,其中B即英語:Brightness。

  • 色相(H)是色彩的基本屬性,就是平常所說的顏色名稱,如紅色黃色等。
  • 飽和度(S)是指色彩的純度,越高色彩越純,低則逐漸變灰,取0-100%的數值。
  • 明度(V),亮度(L),取0-100%。

至於為什麼選用 HSV 色彩模型而不是直接使用 RGB,大家在使用 ReactColor 的過程中應該會發現,只要在下方的 色相 Hue 調節面板上選中了顏色,亮度與飽和度調節面板就會呈現什麼顏色。舉個例子:你選擇了黃色,那麼最上方調節面板呈現的就是黃色,差別也只是飽和度與明度不同而已。這就是使用 HSV 色彩模型的優勢,讓用戶選擇的顏色變成可預知並且方便調節的。

RGB 顏色空間利用三個顏色分量的線性組合來表示顏色,任何顏色都與這三個分量有關,而且這三個分量是高度相關的,所以連續變換顏色時並不直觀,想對圖像的顏色進行調整需要更改這三個分量才行。自然環境下獲取的圖像容易受自然光照、遮擋和陰影等情況的影響,即對亮度比較敏感。而 RGB 顏色空間的三個分量都與亮度密切相關,即只要亮度改變,三個分量都會隨之相應地改變,而沒有一種更直觀的方式來表達,而這就是 HSV 色彩模型的優勢所在。

2.2 HSV 轉 RGB

上面提到,在日常的前端開發過程中還是普遍使用 RGB 色彩模型進行顏色表示,在用戶設置好 HSV 值後我們需要將其轉為 RGB 值,公式如下(該公式來自維基百科)

\(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 血液草莓
橙色 R255,G128,B0 30° 橙子
黃色 R255,G255,B0 60° 香蕉杧果
黃綠 R128,G255,B0 90° 檸檬
綠色 R0,G255,B0 120° 樹葉
青綠 R0,G255,B128 150° 軍裝
青色 R0,G255,B255 180° 水面天空
靛藍 R0,G128,B255 210° 水面天空
藍色 R0,G0,B255 240° 墨水
紫色 R128,G0,B255 270° 葡萄茄子
品紅 R255,G0,B255 300° 桃子
紫紅 R255,G0,B128 330° 墨水

如何橫向表示色相呢,只需要一行 CSS 代碼:

background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);

這樣即可大致表達出 0-360 度的色相值,效果如下:
image.png
根據鼠標拖動的位置距離左邊界的距離就可以計算出色相值。

/**
 * 在顏色值發生變化時實時計算相應的色相值
 * @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)顏色的亮度,不同的顏色具有不同的明度。

在 ReactColor 中按照如下方式來表示飽和度與明度。
image.png
其實用 CSS 表示也比較簡單,使用漸變色來表示就可以實現該效果。

background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));

與色相的計算方式一樣,也是根據鼠標拖動的位置距離左邊界和下邊界的距離來計算,計算方法可以參考色相的思路

三、總結

大家看完這篇文章應該發現代碼部分其實我介紹的不多,更多還是介紹 HSV 色彩模型,以及作者為什麼沒有使用 RGB 表示。

如果大家去看 react-color 源碼就會發現代碼其實不難理解,難點還是在 HSV 的應用方法上面,大家如果有需要自己在項目裏面定製化顏色選擇器的話也可以根據這個思路來,一天之內就可以寫出來。

四、參考資料