Ant Design 色板生成算法演進(jìn)之路

一、前言
此文章為2021年的文章,但是對于理解Ant Design 色板生成算法很有幫助!

以下正文...

最近我在整理一套團(tuán)隊(duì)內(nèi)部使用的設(shè)計(jì)規(guī)范,其中顏色部分參考了 Ant Design 的 “色彩” 部分,相對比2.0之前版本,Ant Design 的 3.0 版本,調(diào)色板做了調(diào)整,借此機(jī)會(huì)我學(xué)習(xí)了一下 Ant Design 迄今為止三個(gè)版本色板生成算法的源碼,感覺其“確定”設(shè)計(jì)思想非常值得學(xué)習(xí)。

“確定” 作為 Ant Design 的設(shè)計(jì)理念之一,在調(diào)色板這一隅發(fā)揮得淋漓盡致:用科學(xué)定義設(shè)計(jì),在設(shè)計(jì)有跡可循的同時(shí)提高了代碼的可維護(hù)性,減少開發(fā)階段樣式代碼的不確定性。

Ant Design 三個(gè)大版本的色板生成算法各不相同,卻一直在完善,本文對其三個(gè)版本的色板生成算法進(jìn)行解讀,聊一聊我的體會(huì)。

在本文中我會(huì)使用一些名詞,如果你不知道這些詞的含義建議先簡單了解一下:

色相、飽和度、明度
HSL和HSV色彩空間
貝塞爾曲線掃盲
二、關(guān)于調(diào)色板
什么是調(diào)色板
在電腦圖形學(xué)中,調(diào)色板(英語:Palette)要么是指用于數(shù)字圖像管理的給定有限顏色組(顏色表),要么是顯示屏上一組有限選擇的小圖形單元(諸如“工具選板”)。
引用自維基百科 調(diào)色板 (電腦圖形學(xué))

調(diào)色板本來是混合各種顏色顏料使用的板,在 Ant Design 中,調(diào)色板指的是一份顏色表(如下圖),顏色表由一系列具有一定代表性的基本色彩及它們的漸變色組成,我們可以在調(diào)色板中尋找需要的顏色并獲取顏色值。



Ant Design 調(diào)色板的一部分

怎樣使用調(diào)色板
設(shè)計(jì)師與程序員都需要使用調(diào)色板工具,以 Ant Design 為例,設(shè)計(jì)師需要根據(jù)調(diào)色板上的色值來進(jìn)行設(shè)計(jì)稿的制作,而程序員在缺乏設(shè)計(jì)稿的時(shí)候也可以直接在調(diào)色板上取色。

一般來說在進(jìn)行設(shè)計(jì)稿制作的時(shí)候,直接使用 Ant Design 的一種基本色彩或與基本色彩相近的顏色作為主色,主色的漸變色可以用于組件的特殊狀態(tài),如 hover/active 狀態(tài)。

如何制作調(diào)色板
Ant Design 的調(diào)色板由一系列具有一定代表性的基本色彩及它們的漸變色組成,其中基本色彩可以由主設(shè)計(jì)師來確定,其漸變色由色板生成算法計(jì)算得到。

三、Ant Design 1.x 色板生成算法
Ant Design 1.x 色彩部分,第一版的實(shí)現(xiàn)較為簡單,這部分主要介紹了:

對顏色部分進(jìn)行了簡要的說明
提供了一套調(diào)色板,并介紹了每種顏色的含義,但不能直接復(fù)制色值
提供了交互指引,介紹了色板生成工具的使用方法
“色彩識別”部分,用簡單的標(biāo)簽數(shù)值計(jì)算來確定對比度是否符合要求,這對于新人的使用特別友好
原理
選取一個(gè)主色作為 5 號色,

將主色與純白色(#fff)混合,主色與純白色之間分成 100 份, 20/40/60/80 的位置分別分割,得到 4/3/2/1 號色;

將主色與純黑色(#000)混合,主色與純黑色之間分成 100 份, 20/40/60/80 的位置分別分割,得到 6/7/8/9 號色;

通過以上方式得到一條完整漸變色板。

Ant Design 將這一版本的色板生成算法稱之為 “tint/shade 色彩系統(tǒng)”。

實(shí)現(xiàn)
這一版本我在 github 上沒看到色彩生成算法的代碼,后來我 google 到了這篇文章:Tint and Shade Functions,作者認(rèn)為單純通過改變顏色亮度實(shí)現(xiàn)顏色的漸變效果并不理想,于是實(shí)現(xiàn)如下:

// 變亮
@function tint($color, $percentage) {
    @return mix(white, $color, $percentage);
}
// 變暗
@function shade($color, $percentage) {
    @return mix(black, $color, $percentage);
}
// 使用
.useage {
    background-color: tint(#2db7f5, 80%);
}
使用了 sass 的 mix 方法來進(jìn)行顏色值的混合,只需傳入主色色值和百分比即可,使用 less 同理。

評價(jià)
這一版的實(shí)現(xiàn)簡單粗暴,在研究顏色色彩之前,我對漸變色板的第一想法也是這樣的實(shí)現(xiàn),后來通過一些調(diào)研發(fā)現(xiàn)這樣實(shí)現(xiàn)并不好:

自然界中幾乎沒有純白/純灰/純黑色的東西,因此在 WEB 中使用這三種顏色給人的感覺不真實(shí),在生成漸變色板的時(shí)候每一種主色都在向著“不真實(shí)”的終點(diǎn)進(jìn)行過渡,這當(dāng)然是不妥的;
當(dāng)主色亮度或飽和度過低的時(shí)候,色號小于 5 / 大于 5 的變化速率差異增大,這在開發(fā)的時(shí)候漸變色的與主色的對比度可能會(huì)失衡,視覺效果不好。
四、Ant Design 2.x 色板生成算法
Ant Design 2.x 色彩部分,相對于第一版,第二版的調(diào)色板的顏色過渡更加平滑,提供了點(diǎn)擊調(diào)色板復(fù)制顏色值的功能。

原理
經(jīng)過設(shè)計(jì)師和程序員的精心調(diào)教,結(jié)合了色彩加白、加黑、加深,貝塞爾曲線,以及針對冷暖色的不同旋轉(zhuǎn)角度,得出一套色板生成算法(用以取代我們原來的 tint/shade 色彩系統(tǒng))。使用者只需指定主色,便可導(dǎo)出一條完整的漸變色板。

最初一看這個(gè)原理感覺很復(fù)雜,其實(shí)不是那么難以理解:

選取一個(gè)顏色作為主色(6 號色);
判斷減淡或加深,進(jìn)行顏色混合
若減淡,則主色與純白色(#fff)混合,根據(jù)色號,獲取貝塞爾曲線上的對應(yīng)值。
若加深,則主色與它對應(yīng)的深色混合,根據(jù)色號,獲取貝塞爾曲線上的對應(yīng)比例值。加深時(shí)主色對應(yīng)的深色進(jìn)行了明度與色相的調(diào)整,其中對色相的調(diào)整也就是上述引用中說的“針對冷暖色的旋轉(zhuǎn)”;
分別取1~9色號的色值,得到一條完整漸變色板。
實(shí)現(xiàn)
源碼

核心代碼:

var primaryEasing = colorEasing(0.6);
this.colorPalette = function(color, index) {
  var currentEasing = colorEasing(index * 0.1);
  // return light colors after tint
  if (index <= 6) {
    return tinycolor.mix(
      '#ffffff',
      color,
      currentEasing * 100 / primaryEasing
    ).toHexString();
  }
  return tinycolor.mix(
    getShadeColor(color),
    color,
    (1 - (currentEasing - primaryEasing) / (1 - primaryEasing)) * 100
  ).toHexString();
};
使用了一個(gè)叫 tinycolor 的庫, mix 方法與上面介紹的 mix 方法類似,也是傳入三個(gè)參數(shù):(目標(biāo)色值,初始色值,比例),不同的是第三個(gè)參數(shù)是 0-100 的一個(gè)數(shù)字,因此計(jì)算比例后還需 *100 來符合參數(shù)單位要求。

這里的 colorEasing 使用了另一個(gè)庫 bezier-easing 用于建立一條貝塞爾曲線并從中取值,在源碼中我看到了獲取貝塞爾曲線的四個(gè)參數(shù)為  (0.26, 0.09, 0.37, 0.18) ,生成的曲線如下圖,基本上與 k=1 的曲線區(qū)別不大,我覺得作者可能是想調(diào)整的是1號色、2號色這樣的淺色更淺,其實(shí)這樣的調(diào)整很細(xì)微,達(dá)到一個(gè)大家都滿意的色值即可(即文檔里說的“經(jīng)過設(shè)計(jì)師和程序員的精心調(diào)教”):



與淺色混合依然與純白色混合,但與深色混合的時(shí)候與 1.x 版本不同,沒有使用純黑,而是區(qū)別冷暖色進(jìn)行不同程度的加深與色相值的旋轉(zhuǎn):2.x 版的色板使用了 HSL 模型,“旋轉(zhuǎn)”這個(gè)詞很有趣:在 HSL 模型中 “H” 表示色相,即色彩名稱,下圖是 HSL 模型的 3D 模型圖,可以看到圖 (a) 中 HSL 圓柱坐標(biāo)系中,繞圓柱中軸線旋轉(zhuǎn)的角度(Hue 色相值)就是顏色種類的調(diào)整:



var warmDark = 0.5;     // warm color darken radio
var warmRotate = -26;  // warm color rotate degree
var coldDark = 0.55;     // cold color darken radio
var coldRotate = 10;   // cold color rotate degree
// 暖色,則旋轉(zhuǎn) HSL 色輪,使顏色更暖
if (shadeColor.toRgb().r > shadeColor.toRgb().b) {
  return shadeColor.darken(shadeColor.toHsl().l * warmDark * 100).spin(warmRotate).toHexString();
}
// 冷色,則旋轉(zhuǎn) HSL 色輪,使顏色更冷
return shadeColor.darken(shadeColor.toHsl().l * coldDark * 100).spin(coldRotate).toHexString();
首先判斷冷暖色,RGB模型中,紅色是暖色,藍(lán)色是冷色,綠色是中性色,因此用 r 與 b 的值判斷冷暖色;
然后根據(jù)冷暖色進(jìn)行不同程度的加深與色相值的旋轉(zhuǎn)。





評價(jià)
Ant Design 2.x 使用了 HSL 模型、貝塞爾曲線等復(fù)雜的邏輯對色彩進(jìn)行漸變,得到完整的漸變色板,相比 1.x 版本來說,色彩過渡更加平滑,添加了冷暖色的細(xì)節(jié)處理。但實(shí)現(xiàn)邏輯較為復(fù)雜,難以理解,事實(shí)上作者也在代碼注釋里開了個(gè)玩笑說沒人看得懂(他還賣上萌了):

// We create a very complex algorithm which take the place of original tint/shade color system
// to make sure no one can understand it ??
其實(shí) 2.x 的算法也有一些缺憾:與 1.x 版本相同,我不認(rèn)為應(yīng)該以純白色作為淺色漸變的終點(diǎn);實(shí)現(xiàn)算法過于復(fù)雜,難以維護(hù)。

五、Ant Design 3.x 色板生成算法
Ant Design 3.x 色彩部分,相對于第二版,增加了幾種主色,整個(gè)色板的飽和度更高,色板生成算法進(jìn)行了重構(gòu),不再使用貝塞爾曲線。

原理
Ant Design 3.x 使用了 HSV 模型,對于 HSV 還是 HSL 更適合于人類用戶界面是有爭議的,這里不做討論。

3.x 版本沒有繼續(xù)使用與某個(gè)淺色/深色值進(jìn)行混合的形式獲取漸變色板,而是直接用 HSV 模型的值進(jìn)行遞減/遞增得到完整漸變色板,不知為何 HSL 更換成 HSV ,可能是便于計(jì)算。

實(shí)現(xiàn)
源碼

3.x 色板生成算法的實(shí)現(xiàn)很簡潔優(yōu)雅:

function main(color, index) {
    var isLight = index <= 6;
    var hsv = tinycolor(color).toHsv();
    var i = isLight ? lightColorCount + 1 - index : index - lightColorCount - 1;
    // i 為index與6的相對距離
    console.log(hsv)
    return tinycolor({
        h: getHue(hsv, i, isLight),
        s: getSaturation(hsv, i, isLight),
        v: getValue(hsv, i, isLight),
    }).toHexString();
};
根據(jù)顏色值、色號與主色色號(6)差的絕對值、減淡/加深這三個(gè)參數(shù)獲取漸變后的色值,其中 HSV 的三個(gè)值分別經(jīng)過了漸變調(diào)整:

// getHue 獲取色相漸變
var hueStep = 2;
if (hsv.h >= 60 && hsv.h <= 240) {
    // 冷色調(diào)
    // 減淡變亮 色相順時(shí)針旋轉(zhuǎn) 更暖
    // 加深變暗 色相逆時(shí)針旋轉(zhuǎn) 更冷
    hue = isLight ? hsv.h - hueStep * i : hsv.h + hueStep * i;
} else {
    // 暖色調(diào)
    // 減淡變亮 色相逆時(shí)針旋轉(zhuǎn) 更暖
    // 加深變暗 色相順時(shí)針旋轉(zhuǎn) 更冷
    hue = isLight ? hsv.h + hueStep * i : hsv.h - hueStep * i;
}
“Hue”(色相)的漸變核心代碼如上,首先判斷冷暖色調(diào),與 2.x 版本不同的是,不再根據(jù) rgb 中 r 與 b 的大小關(guān)系判斷冷暖色調(diào),而是根據(jù) Hue 色相判斷,對于冷暖色調(diào)在減淡與加深的時(shí)候進(jìn)行不同的處理,如冷色調(diào)減淡的時(shí)候變亮的同時(shí)色相更暖,這樣更符合人們對于色彩的認(rèn)知:












// getSaturation 獲取飽和度漸變
var saturationStep = 16;
var saturationStep2 = 5;
var darkColorCount = 4;
if (isLight) {
    // 減淡變亮 飽和度迅速降低
    saturation = Math.round(hsv.s * 100) - saturationStep * i;
} else if (i == darkColorCount) {
    // 加深變暗-最暗 飽和度提高
    saturation = Math.round(hsv.s * 100) + saturationStep;
} else {
    // 加深變暗 飽和度緩慢提高
    saturation = Math.round(hsv.s * 100) + saturationStep2 * i;
}
“Saturation”飽和度的漸變核心代碼如上,對于減淡與加深的飽和度進(jìn)行了不同的處理,其中減淡遞減的值更大,說明減淡的過程中飽和度迅速下降,而由于主色的飽和度一般較高,因此加深的時(shí)候飽和度不必增張過快,尤其是最深的顏色,進(jìn)行了特殊處理,使得 9 號色與 10 號色的飽和度相差無幾。

// getValue 獲取明度漸變
var brightnessStep1 = 5;
var brightnessStep2 = 15;
var getValue = function (hsv, i, isLight) {
    if (isLight) {
        // 減淡變亮
        return Math.round(hsv.v * 100) + brightnessStep1 * i;
    }
    // 加深變暗幅度更大
    return Math.round(hsv.v * 100) - brightnessStep2 * i;
};
“Value”明度的漸變核心代碼如上,對于減淡與加深的明度進(jìn)行了不同的處理,其中加深遞減的值更大,說明加深的過程中明度迅速下降,這是由于主色的明度一般較高,因此減淡的時(shí)候明度不宜增長過多。

評價(jià)
綜合來看 3.x 色板生成算法的實(shí)現(xiàn)可以看到,主色的選取很重要,一般主色選取飽和度較高、明度較高的顏色才能更好地匹配這個(gè)色板生成算法。

3.x 版本舍棄了與某個(gè)淺色/深色值進(jìn)行混合的形式獲取漸變色板的方式,而是直接對 HSV 的三個(gè)值進(jìn)行遞減/遞增,這樣做使得代碼容易理解,但是也有一些弊端,比如上面提到了,飽和度遞減的值/明度遞減的值很大,這對于主設(shè)計(jì)師對主色的正確選取的要求很高:

如果主色的飽和度過低,則漸變色板減淡的部分飽和度迅速遞減,1/2/3色號相差無幾,而加深部分飽和度增長緩慢顯得色板不夠飽和(如下圖左側(cè));
如果主色的明度過低,則漸變色板加深的部分明度迅速遞減,9/10色號相差無幾,而減淡部分由于明度增長緩慢顯得顏色過深(如下圖右側(cè)):



六、Ant Design 色板生成算法現(xiàn)存的問題
雖然經(jīng)歷了幾個(gè)版本的迭代,但是我還是覺得不夠完美,有可能是 Ant Design 本身不完善,也有可能是我理解得不到位,暫時(shí)記錄在這里供大家討論:

Ant Design 文檔介紹說,第 6 格色彩單元格普遍滿足4.5:1 最小對比度(AA 級),但是我發(fā)現(xiàn)部分主色的相對對比度不滿足 4.5:1 標(biāo)準(zhǔn),比如色號同為 6 的醬紫與日出(黃色)兩種顏色,黃色的對比度過低導(dǎo)致文本難以識別:



注:在由淺至深的色板里,第 6 格色彩單元格普遍滿足 WCAG 2.0 的 4.5:1 最小對比度(AA 級),我們將其定義為色板的默認(rèn)品牌色。

同色號的不同顏色差異過大,尤其經(jīng)過漸變色板放大這個(gè)差異后,是否不再適合搭配使用?例如下圖中同色號的醬紫色的明度低于日出色(黃色):



七、感悟
Ant Design 一直在探索更優(yōu)雅的色板生成算法,經(jīng)歷幾次迭代后發(fā)展越來越好。作為前端工程師,我很欣喜地看到技術(shù)對于用戶體驗(yàn)優(yōu)化的實(shí)踐,很欣賞這種科學(xué)定義設(shè)計(jì)的方式。路漫漫其修遠(yuǎn)兮,吾將上下而求索,希望前端對于用戶體驗(yàn)?zāi)苡懈嗟乃伎寂c實(shí)踐。


作者:董文博

歡迎關(guān)注微信公眾號 :前端晚間課

更多文章,收錄于小程序-互聯(lián)網(wǎng)小兵