@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/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 drop straight into `new Uint32Array(imageData.data.buffer)`.
12
+ * order - drop straight into `new Uint32Array(imageData.data.buffer)`.
13
13
  */
14
14
 
15
15
  // ============================================================================
16
- // Authoring (CSS Parsing OKLCH Buffer)
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) polar (C, H) before
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 Björn Ottosson's OKLab (2020) algorithm with defenses against
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, +∞)`. Hue is canonicalized to `[0, 360)`.
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 the format consumed directly by `Canvas ImageData` via
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 OKLCH here` recovers the original byte exactly
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 drop into a `Uint32Array` view of
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 (ΔE-OK)
307
+ // Difference (deltaE-OK)
206
308
  // ============================================================================
207
309
 
208
310
  /**
209
- * ΔE-OK color difference: Euclidean distance in OKLab between two buffered
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 { sRgbToOklchBuffer } from './src/convert.js';
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
- # @zakkster/lite-color-engine LLM Reference
2
-
3
- > Zero-GC, data-oriented OKLCH color engine for WebGL/Canvas rendering pipelines.
4
- > Buffer-and-offset API. Three layers: authoring (CSS parsing), LUT (gradient
5
- > baking), runtime (zero-allocation hot path).
6
-
7
- ## Identity
8
-
9
- - **Package name:** `@zakkster/lite-color-engine`
10
- - **Module type:** ESM only (`"type": "module"`)
11
- - **Peer dependency:** `@zakkster/lite-lerp` (provides `lerp`, `lerpAngle`)
12
- - **Bundle:** sub-2KB min+gzip
13
- - **Side effects:** none (`"sideEffects": false`)
14
- - **Node:** `>=18`
15
-
16
- ## Mental model
17
-
18
- Every color in this library is exactly **3 contiguous Float32 entries** in a `Float32Array`: `[L, C, H]`.
19
-
20
- - `L` (Lightness): `[0, 1]`, perceptually uniform.
21
- - `C` (Chroma): `[0, ~0.4]` in practice, mathematically unbounded but only a small chroma range maps to displayable sRGB.
22
- - `H` (Hue): `[0, 360)` degrees. Always canonicalized after every write.
23
-
24
- Alpha is **not** stored in the OKLCH buffer. Parsers return alpha as a `number`. If you need alpha alongside color, store it in a sibling `Float32Array` or add a fourth slot yourself (the library only reads the first three).
25
-
26
- The output of the pack functions is a 32-bit unsigned integer in **little-endian RGBA byte order** — exactly what `new Uint32Array(imageData.data.buffer)` consumes. Browsers are universally little-endian; do not byte-swap.
27
-
28
- ## Public API surface
29
-
30
- ### Authoring (init-time, allocations OK)
31
-
32
- ```js
33
- import {
34
- parseCSSColor,
35
- parseHexToBuffer,
36
- parseRgbToBuffer,
37
- parseHslToBuffer,
38
- parseOklchToBuffer,
39
- parseOklabToBuffer,
40
- } from '@zakkster/lite-color-engine';
41
- ```
42
-
43
- All five format-specific parsers and `parseCSSColor` have the same signature:
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.1.0",
3
+ "version": "1.3.0",
4
4
  "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
5
- "description": "Zero-GC, data-oriented OKLCH color engine with MINDE gamut mapping and palette-remap kernels for WebGL/Canvas pipelines.",
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"