December 25, 2017
最近我在整理一套团队内部使用的设计规范,其中颜色部分参考了 Ant Design 的 “色彩” 部分,恰逢 Ant Design 发布了 3.0 版本,调色板做了调整,借此机会我学习了一下 Ant Design 迄今为止三个版本色板生成算法的源码,感觉其“确定”设计思想非常值得学习。
“确定” 作为 Ant Design 的设计理念之一,在调色板这一隅发挥得淋漓尽致:用科学定义设计,在设计有迹可循的同时提高了代码的可维护性,减少开发阶段样式代码的不确定性。
Ant Design 三个大版本的色板生成算法各不相同,却一直在完善,本文对其三个版本的色板生成算法进行解读,聊一聊我的体会。
在本文中我会使用一些名词,如果你不知道这些词的含义建议先简单了解一下:
在电脑图形学中,调色板(英语:Palette)要么是指用于数字图像管理的给定有限颜色组(颜色表),要么是显示屏上一组有限选择的小图形单元(诸如“工具选板”)。
引用自维基百科 调色板 (电脑图形学)
调色板本来是混合各种颜色颜料使用的板,在 Ant Design 中,调色板指的是一份颜色表(如下图),颜色表由一系列具有一定代表性的基本色彩及它们的渐变色组成,我们可以在调色板中寻找需要的颜色并获取颜色值。
设计师与程序员都需要使用调色板工具,以 Ant Design 为例,设计师需要根据调色板上的色值来进行设计稿的制作,而程序员在缺乏设计稿的时候也可以直接在调色板上取色。
一般来说在进行设计稿制作的时候,直接使用 Ant Design 的一种基本色彩或与基本色彩相近的颜色作为主色,主色的渐变色可以用于组件的特殊状态,如 hover/active 状态。
Ant Design 的调色板由一系列具有一定代表性的基本色彩及它们的渐变色组成,其中基本色彩可以由主设计师来钦定,其渐变色由色板生成算法计算得到。
Ant Design 1.x 色彩部分,第一版的实现较为简单,这部分主要介绍了:
选取一个主色作为 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 色彩系统”。
这一版本我在 github 上没看到色彩生成算法的代码,后来我 google 到了这篇文章:Tint and Shade Functions,作者认为单纯通过改变颜色亮度实现颜色的渐变效果并不理想,于是实现如下:
// 变亮
@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 方法来进行颜色值的混合,只需传入主色色值和百分比即可,使用 less 同理。
这一版的实现简单粗暴,在研究颜色色彩之前,我对渐变色板的第一想法也是这样的实现,后来通过一些调研发现这样实现并不好:
Ant Design 2.x 色彩部分,相对于第一版,第二版的调色板的颜色过渡更加平滑,提供了点击调色板复制颜色值的功能。
经过设计师和程序员的精心调教,结合了色彩加白、加黑、加深,贝塞尔曲线,以及针对冷暖色的不同旋转角度,得出一套色板生成算法(用以取代我们原来的 tint/shade 色彩系统)。使用者只需指定主色,便可导出一条完整的渐变色板。
最初一看这个原理赶紧很复杂,其实不是那么难以理解:
核心代码:
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();
};
使用了一个叫 tinycolor 的库, mix 方法与上面介绍的 mix 方法类似,也是传入三个参数:(目标色值,初始色值,比例),不同的是第三个参数是 0-100 的一个数字,因此计算比例后还需 *100 来符合参数单位要求。
这里的 colorEasing 使用了另一个库 bezier-easing 用于建立一条贝塞尔曲线并从中取值,在源码中我看到了获取贝塞尔曲线的四个参数为 (0.26, 0.09, 0.37, 0.18),生成的曲线如下图,基本上与 k=1 的曲线区别不大,我觉得作者可能是想调整的是1号色、2号色这样的浅色更浅,其实这样的调整很细微,达到一个大家都满意的色值即可(即文档里说的“经过设计师和程序员的精心调教”):
与浅色混合依然与纯白色混合,但与深色混合的时候与 1.x 版本不同,没有使用纯黑,而是区别冷暖色进行不同程度的加深与色相值的旋转:2.x 版的色板使用了 HSL 模型,“旋转”这个词很有趣:在 HSL 模型中 “H” 表示色相,即色彩名称,下图是 HSL 模型的 3D 模型图,可以看到图 (a) 中 HSL 圆柱坐标系中,绕圆柱中轴线旋转的角度(Hue 色相值)就是颜色种类的调整:
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
// 暖色,则旋转 HSL 色轮,使颜色更暖
if (shadeColor.toRgb().r > shadeColor.toRgb().b) {
return shadeColor.darken(shadeColor.toHsl().l * warmDark * 100).spin(warmRotate).toHexString();
}
// 冷色,则旋转 HSL 色轮,使颜色更冷
return shadeColor.darken(shadeColor.toHsl().l * coldDark * 100).spin(coldRotate).toHexString();
Ant Design 2.x 使用了 HSL 模型、贝塞尔曲线等复杂的逻辑对色彩进行渐变,得到完整的渐变色板,相比 1.x 版本来说,色彩过渡更加平滑,添加了冷暖色的细节处理。但实现逻辑较为复杂,难以理解,事实上作者也在代码注释里开了个玩笑说没人看得懂(他还卖上萌了):
// We create a very complex algorithm which take the place of original tint/shade color system
// to make sure no one can understand it 👻
其实 2.x 的算法也有一些缺憾:与 1.x 版本相同,我不认为应该以纯白色作为浅色渐变的终点;实现算法过于复杂,难以维护。
Ant Design 3.x 色彩部分,相对于第二版,增加了几种主色,整个色板的饱和度更高,色板生成算法进行了重构,不再使用贝塞尔曲线。
Ant Design 3.x 使用了 HSV 模型,对于 HSV 还是 HSL 更适合于人类用户界面是有争议的,这里不做讨论。
3.x 版本没有继续使用与某个浅色/深色值进行混合的形式获取渐变色板,而是直接用 HSV 模型的值进行递减/递增得到完整渐变色板,不知为何 HSL 更换成 HSV ,可能是便于计算。
3.x 色板生成算法的实现很简洁优雅:
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();
};
根据颜色值、色号与主色色号(6)差的绝对值、减淡/加深这三个参数获取渐变后的色值,其中 HSV 的三个值分别经过了渐变调整:
// getHue 获取色相渐变
var hueStep = 2;
if (hsv.h >= 60 && hsv.h <= 240) {
// 冷色调
// 减淡变亮 色相顺时针旋转 更暖
// 加深变暗 色相逆时针旋转 更冷
hue = isLight ? hsv.h - hueStep * i : hsv.h + hueStep * i;
} else {
// 暖色调
// 减淡变亮 色相逆时针旋转 更暖
// 加深变暗 色相顺时针旋转 更冷
hue = isLight ? hsv.h + hueStep * i : hsv.h - hueStep * i;
}
“Hue”(色相)的渐变核心代码如上,首先判断冷暖色调,与 2.x 版本不同的是,不再根据 rgb 中 r 与 b 的大小关系判断冷暖色调,而是根据 Hue 色相判断,对于冷暖色调在减淡与加深的时候进行不同的处理,如冷色调减淡的时候变亮的同时色相更暖,这样更符合人们对于色彩的认知:
// 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”饱和度的渐变核心代码如上,对于减淡与加深的饱和度进行了不同的处理,其中减淡递减的值更大,说明减淡的过程中饱和度迅速下降,而由于主色的饱和度一般较高,因此加深的时候饱和度不必增张过快,尤其是最深的颜色,进行了特殊处理,使得 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”明度的渐变核心代码如上,对于减淡与加深的明度进行了不同的处理,其中加深递减的值更大,说明加深的过程中明度迅速下降,这是由于主色的明度一般较高,因此减淡的时候明度不宜增长过多。
综合来看 3.x 色板生成算法的实现可以看到,主色的选取很重要,一般主色选取饱和度较高、明度较高的颜色才能更好地匹配这个色板生成算法。
3.x 版本舍弃了与某个浅色/深色值进行混合的形式获取渐变色板的方式,而是直接对 HSV 的三个值进行递减/递增,这样做使得代码容易理解,但是也有一些弊端,比如上面提到了,饱和度递减的值/明度递减的值很大,这对于主设计师对主色的正确选取的要求很高:
虽然经历了几个版本的迭代,但是我还是觉得不够完美,有可能是 Ant Design 本身不完善,也有可能是我理解得不到位,暂时记录在这里供大家讨论:
Ant Design 文档介绍说,第 6 格色彩单元格普遍满足4.5:1 最小对比度(AA 级),但是我发现部分主色的相对对比度不满足 4.5:1 标准,比如色号同为 6 的酱紫与日出(黄色)两种颜色,黄色的对比度过低导致文本难以识别:
注:在由浅至深的色板里,第 6 格色彩单元格普遍满足 WCAG 2.0 的 4.5:1 最小对比度(AA 级),我们将其定义为色板的默认品牌色。
Ant Design 一直在探索更优雅的色板生成算法,经历几次迭代后发展越来越好。作为前端工程师,我很欣喜地看到技术对于用户体验优化的实践,很欣赏这种科学定义设计的方式。路漫漫其修远兮,吾将上下而求索,希望前端对于用户体验能有更多的思考与实践。