Skip to main content

ZSL

色彩科學

Published:
Updated:

修復 Blowfish 文字對比度過低的問題,隨筆記錄這次修復學到的東西。

起因

Blowfish 使用 neutral / primary / secondary 的色彩框架,這和 Material Design 的架構類似,扣掉 Material Design 沒有 neutral,primary 是主要品牌顏色、主要的突出顏色,secondary 是次要但需要強調的地方,應該少量謹慎使用。

Blowfish 從建立的第一個 commit 就是這樣想了,能這樣推斷的原因是作者直接覆蓋 Tailwind 所有顏色,限制專案只能用這三種色系,如果沒有這個想法就不會寫成完全覆蓋。

但是問題來了,Blowfish 雖然用了類似 Material Design 的架構,但是幾乎所有顏色都不照 Material Design 設計,這種架構 neutral 理論上應該要是灰色,但是內建主題很多 neutral 都不是灰色系,再加上大量使用 blur 和透明效果就導致對比度過低,火上加油的是作者的 Fugu 工具實際上只是簡易計算 RGB 數值,但是 RGB 是很古老的色彩模型,這樣簡單的計算沒有考慮到色彩的複雜問題導致網站對比度更低。

研究該怎麼系統性的修復的過程中把知識整理成這篇文章,雖然評估後根本沒用到但是把知識寫起來。

色彩模型和色彩空間

別看筆者這樣,筆者當年好歹也是修過顯示器導論的,但是網路繁體中文資源笑死人,連頂著自己名稱是色研所的公司寫的文章都錯,隨便找還找到台師大碩士論文,CTRL+F LCH 一秒就找到論文錯誤,說 LCH 是色彩空間,繁體中文資源真的是有夠爛。

色彩模型是用來描述色彩的座標軸空間,從小學的 RGB 到 HSL 和 LCH 都是不同的色彩模型,不同的座標軸有各自的特性和表現能力,比如 HEX 就沒辦法描述 DCI-P3 色彩空間。

色彩空間則是表達有多豐富的顏色,主要用在硬體規格上,比如說我這台螢幕可以顯示 DCI-P3 色彩空間,那麼電影拍 DCI-P3 這台螢幕就能顯示那些顏色。常見的色彩空間由低階到高階大概有 45% NTSC / 72% NTSC (100% SRGB) / DCI-P3 / Rec.2020 / Adobe RGB,前兩個是一般螢幕,接下來兩個是影視作品,最後一個用於紙本影印。

產品的色彩空間也不是越大越好,比如說 Windows 色彩管理一團亂,色彩空間大反而顏色容易過飽和,尤其是低階硬體的廣色域根本沒在管色彩映射,色準、校色也理所當然直接關係此問題。

然後碩士論文、開公司的開頭就說 LCH 是色彩空間,送他們四個字,媽的智障。

色彩模型

本文還是只會介紹 CSS 用到的那幾個模型,包含 RGB/HEX/HSL/OKLCH。

RGB and Hex

最古老的色彩模型,用紅綠藍三原色組成,最大的問題是

  1. 開發者難以從數值判斷色相、對比度、亮度
  2. 對比度和亮度無法直接手調

HEX 和 RGB 是一樣東西只是改成 16 進位,為了解決此問題於是有了 HSL 模型。

HSL

HSL(Hue 色相,Saturation 飽和度,Lightness明度)解決了上面的問題,可以看到座標完全不同並且直接給你獨立座標專門調整亮度。

這基本上就是 Adobe Lightroom 裡面調整顏色的方式,然而 HSL 只解決了一半問題,另一半的問題是不同色相的亮度數值即使一樣,人眼感受到還是不一樣亮。

hsl(239,100%,52%)
hsl(59,100%,52%)

可以看到亮度設定相同但是黃色明顯更亮,黑色文字在藍色背景下就顯得對比度不足。此外 HSL 和 RGB 都使用 RGB 色彩空間,因此在支援更高色彩空間的顯示器就無法顯示更豐富的顏色。

OKLCH

OKLCH 其中 LCH 分別代表 Lightness 亮度、Chroma 彩度、Hue 色相,此標準 2020 才推出,2023 就光速被所有瀏覽器實作支援,這毫無疑問是非常誇張的速度。

HSL 到 OKLCH 之間其實還有 LAB/OKLAB/LCH 最後才是 OKLCH,OK 代表沒問題版本的 LCH,至於是改了什麼也不用在意了,反正你不會用 LCH,而 OKLCH 最重要的當然就是解決 HSL 的亮度問題,並且支援 DCI-P3 色彩空間。

這樣用文字介紹讀者可能無感,但是實際看設定就知道,RGB 格式對開發者來說就是魔法數字(亂碼):

:root {
  --color-primary-50: rgb(239, 246, 255);
  --color-primary-100: rgb(219, 234, 254);
  --color-primary-200: rgb(191, 219, 254);
  --color-primary-300: rgb(147, 197, 253);
  --color-primary-400: rgb(96, 165, 250);
  --color-primary-500: rgb(59, 130, 246);
  --color-primary-600: rgb(37, 99, 235);
  --color-primary-700: rgb(29, 78, 216);
  --color-primary-800: rgb(30, 64, 175);
  --color-primary-900: rgb(30, 58, 138);
}

轉成 OKLCH 之後就能很明顯的看出都是類似色相並且亮度級減:

:root {
  --color-primary-50: oklch(97.048% 0.01429 254.737);
  --color-primary-100: oklch(93.192% 0.0317 255.644);
  --color-primary-200: oklch(88.235% 0.05716 254.166);
  --color-primary-300: oklch(80.908% 0.09569 251.839);
  --color-primary-400: oklch(71.375% 0.14347 254.643);
  --color-primary-500: oklch(62.31% 0.1881 259.83);
  --color-primary-600: oklch(54.616% 0.21529 262.896);
  --color-primary-700: oklch(48.821% 0.21724 264.392);
  --color-primary-800: oklch(42.446% 0.18094 265.652);
  --color-primary-900: oklch(37.907% 0.13782 265.536);
}

一目了然而且沒有色相之間的亮度問題,非常好模型。

至於是否該採用 OKLCH?他在 2023 之後的瀏覽器才能用,但是如果你的專案和 Blowfish 一樣使用 Tailwind V4 又沒額外設定,那麼 Tailwind V4 預設會使用 2024 之後的瀏覽器才支援的原生 CSS nesting 語法,這也代表用戶都有新瀏覽器,沒必要用 @support 檢查。

但是問題來了,OKLCH 在不支援 DCI-P3 的硬體中會回退成 RGB 色彩空間,我們不知道現在選的顏色有沒有超出 RGB 色彩空間空間,這就讓你精心設計的顏色無法在用戶裝置正確顯示。而且不支援 DCI-P3 的硬體非常多,雖然手機基本上全都支援了,但是低階筆電不用想連 72% NTSC 都沒有,一般筆電也才勉強 100% SRGB,中高階才會看到支援 DCI-P3,電腦螢幕也是如此。

唯一的解決方案可能就是找到 https://oklch.com/ 這種網站然後檢查用的顏色有沒有超出範圍,是目前硬體支援度不夠的妥協。

學到了什麼

整理一下這次修復對比度學了哪些東西。

  1. 不要自創規範,Blowfish 用了 10 級色彩,然而 Tailwind 用了 11 級因此無法直接套用市面上的 Tailwind 色彩產生器。
  2. Blowfish 用了類似 Material Design 的架構但是完全沒有用他的邏輯,造成更多問題。
  3. 這些問題會變成歷史包袱越來越重,如果沒歷史包袱就直接改了沒這麼多問題。
  4. 灰色是整個網站用最多的顏色,忘記在哪裡看到這句話的,可以看到 Tailwind 確實內建一堆灰色。
  5. Refactoring UI 有教學文章說明如何自建色彩主題。
  6. tints.dev 根據 Refactoring UI 建立了 Tailwind 色彩生成器。
  7. Material Design Palette Generator 可以快速建立 Material Design 的顏色。
  8. Tailwind 內建顏色 不固定 OKLCH 的 H,數值會浮動。
  9. oklch-smooth 用 OKLCH 建立視覺鮮明的資料可視化顏色。
  10. colorbrewer2 用於地圖數據的顏色。
  11. Unix 合併輸出可以用 subshell (echo 'html:not(.dark) {'; hugo gen chromastyles --style=emacs; echo '}') >> assets/css/custom.css
  12. PowerShell 合併輸出可以用 array sub-expression operator @("html:not(.dark) {"; (hugo gen chromastyles --style=emacs); "}") | Add-Content -Path "assets/css/custom.css"