关于调色板
调色板本来是混合各种颜色颜料使用的板,在 Ant Design 中,调色板指的是一份颜色表(如下图),颜色表由一系列具有一定代表性的基本色彩及它们的渐变色组成,我们可以在调色板中寻找需要的颜色并获取颜色值
HSV色彩空间
- 色相 (H) 是色彩的基本属性,就是平常所说的颜色名称,如红色、黄色等
- 饱和度 (S) 是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数组
- 明度 (V) 取0-100%
如下图:
Ant Design 色板生成算法
目录
@ant-design/colors
├── src
│ ├── index.ts
│ ├── generate.ts
└── ...
定义 index.ts 设置预设颜色
1.首先我们需要先定义一些预设的颜色属性用于生成衍生颜色
// 1.设置一些预设颜色
const presetPrimaryColors: Record<string, string> = {
red: "#F44336",
};
2.循环遍历预设颜色对象
循环遍历预设颜色对象 presetPrimaryColors 触发 generate 方法生成由预设颜色衍生出的颜色,generate 生成5个浅色数量和4个深色数量,预设的颜色处于数组中间为第5个,衍生出的颜色数组为 [...5个浅色,预设颜色,...4个深色]
export type PalettesProps = Record<string, string[] & { primary?: string }>;
// 存储所有由预设衍生出的调色板
const presetPalettes: PalettesProps = {};
// 2.遍历预设颜色通过 generate 生成从原色衍生出来的颜色
Object.keys(presetPrimaryColors).forEach((key): void => {
// 预设调色板从原色衍生出来的颜色
presetPalettes[key] = generate(presetPrimaryColors[key]);
// 设置基础颜色
presetPalettes[key].primary = presetPalettes[key][5];
});
3.最后导出调色板对象
// 3.导出对应颜色的所有衍生颜色
const red = presetPalettes.red;
export {
red,
};
最终index.ts 如下
// 生成衍生颜色方法
import generate from "./generate";
export type PalettesProps = Record<string, string[] & { primary?: string }>;
// 1.设置一些预设颜色
const presetPrimaryColors: Record<string, string> = {
red: "#F44336",
};
// 存储所有由预设衍生出的调色板
const presetPalettes: PalettesProps = {};
// 2.遍历预设颜色通过 generate 生成从原色衍生出来的颜色
Object.keys(presetPrimaryColors).forEach((key): void => {
// 预设调色板从原色衍生出来的颜色
presetPalettes[key] = generate(presetPrimaryColors[key]);
// 设置基础颜色
presetPalettes[key].primary = presetPalettes[key][5];
});
// 3.导出对应颜色的所有衍生颜色
const red = presetPalettes.red;
export {
red,
};
定义 generate.ts 生成衍生颜色方法
必须引入插件 @ctrl/tinycolor 该库是一个用于颜色操作和转换的小型库
import { inputToRGB, rgbToHex, rgbToHsv } from "@ctrl/tinycolor";
起点
首先我们知道 generate 需要根据传入的 预设颜色 返回一个衍生颜色数组来给调色板
所以代码如下:
export default function generate(color: string): string[] {
// 生成的衍生颜色数组
const patterns: string[] = [];
// 返回生成的衍生颜色数组
return patterns;
}
由于使用的是 HSV 所有我们知道 patterns 里面的每个值应该是一个 HSV颜色,由于需要渲染到页面有更好的兼容所以我们需要把 HSV 转换成 RGB 再转换成 Hex十六进制
所以我们知道应该怎么做了
- 将传入的颜色
color先转换为RGB - 然后我们给定 浅色数量为5个
- 循环遍历 浅色数量 把传入的颜色传转换为 HSV 遍历计算生成对应的浅色颜色,再把浅色颜色转换为
RGB再转换为Hex十六进制,再插入patterns衍生颜色数组 - 插入传入的颜色为数组第5个
- 循环遍历 s深色数量 把传入的颜色传转换为 HSV 遍历计算生成对应的深色颜色,再把深色颜颜色转换为
RGB再转换为Hex十六进制,再插入patterns衍生颜色数组
import { inputToRGB, rgbToHex, rgbToHsv } from "@ctrl/tinycolor";
// 浅色数量插入到主色上
const lightColorCount = 5;
// 深色数量插入到主色下
const darkColorCount = 4;
export default function generate(color: string): string[] {
// 生成的衍生颜色数组
const patterns: string[] = [];
// 将传入的颜色 `color` 先转换为 `RGB`
const rgbColor = inputToRGB(color);
for (let i = lightColorCount; i > 0; i -= 1) {
// 把传入的颜色转换为HSV
const hsv = toHsv(rgbColor);
// 将 RGB 转为 十六进制
const colorString: string = toHex(
// HSV 转换为 RGB
inputToRGB({
// 色相
h: getHue(hsv, i, true),
// 饱和度
s: getSaturation(hsv, i, true),
// 亮度
v: getValue(hsv, i, true),
})
);
// 插入数组
patterns.push(colorString);
}
// 插入传入的颜色为数组第5个
patterns.push(toHex(rgbColor));
// 深色
for (let i = 1; i <= darkColorCount; i += 1) {
// 把传入的颜色转换为HSV
const hsv = toHsv(rgbColor);
// 将 RGB 转为 十六进制
const colorString: string = toHex(
// HSV 转换为 RGB
inputToRGB({
// 色相
h: getHue(hsv, i),
// 饱和度
s: getSaturation(hsv, i),
// 亮度
v: getValue(hsv, i),
})
);
patterns.push(colorString);
}
// 返回生成的衍生颜色数组
return patterns;
}
1.计算色相
首先生成 HSV 涉及到3个属性 色相、饱和度、亮度,我们可以通过不同的计算来生成对应的值,先来看 getHue 生成色相如何去计算
- 根据色相不同,色相转向不同计算
hue值 - 设置冷色调颜色,冷色调有两种情况 (减淡变亮 色相顺时针旋转 更暖) 和 (加深变暗 色相逆时针旋转 更冷)
- 设置暖色调颜色,暖色调也有两种情况 (减淡变亮 色相逆时针旋转 更暖) 和 (加深变暗 色相顺时针旋转 更冷)
- 最后是将hue规范化到位于0到360°之间避免超出范围
- 返回
hue
function getHue(hsv: HsvObject, i: number, light?: boolean): number {
// 计算后的色调
let hue: number;
// 1.根据色相不同,色相转向不同计算 hue 值
// 2.冷色调
// 减淡变亮 色相顺时针旋转 更暖
// 加深变暗 色相逆时针旋转 更冷
if (Math.round(hsv.h) >= 60 && Math.round(hsv.h) <= 240) {
hue = light
// 减淡变亮 色相顺时针旋转 更暖
? Math.round(hsv.h) - hueStep * i
// 加深变暗 色相逆时针旋转 更冷
: Math.round(hsv.h) + hueStep * i;
}
// 3.暖色调
// 减淡变亮 色相逆时针旋转 更暖
// 加深变暗 色相顺时针旋转 更冷
else {
hue = light
// 减淡变亮 色相逆时针旋转 更暖
? Math.round(hsv.h) + hueStep * i
// 加深变暗 色相顺时针旋转 更冷
: Math.round(hsv.h) - hueStep * i;
}
// 4.将hue规范化到位于0到360°之间
if (hue < 0) {
hue += 360;
} else if (hue >= 360) {
hue -= 360;
}
// 返回计算后的色调
return hue;
}
2.计算饱和度
对于减淡和较深的饱和度进行不同的计算, 其中减淡递减的值更大,说明减淡的过程中饱和度迅速下降,而由于主色的饱和度一般较高,因此加深的时候饱和度不必增长过快,尤其是最深的颜色,进行了特殊处理
计算过程如下:
- 判断是否是灰色不改变饱和度
- 设置饱和度变量
- 减淡变亮 饱和度迅速降低
- 加深变暗-最暗 饱和度提高
- 加深变暗 饱和度缓慢提高
- 边界值修正避免超过
1 - 判断 如果是减淡变亮
&&到达浅色上限&&有饱和度 将饱和度重置为0 - 最小为
0.06避免饱和度太小 - 返回设置饱和度变量
// 饱和度
function getSaturation(hsv: HsvObject, i: number, light?: boolean): number {
// 1.判断是否是灰色 不改变饱和度
if (hsv.h === 0 && hsv.s === 0) {
return hsv.s;
}
// 2.设置饱和度变量
let saturation: number;
// 3.减淡变亮 饱和度迅速降低
if (light) {
// s - 0.16 * i
saturation = hsv.s - saturationStep * i;
}
// 4.加深变暗-最暗 饱和度提高
else if (i === darkColorCount) {
// s - 0.16 * i
saturation = hsv.s + saturationStep;
}
// 5.加深变暗 饱和度缓慢提高
else {
// s - 0.05 * i
saturation = hsv.s + saturationStep2 * i;
}
// 6.边界值修正避免超过 1
if (saturation > 1) {
saturation = 1;
}
// 7.减淡变亮 && 到达浅色上限 && 有饱和度 将饱和度重置为0
if (light && i === lightColorCount && saturation > 0.1) {
saturation = 0.1;
}
// 8.最小为 0.06 避免饱和度太小
if (saturation < 0.06) {
saturation = 0.06;
}
// 返回设置饱和度变量
return Number(saturation.toFixed(2));
}
3.计算亮度
对于减淡与加深的明度进行了不同的处理,其中加深递减的值更大,说明加深的过程中明度迅速下降,这是由于主色的明度一般较高,因此减淡的时候明度不宜增长过多
计算过程如下:
- 判断减淡变亮
- 判断加深变暗幅度更大
- 设置最大值为1
// 亮度
function getValue(hsv: HsvObject, i: number, light?: boolean): number {
// 亮度值
let value: number;
// 1.判断减淡变亮
if (light) {
// v + 0.05 * i
value = hsv.v + brightnessStep1 * i;
}
// 2.判断加深变暗幅度更大
else {
// v + 0.15 * i
value = hsv.v - brightnessStep2 * i;
}
// 3.设置最大值为1
if (value > 1) {
value = 1;
}
// 返回亮度值
return Number(value.toFixed(2));
}
完整代码如下
import { inputToRGB, rgbToHex, rgbToHsv } from "@ctrl/tinycolor";
// type
interface HsvObject {
h: number;
s: number;
v: number;
}
interface RgbObject {
r: number;
g: number;
b: number;
}
// 浅色数量插入到主色上
const lightColorCount = 5;
// 深色数量插入到主色下
const darkColorCount = 4;
// 饱和度阶梯,浅色部分
const saturationStep = 0.16;
// 饱和度阶梯,深色部分
const saturationStep2 = 0.05;
// 亮度阶梯,浅色部分
const brightnessStep1 = 0.05;
// 亮度阶梯,深色部分
const brightnessStep2 = 0.15;
// 色相渐变
const hueStep = 2;
// 从 TinyColor.prototype.toHsv 移植的函数
// Keep it here because of `hsv.h * 360`
function toHsv({ r, g, b }: RgbObject): HsvObject {
const hsv = rgbToHsv(r, g, b);
return { h: hsv.h * 360, s: hsv.s, v: hsv.v };
}
// 从 TinyColor.prototype.toHexString 移植的函数
// Keep it here because of the prefix `#`
function toHex({ r, g, b }: RgbObject): string {
// 将 RGB 颜色转换为十六进制
return `#${rgbToHex(r, g, b, false)}`;
}
// 色相
function getHue(hsv: HsvObject, i: number, light?: boolean): number {
// 计算后的色调
let hue: number;
// 1.根据色相不同,色相转向不同计算 hue 值
// 2.设置冷色调颜色
// 减淡变亮 色相顺时针旋转 更暖
// 加深变暗 色相逆时针旋转 更冷
if (Math.round(hsv.h) >= 60 && Math.round(hsv.h) <= 240) {
hue = light
? Math.round(hsv.h) - hueStep * i
: Math.round(hsv.h) + hueStep * i;
}
// 3.设置暖色调颜色
// 减淡变亮 色相逆时针旋转 更暖
// 加深变暗 色相顺时针旋转 更冷
else {
hue = light
? Math.round(hsv.h) + hueStep * i
: Math.round(hsv.h) - hueStep * i;
}
// 4.将hue规范化到位于0到360°之间
if (hue < 0) {
hue += 360;
} else if (hue >= 360) {
hue -= 360;
}
return hue;
}
// 饱和度
function getSaturation(hsv: HsvObject, i: number, light?: boolean): number {
// 1.判断是否是灰色 不改变饱和度
if (hsv.h === 0 && hsv.s === 0) {
return hsv.s;
}
// 2.设置饱和度变量
let saturation: number;
// 3.减淡变亮 饱和度迅速降低
if (light) {
// s - 0.16 * i
saturation = hsv.s - saturationStep * i;
}
// 4.加深变暗-最暗 饱和度提高
else if (i === darkColorCount) {
// s - 0.16 * i
saturation = hsv.s + saturationStep;
}
// 5.加深变暗 饱和度缓慢提高
else {
// s - 0.05 * i
saturation = hsv.s + saturationStep2 * i;
}
// 6.边界值修正避免超过 1
if (saturation > 1) {
saturation = 1;
}
// 7.判断 如果是减淡变亮 && 到达浅色上限 && 有饱和度 将饱和度重置为0
if (light && i === lightColorCount && saturation > 0.1) {
saturation = 0.1;
}
// 8.最小为 0.06 避免饱和度太小
if (saturation < 0.06) {
saturation = 0.06;
}
// 返回设置饱和度变量
return Number(saturation.toFixed(2));
}
// 亮度
function getValue(hsv: HsvObject, i: number, light?: boolean): number {
// 亮度值
let value: number;
// 1.判断减淡变亮
if (light) {
// v + 0.05 * i
value = hsv.v + brightnessStep1 * i;
}
// 2.判断加深变暗幅度更大
else {
// v + 0.15 * i
value = hsv.v - brightnessStep2 * i;
}
// 3.设置最大值为1
if (value > 1) {
value = 1;
}
// 返回亮度值
return Number(value.toFixed(2));
}
export default function generate(color: string): string[] {
// 生成的衍生颜色数组
const patterns: string[] = [];
// 将传入的颜色 `color` 先转换为 `RGB`
const rgbColor = inputToRGB(color);
//
for (let i = lightColorCount; i > 0; i -= 1) {
// 把传入的颜色转换为HSV
const hsv = toHsv(rgbColor);
// 将 RGB 转为 十六进制
const colorString: string = toHex(
// HSV 转换为 RGB
inputToRGB({
// 色相
h: getHue(hsv, i, true),
// 饱和度
s: getSaturation(hsv, i, true),
// 亮度
v: getValue(hsv, i, true),
})
);
// 插入数组
patterns.push(colorString);
}
// 插入传入的颜色为数组第5个
patterns.push(toHex(rgbColor));
// 深色
for (let i = 1; i <= darkColorCount; i += 1) {
// 把传入的颜色转换为HSV
const hsv = toHsv(rgbColor);
// 将 RGB 转为 十六进制
const colorString: string = toHex(
// HSV 转换为 RGB
inputToRGB({
// 色相
h: getHue(hsv, i),
// 饱和度
s: getSaturation(hsv, i),
// 亮度
v: getValue(hsv, i),
})
);
patterns.push(colorString);
}
// 返回生成的衍生颜色数组
return patterns;
}
运行测试用例
最后运行测试用例查看输出衍生颜色是否正确
export.test.ts
import { red, presetPalettes} from "../src";
const presetRedColors = [
"#fff3f0",
"#ffe2db",
"#ffbfb3",
"#ff998a",
"#ff7161",
"#f44336",
"#cf2923",
"#a81414",
"#82090d",
"#5c060b",
].map((color) => color.toLowerCase());
test(`red colors'`, () => {
expect(red.length).toEqual(10);
expect([...presetPalettes.red]).toEqual(presetRedColors);
});
测试用例通过,说明正确的创建了颜色
总结
以上就是 @ant-design/colors 的浅色部分解析,对于暗色主题,原理是一样的,通过只不过中间的计算过程不一样
更多可以点击查看 generate源码地址 里面的代码已经添加详细的说明👇
例如: