1export type HsvColor = {
2 h: number;
3 s: number;
4 v: number;
5};
6
7/** Convert a normalized six-digit sRGB hex color to HSV. */
8export function hexToHsv(hex: string): HsvColor {
9 const numeric = Number.parseInt(hex.slice(1), 16);
10 const red = ((numeric >> 16) & 255) / 255;
11 const green = ((numeric >> 8) & 255) / 255;
12 const blue = (numeric & 255) / 255;
13 const max = Math.max(red, green, blue);
14 const min = Math.min(red, green, blue);
15 const delta = max - min;
16
17 let hue = 0;
18 if (delta !== 0) {
19 if (max === red) {
20 hue = ((green - blue) / delta) % 6;
21 } else if (max === green) {
22 hue = (blue - red) / delta + 2;
23 } else {
24 hue = (red - green) / delta + 4;
25 }
26 hue *= 60;
27 if (hue < 0) hue += 360;
28 }
29
30 return {
31 h: hue,
32 s: max === 0 ? 0 : delta / max,
33 v: max,
34 };
35}
36
37export function hsvToHex(hue: number, saturation: number, value: number) {
38 const normalizedHue = ((hue % 360) + 360) % 360;
39 const chroma = value * saturation;
40 const x = chroma * (1 - Math.abs(((normalizedHue / 60) % 2) - 1));
41 const match = value - chroma;
42 const [red, green, blue] =
43 normalizedHue < 60
44 ? [chroma, x, 0]
45 : normalizedHue < 120
46 ? [x, chroma, 0]
47 : normalizedHue < 180
48 ? [0, chroma, x]
49 : normalizedHue < 240
50 ? [0, x, chroma]
51 : normalizedHue < 300
52 ? [x, 0, chroma]
53 : [chroma, 0, x];
54
55 return `#${[red, green, blue]
56 .map((channel) =>
57 Math.round((channel + match) * 255)
58 .toString(16)
59 .padStart(2, "0"),
60 )
61 .join("")}`;
62}