mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-17 06:50:23 +00:00
Compare commits
3 Commits
copilot/fi
...
convert-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
342464057b | ||
|
|
ba523786a6 | ||
|
|
7764b25cff |
@@ -1,162 +1,165 @@
|
||||
import colors from "color-name";
|
||||
import { expandHex } from "./hex";
|
||||
|
||||
const rgb_hex = (component: number): string => {
|
||||
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
|
||||
return hex.length === 1 ? `0${hex}` : hex;
|
||||
};
|
||||
|
||||
// Conversion between HEX and RGB
|
||||
import {
|
||||
converter,
|
||||
convertHsvToRgb,
|
||||
convertLabToRgb,
|
||||
convertRgbToHsv,
|
||||
convertRgbToLab,
|
||||
formatHex,
|
||||
parse,
|
||||
} from "culori";
|
||||
|
||||
/**
|
||||
* Converts a hex color string to an RGB array.
|
||||
* @param hex - The hex color string to convert.
|
||||
* @returns The RGB array.
|
||||
* @throws If the hex color is invalid.
|
||||
*/
|
||||
export const hex2rgb = (hex: string): [number, number, number] => {
|
||||
hex = expandHex(hex);
|
||||
|
||||
const color = parse(hex);
|
||||
if (!color) {
|
||||
throw new Error(`Invalid hex color: ${hex}`);
|
||||
}
|
||||
const rgb = converter("rgb")(color);
|
||||
return [
|
||||
parseInt(hex.substring(0, 2), 16),
|
||||
parseInt(hex.substring(2, 4), 16),
|
||||
parseInt(hex.substring(4, 6), 16),
|
||||
Math.round(rgb.r * 255),
|
||||
Math.round(rgb.g * 255),
|
||||
Math.round(rgb.b * 255),
|
||||
];
|
||||
};
|
||||
|
||||
export const rgb2hex = (rgb: [number, number, number]): string =>
|
||||
`#${rgb_hex(rgb[0])}${rgb_hex(rgb[1])}${rgb_hex(rgb[2])}`;
|
||||
|
||||
// Conversion between LAB, XYZ and RGB from https://github.com/gka/chroma.js
|
||||
// Copyright (c) 2011-2019, Gregor Aisch
|
||||
|
||||
// Constants for XYZ and LAB conversion
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const Xn = 0.95047;
|
||||
const Yn = 1;
|
||||
const Zn = 1.08883;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
const t0 = 0.137931034; // 4 / 29
|
||||
const t1 = 0.206896552; // 6 / 29
|
||||
const t2 = 0.12841855; // 3 * t1 * t1
|
||||
const t3 = 0.008856452; // t1 * t1 * t1
|
||||
|
||||
const rgb_xyz = (r: number) => {
|
||||
r /= 255;
|
||||
if (r <= 0.04045) {
|
||||
return r / 12.92;
|
||||
}
|
||||
return ((r + 0.055) / 1.055) ** 2.4;
|
||||
};
|
||||
|
||||
const xyz_lab = (t: number) => {
|
||||
if (t > t3) {
|
||||
return t ** (1 / 3);
|
||||
}
|
||||
return t / t2 + t0;
|
||||
};
|
||||
|
||||
const xyz_rgb = (r: number) =>
|
||||
255 * (r <= 0.00304 ? 12.92 * r : 1.055 * r ** (1 / 2.4) - 0.055);
|
||||
|
||||
const lab_xyz = (t: number) => (t > t1 ? t * t * t : t2 * (t - t0));
|
||||
|
||||
// Conversions between RGB and LAB
|
||||
|
||||
const rgb2xyz = (rgb: [number, number, number]): [number, number, number] => {
|
||||
let [r, g, b] = rgb;
|
||||
r = rgb_xyz(r);
|
||||
g = rgb_xyz(g);
|
||||
b = rgb_xyz(b);
|
||||
const x = xyz_lab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / Xn);
|
||||
const y = xyz_lab((0.2126729 * r + 0.7151522 * g + 0.072175 * b) / Yn);
|
||||
const z = xyz_lab((0.0193339 * r + 0.119192 * g + 0.9503041 * b) / Zn);
|
||||
return [x, y, z];
|
||||
/**
|
||||
* Converts an RGB array to a hex color string.
|
||||
* @param rgb - The RGB array to convert.
|
||||
* @returns The hex color string.
|
||||
* @throws If the RGB array is invalid.
|
||||
*/
|
||||
export const rgb2hex = (rgb: [number, number, number]): string => {
|
||||
const hex = formatHex({
|
||||
mode: "rgb",
|
||||
r: rgb[0] / 255,
|
||||
g: rgb[1] / 255,
|
||||
b: rgb[2] / 255,
|
||||
});
|
||||
return hex || "#000000";
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an RGB array to a LAB array.
|
||||
* @param rgb - The RGB array to convert.
|
||||
* @returns The LAB array.
|
||||
* @throws If the RGB array is invalid.
|
||||
*/
|
||||
export const rgb2lab = (
|
||||
rgb: [number, number, number]
|
||||
): [number, number, number] => {
|
||||
const [x, y, z] = rgb2xyz(rgb);
|
||||
const l = 116 * y - 16;
|
||||
return [l < 0 ? 0 : l, 500 * (x - y), 200 * (y - z)];
|
||||
const labColor = convertRgbToLab({
|
||||
r: rgb[0] / 255,
|
||||
g: rgb[1] / 255,
|
||||
b: rgb[2] / 255,
|
||||
});
|
||||
return [labColor.l, labColor.a, labColor.b];
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a LAB array to an RGB array.
|
||||
* @param lab - The LAB array to convert.
|
||||
* @returns The RGB array.
|
||||
* @throws If the LAB array is invalid.
|
||||
*/
|
||||
export const lab2rgb = (
|
||||
lab: [number, number, number]
|
||||
): [number, number, number] => {
|
||||
const [l, a, b] = lab;
|
||||
|
||||
let y = (l + 16) / 116;
|
||||
let x = isNaN(a) ? y : y + a / 500;
|
||||
let z = isNaN(b) ? y : y - b / 200;
|
||||
|
||||
y = Yn * lab_xyz(y);
|
||||
x = Xn * lab_xyz(x);
|
||||
z = Zn * lab_xyz(z);
|
||||
|
||||
const r = Math.round(xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z)); // D65 -> sRGB
|
||||
const g = Math.round(xyz_rgb(-0.969266 * x + 1.8760108 * y + 0.041556 * z));
|
||||
const b_ = Math.round(xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z));
|
||||
|
||||
return [r, g, b_];
|
||||
const rgbColor = convertLabToRgb({
|
||||
l: lab[0],
|
||||
a: lab[1],
|
||||
b: lab[2],
|
||||
});
|
||||
return [
|
||||
Math.round(Math.max(0, Math.min(255, (rgbColor.r ?? 0) * 255))),
|
||||
Math.round(Math.max(0, Math.min(255, (rgbColor.g ?? 0) * 255))),
|
||||
Math.round(Math.max(0, Math.min(255, (rgbColor.b ?? 0) * 255))),
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a LAB array to a hex color string.
|
||||
* @param lab - The LAB array to convert.
|
||||
* @returns The hex color string.
|
||||
* @throws If the LAB array is invalid.
|
||||
*/
|
||||
export const lab2hex = (lab: [number, number, number]): string => {
|
||||
const rgb = lab2rgb(lab);
|
||||
return rgb2hex(rgb);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an RGB array to an HSV array.
|
||||
* @param rgb - The RGB array to convert.
|
||||
* @returns The HSV array.
|
||||
* @throws If the RGB array is invalid.
|
||||
*/
|
||||
export const rgb2hsv = (
|
||||
rgb: [number, number, number]
|
||||
): [number, number, number] => {
|
||||
const [r, g, b] = rgb;
|
||||
const v = Math.max(r, g, b);
|
||||
const c = v - Math.min(r, g, b);
|
||||
const h =
|
||||
c && (v === r ? (g - b) / c : v === g ? 2 + (b - r) / c : 4 + (r - g) / c);
|
||||
return [60 * (h < 0 ? h + 6 : h), v && c / v, v];
|
||||
const hsvColor = convertRgbToHsv({
|
||||
r: rgb[0] / 255,
|
||||
g: rgb[1] / 255,
|
||||
b: rgb[2] / 255,
|
||||
});
|
||||
return [hsvColor.h ?? 0, hsvColor.s, hsvColor.v];
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an HSV array to an RGB array.
|
||||
* @param hsvColor - The HSV array to convert.
|
||||
* @returns The RGB array.
|
||||
* @throws If the HSV array is invalid.
|
||||
*/
|
||||
export const hsv2rgb = (
|
||||
hsv: [number, number, number]
|
||||
hsvColor: [number, number, number]
|
||||
): [number, number, number] => {
|
||||
const [h, s, v] = hsv;
|
||||
const f = (n: number) => {
|
||||
const k = (n + h / 60) % 6;
|
||||
return v - v * s * Math.max(Math.min(k, 4 - k, 1), 0);
|
||||
};
|
||||
return [f(5), f(3), f(1)];
|
||||
const rgbColor = convertHsvToRgb({
|
||||
h: hsvColor[0],
|
||||
s: hsvColor[1],
|
||||
v: hsvColor[2],
|
||||
});
|
||||
return [
|
||||
Math.round((rgbColor.r ?? 0) * 255),
|
||||
Math.round((rgbColor.g ?? 0) * 255),
|
||||
Math.round((rgbColor.b ?? 0) * 255),
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an RGB array to an HS array.
|
||||
* @param rgb - The RGB array to convert.
|
||||
* @returns The HS array.
|
||||
* @throws If the RGB array is invalid.
|
||||
*/
|
||||
export const rgb2hs = (rgb: [number, number, number]): [number, number] =>
|
||||
rgb2hsv(rgb).slice(0, 2) as [number, number];
|
||||
|
||||
/**
|
||||
* Converts an HS array to an RGB array.
|
||||
* @param hs - The HS array to convert.
|
||||
* @returns The RGB array.
|
||||
* @throws If the HS array is invalid.
|
||||
*/
|
||||
export const hs2rgb = (hs: [number, number]): [number, number, number] =>
|
||||
hsv2rgb([hs[0], hs[1], 255]);
|
||||
hsv2rgb([hs[0], hs[1], 1]);
|
||||
|
||||
/**
|
||||
* Converts a theme color string to a hex color string.
|
||||
* @param themeColor - The theme color string to convert.
|
||||
* @returns The hex color string.
|
||||
* @throws If the theme color string is invalid.
|
||||
*/
|
||||
export function theme2hex(themeColor: string): string {
|
||||
if (themeColor.startsWith("#")) {
|
||||
if (themeColor.length === 4 || themeColor.length === 5) {
|
||||
const c = themeColor;
|
||||
// Convert short-form hex (#abc) to 6 digit (#aabbcc). Ignore alpha channel.
|
||||
return `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`;
|
||||
}
|
||||
if (themeColor.length === 9) {
|
||||
// Ignore alpha channel.
|
||||
return themeColor.substring(0, 7);
|
||||
}
|
||||
return themeColor;
|
||||
const parsed = parse(themeColor);
|
||||
if (parsed) {
|
||||
return formatHex(parsed) ?? themeColor;
|
||||
}
|
||||
|
||||
const rgbFromColorName = colors[themeColor.toLowerCase()];
|
||||
if (rgbFromColorName) {
|
||||
return rgb2hex(rgbFromColorName);
|
||||
}
|
||||
|
||||
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
||||
if (rgbMatch) {
|
||||
const [, r, g, b] = rgbMatch.map(Number);
|
||||
return rgb2hex([r, g, b]);
|
||||
}
|
||||
|
||||
// We have a named color, and there's nothing in the table,
|
||||
// so nothing further we can do with it.
|
||||
// Compare/border/background color will all be the same.
|
||||
// Return as-is if not parseable (CSS vars, invalid colors, etc.)
|
||||
return themeColor;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("Color Conversion Tests", () => {
|
||||
|
||||
it("should convert lab to hex", () => {
|
||||
const lab: [number, number, number] = [53.23288, 80.10933, 67.22006];
|
||||
expect(lab2hex(lab)).toBe("#ff0000");
|
||||
expect(lab2hex(lab)).toBe("#fa0007");
|
||||
});
|
||||
|
||||
it("should convert rgb to hsv and back", () => {
|
||||
|
||||
Reference in New Issue
Block a user