Skip to content

Commit 7bb5341

Browse files
committed
feat(layer): support arbitrary color including hsl, rgb, named color etc
1 parent 105d642 commit 7bb5341

File tree

3 files changed

+267
-39
lines changed

3 files changed

+267
-39
lines changed

packages/anu-vue/src/composables/useLayer.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { MaybeRef } from '@vueuse/core'
22
import { defu } from 'defu'
33
import type { ComponentObjectPropsOptions } from 'vue'
44
import { ref, unref, watch } from 'vue'
5-
import { contrast } from '@/utils/color'
5+
import { getContrastColor } from '@/utils/color'
66
import { color } from '@/composables/useProps'
77
import type { ColorProp } from '@/composables/useProps'
88

@@ -61,8 +61,7 @@ export const useLayer = () => {
6161
: '',
6262
]
6363

64-
const isThemeColor = propColor && ['primary', 'success', 'info', 'warning', 'danger'].includes(propColor)
65-
const isHexColor = propColor && /^#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}$/.test(propColor)
64+
const isThemeColor = propColor && (['primary', 'success', 'info', 'warning', 'danger'] as ColorProp[]).includes(propColor)
6665

6766
// 👉 Styles
6867
const styles = []
@@ -71,9 +70,9 @@ export const useLayer = () => {
7170
if (!isThemeColor) {
7271
styles.push({ '--a-layer-color': propColor })
7372

74-
// If color isn't theme color & is HEX color => Calculate contrast color => Assign it to `--a-layer-text`
75-
if (isHexColor) {
76-
const contrastColor = contrast(propColor)
73+
if (propColor) {
74+
// If color isn't theme color & is HEX color => Calculate contrast color => Assign it to `--a-layer-text`
75+
const contrastColor = getContrastColor(propColor as string)
7776

7877
styles.push(`--a-layer-text: ${contrastColor}`)
7978
styles.push(`--un-ring-color: ${propColor}`)

packages/anu-vue/src/composables/useProps.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { createDefu } from 'defu'
22
import type { PropType } from 'vue'
33
import type { ConfigurableValue } from '@/composables/useConfigurable'
4+
import type { NamedColors } from '@/utils/color'
45

56
// ℹ️ We need to move this to some better places
67
export type LooseAutocomplete<T extends string> = T | Omit<string, T>
78

89
export const themeColors = ['primary', 'success', 'info', 'warning', 'danger'] as const
910
export type ThemeColor = typeof themeColors[number]
10-
export type ColorProp = LooseAutocomplete<ThemeColor> | undefined
11+
export type ColorProp = LooseAutocomplete<ThemeColor | NamedColors> | undefined
1112

1213
export const color = {
1314
type: [String, undefined] as PropType<ColorProp>,

packages/anu-vue/src/utils/color.ts

Lines changed: 260 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,271 @@
1-
// Thanks: https://betterprogramming.pub/generate-contrasting-text-for-your-random-background-color-ac302dc87b4
2-
// Thanks: https://css-tricks.com/converting-color-spaces-in-javascript/
3-
interface RGB {
4-
b: number
5-
g: number
6-
r: number
7-
}
1+
// ℹ️ Extracted from https://developer.mozilla.org/en-US/docs/Web/CSS/named-color
2+
export const namedColors = Object.freeze({
3+
// CSS Level 1
4+
black: '#000000',
5+
silver: '#c0c0c0',
6+
gray: '#808080',
7+
white: '#ffffff',
8+
maroon: '#800000',
9+
red: '#ff0000',
10+
purple: '#800080',
11+
fuchsia: '#ff00ff',
12+
green: '#008000',
13+
lime: '#00ff00',
14+
olive: '#808000',
15+
yellow: '#ffff00',
16+
navy: '#000080',
17+
blue: '#0000ff',
18+
teal: '#008080',
19+
aqua: '#00ffff',
820

9-
const rgbToYIQ = ({ r, g, b }: RGB): number => {
10-
return ((r * 299) + (g * 587) + (b * 114)) / 1000
11-
}
21+
// CSS Level 2
22+
orange: '#ffa500',
23+
24+
// CSS Level 3
25+
aliceblue: '#f0f8ff',
26+
antiquewhite: '#faebd7',
27+
aquamarine: '#7fffd4',
28+
azure: '#f0ffff',
29+
beige: '#f5f5dc',
30+
bisque: '#ffe4c4',
31+
blanchedalmond: '#ffebcd',
32+
blueviolet: '#8a2be2',
33+
brown: '#a52a2a',
34+
burlywood: '#deb887',
35+
cadetblue: '#5f9ea0',
36+
chartreuse: '#7fff00',
37+
chocolate: '#d2691e',
38+
coral: '#ff7f50',
39+
cornflowerblue: '#6495ed',
40+
cornsilk: '#fff8dc',
41+
crimson: '#dc143c',
42+
43+
// synonym of aqua
44+
cyan: '#00ffff',
45+
46+
darkblue: '#00008b',
47+
darkcyan: '#008b8b',
48+
darkgoldenrod: '#b8860b',
49+
darkgray: '#a9a9a9',
50+
darkgreen: '#006400',
51+
darkgrey: '#a9a9a9',
52+
darkkhaki: '#bdb76b',
53+
darkmagenta: '#8b008b',
54+
darkolivegreen: '#556b2f',
55+
darkorange: '#ff8c00',
56+
darkorchid: '#9932cc',
57+
darkred: '#8b0000',
58+
darksalmon: '#e9967a',
59+
darkseagreen: '#8fbc8f',
60+
darkslateblue: '#483d8b',
61+
darkslategray: '#2f4f4f',
62+
darkslategrey: '#2f4f4f',
63+
darkturquoise: '#00ced1',
64+
darkviolet: '#9400d3',
65+
deeppink: '#ff1493',
66+
deepskyblue: '#00bfff',
67+
dimgray: '#696969',
68+
dimgrey: '#696969',
69+
dodgerblue: '#1e90ff',
70+
firebrick: '#b22222',
71+
floralwhite: '#fffaf0',
72+
forestgreen: '#228b22',
73+
gainsboro: '#dcdcdc',
74+
ghostwhite: '#f8f8ff',
75+
gold: '#ffd700',
76+
goldenrod: '#daa520',
77+
greenyellow: '#adff2f',
78+
grey: '#808080',
79+
honeydew: '#f0fff0',
80+
hotpink: '#ff69b4',
81+
indianred: '#cd5c5c',
82+
indigo: '#4b0082',
83+
ivory: '#fffff0',
84+
khaki: '#f0e68c',
85+
lavender: '#e6e6fa',
86+
lavenderblush: '#fff0f5',
87+
lawngreen: '#7cfc00',
88+
lemonchiffon: '#fffacd',
89+
lightblue: '#add8e6',
90+
lightcoral: '#f08080',
91+
lightcyan: '#e0ffff',
92+
lightgoldenrodyellow: '#fafad2',
93+
lightgray: '#d3d3d3',
94+
lightgreen: '#90ee90',
95+
lightgrey: '#d3d3d3',
96+
lightpink: '#ffb6c1',
97+
lightsalmon: '#ffa07a',
98+
lightseagreen: '#20b2aa',
99+
lightskyblue: '#87cefa',
100+
lightslategray: '#778899',
101+
lightslategrey: '#778899',
102+
lightsteelblue: '#b0c4de',
103+
lightyellow: '#ffffe0',
104+
limegreen: '#32cd32',
105+
linen: '#faf0e6',
106+
107+
// synonym of fuchsia
108+
magenta: '#ff00ff',
109+
110+
mediumaquamarine: '#66cdaa',
111+
mediumblue: '#0000cd',
112+
mediumorchid: '#ba55d3',
113+
mediumpurple: '#9370db',
114+
mediumseagreen: '#3cb371',
115+
mediumslateblue: '#7b68ee',
116+
mediumspringgreen: '#00fa9a',
117+
mediumturquoise: '#48d1cc',
118+
mediumvioletred: '#c71585',
119+
midnightblue: '#191970',
120+
mintcream: '#f5fffa',
121+
mistyrose: '#ffe4e1',
122+
moccasin: '#ffe4b5',
123+
navajowhite: '#ffdead',
124+
oldlace: '#fdf5e6',
125+
olivedrab: '#6b8e23',
126+
orangered: '#ff4500',
127+
orchid: '#da70d6',
128+
palegoldenrod: '#eee8aa',
129+
palegreen: '#98fb98',
130+
paleturquoise: '#afeeee',
131+
palevioletred: '#db7093',
132+
papayawhip: '#ffefd5',
133+
peachpuff: '#ffdab9',
134+
peru: '#cd853f',
135+
pink: '#ffc0cb',
136+
plum: '#dda0dd',
137+
powderblue: '#b0e0e6',
138+
rosybrown: '#bc8f8f',
139+
royalblue: '#4169e1',
140+
saddlebrown: '#8b4513',
141+
salmon: '#fa8072',
142+
sandybrown: '#f4a460',
143+
seagreen: '#2e8b57',
144+
seashell: '#fff5ee',
145+
sienna: '#a0522d',
146+
skyblue: '#87ceeb',
147+
slateblue: '#6a5acd',
148+
slategray: '#708090',
149+
slategrey: '#708090',
150+
snow: '#fffafa',
151+
springgreen: '#00ff7f',
152+
steelblue: '#4682b4',
153+
tan: '#d2b48c',
154+
thistle: '#d8bfd8',
155+
tomato: '#ff6347',
156+
transparent: '#00000000',
157+
turquoise: '#40e0d0',
158+
violet: '#ee82ee',
159+
wheat: '#f5deb3',
160+
whitesmoke: '#f5f5f5',
161+
yellowgreen: '#9acd32',
162+
163+
// CSS Level 4
164+
rebeccapurple: '#663399',
165+
})
166+
167+
export type NamedColors = keyof typeof namedColors
168+
169+
// export const getContrastColor = (color: string): string => {
170+
// const darkContrastColor = 'var(--a-contrast-dark)'
171+
// const lightContrastColor = 'var(--a-contrast-light)'
172+
// const threshold = 128
173+
174+
// // Check if the color is a named color
175+
// if (color in namedColors)
176+
// color = namedColors[color as keyof typeof namedColors]
177+
178+
// // Check if the color is a hex string
179+
// if (color[0] === '#') {
180+
// if (color.length === 7) {
181+
// // #RRGGBB
182+
// return (parseInt(color.substring(1, 3), 16) > threshold
183+
// || parseInt(color.substring(3, 5), 16) > threshold
184+
// || parseInt(color.substring(5, 7), 16) > threshold)
185+
// ? darkContrastColor
186+
// : lightContrastColor
187+
// }
188+
// else if (color.length === 4) {
189+
// // #RGB
190+
// return (parseInt(color[1], 16) * 0x11 > threshold
191+
// || parseInt(color[2], 16) * 0x11 > threshold
192+
// || parseInt(color[3], 16) * 0x11 > threshold)
193+
// ? darkContrastColor
194+
// : lightContrastColor
195+
// }
196+
// }
197+
198+
// // Check if the color is an RGB or RGBA string
199+
// if (color.substring(0, 3) === 'rgb' || color.substring(0, 4) === 'rgba') {
200+
// const parts = color.substring(color[3] === 'a' ? 5 : 4).split(',')
12201

13-
export const hexToRgb = (h: string): RGB | undefined => {
14-
// 3 digits
15-
if (h.length === 4) {
16-
return {
17-
r: Number(`0x${h[1]}${h[1]}`),
18-
g: Number(`0x${h[2]}${h[2]}`),
19-
b: Number(`0x${h[3]}${h[3]}`),
202+
// return (parseInt(parts[0]) > threshold || parseInt(parts[1]) > threshold || parseInt(parts[2]) > threshold)
203+
// ? darkContrastColor
204+
// : lightContrastColor
205+
// }
206+
207+
// // Check if the color is an HSL or HSLA string
208+
// if (color.substring(0, 3) === 'hsl' || color.substring(0, 4) === 'hsla') {
209+
// const parts = color.substring(color[3] === 'a' ? 5 : 4).split(',')
210+
211+
// return parseInt(parts[2].substring(0, parts[2].length - 1)) > 50
212+
// ? darkContrastColor
213+
// : lightContrastColor
214+
// }
215+
216+
// // If the color could not be parsed, return black as the contrast color
217+
// return darkContrastColor
218+
// }
219+
220+
export const getContrastColor = (color: string): string => {
221+
const darkContrastColor = 'var(--a-contrast-dark)'
222+
const lightContrastColor = 'var(--a-contrast-light)'
223+
224+
// Check if the color is a named color
225+
if (color in namedColors)
226+
color = namedColors[color as keyof typeof namedColors]
227+
228+
// Check if the color is a hex string
229+
if (color[0] === '#') {
230+
if (color.length === 7) {
231+
// #RRGGBB
232+
const r = parseInt(color.substring(1, 3), 16)
233+
const g = parseInt(color.substring(3, 5), 16)
234+
const b = parseInt(color.substring(5, 7), 16)
235+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
236+
237+
return luminance > 128 ? darkContrastColor : lightContrastColor
20238
}
239+
else if (color.length === 4) {
240+
// #RGB
241+
const r = parseInt(color[1], 16) * 0x11
242+
const g = parseInt(color[2], 16) * 0x11
243+
const b = parseInt(color[3], 16) * 0x11
244+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
21245

22-
// 6 digits
23-
}
24-
else if (h.length === 7) {
25-
return {
26-
r: Number(`0x${h[1]}${h[2]}`),
27-
g: Number(`0x${h[3]}${h[4]}`),
28-
b: Number(`0x${h[5]}${h[6]}`),
246+
return luminance > 128 ? darkContrastColor : lightContrastColor
29247
}
30248
}
31-
}
32249

33-
export const contrast = (colorHex: string | undefined, threshold = 128): string => {
34-
if (colorHex === undefined)
35-
return 'var(--a-contrast-dark)'
250+
// Check if the color is an RGB or RGBA string
251+
if (color.substring(0, 3) === 'rgb' || color.substring(0, 4) === 'rgba') {
252+
const parts = color.substring(color[3] === 'a' ? 5 : 4).split(',')
253+
const r = parseInt(parts[0])
254+
const g = parseInt(parts[1])
255+
const b = parseInt(parts[2])
256+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
36257

37-
const rgb: RGB | undefined = hexToRgb(colorHex)
258+
return luminance > 128 ? darkContrastColor : lightContrastColor
259+
}
260+
261+
// Check if the color is an HSL or HSLA string
262+
if (color.substring(0, 3) === 'hsl' || color.substring(0, 4) === 'hsla') {
263+
const parts = color.substring(color[3] === 'a' ? 5 : 4).split(',')
264+
const l = parseInt(parts[2].substring(0, parts[2].length - 1))
38265

39-
if (rgb === undefined)
40-
return 'var(--a-contrast-dark)'
266+
return l > 50 ? darkContrastColor : lightContrastColor
267+
}
41268

42-
return rgbToYIQ(rgb) >= threshold ? 'var(--a-contrast-dark)' : 'var(--a-contrast-light)'
269+
// If the color could not be parsed, return black as the contrast color
270+
return darkContrastColor
43271
}

0 commit comments

Comments
 (0)