JS 关于颜色的相关处理

项目开发中,需要根据网站设定的主题色来动态更改网站的所有配色,例如文字颜色,鼠标 hover 状态的按钮颜色,这就涉及到对颜色的相关处理,接下来我们就讲解下如何处理。

# 颜色的转换

颜色的表示方式目前主要有三种,rgba, hex 和 hls。这三种表示方式可以互相转换。主要是 hex 和 hls 进行转换的公式比较复杂,网上有现成的示例代码可以参考。

# 判断颜色是深色还是浅色

W3C 提供了官方的说明,把 RGB 格式的颜色转换成 YIQ 格式,然后通过 brightness 数值来进行判断。

  isDark() {
    const { R, G, B } = this.rgbColor;
    // ref https://www.w3.org/TR/AERT/#color-contrast
    const brightness = (R * 299 + G * 587 + B * 114) / 1000;
    return brightness < 128;
  }
1
2
3
4
5
6

# 计算对比度

W3C 提供了官方的对比度计算公式,对比度大于等于 4.5 属于 AA,对比度大于等于 7.1 属于 AAA。至少要达到 AA 才能有足够的对比度。

# 调节亮度

把颜色格式转换成 HLS, L 就代表亮度,调节这个数值就可以了。

# 完整代码

export interface RGBColor {
  R: number;
  G: number;
  B: number;
}

export interface HSLColor {
  H: number;
  S: number;
  L: number;
}

export type ColorType = string | RGBColor | HSLColor;

export class Color {
  rgbColor: RGBColor;

  constructor(color: ColorType) {
    if (this.isString(color)) {
      this.rgbColor = this.stringToRGB(color);
    } else if (this.isHSLColor(color)) {
      this.rgbColor = this.HSLToRGB(color);
    } else {
      this.rgbColor = color;
    }
  }

  get hexColor() {
    return this.RGBToHex(this.rgbColor);
  }
  get hslColor() {
    return this.RGBToHSL(this.rgbColor);
  }

  isString(color: string | RGBColor | HSLColor): color is string {
    return typeof color === 'string';
  }

  isRGBColor(color: string | RGBColor | HSLColor): color is RGBColor {
    return (color as RGBColor).R !== undefined;
  }

  isHSLColor(color: string | RGBColor | HSLColor): color is HSLColor {
    return (color as HSLColor).H !== undefined;
  }

  RGBToHex(rgbColor: RGBColor) {
    const { R, G, B } = rgbColor;
    const hexR = R.toString(16).padStart(2, '0');
    const hexG = G.toString(16).padStart(2, '0');
    const hexB = B.toString(16).padStart(2, '0');

    return '#' + hexR + hexG + hexB;
  }

  stringToRGB(color: string) {
    if (color.length === 4) {
      color =
        color[0] +
        color[1] +
        color[1] +
        color[2] +
        color[2] +
        color[3] +
        color[3];
    }

    const R = Number.parseInt(color.substring(1, 3), 16);
    const G = Number.parseInt(color.substring(3, 5), 16);
    const B = Number.parseInt(color.substring(5, 7), 16);
    return { R, G, B };
  }

  HSLToRGB(hslColor: HSLColor) {
    let { S, L } = hslColor;
    const { H } = hslColor;
    S /= 100;
    L /= 100;

    const C = (1 - Math.abs(2 * L - 1)) * S;
    const X = C * (1 - Math.abs(((H / 60) % 2) - 1));
    const M = L - C / 2;
    let R = 0;
    let G = 0;
    let B = 0;
    if (0 <= H && H < 60) {
      R = C;
      G = X;
      B = 0;
    } else if (60 <= H && H < 120) {
      R = X;
      G = C;
      B = 0;
    } else if (120 <= H && H < 180) {
      R = 0;
      G = C;
      B = X;
    } else if (180 <= H && H < 240) {
      R = 0;
      G = X;
      B = C;
    } else if (240 <= H && H < 300) {
      R = X;
      G = 0;
      B = C;
    } else if (300 <= H && H < 360) {
      R = C;
      G = 0;
      B = X;
    }
    R = Math.round((R + M) * 255);
    G = Math.round((G + M) * 255);
    B = Math.round((B + M) * 255);

    return { R, G, B };
  }

  RGBToHSL(rgbColor: RGBColor) {
    let { R, G, B } = rgbColor;
    R /= 255;
    G /= 255;
    B /= 255;

    const cmin = Math.min(R, G, B);
    const cmax = Math.max(R, G, B);
    const delta = cmax - cmin;
    let H, S, L;

    if (delta === 0) {
      H = 0;
    } else if (cmax === R) {
      H = ((G - B) / delta) % 6;
    } else if (cmax == G) {
      H = (B - R) / delta + 2;
    } else {
      H = (R - G) / delta + 4;
    }
    H = Math.round(H * 60);
    if (H < 0) {
      H += 360;
    }
    L = (cmax + cmin) / 2;
    S = delta == 0 ? 0 : delta / (1 - Math.abs(2 * L - 1));
    S = +(S * 100).toFixed(1);
    L = +(L * 100).toFixed(1);

    return { H, S, L };
  }

  isDark() {
    const { R, G, B } = this.rgbColor;
    // ref https://www.w3.org/TR/AERT/#color-contrast
    const brightness = (R * 299 + G * 587 + B * 114) / 1000;
    return brightness < 128;
  }

  isLight() {
    return !this.isDark();
  }

  darken(percent: number, color = this.hslColor) {
    let { L } = color;
    const { H, S } = color;
    L -= L * percent;
    return new Color({ H, S, L });
  }

  lighten(percent: number, color = this.hslColor) {
    let { L } = color;
    const { H, S } = color;
    L += L * percent;
    return new Color({ H, S, L });
  }

  luminance(rgbColor = this.rgbColor) {
    // ref https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
    let { R, G, B } = rgbColor;
    [R, G, B] = [R, G, B].map(color => {
      color = color / 255;
      return color <= 0.03928
        ? color / 12.92
        : Math.pow((color + 0.055) / 1.055, 2.4);
    });
    return 0.2126 * R + 0.7152 * G + 0.0722 * B;
  }

  contrast(bgColor: ColorType = '#fff', contrastColor = this.rgbColor) {
    const color = new Color(bgColor);
    const bgLuminance = this.luminance(color.rgbColor);
    const textLuminance = this.luminance(contrastColor);
    return bgLuminance > textLuminance
      ? (bgLuminance + 0.05) / (textLuminance + 0.05)
      : (textLuminance + 0.05) / (bgLuminance + 0.05);
  }

  level(bgColor: ColorType = '#fff') {
    const color = new Color(bgColor);
    const contrastRatio = this.contrast(color.rgbColor);
    return contrastRatio >= 7.1 ? 'AAA' : contrastRatio >= 4.5 ? 'AA' : '';
  }

  highContrastColor(bgColor: ColorType = '#fff') {
    let contrastColor = new Color(this.rgbColor);
    if (this.isLight()) {
      const color = new Color(bgColor);
      let contrastRatio = this.contrast(color.rgbColor, contrastColor.rgbColor);
      while (contrastRatio < 4.5) {
        contrastColor = contrastColor.darken(0.1);
        contrastRatio = this.contrast(color.rgbColor, contrastColor.rgbColor);
      }
    }
    return contrastColor;
  }

  textColor() {
    if (this.isLight()) {
      return new Color('#000');
    } else {
      return new Color('#fff');
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222

本代码实现了颜色格式的转换,计算颜色属于深色还是浅色,计算两个颜色之间的对比度。给定一个颜色,计算出相应的高对比度颜色,可以用于背景和文本的展示。

# 总结

颜色相关计算有 W3C 提供了标准的计算公式,但是还没有完整的代码库可以参考。希望我总结的代码可以给大家提供参考。

# 参考

Converting Color Spaces in JavaScript (opens new window)

Calculating Color Contrast (opens new window)

Color and contrast accessibility (opens new window)

WebAIM: Contrast Checker (opens new window)

ColorShark – WCAG 2.1 AA and AAA Color Contrast Tool (opens new window)