|
20 | 20 | */ |
21 | 21 | export function perceivedBrightness(color: string): number | null { |
22 | 22 | const value = color.trim().replace(/['"]/g, '').toLowerCase() |
| 23 | + return parseSolidBrightness(value) |
| 24 | +} |
| 25 | + |
| 26 | +/** |
| 27 | + * Perceived brightness of a solid color or static CSS gradient background. |
| 28 | + * Gradient brightness is the average of supported hex/black/white color stops, |
| 29 | + * a small deterministic heuristic for choosing readable tile foregrounds |
| 30 | + * without a browser color parser. Unsupported backgrounds return `null`. |
| 31 | + */ |
| 32 | +export function perceivedBackgroundBrightness(background: string): number | null { |
| 33 | + const value = background.trim().replace(/['"]/g, '').toLowerCase() |
| 34 | + const solidBrightness = parseSolidBrightness(value) |
| 35 | + if (solidBrightness !== null) return solidBrightness |
| 36 | + |
| 37 | + const gradient = value.match(/^(?:repeating-)?(linear|radial|conic)-gradient\((.*)\)$/) |
| 38 | + if (!gradient) return null |
| 39 | + |
| 40 | + const [, gradientType, contents] = gradient |
| 41 | + const parts = contents.split(',').map((part) => part.trim()) |
| 42 | + const firstStop = parseSupportedColorStop(parts[0]) |
| 43 | + const colorStops = firstStop === null ? parts.slice(1) : parts |
| 44 | + if ( |
| 45 | + colorStops.length < 2 || |
| 46 | + (firstStop === null && !isSupportedGradientPreamble(gradientType, parts[0])) |
| 47 | + ) { |
| 48 | + return null |
| 49 | + } |
| 50 | + |
| 51 | + let totalBrightness = 0 |
| 52 | + for (const colorStop of colorStops) { |
| 53 | + const brightness = parseSupportedColorStop(colorStop) |
| 54 | + if (brightness === null) return null |
| 55 | + totalBrightness += brightness |
| 56 | + } |
| 57 | + |
| 58 | + return totalBrightness / colorStops.length |
| 59 | +} |
| 60 | + |
| 61 | +function parseSupportedColorStop(value: string): number | null { |
| 62 | + const match = value.match(/^(#[0-9a-f]{6}\b|#[0-9a-f]{3}\b|(?:white|black)\b)(?:\s|$)/) |
| 63 | + return match ? parseSolidBrightness(match[1]) : null |
| 64 | +} |
| 65 | + |
| 66 | +function isSupportedGradientPreamble(type: string, value: string): boolean { |
| 67 | + if (type === 'linear') { |
| 68 | + return /^(?:-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|grad|rad|turn)|to\s+(?:top|right|bottom|left)(?:\s+(?:top|right|bottom|left))?)$/.test( |
| 69 | + value |
| 70 | + ) |
| 71 | + } |
| 72 | + if (type === 'radial') { |
| 73 | + return /^(?:(?:circle|ellipse|closest-side|closest-corner|farthest-side|farthest-corner|at)\b|-?(?:\d|\.\d))/.test( |
| 74 | + value |
| 75 | + ) |
| 76 | + } |
| 77 | + return /^(?:from|at)\b/.test(value) |
| 78 | +} |
| 79 | + |
| 80 | +function parseSolidBrightness(value: string): number | null { |
23 | 81 | if (value === 'white') return 1 |
24 | 82 | if (value === 'black') return 0 |
25 | 83 | const hex = value.replace('#', '') |
|
0 commit comments