AntDesign5x 色板算法 @ant-design/colors 解析

2,626 阅读7分钟

关于调色板

调色板本来是混合各种颜色颜料使用的板,在 Ant Design 中,调色板指的是一份颜色表(如下图),颜色表由一系列具有一定代表性的基本色彩及它们的渐变色组成,我们可以在调色板中寻找需要的颜色并获取颜色值

image.png

HSV色彩空间

HSV即色相饱和度明度

  • 色相 (H) 是色彩的基本属性,就是平常所说的颜色名称,如红色、黄色等
  • 饱和度 (S) 是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数组
  • 明度 (V) 取0-100%

如下图:

image.png

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十六进制

所以我们知道应该怎么做了

  1. 将传入的颜色 color 先转换为 RGB
  2. 然后我们给定 浅色数量为5个
  3. 循环遍历 浅色数量 把传入的颜色传转换为 HSV 遍历计算生成对应的浅色颜色,再把浅色颜色转换为 RGB 再转换为 Hex十六进制,再插入 patterns 衍生颜色数组
  4. 插入传入的颜色为数组第5个
  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 生成色相如何去计算

  1. 根据色相不同,色相转向不同计算 hue
  2. 设置冷色调颜色,冷色调有两种情况 (减淡变亮 色相顺时针旋转 更暖)(加深变暗 色相逆时针旋转 更冷)
  3. 设置暖色调颜色,暖色调也有两种情况 (减淡变亮 色相逆时针旋转 更暖)(加深变暗 色相顺时针旋转 更冷)
  4. 最后是将hue规范化到位于0到360°之间避免超出范围
  5. 返回 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. 判断是否是灰色不改变饱和度
  2. 设置饱和度变量
  3. 减淡变亮 饱和度迅速降低
  4. 加深变暗-最暗 饱和度提高
  5. 加深变暗 饱和度缓慢提高
  6. 边界值修正避免超过 1
  7. 判断 如果是减淡变亮 && 到达浅色上限 && 有饱和度 将饱和度重置为0
  8. 最小为 0.06 避免饱和度太小
  9. 返回设置饱和度变量
// 饱和度
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. 判断减淡变亮
  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);
});

image.png

测试用例通过,说明正确的创建了颜色

总结

以上就是 @ant-design/colors 的浅色部分解析,对于暗色主题,原理是一样的,通过只不过中间的计算过程不一样

更多可以点击查看 generate源码地址 里面的代码已经添加详细的说明👇

例如:

image.png

相关资料

包含注释的源码地址

官方源码地址

HSL和HSV色彩空间

Ant Design 色板生成算法演进之路