-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathColorLerp.js
More file actions
107 lines (89 loc) · 3.56 KB
/
ColorLerp.js
File metadata and controls
107 lines (89 loc) · 3.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
* lite-color-lerp.js
* Bridges perceptual OKLCH authoring with zero-GC runtime sampling.
*
* The baked LUT packs 32-bit ARGB. If you intend to write directly into a
* Canvas ImageData buffer via a Uint32Array view, see `bakeGradientLUTRGBA`
* (little-endian byte order) below.
*/
import { multiStopGradientTo } from '@zakkster/lite-color';
const _tempOklch = { l: 0, c: 0, h: 0, a: 1 };
const DEG_TO_RAD = Math.PI / 180;
export function bakeGradientLUT(oklchColors, resolution = 256) {
return _bakeWith(oklchColors, resolution, oklchToArgbUint32);
}
/**
* Variant: packs as RGBA in little-endian uint32, suitable for direct-writing
* into a Uint32Array view of Canvas ImageData (browsers are universally LE).
*/
export function bakeGradientLUTRGBA(oklchColors, resolution = 256) {
return _bakeWith(oklchColors, resolution, oklchToRgbaUint32LE);
}
function _bakeWith(oklchColors, resolution, packer) {
if (!oklchColors || oklchColors.length === 0) {
throw new Error('lite-color-lerp: oklchColors array must be non-empty.');
}
if (resolution < 2) {
throw new Error('lite-color-lerp: resolution must be at least 2.');
}
const lut = new Uint32Array(resolution);
const maxIdx = resolution - 1;
for (let i = 0; i < resolution; i++) {
const t = i / maxIdx;
_tempOklch.a = 1.0; // reset in case the gradient doesn't define alpha
multiStopGradientTo(oklchColors, t, _tempOklch);
lut[i] = packer(_tempOklch.l, _tempOklch.c, _tempOklch.h, _tempOklch.a);
}
return lut;
}
/**
* Hot-path sampler. WARNING: assumes a populated Uint32Array, no bounds checks.
*/
export function sampleColorLUT(lut, t) {
const maxIndex = lut.length - 1;
// inline clamp + fast-floor
const tc = t < 0 ? 0 : t > 1 ? 1 : t;
return lut[(tc * maxIndex) | 0];
}
// ---------- conversion (Ottosson 2020) ----------
/**
* OKLCH -> linear sRGB. Writes into the provided 3-element output object.
* Internal helper; keeps the per-channel math out of the packers.
*/
function oklchToLinearSrgb(L, C, H, out) {
const hRad = H * DEG_TO_RAD;
const a = C * Math.cos(hRad);
const b = C * Math.sin(hRad);
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
const l = l_ * l_ * l_;
const m = m_ * m_ * m_;
const s = s_ * s_ * s_;
out.r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
out.g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
out.b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
}
function linearToSrgbByte(c) {
if (c <= 0) return 0;
if (c >= 1) return 255;
const enc = c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
return (enc * 255 + 0.5) | 0;
}
const _tempLinRgb = { r: 0, g: 0, b: 0 };
function oklchToArgbUint32(l, c, h, alpha) {
oklchToLinearSrgb(l, c, h, _tempLinRgb);
const R = linearToSrgbByte(_tempLinRgb.r);
const G = linearToSrgbByte(_tempLinRgb.g);
const B = linearToSrgbByte(_tempLinRgb.b);
const A = alpha <= 0 ? 0 : alpha >= 1 ? 255 : (alpha * 255 + 0.5) | 0;
return ((A << 24) | (R << 16) | (G << 8) | B) >>> 0;
}
function oklchToRgbaUint32LE(l, c, h, alpha) {
oklchToLinearSrgb(l, c, h, _tempLinRgb);
const R = linearToSrgbByte(_tempLinRgb.r);
const G = linearToSrgbByte(_tempLinRgb.g);
const B = linearToSrgbByte(_tempLinRgb.b);
const A = alpha <= 0 ? 0 : alpha >= 1 ? 255 : (alpha * 255 + 0.5) | 0;
return ((A << 24) | (B << 16) | (G << 8) | R) >>> 0;
}