JavaScript中的顏色空間轉換

我在做 webapp 的頂部導航欄時,碰到了一個挑戰,導航欄的字體與圖標要根據背景的顏色深淺來顯示不同白色和黑色,但是導航欄的顏色是支持多種配色的,我不可能根據每一個配色去定義這個顏色的深淺,於是我開始研究起了顏色空間的轉換……

對於CSS,我們最常見的就是 16進制的 RGB。 從黑色到白色依次是 #000000 ... #FFFFFF

RGB

RGB也稱三原色光模式(RGB color mode),原理是將紅綠藍三種顏色的色光已不同的比例相加,已形成多種多樣的色光(三原色不可能用其他燈光的顏色合成) -- 維基百科

當前計算機硬件採取每一個像素用24Bit來表示不同的顏色,每8位表示一個原色的強度,最高值爲2^8,也就是256個值,組合起來可以表示 16777216(256^3) 種顏色。之所以24位,這是因爲人眼最高只能分辨出1000萬種顏色,因此足矣。

當然也有32Bit的模式,但是實際上也是24Bit,餘下的8Bit不分配到像素中,主要是爲了提高數據輸送的速度(一般而言1word爲16Bit,32Bit === 1 double word,處理器不需要做多餘的換算),同樣在一些特殊情況下,餘下的8Bit 用來表示像素的透明度

因此 #FFFFFF 同樣可以表示爲 rgb(255, 255, 255)

很自然地就可以採用三維空間來描述RGB的全值域,如圖所示x軸爲紅色,y軸爲藍色,z軸爲綠色。黑色藏在了立方體的背面。MAX=255,MIN=0。用這種方式表示可以很簡單得通過計算兩個點的距離遠近來判斷顏色是否相近。

由SharkD - 自己的作品. Download source code.,CC BY-SA 3.0,https://commons.wikimedia.org/w/index.php?curid=9803283

幾個極點的座標分別表示的顏色

r g b name
0 0 0 黑(black)
255 255 255 白(white)
255 0 0 紅(red)
255 255 0 黃(yellow)
0 255 0 綠(green)
0 255 255 青(cyan-blue)
0 0 255 藍(blue)
255 0 255 品紅(magenta)

但是這不足以解決文章開頭需要解決的問題,因爲從當前的顏色空間中,我們無法要直觀地去辨別那些顏色是亮色,哪些顏色是暗色,很難,我們只知道一個顏色的紅綠藍混合比例。我們需要找出一種規律,去分類顏色的明暗。

HSL/HSV

出於我們感性的角度,顏色混合並不直觀,我們判別一種顏色的思維首先會看看這是什麼顏色,然後再確認顏色深淺如何、明暗度如何。事實上大部分藝術家在創作的時候也更傾向於這種思維。

所以我們很多軟件上的調色工具都會基於這樣的思路去設計,首先會有一個色板、然後會有飽和度、亮度這樣的調整。

然而一早出現HSL的時候卻不是爲了此目的,記錄最早顯示的是1938年 Georges Valensi 爲了解決彩色電視信號兼容單色電視信號的問題發明了HSL色彩空間(單色電視信號僅包括L信號)。往後1978年 Alvy Ray Smith 在編寫 SuperPaint (SuperPaint是第一個計算機光柵圖形編輯器或“繪畫”程序之一)的時候發明了HSV(HSB)模型。經過實踐反映了這兩種模型給使用者可以帶來更直觀的感受。

HSL 三種維度分辨爲 Hue(色相)、Saturation(飽和度)、Lightness(亮度),幾何表示爲一個圓柱座標系。色相是這個圓柱的偏角,飽和度爲圓柱水平切面的半徑,亮度以圓柱的高度表示。

By SharkD - Own work. Source-code available at the POV-Ray Object Collection., CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=8421400

HSV 的三種維度分別是 Hue(色相)、Saturation(飽和度)、Value(明度),在色相的定義上與HSL保持一致,但是在飽和度上的定義是有區別的。
By HSV_color_solid_cylinder.png: SharkDderivative work: SharkD  Talk - HSV_color_solid_cylinder.png, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=9801673

這兩者之間應該使用哪種模型,目前是非常有爭議的。支持HSL的人認爲他更好的反映了飽和度和亮度作爲兩個獨立參數的概念直覺(HSV最低的飽和度爲白色是非常反直覺的),而另一部分的人認爲,HSL的飽和度的定義容易給人造成迷惑,比如亮度極高時,白色被認爲是高飽和度的。這意味着

  • HSL中,飽和度總是從完全飽和變化到等價的灰色,而在HSV中是從完全飽和變化爲白色。
  • HSL中亮度的變化跨域從黑色到選擇的色相再到白色的過程,HSV中明度的定義只從黑色過渡到選擇的色相。

因此通常在繪製座標時,飽和度會被替換爲 色度(Chroma)表示,用以過濾一些不符合直覺的座標,HSL 對應呈雙錐型的 HCL,而HSV 則對應錐形的 HCV 模型。

By Hcl-hcv_models.svg: Jacob RusHSL_color_solid_dblcone.png: SharkDderivative work: SharkD  Talk - Hcl-hcv_models.svgHSL_color_solid_dblcone.png, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=9802536

By Hcl-hcv_models.svg: Jacob RusHSV_color_solid_cone.png: SharkDderivative work: SharkD  Talk - Hcl-hcv_models.svgHSV_color_solid_cone.png, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=9802544

如今HSL 與 HSV 在軟件上已經有了大量的運用。比如

  • Adobe 套件(Photoshop,Illustrator...)-- HSV
  • APPLE Mac OS X 系統顏色選擇器 --HSV
  • CSS3 -- HSL
  • Windows系統顏色選擇器 -- HSL

當然隨着系統的升級與支持,也不乏有兩者都支持的軟件。

沒有孰優孰劣,在做選擇的時候只考慮使用的場景哪種更適合,當然在WEB的範疇中,由於CSS3的標準規定,HSL更有利於顏色的換算。而瞭解到這裏,我已經對開頭的需求有了一個明確的實現方案。

RGB 到 HSL 的換算

在數學上定義爲 RGB 空間的r,g,b座標到 HSL 空間的 h,s,l 座標的換算。

  • r,g,b ∈ [0, 1] ,max = max(r, g, b), min = min(r, g, b)
  • h ∈ [0, 360], s,l ∈ [0, 1]

首先會先計算色相值,對應是在圓柱橫切面角度六等分的不同夾角下的值有不同的換算公式,從上往下1到5對應的區域

實際上 max = min 時是的灰色,h = undefined,上圖中的第一條公式表示有誤。當 h = 0° 一般表示計算爲紅色,也就是包含在了第二條計算公式中。這點需要特別注意

其次是亮度的計算,其實亮度的定義這方面是有爭議的,並不是真正意義上明確的,而是基於不同的模型做不同的定義,這裏就不做具體的討論,HSL中亮度的定義取RGB中最大值與最小值相加的二分之一

最後是飽和度的計算公式,首先定義色度Chroma = max - min,從生理角度理解三種視錐細胞中,刺激最大與刺激最小之間的差異,
讓人產生了顏色的鮮豔感,而與刺激中等的細胞關係不大。

我們一開始介紹HSL的時候有提到過,HSL模型中有些值實際上已經超出了RGB定義的範疇,超出的部分實際上是沒有意義的,所以圈定了另外一個範圍爲 HCL,是呈現一種雙錐形的幾何表示。當爲亮度爲極值時,飽和度恆等於0。
中間分成兩節對應不同的計算公式。下半截對應公式2,上半截對應公式1,緊接着飽和度是受到亮度的制約,因此配合亮度進行計算。

代碼實現

function RGB2HSL(r, g, b) {
  r = r / 255
  g = g / 255
  b = b / 255

  const max = Math.max(r, g, b)
  const min = Math.min(r, g, b)
  const delta = max - min
  let h, s, l

  if (max ==== min) {
    h = 0
  } else if (max === r) {
    h = ((g - b) / delta) % 6
  } else if (max === g) {
    h = (b - r) / delta + 2
  } else {
    h = (r - g) / delta + 4
  }
  h = Math.round(h * 60)
  if (h < 0) h += 360

  l = (max + min) / 2,
  s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));

  // 切換爲百分比模式
  s = +(s * 100).toFixed(1);
  l = +(l * 100).toFixed(1);

  return { h, s, l }
}

RGB 到 HSV(HSB)

色相的換算跟HSL是一致的。飽和度和明度定義分別是

代碼實現

function RGB2HSV(r, g, b) {
  r = r / 255
  g = g / 255
  b = b / 255

  const max = Math.max(r, g, b)
  const min = Math.min(r, g, b)
  const delta = max - min
  let h, s, l

  if (max ==== min) {
    h = 0
  } else if (max === r) {
    h = ((g - b) / delta) % 6
  } else if (max === g) {
    h = (b - r) / delta + 2
  } else {
    h = (r - g) / delta + 4
  }
  h = Math.round(h * 60)
  if (h < 0) h += 360

  // 基於HSL函數簡單的變化即可適用
  l = max,
  s = delta === 0 ? 0 : delta / max;

  s = +(s * 100).toFixed(1);
  l = +(l * 100).toFixed(1);

  return { h, s, l }
}

HSL 到 RGB

🚧 施工中...
(理解起來有點難度,待續)

應用

  • 首先解決文章開頭的問題。我們只需要知道背景顏色,比如輸入"#1388F5"、簡單地切換爲十進制後,套用RGB2HSL從而獲取到亮度值 L。只需要設定一個亮度閾值,判斷L是否大於這個值,來加載響應的樣式。
const HSL = RGB2HSL(hex2RGB("#1388F5"))
// 假定閾值是55, 這個可以按需要調整
const className = HSL.l > 55 ? "light" : "dark"

// 如果是亮色則加載黑字和黑色圖標、如果是暗色則加載白色字體和白圖標
// ps: 黑夜模式🙂
render(className)
  • 同樣的通過HSL模型,通過設置圖片中某個色相範圍的顏色飽和度爲0,我們可以很簡單地幫一張圖片去色,又或者是指保留圖片只的某個顏色達到局部彩色效果(RGB通道開關)


  • 如果要製作一個web的調色板,不可避免地一定會應用到HSL模型
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章