Skip to content

Commit bbb834d

Browse files
authored
feat(@typegpu/color): hexToRgb, hexToRgba and hexToOklab utilities (#2284)
1 parent 5c47d38 commit bbb834d

3 files changed

Lines changed: 69 additions & 5 deletions

File tree

packages/typegpu-color/src/hex.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import tgpu, { d } from 'typegpu';
2+
import { rgbToOklab } from './oklab.ts';
3+
4+
export const hexToRgb = tgpu.comptime((hex: string | number): d.v3f => {
5+
let h: number;
6+
if (typeof hex === 'string') {
7+
let normalized = hex.trim();
8+
if (normalized.startsWith('#')) {
9+
normalized = normalized.slice(1);
10+
}
11+
// Allow 6-digit (RRGGBB) or 8-digit (RRGGBBAA) hex strings.
12+
if (normalized.length !== 6 && normalized.length !== 8) {
13+
throw new Error(`hexToRgb expected a 6- or 8-digit hex string, got "${hex}".`);
14+
}
15+
// For 8-digit input, drop the alpha channel and keep RRGGBB.
16+
if (normalized.length === 8) {
17+
normalized = normalized.slice(0, 6);
18+
}
19+
h = Number.parseInt(normalized, 16);
20+
if (Number.isNaN(h)) {
21+
throw new Error(`hexToRgb could not parse hex string "${hex}".`);
22+
}
23+
} else {
24+
h = hex;
25+
if (!Number.isFinite(h)) {
26+
throw new Error(`hexToRgb expected a finite numeric value, got ${hex}.`);
27+
}
28+
}
29+
return d.vec3f((h >> 16) & 0xff, (h >> 8) & 0xff, h & 0xff).div(255);
30+
});
31+
32+
export const hexToRgba = tgpu.comptime((hex: string | number): d.v4f => {
33+
let h: number;
34+
if (typeof hex === 'string') {
35+
let normalized = hex.trim();
36+
if (normalized.startsWith('#')) {
37+
normalized = normalized.slice(1);
38+
}
39+
// Allow 6-digit (RRGGBB) or 8-digit (RRGGBBAA) hex strings.
40+
// For 6-digit input, assume an implicit fully opaque alpha (FF).
41+
if (normalized.length === 6) {
42+
normalized = normalized + 'ff';
43+
} else if (normalized.length !== 8) {
44+
throw new Error(`hexToRgba expected a 6- or 8-digit hex string, got "${hex}".`);
45+
}
46+
h = Number.parseInt(normalized, 16);
47+
if (Number.isNaN(h)) {
48+
throw new Error(`hexToRgba could not parse hex string "${hex}".`);
49+
}
50+
} else {
51+
h = hex;
52+
if (!Number.isFinite(h)) {
53+
throw new Error(`hexToRgba expected a finite numeric value, got ${hex}.`);
54+
}
55+
}
56+
return d.vec4f((h >> 24) & 0xff, (h >> 16) & 0xff, (h >> 8) & 0xff, h & 0xff).div(255);
57+
});
58+
59+
export const hexToOklab = tgpu.comptime((hex: string | number): d.v3f => {
60+
return rgbToOklab(hexToRgb(hex));
61+
});

packages/typegpu-color/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export {
1010
oklabToRgb,
1111
rgbToOklab,
1212
} from './oklab.ts';
13+
export { hexToRgb, hexToRgba, hexToOklab } from './hex.ts';

packages/typegpu-color/src/srgb.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import tgpu from 'typegpu';
22
import { vec3f } from 'typegpu/data';
3-
import { add, gt, mul, pow, select, sub } from 'typegpu/std';
3+
import { gt, pow, select } from 'typegpu/std';
44

55
export const linearToSrgb = tgpu.fn(
66
[vec3f],
77
vec3f,
88
)((linear) => {
9+
'use gpu';
910
return select(
10-
mul(12.92, linear),
11-
sub(mul(1.055, pow(linear, vec3f(1.0 / 2.4))), vec3f(0.055)),
11+
12.92 * linear,
12+
1.055 * pow(linear, vec3f(1.0 / 2.4)) - vec3f(0.055),
1213
gt(linear, vec3f(0.0031308)),
1314
);
1415
});
@@ -17,9 +18,10 @@ export const srgbToLinear = tgpu.fn(
1718
[vec3f],
1819
vec3f,
1920
)((rgb) => {
21+
'use gpu';
2022
return select(
21-
mul(1.0 / 12.92, rgb),
22-
pow(mul(add(rgb, vec3f(0.055)), vec3f(1 / (1 + 0.055))), vec3f(2.4)),
23+
rgb / 12.92,
24+
pow((rgb + vec3f(0.055)) / (1 + 0.055), vec3f(2.4)),
2325
gt(rgb, vec3f(0.04045)),
2426
);
2527
});

0 commit comments

Comments
 (0)