@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/CHANGELOG.md +22 -102
- package/README.md +37 -356
- package/index.d.ts +83 -11
- package/index.js +9 -2
- package/llms.txt +29 -266
- package/package.json +1 -1
- package/src/authoring.js +47 -6
- package/src/convert.js +127 -5
- package/src/runtime.js +64 -4
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
|
*
|
|
@@ -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
|
|
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
|
|
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
|
|
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 (
|
|
277
|
+
// Difference (deltaE-OK)
|
|
206
278
|
// ============================================================================
|
|
207
279
|
|
|
208
280
|
/**
|
|
209
|
-
*
|
|
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 {
|
|
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
|
-
#
|
|
1
|
+
# lite-color-engine — LLM Reference (v1.2)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
33
|
+
## Accuracy Tiers (v1.2)
|
|
34
|
+
1. Fast
|
|
35
|
+
2. Accurate-Clamp (default)
|
|
36
|
+
3. Gamut-Mapped (MINDE)
|
|
27
37
|
|
|
28
|
-
|
|
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.
|
|
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',
|
|
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: '#
|
|
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
|
-
|
|
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
|
|
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
|
};
|