Skip to content

Commit 0b1785b

Browse files
committed
chore: Add some oklab to hex utils and fix hex casting
1 parent 2196d5c commit 0b1785b

File tree

1 file changed

+120
-14
lines changed

1 file changed

+120
-14
lines changed

src/utils/color.ts

Lines changed: 120 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ export type Hex = string & { __hex: true };
88
* @property B A number from 0-255 representing blue.
99
* @property a A number from 0-1 representing alpha. Assumed 1 if not set.
1010
*/
11-
export type Rgb = { R: number; G: number; B: number; a?: number; colorspace: 'rgba' };
11+
export type Rgb = {
12+
R: number;
13+
G: number;
14+
B: number;
15+
a?: number;
16+
colorspace: 'rgba';
17+
};
1218
export type RgbString = string & { __rgba: true };
1319

1420
/**
@@ -18,7 +24,28 @@ export type RgbString = string & { __rgba: true };
1824
* @property B A number from 0-255 representing blue.
1925
* @property a A number from 0-1 representing alpha. Assumed 1 if not set.
2026
*/
21-
export type Lrgb = { R: number; G: number; B: number; a?: number; colorspace: 'lrgb' };
27+
export type Lrgb = {
28+
R: number;
29+
G: number;
30+
B: number;
31+
a?: number;
32+
colorspace: 'lrgb';
33+
};
34+
35+
/**
36+
* CIE XYZ D50 colorspace.
37+
* @property X No idea. I'm sorry.
38+
* @property Y No idea. I'm sorry.
39+
* @property Z No idea. I'm sorry.
40+
* @property a A number from 0-1 representing alpha. Assumed 1 if not set.
41+
*/
42+
export type Xyz50 = {
43+
X: number;
44+
Y: number;
45+
Z: number;
46+
a?: number;
47+
colorspace: 'xyz50';
48+
};
2249

2350
/**
2451
* hsla() colorspace.
@@ -27,7 +54,13 @@ export type Lrgb = { R: number; G: number; B: number; a?: number; colorspace: 'l
2754
* @property L A number from 0-100 representing lightness.
2855
* @property a A number from 0-1 representing alpha. Assumed 1 if not set.
2956
*/
30-
export type Hsl = { H: number; S: number; L: number; a?: number; colorspace: 'hsla' };
57+
export type Hsl = {
58+
H: number;
59+
S: number;
60+
L: number;
61+
a?: number;
62+
colorspace: 'hsla';
63+
};
3164
export type HslString = string & { __hsla: true };
3265

3366
/**
@@ -37,7 +70,13 @@ export type HslString = string & { __hsla: true };
3770
* @property B A number from ~-0.5-~0.5 representing blue (negative) - yellow (positive).
3871
* @property a A number from 0-1 representing alpha. Assumed 1 if not set.
3972
*/
40-
export type Oklab = { L: number; A: number; B: number; a?: number; colorspace: 'oklab' };
73+
export type Oklab = {
74+
L: number;
75+
A: number;
76+
B: number;
77+
a?: number;
78+
colorspace: 'oklab';
79+
};
4180

4281
/**
4382
* oklch() colorspace.
@@ -46,7 +85,13 @@ export type Oklab = { L: number; A: number; B: number; a?: number; colorspace: '
4685
* @property H A number from 0-360 representing hue angle.
4786
* @property a A number from 0-1 representing alpha. Assumed 1 if not set.
4887
*/
49-
export type Oklch = { L: number; C: number; H: number; a?: number; colorspace: 'oklch' };
88+
export type Oklch = {
89+
L: number;
90+
C: number;
91+
H: number;
92+
a?: number;
93+
colorspace: 'oklch';
94+
};
5095
export type OklchString = string & { __oklch: string };
5196

5297
// endregion Types
@@ -66,15 +111,17 @@ export function StringToHex(hex: string): Hex | null {
66111
}
67112

68113
export function HexToRgb(hex: Hex): Rgb {
69-
const hexVal = hex.replace(/^#?/, '#');
70-
if (!StringToHex(hexVal)) return { R: 0, G: 0, B: 0, colorspace: 'rgba' };
114+
const hexVal = StringToHex(hex);
115+
if (!hexVal) return { R: 0, G: 0, B: 0, colorspace: 'rgba' };
71116
const isShortHand = hex.length === 3 || hex.length === 4;
72117

73-
const R = parseInt(isShortHand ? hexVal.substring(1, 2).repeat(2) : hexVal.substring(1, 3), 16);
74-
const G = parseInt(isShortHand ? hexVal.substring(2, 3).repeat(2) : hexVal.substring(3, 5), 16);
75-
const B = parseInt(isShortHand ? hexVal.substring(3, 4).repeat(2) : hexVal.substring(5, 7), 16);
76-
const a = parseInt(isShortHand ? hexVal.substring(4, 5).repeat(2) : hexVal.substring(7, 9), 16);
77-
return { colorspace: 'rgba', R, G, B, ...(a < 255 ? { a: a / 255 } : undefined) };
118+
const R = parseInt(isShortHand ? hexVal.substring(0, 1).repeat(2) : hexVal.substring(0, 2), 16);
119+
const G = parseInt(isShortHand ? hexVal.substring(1, 2).repeat(2) : hexVal.substring(2, 4), 16);
120+
const B = parseInt(isShortHand ? hexVal.substring(2, 3).repeat(2) : hexVal.substring(4, 6), 16);
121+
const a = parseInt(isShortHand ? hexVal.substring(3, 4).repeat(2) : hexVal.substring(6, 8), 16);
122+
const rgba: Rgb = { colorspace: 'rgba', R, G, B };
123+
if (hex.length === 4 || hex.length === 8) rgba.a = a;
124+
return rgba;
78125
}
79126

80127
export function RgbToHex({ R, G, B, a }: Rgb): Hex {
@@ -131,11 +178,34 @@ export function RgbToLrgb({ R, G, B, a }: Rgb): Lrgb {
131178
}
132179
return (Math.sign(c) || 1) * Math.pow((abs + 0.055) / 1.055, 2.4);
133180
};
134-
const lrgb: Lrgb = { colorspace: 'lrgb', R: mapper(R / 255), G: mapper(G / 255), B: mapper(B / 255) };
181+
const lrgb: Lrgb = {
182+
colorspace: 'lrgb',
183+
R: mapper(R / 255),
184+
G: mapper(G / 255),
185+
B: mapper(B / 255),
186+
};
135187
if (typeof a === 'number') lrgb.a = a;
136188
return lrgb;
137189
}
138190

191+
export function LrgbToRgb({ R, G, B, a }: Lrgb): Rgb {
192+
const mapper = (c: number): number => {
193+
const abs = Math.abs(c);
194+
if (abs > 0.0031308) {
195+
return Math.round((Math.sign(c) || 1) * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055) * 255);
196+
}
197+
return Math.round(c * 12.92 * 255);
198+
};
199+
const rgb: Rgb = {
200+
colorspace: 'rgba',
201+
R: mapper(R),
202+
G: mapper(G),
203+
B: mapper(B),
204+
};
205+
if (typeof a === 'number') rgb.a = a;
206+
return rgb;
207+
}
208+
139209
export function LrgbToOklab({ R, G, B, a }: Lrgb): Oklab {
140210
const L = Math.cbrt(0.412221469470763 * R + 0.5363325372617348 * G + 0.0514459932675022 * B);
141211
const M = Math.cbrt(0.2119034958178252 * R + 0.6806995506452344 * G + 0.1073969535369406 * B);
@@ -152,13 +222,45 @@ export function LrgbToOklab({ R, G, B, a }: Lrgb): Oklab {
152222
return oklab;
153223
}
154224

225+
export function OklabToLrgb({ L, A, B, a }: Oklab): Lrgb {
226+
const l = Math.pow(L + 0.3963377773761749 * A + 0.2158037573099136 * B, 3);
227+
const m = Math.pow(L - 0.1055613458156586 * A - 0.0638541728258133 * B, 3);
228+
const s = Math.pow(L - 0.0894841775298119 * A - 1.2914855480194092 * B, 3);
229+
230+
const lrgb: Lrgb = {
231+
colorspace: 'lrgb',
232+
R: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
233+
G: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
234+
B: -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
235+
};
236+
237+
if (typeof a === 'number') lrgb.a = a;
238+
return lrgb;
239+
}
240+
155241
export function OklabToOklch({ L, A, B, a }: Oklab): Oklch {
156242
const C = Math.sqrt(A ** 2 + B ** 2);
157-
const oklch: Oklch = { colorspace: 'oklch', L, C, H: C ? normalizeHue((Math.atan2(B, A) * 180) / Math.PI) : 0 };
243+
const oklch: Oklch = {
244+
colorspace: 'oklch',
245+
L,
246+
C,
247+
H: C ? normalizeHue((Math.atan2(B, A) * 180) / Math.PI) : 0,
248+
};
158249
if (typeof a === 'number') oklch.a = a;
159250
return oklch;
160251
}
161252

253+
export function OklchToOklab({ L, C, H, a }: Oklch): Oklab {
254+
const oklab: Oklab = {
255+
colorspace: 'oklab',
256+
L,
257+
A: C ? C * Math.cos((H * Math.PI) / 180) : 0,
258+
B: C ? C * Math.sin((H * Math.PI) / 180) : 0,
259+
};
260+
if (typeof a === 'number') oklab.a = a;
261+
return oklab;
262+
}
263+
162264
export function HexToOklab(hex: Hex): Oklab {
163265
return LrgbToOklab(RgbToLrgb(HexToRgb(hex)));
164266
}
@@ -171,6 +273,10 @@ export function HexToOklch(hex: Hex): Oklch {
171273
return RgbToOklch(HexToRgb(hex));
172274
}
173275

276+
export function OklchToHex(oklch: Oklch): Hex {
277+
return RgbToHex(LrgbToRgb(OklabToLrgb(OklchToOklab(oklch))));
278+
}
279+
174280
// endregion Conversion Methods
175281

176282
/**

0 commit comments

Comments
 (0)