Files
webmail/lib/color-transform.ts
T

130 lines
3.6 KiB
TypeScript

type RGB = { r: number; g: number; b: number; a?: number };
const namedColors: Record<string, string> = {
transparent: 'rgba(0,0,0,0)',
black: '#000000',
white: '#ffffff',
red: '#ff0000',
green: '#008000',
blue: '#0000ff',
yellow: '#ffff00',
cyan: '#00ffff',
magenta: '#ff00ff',
gray: '#808080',
grey: '#808080',
silver: '#c0c0c0',
maroon: '#800000',
olive: '#808000',
lime: '#00ff00',
aqua: '#00ffff',
teal: '#008080',
navy: '#000080',
fuchsia: '#ff00ff',
purple: '#800080',
};
export function parseColor(colorString: string): RGB | null {
if (!colorString || typeof colorString !== 'string') {
return null;
}
const color = colorString.trim().toLowerCase();
if (color === 'inherit' || color === 'currentcolor') {
return null;
}
if (namedColors[color]) {
return parseColor(namedColors[color]);
}
if (color === 'transparent') {
return { r: 0, g: 0, b: 0, a: 0 };
}
const hexMatch = color.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
if (hexMatch) {
const hex = hexMatch[1];
if (hex.length === 3) {
return {
r: parseInt(hex[0] + hex[0], 16),
g: parseInt(hex[1] + hex[1], 16),
b: parseInt(hex[2] + hex[2], 16),
};
}
return {
r: parseInt(hex.substr(0, 2), 16),
g: parseInt(hex.substr(2, 2), 16),
b: parseInt(hex.substr(4, 2), 16),
};
}
const rgbMatch = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)$/);
if (rgbMatch) {
const r = parseInt(rgbMatch[1], 10);
const g = parseInt(rgbMatch[2], 10);
const b = parseInt(rgbMatch[3], 10);
const a = rgbMatch[4] ? parseFloat(rgbMatch[4]) : undefined;
if (r > 255 || g > 255 || b > 255 || r < 0 || g < 0 || b < 0) {
return null;
}
if (a !== undefined && (a < 0 || a > 1)) {
return null;
}
return { r, g, b, a };
}
const hslMatch = color.match(/^hsla?\((\d+),\s*([\d.]+)%,\s*([\d.]+)%(?:,\s*([\d.]+))?\)$/);
if (hslMatch) {
const h = parseInt(hslMatch[1], 10) / 360;
const s = parseFloat(hslMatch[2]) / 100;
const l = parseFloat(hslMatch[3]) / 100;
const a = hslMatch[4] ? parseFloat(hslMatch[4]) : undefined;
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
return {
r: Math.round(hue2rgb(p, q, h + 1 / 3) * 255),
g: Math.round(hue2rgb(p, q, h) * 255),
b: Math.round(hue2rgb(p, q, h - 1 / 3) * 255),
a,
};
}
return null;
}
export function getLuminance(r: number, g: number, b: number): number {
const [rs, gs, bs] = [r, g, b].map((c) => {
const val = c / 255;
return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
/**
* Text colour for a label on a solid fill: white while white reaches the
* 4.5:1 that small text needs, otherwise whichever of white and near-black
* (#111827) contrasts more with the fill.
*/
export function readableTextOn(colorString: string): string {
const rgb = parseColor(colorString);
if (!rgb) return '#ffffff';
const fill = getLuminance(rgb.r, rgb.g, rgb.b);
const contrastWithWhite = 1.05 / (fill + 0.05);
const contrastWithDark = (fill + 0.05) / (getLuminance(17, 24, 39) + 0.05);
return contrastWithWhite >= 4.5 || contrastWithWhite >= contrastWithDark ? '#ffffff' : '#111827';
}