@zakkster/lite-color-engine 1.3.0 → 1.5.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 +202 -0
- package/README.md +465 -68
- package/index.d.ts +110 -5
- package/index.js +8 -2
- package/llms.txt +38 -7
- package/package.json +4 -3
- package/src/Gamut.d.ts +18 -0
- package/src/Gamut.js +33 -0
- package/src/authoring.js +60 -3
- package/src/lut.js +37 -16
- package/src/runtime.js +297 -2
package/index.d.ts
CHANGED
|
@@ -87,6 +87,25 @@ export function parseCSSColor(str: string, outBuf: Float32Array, offset: number)
|
|
|
87
87
|
*/
|
|
88
88
|
export function parseDisplayP3ToBuffer(str: string, outBuf: Float32Array, offset: number): number;
|
|
89
89
|
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Formatters (round-trip emit - authoring layer)
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Emits a modern CSS `oklch(L% C H)` string (or with `/ alpha`) from a buffered
|
|
96
|
+
* OKLCH triplet. Precision is chosen for good visual and numeric round-tripping
|
|
97
|
+
* through `parseCSSColor`. Alpha is omitted when not supplied or >= 0.9995.
|
|
98
|
+
* Authoring-time helper (allocates a string); not for per-frame hot paths.
|
|
99
|
+
*/
|
|
100
|
+
export function formatOklchCss(buf: Float32Array, off: number, alpha?: number): string;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Converts an OKLCH triplet to a `#rrggbb` hex string via the accurate sRGB
|
|
104
|
+
* pack path. Round-trips with `parseHexToBuffer` to within 1 LSB per channel.
|
|
105
|
+
* Authoring-time helper (allocates a string); not for per-frame hot paths.
|
|
106
|
+
*/
|
|
107
|
+
export function formatHex(buf: Float32Array, off: number): string;
|
|
108
|
+
|
|
90
109
|
// ============================================================================
|
|
91
110
|
// Convert (Raw Math)
|
|
92
111
|
// ============================================================================
|
|
@@ -212,6 +231,28 @@ export function packOklchBufferToUint32Fast(
|
|
|
212
231
|
alpha?: number
|
|
213
232
|
): number;
|
|
214
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Dithered sibling of {@link packOklchBufferToUint32}. Applies a threshold-
|
|
236
|
+
* offset dither in gamma-encoded space with a caller-supplied `noise01` value,
|
|
237
|
+
* shared across R/G/B so the dither is a luminance pattern (no chroma speckle).
|
|
238
|
+
* Alpha is undithered.
|
|
239
|
+
*
|
|
240
|
+
* Parity: `noise01 === 0.5` reproduces the byte output of the plain packer
|
|
241
|
+
* exactly (asserted by the test suite).
|
|
242
|
+
*
|
|
243
|
+
* `noise01` is expected in `[0, 1)`. When driven from the shared blue-noise
|
|
244
|
+
* tile, use `getBlueNoise64()[((y & 63) << 6) | (x & 63)] / 256`, which stays
|
|
245
|
+
* strictly below 1 and gives a low-discrepancy sample.
|
|
246
|
+
*
|
|
247
|
+
* @returns 32-bit unsigned integer in little-endian RGBA byte order.
|
|
248
|
+
*/
|
|
249
|
+
export function packOklchBufferToUint32Dithered(
|
|
250
|
+
buf: Float32Array,
|
|
251
|
+
offset: number,
|
|
252
|
+
alpha?: number,
|
|
253
|
+
noise01?: number
|
|
254
|
+
): number;
|
|
255
|
+
|
|
215
256
|
/**
|
|
216
257
|
* Batch packer: n OKLCH triplets -> n Uint32 packed colors (stride 1 in dst).
|
|
217
258
|
* Opt-in 4k sRGB LUT (`useLut=true`) for fast-packer throughput at near-exact accuracy.
|
|
@@ -226,6 +267,30 @@ export function packOklchBufferToUint32IntoN(
|
|
|
226
267
|
useLut?: boolean
|
|
227
268
|
): void;
|
|
228
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Dithered batch packer: walks a 64x64 blue-noise tile in row-major order and
|
|
272
|
+
* applies a per-pixel luminance-patterned threshold dither (same noise value
|
|
273
|
+
* across R/G/B; alpha undithered). The tile is torus-indexed
|
|
274
|
+
* (`tile[((y & 63) << 6) | (x & 63)]`) so any `x0`/`y0`/`rowWidth` combination
|
|
275
|
+
* is safe. Zero allocations; no modulo/division in the hot loop.
|
|
276
|
+
*
|
|
277
|
+
* The 4k sRGB transfer LUT is *not* an option here — that LUT stores rounded
|
|
278
|
+
* bytes and would silently degrade the dither to plain rounding. A companion
|
|
279
|
+
* float LUT restoring this axis is a v1.6 candidate.
|
|
280
|
+
*/
|
|
281
|
+
export function packOklchBufferToUint32IntoNDithered(
|
|
282
|
+
src: Float32Array,
|
|
283
|
+
offSrc: number,
|
|
284
|
+
dst: Uint32Array,
|
|
285
|
+
offDst: number,
|
|
286
|
+
n: number,
|
|
287
|
+
alpha: number,
|
|
288
|
+
noiseTile: Uint8Array,
|
|
289
|
+
x0: number,
|
|
290
|
+
y0: number,
|
|
291
|
+
rowWidth: number
|
|
292
|
+
): void;
|
|
293
|
+
|
|
229
294
|
/**
|
|
230
295
|
* Encodes an OKLCH triplet to a 32-bit unsigned integer in little-endian RGBA
|
|
231
296
|
* byte order for a **Display P3** canvas context
|
|
@@ -259,6 +324,37 @@ export function packOklchBufferToUint32P3Fast(
|
|
|
259
324
|
alpha?: number
|
|
260
325
|
): number;
|
|
261
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Batch sibling of {@link packOklchBufferToUint32P3}. Packs `n` OKLCH triplets
|
|
329
|
+
* (stride 3) into `n` Uint32 pixels encoded for Display P3. `useLut=true`
|
|
330
|
+
* shares the 4k sRGB transfer LUT (the P3 transfer function is IEC 61966-2-1,
|
|
331
|
+
* identical to sRGB — no separate table is allocated), giving fast-packer
|
|
332
|
+
* throughput at within ~1 LSB of the exact encoder.
|
|
333
|
+
*/
|
|
334
|
+
export function packOklchBufferToUint32P3IntoN(
|
|
335
|
+
src: Float32Array,
|
|
336
|
+
offSrc: number,
|
|
337
|
+
dst: Uint32Array,
|
|
338
|
+
offDst: number,
|
|
339
|
+
n: number,
|
|
340
|
+
alpha?: number,
|
|
341
|
+
useLut?: boolean
|
|
342
|
+
): void;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Returns the shared 64x64 blue-noise tile as a `Uint8Array(4096)`, lazily
|
|
346
|
+
* decoded from an inlined base64 blob on first call and reused thereafter.
|
|
347
|
+
*
|
|
348
|
+
* Index a pixel via `tile[((y & 63) << 6) | (x & 63)]`. The tile is
|
|
349
|
+
* torus-tileable: repeating it end-to-end on either axis is spectrally
|
|
350
|
+
* seamless. Each byte 0..255 appears exactly 16 times (uniform histogram),
|
|
351
|
+
* so `byte / 256` gives a low-discrepancy sample in `[0, 1)`.
|
|
352
|
+
*
|
|
353
|
+
* The returned reference is shared and must not be mutated; callers that
|
|
354
|
+
* need to modify the values should copy first.
|
|
355
|
+
*/
|
|
356
|
+
export function getBlueNoise64(): Uint8Array;
|
|
357
|
+
|
|
262
358
|
/**
|
|
263
359
|
* Zero-GC sampler for a baked LUT. Inline `t`-clamp + bitwise-truncated index.
|
|
264
360
|
*
|
|
@@ -281,10 +377,17 @@ export type OklchPackerFn = (buf: Float32Array, offset: number, alpha?: number)
|
|
|
281
377
|
* Output bytes are little-endian RGBA - drop into a `Uint32Array` view of
|
|
282
378
|
* `Canvas ImageData`, or upload as `RGBA / UNSIGNED_BYTE` via `texImage2D`.
|
|
283
379
|
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
380
|
+
* **Open mode** (default): stops are evenly distributed — stop _i_ at
|
|
381
|
+
* `i / (numStops - 1)`; LUT sample _j_ at `j / (resolution - 1)`. Sample 0
|
|
382
|
+
* lands on stop 0, sample `resolution-1` on the last stop. Overshoot from
|
|
383
|
+
* `easeFn` is clamped to `[0, 1]`.
|
|
384
|
+
*
|
|
385
|
+
* **Closed mode** (`opts.closed = true`, new in v1.5): stops are treated
|
|
386
|
+
* cyclically — the wrap segment runs from `stops[numStops-1]` back to
|
|
387
|
+
* `stops[0]`. Sample _j_ is at `j / resolution` (period spacing, no duplicated
|
|
388
|
+
* endpoint). `easeFn` outputs are wrapped via `t - floor(t)` instead of
|
|
389
|
+
* clamped. Pair with `sampleColorLUTWrapped` on the consumer side (hue wheels,
|
|
390
|
+
* cyclic colorways).
|
|
288
391
|
*
|
|
289
392
|
* @param keyframesBuf Contiguous buffer of `[L0, C0, H0, L1, C1, H1, ...]`.
|
|
290
393
|
* @param numStops Stop count; must be `>= 2`.
|
|
@@ -293,6 +396,7 @@ export type OklchPackerFn = (buf: Float32Array, offset: number, alpha?: number)
|
|
|
293
396
|
* @param packer Optional packer override. Defaults to the accurate
|
|
294
397
|
* {@link packOklchBufferToUint32}. Pass
|
|
295
398
|
* {@link packOklchBufferToUint32Fast} for ~2x bake throughput.
|
|
399
|
+
* @param opts Optional bake options. `{ closed: true }` bakes a cyclic LUT.
|
|
296
400
|
* @throws If `numStops < 2` or `resolution < 2`.
|
|
297
401
|
*/
|
|
298
402
|
export function bakeGradientToUint32(
|
|
@@ -300,7 +404,8 @@ export function bakeGradientToUint32(
|
|
|
300
404
|
numStops: number,
|
|
301
405
|
resolution?: number,
|
|
302
406
|
easeFn?: (t: number) => number,
|
|
303
|
-
packer?: OklchPackerFn
|
|
407
|
+
packer?: OklchPackerFn,
|
|
408
|
+
opts?: { closed?: boolean }
|
|
304
409
|
): Uint32Array;
|
|
305
410
|
|
|
306
411
|
// ============================================================================
|
package/index.js
CHANGED
|
@@ -5,7 +5,9 @@ export {
|
|
|
5
5
|
parseOklabToBuffer,
|
|
6
6
|
parseOklchToBuffer,
|
|
7
7
|
parseRgbToBuffer,
|
|
8
|
-
parseDisplayP3ToBuffer
|
|
8
|
+
parseDisplayP3ToBuffer,
|
|
9
|
+
formatOklchCss,
|
|
10
|
+
formatHex
|
|
9
11
|
} from './src/authoring.js';
|
|
10
12
|
|
|
11
13
|
export { bakeGradientToUint32 } from './src/lut.js';
|
|
@@ -15,10 +17,14 @@ export {
|
|
|
15
17
|
lerpOklchBufferN,
|
|
16
18
|
packOklchBufferToUint32,
|
|
17
19
|
packOklchBufferToUint32Fast,
|
|
20
|
+
packOklchBufferToUint32Dithered,
|
|
18
21
|
packOklchBufferToUint32IntoN,
|
|
22
|
+
packOklchBufferToUint32IntoNDithered,
|
|
19
23
|
packOklchBufferToUint32P3,
|
|
20
24
|
packOklchBufferToUint32P3Fast,
|
|
21
|
-
|
|
25
|
+
packOklchBufferToUint32P3IntoN,
|
|
26
|
+
sampleColorLUT,
|
|
27
|
+
getBlueNoise64
|
|
22
28
|
} from './src/runtime.js';
|
|
23
29
|
|
|
24
30
|
export {
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# lite-color-engine - LLM Reference (v1.
|
|
1
|
+
# lite-color-engine - LLM Reference (v1.5)
|
|
2
2
|
|
|
3
3
|
## Core Exports
|
|
4
4
|
|
|
@@ -6,29 +6,60 @@
|
|
|
6
6
|
- `parseCSSColor(str, outBuf, offset)` — Universal parser. Now supports `color(display-p3 r g b / alpha)`
|
|
7
7
|
- `parseDisplayP3ToBuffer(str, outBuf, offset)` — Direct P3 parser
|
|
8
8
|
|
|
9
|
+
### Formatters (v1.4, authoring-time - allocate a string)
|
|
10
|
+
- `formatOklchCss(buf, off, alpha?)` - OKLCH triplet -> `oklch(L% C H)` (or `/ alpha`); round-trips via parseCSSColor.
|
|
11
|
+
- `formatHex(buf, off)` - OKLCH triplet -> `#rrggbb` via the accurate pack path; round-trips with parseHexToBuffer within 1 LSB.
|
|
12
|
+
|
|
9
13
|
### Conversion
|
|
10
14
|
- `sRgbToOklchBuffer(r, g, b, out, off)`
|
|
11
|
-
- `displayP3ToOklchBuffer(r, g, b, out, off)`
|
|
15
|
+
- `displayP3ToOklchBuffer(r, g, b, out, off)`
|
|
12
16
|
- `oklchToLinearP3(L, C, H, out)` — Inverse for P3 packing
|
|
13
17
|
|
|
14
18
|
### Runtime (Hot Path - Zero GC)
|
|
15
19
|
- `lerpOklchBuffer(...)`
|
|
16
20
|
- `packOklchBufferToUint32(buf, off, alpha?)` - Accurate sRGB
|
|
17
21
|
- `packOklchBufferToUint32Fast(...)` - Fast approx
|
|
22
|
+
- `packOklchBufferToUint32Dithered(buf, off, alpha?, noise01?)` — defaults `alpha=1.0, noise01=0.5`, so a 2-arg call is exactly `packOklchBufferToUint32` — **v1.5** dithered sRGB packer.
|
|
23
|
+
Luminance-only threshold dither (same `noise01` across R/G/B, no chroma speckle).
|
|
24
|
+
`noise01 = 0.5` reproduces the plain packer exactly.
|
|
18
25
|
- `packOklchBufferToUint32P3(buf, off, alpha?)` - Accurate P3 output
|
|
19
26
|
- `packOklchBufferToUint32P3Fast(...)` - Fast P3 output
|
|
20
27
|
- `sampleColorLUT(lut, t)` - O(1) lookup into a baked gradient LUT
|
|
21
28
|
|
|
22
29
|
### Batch kernels (v1.3, for 100k+ particle systems)
|
|
23
30
|
- `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
|
|
25
|
-
-
|
|
31
|
+
- `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).
|
|
32
|
+
- `packOklchBufferToUint32P3IntoN(src, offSrc, dst, offDst, n, alpha?, useLut?)` — **v1.5** P3 batch. `useLut=true` reuses the same `SRGB_LUT` (identical transfer function per IEC 61966-2-1); no separate table is allocated.
|
|
33
|
+
- `packOklchBufferToUint32IntoNDithered(src, offSrc, dst, offDst, n, alpha, noiseTile, x0, y0, rowWidth)` — **v1.5** dithered batch. Walks a 64x64 blue-noise tile in row-major order over destination `(x0, y0)` with `rowWidth`. Zero allocations, no modulo in hot loop. Exact-encoder only (see note below).
|
|
34
|
+
- Note: the batch win comes from `useLut`, not from call-amortization (accurate batch ~= scalar loop in V8; pow-bound). The dithered batch has no `useLut` axis — `SRGB_LUT` stores rounded bytes, which would collapse the dither threshold; a float LUT restoring this is a v1.6 candidate.
|
|
35
|
+
|
|
36
|
+
### Blue-noise tile (v1.5)
|
|
37
|
+
- `getBlueNoise64()` — Returns the shared 64x64 blue-noise tile as `Uint8Array(4096)`.
|
|
38
|
+
Lazily decoded from an inlined base64 blob on first call (sub-ms), then shared. Void-and-cluster,
|
|
39
|
+
histogram exactly uniform (16x each byte). Index via `tile[((y & 63) << 6) | (x & 63)]`.
|
|
40
|
+
Do not mutate the returned reference. Torus-tileable on both axes.
|
|
41
|
+
- The tile is gated on SPECTRAL properties, not just its SHA-256 fingerprint
|
|
42
|
+
(`test/bluenoise-spectral.test.js`): minority-phase clumpiness < 0.70 across thresholds
|
|
43
|
+
16..240, that curve symmetric about T=128, radial power for r<=4 below the white-noise
|
|
44
|
+
floor of 1/12, high-band power above it, wrap edges as decorrelated as the interior.
|
|
45
|
+
A uniform histogram does NOT imply blue noise — a linear ramp has a perfectly uniform
|
|
46
|
+
histogram. The 1.5.0 release candidate shipped a tile that passed every histogram /
|
|
47
|
+
fingerprint / banding gate and was still 7x over-clustered above mid-gray (inverted
|
|
48
|
+
Phase 3 selector in the generator). Do not treat the fingerprint as the gate.
|
|
26
49
|
|
|
27
50
|
### Gamut mapping (subpath: `@zakkster/lite-color-engine/gamut`, NOT hot-path)
|
|
28
|
-
- `
|
|
51
|
+
- `gamutMapToSrgbBuffer(inBuf, off, outBuf, offOut)` — MINDE chroma-reduction gamut map (scalar).
|
|
52
|
+
- `gamutMapToSrgbBufferN(inBuf, off, outBuf, offOut, n)` — **v1.5** batch MINDE. Bit-for-bit identical to n scalar calls; for authoring / LUT-build bulk mapping.
|
|
53
|
+
- `packOklchBufferToUint32MINDE(buf, off, alpha?)` — MINDE + pack. ~30x slower than the core packer; LUT-build/authoring time only.
|
|
54
|
+
|
|
55
|
+
### LUT (Gradient Baking)
|
|
56
|
+
- `bakeGradientToUint32(keyframesBuf, numStops, resolution?, easeFn?, packer?, opts?)` — accepts any of the above packers.
|
|
57
|
+
- **v1.5:** trailing `opts` object. `{ closed: true }` bakes a cyclic LUT: sample positions `i / resolution` (period, no duplicated endpoint), wrap segment last->first, ease outputs period-wrapped via `t - floor(t)`. Pair with `sampleColorLUTWrapped` on the consumer side (hue wheels, cyclic colorways).
|
|
58
|
+
- Open mode (default) is byte-identical to v1.4.
|
|
59
|
+
|
|
60
|
+
## Session Gate (v1.5)
|
|
29
61
|
|
|
30
|
-
|
|
31
|
-
- `bakeGradientToUint32(..., packer?)` — Accepts any of the above packers
|
|
62
|
+
- `npm run preflight` — mandatory before feature work. Fetches the published tarball via `npm pack` and hash-compares against HEAD. Hard-fails on code drift (`index.js`, `index.d.ts`, `src/**`, `package.json`); warns for doc drift.
|
|
32
63
|
|
|
33
64
|
## Key Principles
|
|
34
65
|
- All hot-path functions are allocation-free
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-color-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
5
5
|
"description": "Zero-GC, data-oriented OKLCH color engine and WebGL/Canvas buffer pipeline.",
|
|
6
6
|
"type": "module",
|
|
@@ -60,13 +60,14 @@
|
|
|
60
60
|
],
|
|
61
61
|
"scripts": {
|
|
62
62
|
"test": "vitest run",
|
|
63
|
-
"bench": "node bench/benchmark.mjs"
|
|
63
|
+
"bench": "node bench/benchmark.mjs",
|
|
64
|
+
"preflight": "node tools/preflight.mjs"
|
|
64
65
|
},
|
|
65
66
|
"dependencies": {
|
|
66
67
|
"@zakkster/lite-lerp": "^1.0.0"
|
|
67
68
|
},
|
|
68
69
|
"devDependencies": {
|
|
69
|
-
"vitest": "^1.
|
|
70
|
+
"vitest": "^4.1.10"
|
|
70
71
|
},
|
|
71
72
|
"license": "MIT",
|
|
72
73
|
"homepage": "https://github.com/PeshoVurtoleta/lite-color-engine#readme",
|
package/src/Gamut.d.ts
CHANGED
|
@@ -19,6 +19,24 @@ export function gamutMapToSrgbBuffer(
|
|
|
19
19
|
outOffset: number
|
|
20
20
|
): void;
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Batch sibling of `gamutMapToSrgbBuffer`. Maps `n` OKLCH triplets (stride 3)
|
|
24
|
+
* from `inBuf` into `outBuf` at the equivalent stride, using the same CSS
|
|
25
|
+
* Color 4 MINDE bisection. Bit-for-bit identical to `n` scalar calls; the
|
|
26
|
+
* batch exists to amortize call overhead for authoring / LUT-building large
|
|
27
|
+
* color runs (hueforge P3 exporters, studio bake paths).
|
|
28
|
+
*
|
|
29
|
+
* Setup-time / bake-time convenience — MINDE stays out of the per-frame hot
|
|
30
|
+
* path (~30x slower than the core packer). Zero allocations.
|
|
31
|
+
*/
|
|
32
|
+
export function gamutMapToSrgbBufferN(
|
|
33
|
+
inBuf: Float32Array,
|
|
34
|
+
inOffset: number,
|
|
35
|
+
outBuf: Float32Array,
|
|
36
|
+
outOffset: number,
|
|
37
|
+
n: number
|
|
38
|
+
): void;
|
|
39
|
+
|
|
22
40
|
/**
|
|
23
41
|
* Accurate sibling of `packOklchBufferToUint32`. Runs MINDE gamut mapping
|
|
24
42
|
* before packing, eliminating the hue-shift artifacts of the hard channel
|
package/src/Gamut.js
CHANGED
|
@@ -189,6 +189,39 @@ export function gamutMapToSrgbBuffer(inBuf, inOffset, outBuf, outOffset) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
|
|
192
|
+
// -----------------------------------------------------------------------------
|
|
193
|
+
// gamutMapToSrgbBufferN
|
|
194
|
+
// -----------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Batch sibling of {@link gamutMapToSrgbBuffer}. Maps `n` OKLCH triplets
|
|
198
|
+
* (stride 3) from `inBuf` into `outBuf` at the equivalent stride, using the
|
|
199
|
+
* same CSS Color 4 MINDE bisection as the scalar path. Bit-for-bit identical
|
|
200
|
+
* to calling {@link gamutMapToSrgbBuffer} `n` times; the batch exists to
|
|
201
|
+
* amortize call overhead when authoring / LUT-building large color runs
|
|
202
|
+
* (hueforge P3 exporters, studio bake paths) that today loop the scalar.
|
|
203
|
+
*
|
|
204
|
+
* MINDE is ~30x slower than the core packer and stays out of the per-frame
|
|
205
|
+
* hot path — this batch is a setup-time / bake-time convenience. Zero
|
|
206
|
+
* allocations (reuses the module-level scratch). In-place is safe when
|
|
207
|
+
* `inBuf === outBuf` at aligned offsets: each iteration reads its three
|
|
208
|
+
* source lanes into `_rgb`/`_clipLch` scratch before writing back.
|
|
209
|
+
*
|
|
210
|
+
* @param {Float32Array} inBuf - Source OKLCH buffer (stride 3)
|
|
211
|
+
* @param {number} inOffset - Base offset of the first triplet in inBuf
|
|
212
|
+
* @param {Float32Array} outBuf - Destination OKLCH buffer (stride 3)
|
|
213
|
+
* @param {number} outOffset - Base offset of the first triplet in outBuf
|
|
214
|
+
* @param {number} n - Number of triplets to map (n <= 0 is a no-op)
|
|
215
|
+
* @returns {void}
|
|
216
|
+
*/
|
|
217
|
+
export function gamutMapToSrgbBufferN(inBuf, inOffset, outBuf, outOffset, n) {
|
|
218
|
+
if (n <= 0) return;
|
|
219
|
+
for (let i = 0; i < n; i++) {
|
|
220
|
+
gamutMapToSrgbBuffer(inBuf, inOffset + i * 3, outBuf, outOffset + i * 3);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
192
225
|
// -----------------------------------------------------------------------------
|
|
193
226
|
// packOklchBufferToUint32MINDE
|
|
194
227
|
// -----------------------------------------------------------------------------
|
package/src/authoring.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sRgbToOklchBuffer, displayP3ToOklchBuffer } from './convert.js';
|
|
2
|
+
import { packOklchBufferToUint32 } from './runtime.js';
|
|
2
3
|
|
|
3
4
|
const RAD_TO_DEG = 180 / Math.PI;
|
|
4
5
|
|
|
@@ -80,7 +81,7 @@ const NAMED_COLORS = {
|
|
|
80
81
|
darkgray: '#a9a9a9', darkgreen: '#006400', darkgrey: '#a9a9a9', darkkhaki: '#bdb76b',
|
|
81
82
|
darkmagenta: '#8b008b', darkolivegreen: '#556b2f', darkorange: '#ff8c00', darkorchid: '#9932cc',
|
|
82
83
|
darkred: '#8b0000', darksalmon: '#e9967a', darkseagreen: '#8fbc8f', darkslateblue: '#483d8b',
|
|
83
|
-
darkslategray: '#2f4f4f',
|
|
84
|
+
darkslategray: '#2f4f4f', darkslategrey: '#2f4f4f', darkturquoise: '#00ced1', darkviolet: '#9400d3',
|
|
84
85
|
deeppink: '#ff1493', deepskyblue: '#00bfff', dimgray: '#696969', dimgrey: '#696969',
|
|
85
86
|
dodgerblue: '#1e90ff', firebrick: '#b22222', floralwhite: '#fffaf0', forestgreen: '#228b22',
|
|
86
87
|
fuchsia: '#ff00ff', gainsboro: '#dcdcdc', ghostwhite: '#f8f8ff', gold: '#ffd700',
|
|
@@ -91,12 +92,12 @@ const NAMED_COLORS = {
|
|
|
91
92
|
lightcoral: '#f08080', lightcyan: '#e0ffff', lightgoldenrodyellow: '#fafad2', lightgray: '#d3d3d3',
|
|
92
93
|
lightgreen: '#90ee90', lightgrey: '#d3d3d3', lightpink: '#ffb6c1', lightsalmon: '#ffa07a',
|
|
93
94
|
lightseagreen: '#20b2aa', lightskyblue: '#87cefa', lightslategray: '#778899', lightslategrey: '#778899',
|
|
94
|
-
lightsteelblue: '#
|
|
95
|
+
lightsteelblue: '#b0c4de', lightyellow: '#ffffe0', lime: '#00ff00', limegreen: '#32cd32',
|
|
95
96
|
linen: '#faf0e6', magenta: '#ff00ff', maroon: '#800000', mediumaquamarine: '#66cdaa',
|
|
96
97
|
mediumblue: '#0000cd', mediumorchid: '#ba55d3', mediumpurple: '#9370db', mediumseagreen: '#3cb371',
|
|
97
98
|
mediumslateblue: '#7b68ee', mediumspringgreen: '#00fa9a', mediumturquoise: '#48d1cc', mediumvioletred: '#c71585',
|
|
98
99
|
midnightblue: '#191970', mintcream: '#f5fffa', mistyrose: '#ffe4e1', moccasin: '#ffe4b5',
|
|
99
|
-
|
|
100
|
+
navajowhite: '#ffdead', navy: '#000080', oldlace: '#fdf5e6', olive: '#808000',
|
|
100
101
|
olivedrab: '#6b8e23', orange: '#ffa500', orangered: '#ff4500', orchid: '#da70d6',
|
|
101
102
|
palegoldenrod: '#eee8aa', palegreen: '#98fb98', paleturquoise: '#afeeee', palevioletred: '#db7093',
|
|
102
103
|
papayawhip: '#ffefd5', peachpuff: '#ffdab9', peru: '#cd853f', pink: '#ffc0cb',
|
|
@@ -301,3 +302,59 @@ export const parseCSSColor = (str, outBuf, offset) => {
|
|
|
301
302
|
|
|
302
303
|
throw new Error(`lite-color-engine: Unsupported color format "${str}".`);
|
|
303
304
|
};
|
|
305
|
+
|
|
306
|
+
// ============================================================================
|
|
307
|
+
// 6. FORMATTERS (round-trip emit - authoring layer, string allocation is fine)
|
|
308
|
+
// ============================================================================
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Emits a modern CSS `oklch(...)` string from an OKLCH buffer triplet.
|
|
312
|
+
* Suitable for exports, CSS custom properties, gradient studios, telemetry, etc.
|
|
313
|
+
*
|
|
314
|
+
* Lightness is emitted as a percentage (e.g. `60.0%`), chroma and hue with
|
|
315
|
+
* enough precision to round-trip through `parseOklchToBuffer`/`parseCSSColor`.
|
|
316
|
+
* Alpha is included only when supplied and below 1.
|
|
317
|
+
*
|
|
318
|
+
* This is an authoring-time helper: it allocates a string and is not intended
|
|
319
|
+
* for per-frame hot paths.
|
|
320
|
+
*
|
|
321
|
+
* @param {Float32Array} buf - Buffer containing [L, C, H]
|
|
322
|
+
* @param {number} off - Offset of the triplet
|
|
323
|
+
* @param {number} [alpha] - Optional alpha [0, 1]. Omitted from output when >= 0.9995 or not supplied.
|
|
324
|
+
* @returns {string} e.g. "oklch(60.0% 0.150 250.0)" or with " / 0.800"
|
|
325
|
+
*/
|
|
326
|
+
export const formatOklchCss = (buf, off, alpha) => {
|
|
327
|
+
const l = buf[off] * 100;
|
|
328
|
+
const c = buf[off + 1];
|
|
329
|
+
const h = buf[off + 2];
|
|
330
|
+
let s = `oklch(${l.toFixed(1)}% ${c.toFixed(3)} ${h.toFixed(1)})`;
|
|
331
|
+
if (alpha != null && alpha < 0.9995) {
|
|
332
|
+
s += ` / ${alpha.toFixed(3)}`;
|
|
333
|
+
}
|
|
334
|
+
return s;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Converts an OKLCH buffer triplet to a 6-digit `#rrggbb` hex string (no alpha).
|
|
339
|
+
*
|
|
340
|
+
* Uses the accurate sRGB transfer + pack path, so it round-trips with
|
|
341
|
+
* `parseHexToBuffer` + `packOklchBufferToUint32` to within 1 LSB per channel
|
|
342
|
+
* (the accurate packer's rounding tolerance). Out-of-sRGB-gamut input is hard
|
|
343
|
+
* clamped, matching the packer.
|
|
344
|
+
*
|
|
345
|
+
* Authoring-time helper: allocates a string, not for per-frame hot paths.
|
|
346
|
+
*
|
|
347
|
+
* @param {Float32Array} buf - Buffer containing [L, C, H]
|
|
348
|
+
* @param {number} off - Offset of the triplet
|
|
349
|
+
* @returns {string} e.g. "#ff0000"
|
|
350
|
+
*/
|
|
351
|
+
export const formatHex = (buf, off) => {
|
|
352
|
+
const u = packOklchBufferToUint32(buf, off);
|
|
353
|
+
const r = u & 0xff;
|
|
354
|
+
const g = (u >>> 8) & 0xff;
|
|
355
|
+
const b = (u >>> 16) & 0xff;
|
|
356
|
+
return '#' +
|
|
357
|
+
r.toString(16).padStart(2, '0') +
|
|
358
|
+
g.toString(16).padStart(2, '0') +
|
|
359
|
+
b.toString(16).padStart(2, '0');
|
|
360
|
+
};
|
package/src/lut.js
CHANGED
|
@@ -6,11 +6,17 @@ import { lerpOklchBuffer, packOklchBufferToUint32 } from './runtime.js';
|
|
|
6
6
|
* The output is in little-endian RGBA byte order — drop it directly into a
|
|
7
7
|
* `Uint32Array` view of `Canvas ImageData`, or `texImage2D` with `RGBA / UNSIGNED_BYTE`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* **Open mode (default):** stop _i_ is at `i / (numStops - 1)`; LUT sample _j_
|
|
10
|
+
* is at `j / (resolution - 1)`. Sample 0 lands on stop 0, sample `resolution-1`
|
|
11
|
+
* lands on the last stop. Overshoot from `easeFn` is clamped to `[0, 1]`.
|
|
12
|
+
*
|
|
13
|
+
* **Closed mode** (`opts.closed = true`, new in v1.5): stops are treated
|
|
14
|
+
* cyclically — the wrap segment runs from `stops[numStops-1]` back to
|
|
15
|
+
* `stops[0]`. Sample _j_ is at `j / resolution` (period spacing, no duplicated
|
|
16
|
+
* endpoint), so `sample[resolution-1]` sits just before wrapping back to
|
|
17
|
+
* sample 0. This is the LUT baked by hue-wheel gradients / cyclic colorways —
|
|
18
|
+
* pair it with `sampleColorLUTWrapped` on the consumer side. `easeFn` outputs
|
|
19
|
+
* in closed mode are wrapped via `t - floor(t)` (period) instead of clamped.
|
|
14
20
|
*
|
|
15
21
|
* @param {Float32Array} keyframesBuf - Contiguous buffer of `[L0, C0, H0, L1, C1, H1, ...]`
|
|
16
22
|
* @param {number} numStops - Count of color stops in the buffer (must be >= 2)
|
|
@@ -19,6 +25,8 @@ import { lerpOklchBuffer, packOklchBufferToUint32 } from './runtime.js';
|
|
|
19
25
|
* @param {(buf: Float32Array, offset: number, alpha?: number) => number} [packer]
|
|
20
26
|
* Optional packer override. Defaults to the accurate `packOklchBufferToUint32`.
|
|
21
27
|
* Pass `packOklchBufferToUint32Fast` for ~2x throughput at the cost of round-trip accuracy.
|
|
28
|
+
* @param {{ closed?: boolean }} [opts]
|
|
29
|
+
* Optional bake options. `closed: true` bakes a cyclic LUT (see above).
|
|
22
30
|
* @returns {Uint32Array} LUT of 32-bit packed colors
|
|
23
31
|
* @throws If `numStops < 2` or `resolution < 2`
|
|
24
32
|
*/
|
|
@@ -27,7 +35,8 @@ export const bakeGradientToUint32 = (
|
|
|
27
35
|
numStops,
|
|
28
36
|
resolution = 256,
|
|
29
37
|
easeFn,
|
|
30
|
-
packer = packOklchBufferToUint32
|
|
38
|
+
packer = packOklchBufferToUint32,
|
|
39
|
+
opts
|
|
31
40
|
) => {
|
|
32
41
|
if (numStops < 2) {
|
|
33
42
|
throw new Error('lite-color-engine: bakeGradientToUint32 requires numStops >= 2');
|
|
@@ -36,29 +45,41 @@ export const bakeGradientToUint32 = (
|
|
|
36
45
|
throw new Error('lite-color-engine: bakeGradientToUint32 requires resolution >= 2');
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
const closed = opts != null && opts.closed === true;
|
|
39
49
|
const outLUT = new Uint32Array(resolution);
|
|
40
50
|
const tempOklch = new Float32Array(3);
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
// Open: N-1 segments, samples at i/(res-1), t clamped to [0,1].
|
|
53
|
+
// Closed: N segments (last wraps last->first), samples at i/res, t period-wrapped.
|
|
54
|
+
const step = closed ? 1 / resolution : 1 / (resolution - 1);
|
|
55
|
+
const scale = closed ? numStops : (numStops - 1);
|
|
56
|
+
const maxIndex = closed ? (numStops - 1) : (numStops - 2);
|
|
57
|
+
const wrapIndex = numStops - 1; // only meaningful in closed mode
|
|
44
58
|
const hasEase = typeof easeFn === 'function';
|
|
45
59
|
|
|
46
60
|
for (let i = 0; i < resolution; i++) {
|
|
47
61
|
const rawT = i * step;
|
|
48
62
|
let t = hasEase ? easeFn(rawT) : rawT;
|
|
49
63
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
if (closed) {
|
|
65
|
+
// Period wrap: `t - floor(t)` maps R to [0, 1). Handles ease overshoot
|
|
66
|
+
// and negative outputs uniformly; the cyclic contract has no clamp.
|
|
67
|
+
t = t - Math.floor(t);
|
|
68
|
+
} else {
|
|
69
|
+
// LUTs are fixed-resolution; overshoot/undershoot is undefined and
|
|
70
|
+
// would either NaN-poison the buffer or extrapolate silently.
|
|
71
|
+
if (t < 0) t = 0;
|
|
72
|
+
else if (t > 1) t = 1;
|
|
73
|
+
}
|
|
54
74
|
|
|
55
|
-
const scaledT = t *
|
|
56
|
-
let index = scaledT | 0; // bitwise fast-floor for
|
|
57
|
-
if (index
|
|
75
|
+
const scaledT = t * scale;
|
|
76
|
+
let index = scaledT | 0; // bitwise fast-floor for non-negative values
|
|
77
|
+
if (index > maxIndex) index = maxIndex;
|
|
58
78
|
|
|
59
79
|
const localT = scaledT - index;
|
|
60
80
|
const offsetA = index * 3;
|
|
61
|
-
|
|
81
|
+
// Closed mode: the wrap segment (last -> first) reads keyframe 0 as B.
|
|
82
|
+
const offsetB = (closed && index === wrapIndex) ? 0 : offsetA + 3;
|
|
62
83
|
|
|
63
84
|
lerpOklchBuffer(keyframesBuf, offsetA, keyframesBuf, offsetB, localT, tempOklch, 0);
|
|
64
85
|
outLUT[i] = packer(tempOklch, 0, 1.0);
|