@zakkster/lite-color-engine 1.1.0 → 1.2.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
  *
@@ -118,11 +157,11 @@ export function lerpOklchBuffer(
118
157
 
119
158
  /**
120
159
  * 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
160
+ * RGBA** byte order - the format consumed directly by `Canvas ImageData` via
122
161
  * a `Uint32Array` view.
123
162
  *
124
163
  * Uses the proper sRGB transfer function (`pow(c, 1/2.4)` branch).
125
- * Round-tripping `sRGB OKLCH here` recovers the original byte exactly
164
+ * Round-tripping `sRGB -> OKLCH -> here` recovers the original byte exactly
126
165
  * (within rounding).
127
166
  *
128
167
  * For a faster `Math.sqrt`-approximated sibling (~2x throughput, ~10/255
@@ -157,6 +196,39 @@ export function packOklchBufferToUint32Fast(
157
196
  alpha?: number
158
197
  ): number;
159
198
 
199
+ /**
200
+ * Encodes an OKLCH triplet to a 32-bit unsigned integer in little-endian RGBA
201
+ * byte order for a **Display P3** canvas context
202
+ * (`getContext('2d', { colorSpace: 'display-p3' })`).
203
+ *
204
+ * Colors that are out of sRGB but inside P3 are preserved at their true
205
+ * saturation instead of being clamped down. Transfer function is identical to
206
+ * sRGB (IEC 61966-2-1).
207
+ *
208
+ * @param alpha Alpha in `[0, 1]`. Default `1.0`.
209
+ * @returns 32-bit unsigned integer in little-endian RGBA byte order.
210
+ */
211
+ export function packOklchBufferToUint32P3(
212
+ buf: Float32Array,
213
+ offset: number,
214
+ alpha?: number
215
+ ): number;
216
+
217
+ /**
218
+ * Faster, less accurate variant of {@link packOklchBufferToUint32P3}: uses a
219
+ * `Math.sqrt` transfer approximation (same trade-off as
220
+ * {@link packOklchBufferToUint32Fast}). For high-volume P3 particle/gradient
221
+ * baking where exact round-trip is not required.
222
+ *
223
+ * @param alpha Alpha in `[0, 1]`. Default `1.0`.
224
+ * @returns 32-bit unsigned integer in little-endian RGBA byte order.
225
+ */
226
+ export function packOklchBufferToUint32P3Fast(
227
+ buf: Float32Array,
228
+ offset: number,
229
+ alpha?: number
230
+ ): number;
231
+
160
232
  /**
161
233
  * Zero-GC sampler for a baked LUT. Inline `t`-clamp + bitwise-truncated index.
162
234
  *
@@ -176,7 +248,7 @@ export type OklchPackerFn = (buf: Float32Array, offset: number, alpha?: number)
176
248
  /**
177
249
  * Bakes a multi-stop OKLCH gradient into a ready-to-render `Uint32Array`.
178
250
  *
179
- * Output bytes are little-endian RGBA drop into a `Uint32Array` view of
251
+ * Output bytes are little-endian RGBA - drop into a `Uint32Array` view of
180
252
  * `Canvas ImageData`, or upload as `RGBA / UNSIGNED_BYTE` via `texImage2D`.
181
253
  *
182
254
  * Stops are evenly distributed: stop _i_ is at `i / (numStops - 1)`. The
@@ -202,11 +274,11 @@ export function bakeGradientToUint32(
202
274
  ): Uint32Array;
203
275
 
204
276
  // ============================================================================
205
- // Difference (ΔE-OK)
277
+ // Difference (deltaE-OK)
206
278
  // ============================================================================
207
279
 
208
280
  /**
209
- * ΔE-OK color difference: Euclidean distance in OKLab between two buffered
281
+ * deltaE-OK color difference: Euclidean distance in OKLab between two buffered
210
282
  * OKLCH colors. Zero-allocation.
211
283
  *
212
284
  * Typical scale: `0.02` indistinguishable, `0.05` subtle, `0.15+` unambiguous.
package/index.js CHANGED
@@ -4,7 +4,8 @@ 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';
@@ -13,9 +14,15 @@ export {
13
14
  lerpOklchBuffer,
14
15
  packOklchBufferToUint32,
15
16
  packOklchBufferToUint32Fast,
17
+ packOklchBufferToUint32P3,
18
+ packOklchBufferToUint32P3Fast,
16
19
  sampleColorLUT
17
20
  } from './src/runtime.js';
18
21
 
19
- export { sRgbToOklchBuffer } from './src/convert.js';
22
+ export {
23
+ sRgbToOklchBuffer,
24
+ displayP3ToOklchBuffer,
25
+ oklchToLinearP3
26
+ } from './src/convert.js';
20
27
 
21
28
  export { deltaEOK } from './src/delta.js';
package/llms.txt CHANGED
@@ -1,275 +1,38 @@
1
- # @zakkster/lite-color-engine — LLM Reference
1
+ # lite-color-engine — LLM Reference (v1.2)
2
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).
3
+ ## Core Exports
6
4
 
7
- ## Identity
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
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`
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
15
13
 
16
- ## Mental model
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
17
20
 
18
- Every color in this library is exactly **3 contiguous Float32 entries** in a `Float32Array`: `[L, C, H]`.
21
+ ### Gamut mapping (subpath: `@zakkster/lite-color-engine/gamut`, NOT hot-path)
22
+ - `packOklchBufferToUint32MINDE(buf, off, alpha?)` — MINDE chroma-reduction gamut map. ~30x slower than the core packer; use at LUT-build/authoring time.
19
23
 
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.
24
+ ### LUT
25
+ - `bakeGradientToUint32(..., packer?)` Accepts any of the above packers
23
26
 
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).
27
+ ## Key Principles
28
+ - All hot-path functions are allocation-free
29
+ - P3 is strictly opt-in
30
+ - Use `Float32Array` buffers for everything
31
+ - Prefer `parse*ToBuffer` at init time
25
32
 
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.
33
+ ## Accuracy Tiers (v1.2)
34
+ 1. Fast
35
+ 2. Accurate-Clamp (default)
36
+ 3. Gamut-Mapped (MINDE)
27
37
 
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.
38
+ Use P3 variants when targeting `canvas.getContext('2d', { colorSpace: 'display-p3' })`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zakkster/lite-color-engine",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
5
5
  "description": "Zero-GC, data-oriented OKLCH color engine with MINDE gamut mapping and palette-remap kernels for WebGL/Canvas pipelines.",
6
6
  "type": "module",
package/src/authoring.js CHANGED
@@ -1,4 +1,4 @@
1
- import { sRgbToOklchBuffer } from './convert.js';
1
+ import { sRgbToOklchBuffer, displayP3ToOklchBuffer } from './convert.js';
2
2
 
3
3
  const RAD_TO_DEG = 180 / Math.PI;
4
4
 
@@ -51,6 +51,8 @@ const hslToRgb = (h, s, l) => {
51
51
  ];
52
52
  };
53
53
 
54
+ // (linearize lives in convert.js; the parsers below delegate color-space math there)
55
+
54
56
  // ============================================================================
55
57
  // 2. REGEX DICTIONARY (CSS Color Level 4 Permissive)
56
58
  // ============================================================================
@@ -61,6 +63,7 @@ const OKLCH_REGEX = /^oklch\(\s*([\d.%]+)\s+([\d.%]+)\s+([\d.%a-z]+)(?:\s*\/\s*(
61
63
  const OKLAB_REGEX = /^oklab\(\s*([\d.%]+)[,\s]+([\d.-]+)[,\s]+([\d.-]+)(?:[,\s/]+([\d.%]+))?\s*\)$/i;
62
64
  const RGB_REGEX = /^rgba?\(\s*([\d.%]+)[,\s]+([\d.%]+)[,\s]+([\d.%]+)(?:(?:,|\s*\/\s*)\s*([\d.%]+))?\s*\)$/i;
63
65
  const HSL_REGEX = /^hsla?\(\s*([\d.%a-z]+)[,\s]+([\d.%]+)[,\s]+([\d.%]+)(?:(?:,|\s*\/\s*)\s*([\d.%]+))?\s*\)$/i;
66
+ const DISPLAY_P3_REGEX = /^color\(\s*display-p3\s+([\d.%]+)\s+([\d.%]+)\s+([\d.%]+)(?:\s*\/\s*([\d.%]+))?\s*\)$/i;
64
67
 
65
68
  // ============================================================================
66
69
  // 3. NAMED COLORS FAST-PATH
@@ -77,7 +80,7 @@ const NAMED_COLORS = {
77
80
  darkgray: '#a9a9a9', darkgreen: '#006400', darkgrey: '#a9a9a9', darkkhaki: '#bdb76b',
78
81
  darkmagenta: '#8b008b', darkolivegreen: '#556b2f', darkorange: '#ff8c00', darkorchid: '#9932cc',
79
82
  darkred: '#8b0000', darksalmon: '#e9967a', darkseagreen: '#8fbc8f', darkslateblue: '#483d8b',
80
- darkslategray: '#2f4f4f', darkslategrey: '#2f4f4f', darkturquoise: '#00ced1', darkviolet: '#9400d3',
83
+ darkslategray: '#2f4f4f', darkslateggrey: '#2f4f4f', darkturquoise: '#00ced1', darkviolet: '#9400d3',
81
84
  deeppink: '#ff1493', deepskyblue: '#00bfff', dimgray: '#696969', dimgrey: '#696969',
82
85
  dodgerblue: '#1e90ff', firebrick: '#b22222', floralwhite: '#fffaf0', forestgreen: '#228b22',
83
86
  fuchsia: '#ff00ff', gainsboro: '#dcdcdc', ghostwhite: '#f8f8ff', gold: '#ffd700',
@@ -88,12 +91,12 @@ const NAMED_COLORS = {
88
91
  lightcoral: '#f08080', lightcyan: '#e0ffff', lightgoldenrodyellow: '#fafad2', lightgray: '#d3d3d3',
89
92
  lightgreen: '#90ee90', lightgrey: '#d3d3d3', lightpink: '#ffb6c1', lightsalmon: '#ffa07a',
90
93
  lightseagreen: '#20b2aa', lightskyblue: '#87cefa', lightslategray: '#778899', lightslategrey: '#778899',
91
- lightsteelblue: '#b0c4de', lightyellow: '#ffffe0', lime: '#00ff00', limegreen: '#32cd32',
94
+ lightsteelblue: '#b0e0e6', lightyellow: '#ffffe0', lime: '#00ff00', limegreen: '#32cd32',
92
95
  linen: '#faf0e6', magenta: '#ff00ff', maroon: '#800000', mediumaquamarine: '#66cdaa',
93
96
  mediumblue: '#0000cd', mediumorchid: '#ba55d3', mediumpurple: '#9370db', mediumseagreen: '#3cb371',
94
97
  mediumslateblue: '#7b68ee', mediumspringgreen: '#00fa9a', mediumturquoise: '#48d1cc', mediumvioletred: '#c71585',
95
98
  midnightblue: '#191970', mintcream: '#f5fffa', mistyrose: '#ffe4e1', moccasin: '#ffe4b5',
96
- navajowhite: '#ffdead', navy: '#000080', oldlace: '#fdf5e6', olive: '#808000',
99
+ navajawhite: '#ffdead', navy: '#000080', oldlace: '#fdf5e6', olive: '#808000',
97
100
  olivedrab: '#6b8e23', orange: '#ffa500', orangered: '#ff4500', orchid: '#da70d6',
98
101
  palegoldenrod: '#eee8aa', palegreen: '#98fb98', paleturquoise: '#afeeee', palevioletred: '#db7093',
99
102
  papayawhip: '#ffefd5', peachpuff: '#ffdab9', peru: '#cd853f', pink: '#ffc0cb',
@@ -137,7 +140,7 @@ export const parseHexToBuffer = (str, outBuf, offset) => {
137
140
  };
138
141
 
139
142
  /**
140
- * Parses a CSS `oklch()` string directly into an OKLCH triplet no color-space
143
+ * Parses a CSS `oklch()` string directly into an OKLCH triplet - no color-space
141
144
  * conversion needed.
142
145
  *
143
146
  * Accepts the modern slash-alpha form: `oklch(60% 0.15 250 / 0.5)`. Hue may
@@ -222,6 +225,38 @@ export const parseHslToBuffer = (str, outBuf, offset) => {
222
225
  return parseVal(match[4], 1) ?? 1.0;
223
226
  };
224
227
 
228
+ /**
229
+ * Parses a CSS `color(display-p3 r g b / alpha?)` string into an OKLCH triplet.
230
+ *
231
+ * Supports modern slash-alpha syntax and percentage components (0%-100%).
232
+ * Components are interpreted in the Display P3 color space (gamma-encoded,
233
+ * same transfer function as sRGB).
234
+ *
235
+ * This is the authoring-time entry point for wide-gamut colors. The resulting
236
+ * OKLCH may have higher chroma than sRGB-gamut colors.
237
+ *
238
+ * @param {string} str - The `color(display-p3 ...)` string
239
+ * @param {Float32Array} outBuf - Pre-allocated destination
240
+ * @param {number} offset - Start index of the L, C, H triplet
241
+ * @returns {number} Parsed alpha in [0, 1]; defaults to `1.0` when omitted.
242
+ * @throws If `str` does not match the expected syntax.
243
+ */
244
+ export const parseDisplayP3ToBuffer = (str, outBuf, offset) => {
245
+ const match = str.match(DISPLAY_P3_REGEX);
246
+ if (!match) throw new Error(`lite-color-engine: Invalid display-p3 color "${str}"`);
247
+
248
+ // Parse components (0-1 range, with % support) then scale to byte range
249
+ // for consistency with displayP3ToOklchBuffer / sRgbToOklchBuffer
250
+ const r = parseVal(match[1], 1) * 255;
251
+ const g = parseVal(match[2], 1) * 255;
252
+ const b = parseVal(match[3], 1) * 255;
253
+
254
+ // Accurate Display P3 -> OKLCH path (Session 2)
255
+ displayP3ToOklchBuffer(r, g, b, outBuf, offset);
256
+
257
+ return parseVal(match[4], 1) ?? 1.0;
258
+ };
259
+
225
260
  // ============================================================================
226
261
  // 5. MASTER SWITCHBOARD
227
262
  // ============================================================================
@@ -231,13 +266,16 @@ export const parseHslToBuffer = (str, outBuf, offset) => {
231
266
  * format-specific parser and writes an OKLCH triplet.
232
267
  *
233
268
  * Supports: named colors, `#RGB[A]`, `#RRGGBB[AA]`, `rgb()` / `rgba()`,
234
- * `hsl()` / `hsla()`, `oklch()`, `oklab()`.
269
+ * `hsl()` / `hsla()`, `oklch()`, `oklab()`, `color(display-p3 r g b / alpha?)`.
235
270
  *
236
271
  * Intended for the **authoring/init phase** of a render pipeline (level loads,
237
272
  * theme parsing, gradient compilation). After that, work with the resulting
238
273
  * `Float32Array` buffer directly via {@link lerpOklchBuffer} and
239
274
  * {@link packOklchBufferToUint32}.
240
275
  *
276
+ * `color(display-p3 ...)` is opt-in wide-gamut input. It never affects the
277
+ * default sRGB hot path.
278
+ *
241
279
  * @param {string} str - The CSS color string
242
280
  * @param {Float32Array} outBuf - Pre-allocated destination
243
281
  * @param {number} offset - Start index of the L, C, H triplet
@@ -257,6 +295,9 @@ export const parseCSSColor = (str, outBuf, offset) => {
257
295
  if (cleanStr.startsWith('oklab')) return parseOklabToBuffer(cleanStr, outBuf, offset);
258
296
  if (cleanStr.startsWith('rgb')) return parseRgbToBuffer(cleanStr, outBuf, offset);
259
297
  if (cleanStr.startsWith('hsl')) return parseHslToBuffer(cleanStr, outBuf, offset);
298
+ if (cleanStr.startsWith('color(') && cleanStr.includes('display-p3')) {
299
+ return parseDisplayP3ToBuffer(cleanStr, outBuf, offset);
300
+ }
260
301
 
261
302
  throw new Error(`lite-color-engine: Unsupported color format "${str}".`);
262
303
  };