@zakkster/lite-color-engine 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -100
- package/README.md +72 -337
- package/index.d.ts +113 -11
- package/index.js +11 -2
- package/llms.txt +44 -275
- package/package.json +4 -3
- package/src/authoring.js +47 -6
- package/src/convert.js +127 -5
- package/src/runtime.js +181 -6
package/index.d.ts
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* - H (Hue): [0, 360) degrees
|
|
10
10
|
*
|
|
11
11
|
* Pack output is a 32-bit unsigned integer in **little-endian RGBA** byte
|
|
12
|
-
* order
|
|
12
|
+
* order - drop straight into `new Uint32Array(imageData.data.buffer)`.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
// ============================================================================
|
|
16
|
-
// Authoring (CSS Parsing
|
|
16
|
+
// Authoring (CSS Parsing -> OKLCH Buffer)
|
|
17
17
|
// ============================================================================
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -33,7 +33,7 @@ export function parseHexToBuffer(str: string, outBuf: Float32Array, offset: numb
|
|
|
33
33
|
export function parseOklchToBuffer(str: string, outBuf: Float32Array, offset: number): number;
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Parses a CSS `oklab(...)` string and converts (a, b)
|
|
36
|
+
* Parses a CSS `oklab(...)` string and converts (a, b) -> polar (C, H) before
|
|
37
37
|
* writing the OKLCH triplet.
|
|
38
38
|
* @returns Parsed alpha in `[0, 1]`; defaults to `1.0` when omitted.
|
|
39
39
|
* @throws If the string is not a valid `oklab(...)` literal.
|
|
@@ -61,17 +61,32 @@ export function parseHslToBuffer(str: string, outBuf: Float32Array, offset: numb
|
|
|
61
61
|
* format-specific parser.
|
|
62
62
|
*
|
|
63
63
|
* Supports: 148 named colors, `#RGB[A]`, `#RRGGBB[AA]`, `rgb()`, `rgba()`,
|
|
64
|
-
* `hsl()`, `hsla()`, `oklch()`, `oklab()`.
|
|
64
|
+
* `hsl()`, `hsla()`, `oklch()`, `oklab()`, `color(display-p3 r g b / alpha?)`.
|
|
65
65
|
*
|
|
66
66
|
* Intended for the **authoring/init phase** of a render pipeline. After
|
|
67
67
|
* compilation, work with the resulting `Float32Array` buffers directly via
|
|
68
68
|
* {@link lerpOklchBuffer} and {@link packOklchBufferToUint32}.
|
|
69
69
|
*
|
|
70
|
+
* `color(display-p3 ...)` is opt-in wide-gamut input; it never affects the
|
|
71
|
+
* default sRGB hot path.
|
|
72
|
+
*
|
|
70
73
|
* @returns Parsed alpha in `[0, 1]`.
|
|
71
74
|
* @throws If the string is not a recognized format.
|
|
72
75
|
*/
|
|
73
76
|
export function parseCSSColor(str: string, outBuf: Float32Array, offset: number): number;
|
|
74
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Parses a CSS `color(display-p3 r g b / alpha?)` string into the OKLCH triplet
|
|
80
|
+
* at `outBuf[offset..offset+2]`. Components accept `0-1` numbers or `%`.
|
|
81
|
+
*
|
|
82
|
+
* Wide-gamut authoring entry point: the resulting OKLCH may carry higher chroma
|
|
83
|
+
* than any sRGB-gamut color.
|
|
84
|
+
*
|
|
85
|
+
* @returns Parsed alpha in `[0, 1]`; defaults to `1.0` when omitted.
|
|
86
|
+
* @throws If the string is not a valid `color(display-p3 ...)` literal.
|
|
87
|
+
*/
|
|
88
|
+
export function parseDisplayP3ToBuffer(str: string, outBuf: Float32Array, offset: number): number;
|
|
89
|
+
|
|
75
90
|
// ============================================================================
|
|
76
91
|
// Convert (Raw Math)
|
|
77
92
|
// ============================================================================
|
|
@@ -80,7 +95,7 @@ export function parseCSSColor(str: string, outBuf: Float32Array, offset: number)
|
|
|
80
95
|
* Converts standard sRGB (0-255 byte channels) into a flat OKLCH buffer at
|
|
81
96
|
* `outBuf[outOffset..outOffset+2]`.
|
|
82
97
|
*
|
|
83
|
-
* Implements
|
|
98
|
+
* Implements Bjorn Ottosson's OKLab (2020) algorithm with defenses against
|
|
84
99
|
* NaN-propagating negative cube-root inputs and a strict lightness clamp.
|
|
85
100
|
*/
|
|
86
101
|
export function sRgbToOklchBuffer(
|
|
@@ -91,6 +106,30 @@ export function sRgbToOklchBuffer(
|
|
|
91
106
|
outOffset: number
|
|
92
107
|
): void;
|
|
93
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Converts Display P3 (0-255 byte channels) into a flat OKLCH buffer, using the
|
|
111
|
+
* P3 primaries via a P3 -> XYZ -> LMS -> OKLab pipeline. Lets colors outside the
|
|
112
|
+
* sRGB gamut be represented with their true (higher) chroma.
|
|
113
|
+
*
|
|
114
|
+
* Same OKLab defenses and lightness clamp as {@link sRgbToOklchBuffer}.
|
|
115
|
+
*/
|
|
116
|
+
export function displayP3ToOklchBuffer(
|
|
117
|
+
r: number,
|
|
118
|
+
g: number,
|
|
119
|
+
b: number,
|
|
120
|
+
outBuf: Float32Array,
|
|
121
|
+
outOffset: number
|
|
122
|
+
): void;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Inverse of {@link displayP3ToOklchBuffer}: converts an OKLCH triplet to
|
|
126
|
+
* **linear-light** Display P3 and writes `[r, g, b]` into `out` (length >= 3).
|
|
127
|
+
*
|
|
128
|
+
* Output channels may fall outside `[0, 1]` for colors beyond the P3 gamut;
|
|
129
|
+
* clamp or gamut-map before encoding. Used by the P3 packers.
|
|
130
|
+
*/
|
|
131
|
+
export function oklchToLinearP3(L: number, C: number, H: number, out: Float32Array): void;
|
|
132
|
+
|
|
94
133
|
// ============================================================================
|
|
95
134
|
// Runtime (Zero-GC Hot Path)
|
|
96
135
|
// ============================================================================
|
|
@@ -100,7 +139,7 @@ export function sRgbToOklchBuffer(
|
|
|
100
139
|
*
|
|
101
140
|
* Hue uses **shortest-path** interpolation (`lerpAngle`) so gradients never
|
|
102
141
|
* wrap the long way around the wheel. Lightness is hard-clamped to `[0, 1]`
|
|
103
|
-
* and chroma to `[0,
|
|
142
|
+
* and chroma to `[0, +inf)`. Hue is canonicalized to `[0, 360)`.
|
|
104
143
|
*
|
|
105
144
|
* Source and destination buffers may alias (same buffer, different offsets).
|
|
106
145
|
*
|
|
@@ -116,13 +155,29 @@ export function lerpOklchBuffer(
|
|
|
116
155
|
outOffset: number
|
|
117
156
|
): void;
|
|
118
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Batch lerp for particle systems / high-N use cases. Amortizes JS call overhead.
|
|
160
|
+
*
|
|
161
|
+
* Lerps n triplets with shared `t`: a[offA + i*3...] <-> b[offB + i*3...] -> out[offOut + i*3...]
|
|
162
|
+
*/
|
|
163
|
+
export function lerpOklchBufferN(
|
|
164
|
+
a: Float32Array,
|
|
165
|
+
offA: number,
|
|
166
|
+
b: Float32Array,
|
|
167
|
+
offB: number,
|
|
168
|
+
t: number,
|
|
169
|
+
out: Float32Array,
|
|
170
|
+
offOut: number,
|
|
171
|
+
n: number
|
|
172
|
+
): void;
|
|
173
|
+
|
|
119
174
|
/**
|
|
120
175
|
* Encodes an OKLCH triplet to a 32-bit unsigned integer in **little-endian
|
|
121
|
-
* RGBA** byte order
|
|
176
|
+
* RGBA** byte order - the format consumed directly by `Canvas ImageData` via
|
|
122
177
|
* a `Uint32Array` view.
|
|
123
178
|
*
|
|
124
179
|
* Uses the proper sRGB transfer function (`pow(c, 1/2.4)` branch).
|
|
125
|
-
* Round-tripping `sRGB
|
|
180
|
+
* Round-tripping `sRGB -> OKLCH -> here` recovers the original byte exactly
|
|
126
181
|
* (within rounding).
|
|
127
182
|
*
|
|
128
183
|
* For a faster `Math.sqrt`-approximated sibling (~2x throughput, ~10/255
|
|
@@ -157,6 +212,53 @@ export function packOklchBufferToUint32Fast(
|
|
|
157
212
|
alpha?: number
|
|
158
213
|
): number;
|
|
159
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Batch packer: n OKLCH triplets -> n Uint32 packed colors (stride 1 in dst).
|
|
217
|
+
* Opt-in 4k sRGB LUT (`useLut=true`) for fast-packer throughput at near-exact accuracy.
|
|
218
|
+
*/
|
|
219
|
+
export function packOklchBufferToUint32IntoN(
|
|
220
|
+
src: Float32Array,
|
|
221
|
+
offSrc: number,
|
|
222
|
+
dst: Uint32Array,
|
|
223
|
+
offDst: number,
|
|
224
|
+
n: number,
|
|
225
|
+
alpha?: number,
|
|
226
|
+
useLut?: boolean
|
|
227
|
+
): void;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Encodes an OKLCH triplet to a 32-bit unsigned integer in little-endian RGBA
|
|
231
|
+
* byte order for a **Display P3** canvas context
|
|
232
|
+
* (`getContext('2d', { colorSpace: 'display-p3' })`).
|
|
233
|
+
*
|
|
234
|
+
* Colors that are out of sRGB but inside P3 are preserved at their true
|
|
235
|
+
* saturation instead of being clamped down. Transfer function is identical to
|
|
236
|
+
* sRGB (IEC 61966-2-1).
|
|
237
|
+
*
|
|
238
|
+
* @param alpha Alpha in `[0, 1]`. Default `1.0`.
|
|
239
|
+
* @returns 32-bit unsigned integer in little-endian RGBA byte order.
|
|
240
|
+
*/
|
|
241
|
+
export function packOklchBufferToUint32P3(
|
|
242
|
+
buf: Float32Array,
|
|
243
|
+
offset: number,
|
|
244
|
+
alpha?: number
|
|
245
|
+
): number;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Faster, less accurate variant of {@link packOklchBufferToUint32P3}: uses a
|
|
249
|
+
* `Math.sqrt` transfer approximation (same trade-off as
|
|
250
|
+
* {@link packOklchBufferToUint32Fast}). For high-volume P3 particle/gradient
|
|
251
|
+
* baking where exact round-trip is not required.
|
|
252
|
+
*
|
|
253
|
+
* @param alpha Alpha in `[0, 1]`. Default `1.0`.
|
|
254
|
+
* @returns 32-bit unsigned integer in little-endian RGBA byte order.
|
|
255
|
+
*/
|
|
256
|
+
export function packOklchBufferToUint32P3Fast(
|
|
257
|
+
buf: Float32Array,
|
|
258
|
+
offset: number,
|
|
259
|
+
alpha?: number
|
|
260
|
+
): number;
|
|
261
|
+
|
|
160
262
|
/**
|
|
161
263
|
* Zero-GC sampler for a baked LUT. Inline `t`-clamp + bitwise-truncated index.
|
|
162
264
|
*
|
|
@@ -176,7 +278,7 @@ export type OklchPackerFn = (buf: Float32Array, offset: number, alpha?: number)
|
|
|
176
278
|
/**
|
|
177
279
|
* Bakes a multi-stop OKLCH gradient into a ready-to-render `Uint32Array`.
|
|
178
280
|
*
|
|
179
|
-
* Output bytes are little-endian RGBA
|
|
281
|
+
* Output bytes are little-endian RGBA - drop into a `Uint32Array` view of
|
|
180
282
|
* `Canvas ImageData`, or upload as `RGBA / UNSIGNED_BYTE` via `texImage2D`.
|
|
181
283
|
*
|
|
182
284
|
* Stops are evenly distributed: stop _i_ is at `i / (numStops - 1)`. The
|
|
@@ -202,11 +304,11 @@ export function bakeGradientToUint32(
|
|
|
202
304
|
): Uint32Array;
|
|
203
305
|
|
|
204
306
|
// ============================================================================
|
|
205
|
-
// Difference (
|
|
307
|
+
// Difference (deltaE-OK)
|
|
206
308
|
// ============================================================================
|
|
207
309
|
|
|
208
310
|
/**
|
|
209
|
-
*
|
|
311
|
+
* deltaE-OK color difference: Euclidean distance in OKLab between two buffered
|
|
210
312
|
* OKLCH colors. Zero-allocation.
|
|
211
313
|
*
|
|
212
314
|
* Typical scale: `0.02` indistinguishable, `0.05` subtle, `0.15+` unambiguous.
|
package/index.js
CHANGED
|
@@ -4,18 +4,27 @@ export {
|
|
|
4
4
|
parseHslToBuffer,
|
|
5
5
|
parseOklabToBuffer,
|
|
6
6
|
parseOklchToBuffer,
|
|
7
|
-
parseRgbToBuffer
|
|
7
|
+
parseRgbToBuffer,
|
|
8
|
+
parseDisplayP3ToBuffer
|
|
8
9
|
} from './src/authoring.js';
|
|
9
10
|
|
|
10
11
|
export { bakeGradientToUint32 } from './src/lut.js';
|
|
11
12
|
|
|
12
13
|
export {
|
|
13
14
|
lerpOklchBuffer,
|
|
15
|
+
lerpOklchBufferN,
|
|
14
16
|
packOklchBufferToUint32,
|
|
15
17
|
packOklchBufferToUint32Fast,
|
|
18
|
+
packOklchBufferToUint32IntoN,
|
|
19
|
+
packOklchBufferToUint32P3,
|
|
20
|
+
packOklchBufferToUint32P3Fast,
|
|
16
21
|
sampleColorLUT
|
|
17
22
|
} from './src/runtime.js';
|
|
18
23
|
|
|
19
|
-
export {
|
|
24
|
+
export {
|
|
25
|
+
sRgbToOklchBuffer,
|
|
26
|
+
displayP3ToOklchBuffer,
|
|
27
|
+
oklchToLinearP3
|
|
28
|
+
} from './src/convert.js';
|
|
20
29
|
|
|
21
30
|
export { deltaEOK } from './src/delta.js';
|
package/llms.txt
CHANGED
|
@@ -1,275 +1,44 @@
|
|
|
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
|
-
```ts
|
|
46
|
-
(str: string, outBuf: Float32Array, offset: number) => number /* alpha [0,1] */
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
`parseCSSColor` dispatches by string prefix. Use it unless you know the format and care about shaving the dispatch.
|
|
50
|
-
|
|
51
|
-
Throws on invalid input.
|
|
52
|
-
|
|
53
|
-
### Convert (raw math)
|
|
54
|
-
|
|
55
|
-
```js
|
|
56
|
-
import { sRgbToOklchBuffer } from '@zakkster/lite-color-engine';
|
|
57
|
-
|
|
58
|
-
sRgbToOklchBuffer(r, g, b, outBuf, outOffset);
|
|
59
|
-
// r, g, b are 0-255 bytes. Writes 3 floats to outBuf.
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### Runtime (zero allocations)
|
|
63
|
-
|
|
64
|
-
```js
|
|
65
|
-
import {
|
|
66
|
-
lerpOklchBuffer,
|
|
67
|
-
packOklchBufferToUint32,
|
|
68
|
-
packOklchBufferToUint32Fast,
|
|
69
|
-
sampleColorLUT,
|
|
70
|
-
} from '@zakkster/lite-color-engine';
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Signatures:
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
lerpOklchBuffer(
|
|
77
|
-
bufA: Float32Array, offsetA: number,
|
|
78
|
-
bufB: Float32Array, offsetB: number,
|
|
79
|
-
t: number,
|
|
80
|
-
outBuf: Float32Array, outOffset: number
|
|
81
|
-
): void;
|
|
82
|
-
|
|
83
|
-
packOklchBufferToUint32(buf: Float32Array, offset: number, alpha?: number): number;
|
|
84
|
-
packOklchBufferToUint32Fast(buf: Float32Array, offset: number, alpha?: number): number;
|
|
85
|
-
|
|
86
|
-
sampleColorLUT(lut: Uint32Array, t: number): number;
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### Difference (ΔE-OK)
|
|
90
|
-
|
|
91
|
-
```js
|
|
92
|
-
import { deltaEOK } from '@zakkster/lite-color-engine';
|
|
93
|
-
|
|
94
|
-
deltaEOK(bufA: Float32Array, offsetA: number,
|
|
95
|
-
bufB: Float32Array, offsetB: number): number;
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Euclidean distance in OKLab between two buffered OKLCH colors. Zero allocations. Typical scale: `0.02` indistinguishable, `0.05` subtle, `0.15+` unambiguously different.
|
|
99
|
-
|
|
100
|
-
### Gamut sub-export (`/gamut`)
|
|
101
|
-
|
|
102
|
-
```js
|
|
103
|
-
import {
|
|
104
|
-
gamutMapToSrgbBuffer,
|
|
105
|
-
packOklchBufferToUint32MINDE,
|
|
106
|
-
} from '@zakkster/lite-color-engine/gamut';
|
|
107
|
-
|
|
108
|
-
gamutMapToSrgbBuffer(
|
|
109
|
-
inBuf: Float32Array, inOffset: number,
|
|
110
|
-
outBuf: Float32Array, outOffset: number
|
|
111
|
-
): void;
|
|
112
|
-
|
|
113
|
-
packOklchBufferToUint32MINDE(
|
|
114
|
-
buf: Float32Array, offset: number, alpha?: number
|
|
115
|
-
): number;
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
CSS Color 4 MINDE chroma-reduction gamut mapping. Bisection on chroma at fixed L and H with a JND threshold. `packOklchBufferToUint32MINDE` is signature-compatible with the core packer, so you can pass it directly as the `packer` argument to `bakeGradientToUint32` to bake gamut-accurate LUTs.
|
|
119
|
-
|
|
120
|
-
~30× slower than the core packer. Belongs at LUT-build or authoring time, not per-frame.
|
|
121
|
-
|
|
122
|
-
### Remap sub-export (`/remap`)
|
|
123
|
-
|
|
124
|
-
```js
|
|
125
|
-
import {
|
|
126
|
-
sRgba8ToOklabBuffer,
|
|
127
|
-
oklchToOklabBuffer,
|
|
128
|
-
oklabToOklchBuffer,
|
|
129
|
-
nearestPaletteIndexBuffer,
|
|
130
|
-
remapPixelsToPalette,
|
|
131
|
-
} from '@zakkster/lite-color-engine/remap';
|
|
132
|
-
|
|
133
|
-
sRgba8ToOklabBuffer(
|
|
134
|
-
inU8: Uint8Array | Uint8ClampedArray,
|
|
135
|
-
outLab: Float32Array, pixelCount: number
|
|
136
|
-
): void;
|
|
137
|
-
|
|
138
|
-
oklchToOklabBuffer(inLch: Float32Array, outLab: Float32Array, pixelCount: number): void;
|
|
139
|
-
oklabToOklchBuffer(inLab: Float32Array, outLch: Float32Array, pixelCount: number): void;
|
|
140
|
-
|
|
141
|
-
nearestPaletteIndexBuffer(
|
|
142
|
-
pixelsLab: Float32Array, paletteLab: Float32Array,
|
|
143
|
-
indicesOut: Uint32Array | Uint16Array | Uint8Array,
|
|
144
|
-
pixelCount: number, paletteCount: number,
|
|
145
|
-
opts?: { preserveLightness?: boolean }
|
|
146
|
-
): void;
|
|
147
|
-
|
|
148
|
-
remapPixelsToPalette(
|
|
149
|
-
inU8: Uint8Array | Uint8ClampedArray,
|
|
150
|
-
paletteLch: Float32Array,
|
|
151
|
-
outU32: Uint32Array,
|
|
152
|
-
pixelCount: number, paletteCount: number,
|
|
153
|
-
opts?: { preserveLightness?: boolean }
|
|
154
|
-
): void;
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
SoA palette-remap kernels. Search happens in OKLab (Euclidean distance). Palette is authored in OKLCH and converted to OKLab once per call. `remapPixelsToPalette` uses grow-only module-level scratch for the palette conversion — no per-frame allocation. Alpha byte from the input passes through unchanged in the output. `preserveLightness` searches by `(a, b)` only and synthesizes output as `(pixel.L, palette.a, palette.b)` — retains shading structure under recolor.
|
|
158
|
-
|
|
159
|
-
### LUT (gradient baking)
|
|
160
|
-
|
|
161
|
-
```js
|
|
162
|
-
import { bakeGradientToUint32 } from '@zakkster/lite-color-engine';
|
|
163
|
-
|
|
164
|
-
bakeGradientToUint32(
|
|
165
|
-
keyframesBuf: Float32Array, // [L0,C0,H0, L1,C1,H1, ...]
|
|
166
|
-
numStops: number, // >= 2
|
|
167
|
-
resolution?: number, // default 256, >= 2
|
|
168
|
-
easeFn?: (t: number) => number,
|
|
169
|
-
packer?: (buf: Float32Array, offset: number, alpha?: number) => number
|
|
170
|
-
): Uint32Array;
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
## Integration patterns
|
|
174
|
-
|
|
175
|
-
### Pattern 1 — palette load
|
|
176
|
-
|
|
177
|
-
```js
|
|
178
|
-
const palette = new Float32Array(3 * COLOR_COUNT);
|
|
179
|
-
const alphas = new Float32Array(COLOR_COUNT);
|
|
180
|
-
for (let i = 0; i < cssStrings.length; i++) {
|
|
181
|
-
alphas[i] = parseCSSColor(cssStrings[i], palette, i * 3);
|
|
182
|
-
}
|
|
183
|
-
// palette is now your color storage for the program lifetime.
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Pattern 2 — per-frame tween
|
|
187
|
-
|
|
188
|
-
```js
|
|
189
|
-
const scratch = new Float32Array(3); // module-level, allocated ONCE
|
|
190
|
-
|
|
191
|
-
function frame(t) {
|
|
192
|
-
lerpOklchBuffer(palette, 0, palette, 3, t, scratch, 0);
|
|
193
|
-
return packOklchBufferToUint32(scratch, 0, 1.0);
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
### Pattern 3 — gradient LUT for a particle system
|
|
198
|
-
|
|
199
|
-
```js
|
|
200
|
-
const stops = new Float32Array(9);
|
|
201
|
-
parseCSSColor('#ff0000', stops, 0);
|
|
202
|
-
parseCSSColor('#ffd700', stops, 3);
|
|
203
|
-
parseCSSColor('#00aaff', stops, 6);
|
|
204
|
-
|
|
205
|
-
const lut = bakeGradientToUint32(stops, 3, 256);
|
|
206
|
-
|
|
207
|
-
// In the render loop:
|
|
208
|
-
for (let i = 0; i < particles.count; i++) {
|
|
209
|
-
const lifeT = particles.life[i];
|
|
210
|
-
pixels32[particles.pixelIdx[i]] = sampleColorLUT(lut, lifeT);
|
|
211
|
-
}
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Pattern 4 — direct ImageData write
|
|
215
|
-
|
|
216
|
-
```js
|
|
217
|
-
const imgData = ctx.createImageData(width, height);
|
|
218
|
-
const pixels = new Uint32Array(imgData.data.buffer); // RGBA-LE view
|
|
219
|
-
|
|
220
|
-
for (let i = 0; i < pixels.length; i++) {
|
|
221
|
-
pixels[i] = sampleColorLUT(lut, i / pixels.length);
|
|
222
|
-
}
|
|
223
|
-
ctx.putImageData(imgData, 0, 0);
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
## Invariants the library guarantees
|
|
227
|
-
|
|
228
|
-
- After any successful write, `outBuf[offset + 2]` (hue) is in `[0, 360)`.
|
|
229
|
-
- After any successful write, `outBuf[offset]` (lightness) is in `[0, 1]`.
|
|
230
|
-
- After any successful write, `outBuf[offset + 1]` (chroma) is in `[0, +∞)`.
|
|
231
|
-
- `packOklchBufferToUint32` and `packOklchBufferToUint32Fast` return values that are **always** non-negative integers (they apply `>>> 0`).
|
|
232
|
-
- Hue interpolation is **always** shortest-path (uses `lerpAngle` from `@zakkster/lite-lerp`).
|
|
233
|
-
- Bake output bytes are **always** little-endian RGBA. Same on every browser.
|
|
234
|
-
|
|
235
|
-
## Common pitfalls
|
|
236
|
-
|
|
237
|
-
- **Mistaking the byte order.** The pack functions return little-endian RGBA, so on x86 the low byte is R. If you mask with `(packed >> 24) & 0xff` expecting red, you'll get alpha. Use `packed & 0xff` for red.
|
|
238
|
-
|
|
239
|
-
- **Storing per-color objects anyway.** The library is explicitly buffer-first. If you find yourself writing `{ l, c, h }` wrappers, you've reintroduced GC pressure; just use the offset-based API.
|
|
240
|
-
|
|
241
|
-
- **Choosing the Fast packer for UI.** `packOklchBufferToUint32Fast` darkens mid-tones by ~10/255 and shifts warm midtones toward black. Use it for transient pixels (particle trails, alpha-blended sprites). Use the accurate variant for anything a designer or QA might compare against the source color.
|
|
242
|
-
|
|
243
|
-
- **Using non-monotonic eases with the LUT.** `bakeGradientToUint32` clamps eased `t` to `[0, 1]`. Overshoot easings (`easeOutBack`, etc.) get silently capped because a fixed-resolution LUT cannot represent overshoot. If you need overshoot, drive the eased `t` at sample time with `sampleColorLUT(lut, easeFn(t))` instead.
|
|
244
|
-
|
|
245
|
-
- **Forgetting alpha is returned, not written.** Parsers return alpha. They do not write it to the buffer. If you mix parsers and hand-written buffers, decide on an alpha layout up front (sibling array, stride-4, or per-instance) and stick with it.
|
|
246
|
-
|
|
247
|
-
## What this library is NOT
|
|
248
|
-
|
|
249
|
-
- It is not a general-purpose color manipulation library. No `darken`, `lighten`, `mix`, `saturate`, `complement`, etc. methods. Compose your own from the buffer math primitives.
|
|
250
|
-
- The core `packOklchBufferToUint32` uses a hard channel clamp for its runtime fast path. Accurate CSS Color 4 MINDE chroma-reduction gamut mapping ships in the `/gamut` sub-export (see below) — use that at LUT-build or authoring time when the hue-shift artifacts of the hard clamp matter.
|
|
251
|
-
- It is not a CSS parser for everything. It does CSS Color Level 4 colors. It does not handle `color()`, `color-mix()`, relative color syntax, or system colors.
|
|
252
|
-
- It is not async. There are no Promises. Every function is synchronous and side-effect-free (writes only to its `outBuf` argument).
|
|
253
|
-
|
|
254
|
-
## Performance budget
|
|
255
|
-
|
|
256
|
-
| Operation | Wall-clock estimate (modern V8, 2024 laptop) |
|
|
257
|
-
|---|---|
|
|
258
|
-
| `parseCSSColor` | ~1µs per call (not the hot path; numbers vary by format) |
|
|
259
|
-
| `lerpOklchBuffer` | ~50ns per call |
|
|
260
|
-
| `packOklchBufferToUint32` | ~150ns per call |
|
|
261
|
-
| `packOklchBufferToUint32Fast` | ~80ns per call |
|
|
262
|
-
| `sampleColorLUT` | ~10ns per call |
|
|
263
|
-
| `bakeGradientToUint32(..., 256)` | ~50µs total |
|
|
264
|
-
|
|
265
|
-
Absolute numbers depend on hardware. Relative ratios are stable.
|
|
266
|
-
|
|
267
|
-
## Versioning
|
|
268
|
-
|
|
269
|
-
`1.0.x` — the locked core public API surface. No breaking changes within the major. Patch releases for bug fixes; minor releases may **add** functions but not remove or rename.
|
|
270
|
-
|
|
271
|
-
`1.1.0` — adds `deltaEOK` in core, plus two additive sub-exports:
|
|
272
|
-
- `@zakkster/lite-color-engine/gamut` — CSS Color 4 MINDE gamut mapping and an accurate packer.
|
|
273
|
-
- `@zakkster/lite-color-engine/remap` — SoA batch converters and palette-remap kernels.
|
|
274
|
-
|
|
275
|
-
No breaking changes; the v1.0 surface is untouched. See `CHANGELOG.md` for full details.
|
|
1
|
+
# lite-color-engine - LLM Reference (v1.3)
|
|
2
|
+
|
|
3
|
+
## Core Exports
|
|
4
|
+
|
|
5
|
+
### Parsing
|
|
6
|
+
- `parseCSSColor(str, outBuf, offset)` — Universal parser. Now supports `color(display-p3 r g b / alpha)`
|
|
7
|
+
- `parseDisplayP3ToBuffer(str, outBuf, offset)` — Direct P3 parser
|
|
8
|
+
|
|
9
|
+
### Conversion
|
|
10
|
+
- `sRgbToOklchBuffer(r, g, b, out, off)`
|
|
11
|
+
- `displayP3ToOklchBuffer(r, g, b, out, off)` — New in v1.2
|
|
12
|
+
- `oklchToLinearP3(L, C, H, out)` — Inverse for P3 packing
|
|
13
|
+
|
|
14
|
+
### Runtime (Hot Path - Zero GC)
|
|
15
|
+
- `lerpOklchBuffer(...)`
|
|
16
|
+
- `packOklchBufferToUint32(buf, off, alpha?)` - Accurate sRGB
|
|
17
|
+
- `packOklchBufferToUint32Fast(...)` - Fast approx
|
|
18
|
+
- `packOklchBufferToUint32P3(buf, off, alpha?)` - Accurate P3 output
|
|
19
|
+
- `packOklchBufferToUint32P3Fast(...)` - Fast P3 output
|
|
20
|
+
- `sampleColorLUT(lut, t)` - O(1) lookup into a baked gradient LUT
|
|
21
|
+
|
|
22
|
+
### Batch kernels (v1.3, for 100k+ particle systems)
|
|
23
|
+
- `lerpOklchBufferN(a, offA, b, offB, t, out, offOut, n)` - bulk lerp n triplets (stride 3), bit-exact to n scalar calls.
|
|
24
|
+
- `packOklchBufferToUint32IntoN(src, offSrc, dst, offDst, n, alpha?, useLut?)` - bulk pack n OKLCH -> Uint32 (dst stride 1). `useLut=true` uses the 4k transfer LUT (~4.4x throughput vs accurate, within 1 LSB; Fast-packer speed at near-exact accuracy).
|
|
25
|
+
- Note: the batch win comes from `useLut`, not from call-amortization (accurate batch ~= scalar loop in V8; pow-bound).
|
|
26
|
+
|
|
27
|
+
### Gamut mapping (subpath: `@zakkster/lite-color-engine/gamut`, NOT hot-path)
|
|
28
|
+
- `packOklchBufferToUint32MINDE(buf, off, alpha?)` — MINDE chroma-reduction gamut map. ~30x slower than the core packer; use at LUT-build/authoring time.
|
|
29
|
+
|
|
30
|
+
### LUT
|
|
31
|
+
- `bakeGradientToUint32(..., packer?)` — Accepts any of the above packers
|
|
32
|
+
|
|
33
|
+
## Key Principles
|
|
34
|
+
- All hot-path functions are allocation-free
|
|
35
|
+
- P3 is strictly opt-in
|
|
36
|
+
- Use `Float32Array` buffers for everything
|
|
37
|
+
- Prefer `parse*ToBuffer` at init time
|
|
38
|
+
|
|
39
|
+
## Accuracy Tiers (v1.2)
|
|
40
|
+
1. Fast
|
|
41
|
+
2. Accurate-Clamp (default)
|
|
42
|
+
3. Gamut-Mapped (MINDE)
|
|
43
|
+
|
|
44
|
+
Use P3 variants when targeting `canvas.getContext('2d', { colorSpace: 'display-p3' })`.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-color-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
5
|
-
"description": "Zero-GC, data-oriented OKLCH color engine
|
|
5
|
+
"description": "Zero-GC, data-oriented OKLCH color engine and WebGL/Canvas buffer pipeline.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
8
8
|
"main": "./index.js",
|
|
@@ -59,7 +59,8 @@
|
|
|
59
59
|
"game-dev"
|
|
60
60
|
],
|
|
61
61
|
"scripts": {
|
|
62
|
-
"test": "vitest run"
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"bench": "node bench/benchmark.mjs"
|
|
63
64
|
},
|
|
64
65
|
"dependencies": {
|
|
65
66
|
"@zakkster/lite-lerp": "^1.0.0"
|