@zakkster/lite-color-engine 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -7
- package/README.md +54 -0
- package/index.d.ts +30 -0
- package/index.js +2 -0
- package/llms.txt +12 -6
- package/package.json +4 -3
- package/src/runtime.js +119 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,21 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.0] - 2026-07-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Batch kernels for large particle systems (100k+):**
|
|
7
|
+
- `lerpOklchBufferN(a, offA, b, offB, t, out, offOut, n)` - interpolate `n` OKLCH
|
|
8
|
+
triplets (stride 3) with a shared `t`. Bit-for-bit identical to `n` scalar
|
|
9
|
+
`lerpOklchBuffer` calls; supports in-place and offset addressing.
|
|
10
|
+
- `packOklchBufferToUint32IntoN(src, offSrc, dst, offDst, n, alpha?, useLut?)` -
|
|
11
|
+
pack `n` OKLCH triplets straight into a `Uint32Array` (dst stride 1), the exact
|
|
12
|
+
shape a SoA particle system blits into `ImageData`.
|
|
13
|
+
- **Opt-in 4096-entry sRGB transfer LUT** (`useLut: true` on the batch packer):
|
|
14
|
+
removes `pow()` from the hot path for **~2.7x** packing throughput at **within
|
|
15
|
+
1 LSB** of the exact encoder. Single one-time table allocation.
|
|
16
|
+
|
|
17
|
+
### Performance (measured, Node 22 / V8, 100k in-gamut triplets, median of 80)
|
|
18
|
+
- Packing: accurate ~3.2M colors/s (~32 ms/frame); LUT ~14M colors/s (~7 ms/frame) - **~4.4x**.
|
|
19
|
+
The LUT runs at Fast-packer speed (`sqrt`) while staying within 1 LSB of exact.
|
|
20
|
+
- 100k lerp+pack fits a 60fps frame only with the LUT (~10 ms vs ~35 ms accurate).
|
|
21
|
+
- Honest caveat: the speedup is the **LUT**, not the batching. Accurate batch
|
|
22
|
+
packing runs at the same speed as a scalar loop (pow-bound), and batch lerp is
|
|
23
|
+
not measurably faster than a scalar loop in V8. The batch APIs are primarily
|
|
24
|
+
about ergonomics (whole-buffer ops) and unlocking the LUT path. Reproduce with
|
|
25
|
+
`node bench/benchmark.mjs`.
|
|
26
|
+
|
|
27
|
+
### Notes
|
|
28
|
+
- All hot paths remain zero-GC and allocation-free per frame.
|
|
29
|
+
- No breaking changes; all v1.2 exports (including Display P3 and `sampleColorLUT`)
|
|
30
|
+
are retained.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
3
34
|
## [1.2.0] - 2026-07-10
|
|
4
35
|
|
|
5
36
|
### Added
|
|
6
37
|
- **Wide Gamut Support (Display P3)**
|
|
7
38
|
- `parseCSSColor('color(display-p3 r g b / alpha)')` now supported in the universal parser
|
|
8
|
-
- New `displayP3ToOklchBuffer()` conversion function with accurate P3
|
|
9
|
-
- `packOklchBufferToUint32P3()`
|
|
10
|
-
- `packOklchBufferToUint32P3Fast()`
|
|
39
|
+
- New `displayP3ToOklchBuffer()` conversion function with accurate P3 -> XYZ -> OKLab pipeline
|
|
40
|
+
- `packOklchBufferToUint32P3()` - accurate packer for `display-p3` canvas contexts
|
|
41
|
+
- `packOklchBufferToUint32P3Fast()` - high-speed sqrt approximation for P3 output
|
|
11
42
|
- `oklchToLinearP3()` inverse conversion helper
|
|
12
43
|
|
|
13
44
|
- **Accuracy Tiers** (now clearly documented)
|
|
14
45
|
- Fast (`packOklchBufferToUint32Fast`)
|
|
15
46
|
- Accurate-Clamp (default `packOklchBufferToUint32`)
|
|
16
|
-
- Gamut-Mapped (`packOklchBufferToUint32MINDE
|
|
47
|
+
- Gamut-Mapped (`packOklchBufferToUint32MINDE`, on the `/gamut` subpath)
|
|
17
48
|
|
|
18
|
-
- `bakeGradientToUint32()` already supported custom packers
|
|
49
|
+
- `bakeGradientToUint32()` already supported custom packers - now officially documented for P3 usage.
|
|
19
50
|
|
|
20
51
|
### Changed
|
|
21
52
|
- Improved documentation around color gamut handling and when to use each packer.
|
|
@@ -28,5 +59,6 @@
|
|
|
28
59
|
---
|
|
29
60
|
|
|
30
61
|
## [1.1.0] - Previous
|
|
31
|
-
|
|
32
|
-
-
|
|
62
|
+
|
|
63
|
+
- Core zero-GC OKLCH engine: parsing, buffer lerp, packers, delta-E, gradient LUT baking,
|
|
64
|
+
gamut mapping (`/gamut`) and palette remap (`/remap`) subpaths.
|
package/README.md
CHANGED
|
@@ -1,7 +1,41 @@
|
|
|
1
1
|
# @zakkster/lite-color-engine
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@zakkster/lite-color-engine)
|
|
4
|
+

|
|
5
|
+
[](https://github.com/sponsors/PeshoVurtoleta)
|
|
6
|
+
[](https://bundlephobia.com/result?p=@zakkster/lite-color-engine)
|
|
7
|
+
[](https://www.npmjs.com/package/@zakkster/lite-color-engine)
|
|
8
|
+
[](https://www.npmjs.com/package/@zakkster/lite-color-engine)
|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+
[](https://opensource.org/licenses/MIT)
|
|
13
|
+
|
|
3
14
|
**Zero-GC, data-oriented OKLCH color engine** for high-performance WebGL / Canvas pipelines.
|
|
4
15
|
|
|
16
|
+
## v1.3 Highlights — Batch Kernels + 4k Transfer LUT
|
|
17
|
+
|
|
18
|
+
Built for the 100k-particle case: bulk buffer -> buffer operations plus an opt-in
|
|
19
|
+
transfer LUT that removes `pow()` from the hot path.
|
|
20
|
+
|
|
21
|
+
- `lerpOklchBufferN(a, offA, b, offB, t, out, offOut, n)` — interpolate `n` triplets in one call.
|
|
22
|
+
- `packOklchBufferToUint32IntoN(src, offSrc, dst, offDst, n, alpha?, useLut?)` — pack `n` OKLCH triplets straight into a `Uint32Array`.
|
|
23
|
+
- `useLut: true` opts into a precomputed 4096-entry sRGB transfer LUT — **~4.4x** the packing throughput of the accurate path, within **1 LSB** of exact. That is essentially Fast-packer speed at near-exact accuracy (the `sqrt` Fast packer drifts ~10/255). One module-load allocation; still zero-GC per frame.
|
|
24
|
+
|
|
25
|
+
**Honest note on batching.** The LUT is the real win. The batch functions themselves
|
|
26
|
+
mostly save ergonomics, not cycles: in V8 the accurate batch packer runs at essentially
|
|
27
|
+
the same speed as a scalar loop (the cost is `pow()`, not call overhead), and batch lerp
|
|
28
|
+
is only marginally faster. Reach for `useLut: true` when you need the speedup.
|
|
29
|
+
|
|
30
|
+
| Packing 100k OKLCH -> Uint32 / frame | Throughput | ms/frame |
|
|
31
|
+
|--------------------------------------|-----------|----------|
|
|
32
|
+
| scalar loop / batch accurate | ~3.2M/s | ~32 ms |
|
|
33
|
+
| **batch + `useLut: true`** | **~14M/s**| **~7 ms**|
|
|
34
|
+
| Fast (`sqrt`, ~10/255 error) | ~15M/s | ~7 ms |
|
|
35
|
+
|
|
36
|
+
*(Node 22 / V8, 100k in-gamut triplets, median of 80 runs; browser V8 is comparable.
|
|
37
|
+
Reproduce with `node bench/benchmark.mjs`. Your numbers will vary.)*
|
|
38
|
+
|
|
5
39
|
## v1.2 Highlights — Wide Gamut + Accuracy Tiers
|
|
6
40
|
|
|
7
41
|
- Full `color(display-p3 r g b / alpha)` parsing support
|
|
@@ -41,6 +75,26 @@ lerpOklchBuffer(bufA, 0, bufB, 0, t, temp, 0);
|
|
|
41
75
|
const pixel = packOklchBufferToUint32P3(temp, 0); // or packOklchBufferToUint32
|
|
42
76
|
```
|
|
43
77
|
|
|
78
|
+
### Batch (100k particles)
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
import {
|
|
82
|
+
lerpOklchBufferN,
|
|
83
|
+
packOklchBufferToUint32IntoN
|
|
84
|
+
} from '@zakkster/lite-color-engine';
|
|
85
|
+
|
|
86
|
+
const N = 100000;
|
|
87
|
+
const from = new Float32Array(N * 3); // OKLCH triplets
|
|
88
|
+
const to = new Float32Array(N * 3);
|
|
89
|
+
const cur = new Float32Array(N * 3);
|
|
90
|
+
const px = new Uint32Array(N); // -> view of ImageData
|
|
91
|
+
|
|
92
|
+
// Per frame, zero allocations:
|
|
93
|
+
lerpOklchBufferN(from, 0, to, 0, t, cur, 0, N);
|
|
94
|
+
packOklchBufferToUint32IntoN(cur, 0, px, 0, N, 1.0, /* useLut */ true);
|
|
95
|
+
// px now holds N packed RGBA pixels; blit via a Uint32Array view of ImageData.
|
|
96
|
+
```
|
|
97
|
+
|
|
44
98
|
## Accuracy Tiers
|
|
45
99
|
|
|
46
100
|
| Tier | Function | Use Case | Trade-off |
|
package/index.d.ts
CHANGED
|
@@ -155,6 +155,22 @@ export function lerpOklchBuffer(
|
|
|
155
155
|
outOffset: number
|
|
156
156
|
): void;
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Batch lerp for particle systems / high-N use cases. Amortizes JS call overhead.
|
|
160
|
+
*
|
|
161
|
+
* Lerps n triplets with shared `t`: a[offA + i*3...] <-> b[offB + i*3...] -> out[offOut + i*3...]
|
|
162
|
+
*/
|
|
163
|
+
export function lerpOklchBufferN(
|
|
164
|
+
a: Float32Array,
|
|
165
|
+
offA: number,
|
|
166
|
+
b: Float32Array,
|
|
167
|
+
offB: number,
|
|
168
|
+
t: number,
|
|
169
|
+
out: Float32Array,
|
|
170
|
+
offOut: number,
|
|
171
|
+
n: number
|
|
172
|
+
): void;
|
|
173
|
+
|
|
158
174
|
/**
|
|
159
175
|
* Encodes an OKLCH triplet to a 32-bit unsigned integer in **little-endian
|
|
160
176
|
* RGBA** byte order - the format consumed directly by `Canvas ImageData` via
|
|
@@ -196,6 +212,20 @@ export function packOklchBufferToUint32Fast(
|
|
|
196
212
|
alpha?: number
|
|
197
213
|
): number;
|
|
198
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Batch packer: n OKLCH triplets -> n Uint32 packed colors (stride 1 in dst).
|
|
217
|
+
* Opt-in 4k sRGB LUT (`useLut=true`) for fast-packer throughput at near-exact accuracy.
|
|
218
|
+
*/
|
|
219
|
+
export function packOklchBufferToUint32IntoN(
|
|
220
|
+
src: Float32Array,
|
|
221
|
+
offSrc: number,
|
|
222
|
+
dst: Uint32Array,
|
|
223
|
+
offDst: number,
|
|
224
|
+
n: number,
|
|
225
|
+
alpha?: number,
|
|
226
|
+
useLut?: boolean
|
|
227
|
+
): void;
|
|
228
|
+
|
|
199
229
|
/**
|
|
200
230
|
* Encodes an OKLCH triplet to a 32-bit unsigned integer in little-endian RGBA
|
|
201
231
|
* byte order for a **Display P3** canvas context
|
package/index.js
CHANGED
|
@@ -12,8 +12,10 @@ export { bakeGradientToUint32 } from './src/lut.js';
|
|
|
12
12
|
|
|
13
13
|
export {
|
|
14
14
|
lerpOklchBuffer,
|
|
15
|
+
lerpOklchBufferN,
|
|
15
16
|
packOklchBufferToUint32,
|
|
16
17
|
packOklchBufferToUint32Fast,
|
|
18
|
+
packOklchBufferToUint32IntoN,
|
|
17
19
|
packOklchBufferToUint32P3,
|
|
18
20
|
packOklchBufferToUint32P3Fast,
|
|
19
21
|
sampleColorLUT
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# lite-color-engine
|
|
1
|
+
# lite-color-engine - LLM Reference (v1.3)
|
|
2
2
|
|
|
3
3
|
## Core Exports
|
|
4
4
|
|
|
@@ -11,12 +11,18 @@
|
|
|
11
11
|
- `displayP3ToOklchBuffer(r, g, b, out, off)` — New in v1.2
|
|
12
12
|
- `oklchToLinearP3(L, C, H, out)` — Inverse for P3 packing
|
|
13
13
|
|
|
14
|
-
### Runtime (Hot Path
|
|
14
|
+
### Runtime (Hot Path - Zero GC)
|
|
15
15
|
- `lerpOklchBuffer(...)`
|
|
16
|
-
- `packOklchBufferToUint32(buf, off, alpha?)`
|
|
17
|
-
- `packOklchBufferToUint32Fast(...)`
|
|
18
|
-
- `packOklchBufferToUint32P3(buf, off, alpha?)`
|
|
19
|
-
- `packOklchBufferToUint32P3Fast(...)`
|
|
16
|
+
- `packOklchBufferToUint32(buf, off, alpha?)` - Accurate sRGB
|
|
17
|
+
- `packOklchBufferToUint32Fast(...)` - Fast approx
|
|
18
|
+
- `packOklchBufferToUint32P3(buf, off, alpha?)` - Accurate P3 output
|
|
19
|
+
- `packOklchBufferToUint32P3Fast(...)` - Fast P3 output
|
|
20
|
+
- `sampleColorLUT(lut, t)` - O(1) lookup into a baked gradient LUT
|
|
21
|
+
|
|
22
|
+
### Batch kernels (v1.3, for 100k+ particle systems)
|
|
23
|
+
- `lerpOklchBufferN(a, offA, b, offB, t, out, offOut, n)` - bulk lerp n triplets (stride 3), bit-exact to n scalar calls.
|
|
24
|
+
- `packOklchBufferToUint32IntoN(src, offSrc, dst, offDst, n, alpha?, useLut?)` - bulk pack n OKLCH -> Uint32 (dst stride 1). `useLut=true` uses the 4k transfer LUT (~4.4x throughput vs accurate, within 1 LSB; Fast-packer speed at near-exact accuracy).
|
|
25
|
+
- Note: the batch win comes from `useLut`, not from call-amortization (accurate batch ~= scalar loop in V8; pow-bound).
|
|
20
26
|
|
|
21
27
|
### Gamut mapping (subpath: `@zakkster/lite-color-engine/gamut`, NOT hot-path)
|
|
22
28
|
- `packOklchBufferToUint32MINDE(buf, off, alpha?)` — MINDE chroma-reduction gamut map. ~30x slower than the core packer; use at LUT-build/authoring time.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-color-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
5
|
-
"description": "Zero-GC, data-oriented OKLCH color engine
|
|
5
|
+
"description": "Zero-GC, data-oriented OKLCH color engine and WebGL/Canvas buffer pipeline.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
8
8
|
"main": "./index.js",
|
|
@@ -59,7 +59,8 @@
|
|
|
59
59
|
"game-dev"
|
|
60
60
|
],
|
|
61
61
|
"scripts": {
|
|
62
|
-
"test": "vitest run"
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"bench": "node bench/benchmark.mjs"
|
|
63
64
|
},
|
|
64
65
|
"dependencies": {
|
|
65
66
|
"@zakkster/lite-lerp": "^1.0.0"
|
package/src/runtime.js
CHANGED
|
@@ -36,9 +36,50 @@ export const lerpOklchBuffer = (bufA, offsetA, bufB, offsetB, t, outBuf, outOffs
|
|
|
36
36
|
outBuf[outOffset + 2] = h < 0 ? h + 360 : h;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Batch sibling of {@link lerpOklchBuffer}. Interpolates `n` consecutive OKLCH
|
|
41
|
+
* triplets (stride 3) with a single shared `t`, amortizing per-call overhead
|
|
42
|
+
* across large particle systems (100k+ entities).
|
|
43
|
+
*
|
|
44
|
+
* For each i in [0, n): lerps a[offA + i*3 ..] with b[offB + i*3 ..] into
|
|
45
|
+
* out[offOut + i*3 ..]. Per-triplet math is identical to the scalar version,
|
|
46
|
+
* so results are bit-for-bit equal to calling it n times.
|
|
47
|
+
*
|
|
48
|
+
* In-place is safe (a === out, offA === offOut): each iteration reads its three
|
|
49
|
+
* source lanes before writing them. Zero allocations; monomorphic hot loop.
|
|
50
|
+
*
|
|
51
|
+
* @param {Float32Array} a - Source buffer A
|
|
52
|
+
* @param {number} offA - Base offset of the first color in A
|
|
53
|
+
* @param {Float32Array} b - Source buffer B
|
|
54
|
+
* @param {number} offB - Base offset of the first color in B
|
|
55
|
+
* @param {number} t - Interpolation factor [0, 1] (extrapolates then clamps)
|
|
56
|
+
* @param {Float32Array} out - Destination buffer
|
|
57
|
+
* @param {number} offOut - Base offset of the first output color
|
|
58
|
+
* @param {number} n - Number of triplets to process (n <= 0 is a no-op)
|
|
59
|
+
* @returns {void}
|
|
60
|
+
*/
|
|
61
|
+
export const lerpOklchBufferN = (a, offA, b, offB, t, out, offOut, n) => {
|
|
62
|
+
if (n <= 0) return;
|
|
63
|
+
for (let i = 0; i < n; i++) {
|
|
64
|
+
const ia = offA + i * 3;
|
|
65
|
+
const ib = offB + i * 3;
|
|
66
|
+
const io = offOut + i * 3;
|
|
67
|
+
|
|
68
|
+
const l = lerp(a[ia], b[ib], t);
|
|
69
|
+
out[io] = l < 0 ? 0 : (l > 1 ? 1 : l);
|
|
70
|
+
|
|
71
|
+
const c = lerp(a[ia + 1], b[ib + 1], t);
|
|
72
|
+
out[io + 1] = c < 0 ? 0 : c;
|
|
73
|
+
|
|
74
|
+
let h = lerpAngle(a[ia + 2], b[ib + 2], t);
|
|
75
|
+
h = h % 360;
|
|
76
|
+
out[io + 2] = h < 0 ? h + 360 : h;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
39
80
|
/**
|
|
40
81
|
* Internal: shared OKLCH -> linear sRGB kernel.
|
|
41
|
-
* Inlined into
|
|
82
|
+
* Inlined into every sRGB pack variant so V8 monomorphizes it.
|
|
42
83
|
*
|
|
43
84
|
* Returns a 3-tuple via `outRgb` (length 3). Strictly clamped to [0, 1].
|
|
44
85
|
* @internal
|
|
@@ -60,7 +101,7 @@ const oklchToLinearSrgbClamped = (l, c, h, outRgb) => {
|
|
|
60
101
|
const g = -1.2684380046 * lms_l + 2.6097574011 * lms_m - 0.3413193965 * lms_s;
|
|
61
102
|
const b = -0.0041960863 * lms_l - 0.7034186147 * lms_m + 1.7076147010 * lms_s;
|
|
62
103
|
|
|
63
|
-
// Hard gamut clamp (fast path;
|
|
104
|
+
// Hard gamut clamp (fast path; MINDE chroma reduction lives on the /gamut subpath).
|
|
64
105
|
outRgb[0] = r < 0 ? 0 : (r > 1 ? 1 : r);
|
|
65
106
|
outRgb[1] = g < 0 ? 0 : (g > 1 ? 1 : g);
|
|
66
107
|
outRgb[2] = b < 0 ? 0 : (b > 1 ? 1 : b);
|
|
@@ -70,6 +111,34 @@ const oklchToLinearSrgbClamped = (l, c, h, outRgb) => {
|
|
|
70
111
|
const _scratchRgb = new Float32Array(3);
|
|
71
112
|
const _scratchRgbP3 = new Float32Array(3);
|
|
72
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Precomputed 4096-entry LUT for the sRGB transfer (linear -> encoded byte).
|
|
116
|
+
* Entry i corresponds to linear value i/4095, so the table spans [0, 1] exactly.
|
|
117
|
+
* The linear branch (slope 12.92) is the steepest region; at 4096 samples its
|
|
118
|
+
* per-step delta is < 1 byte, so nearest-index lookups stay within ~1 LSB of
|
|
119
|
+
* the exact pow() encoding. One-time module-load cost, then O(1) lookup.
|
|
120
|
+
* @internal
|
|
121
|
+
*/
|
|
122
|
+
const SRGB_LUT = new Uint8ClampedArray(4096);
|
|
123
|
+
{
|
|
124
|
+
for (let i = 0; i < 4096; i++) {
|
|
125
|
+
const c = i / 4095;
|
|
126
|
+
const enc = c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
127
|
+
SRGB_LUT[i] = (enc * 255 + 0.5) | 0;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* LUT-based linear-to-sRGB byte using nearest-index lookup. Near-exact (~1 LSB)
|
|
133
|
+
* versus {@link linearToSrgbByte} but branch-free and pow-free on the hot path.
|
|
134
|
+
* @internal
|
|
135
|
+
*/
|
|
136
|
+
const linearToSrgbByteLut = (c) => {
|
|
137
|
+
if (c <= 0) return 0;
|
|
138
|
+
if (c >= 1) return 255;
|
|
139
|
+
return SRGB_LUT[(c * 4095 + 0.5) | 0];
|
|
140
|
+
};
|
|
141
|
+
|
|
73
142
|
/**
|
|
74
143
|
* Encodes a linear-light channel (already clamped to [0, 1]) to an sRGB
|
|
75
144
|
* 8-bit byte using the proper IEC 61966-2-1 transfer function. Round-tripping
|
|
@@ -136,8 +205,54 @@ export const packOklchBufferToUint32Fast = (buf, offset, alpha = 1.0) => {
|
|
|
136
205
|
};
|
|
137
206
|
|
|
138
207
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
208
|
+
* Batch sibling of {@link packOklchBufferToUint32}. Packs `n` OKLCH triplets
|
|
209
|
+
* (stride 3, from `src`) into `n` consecutive Uint32 pixels (stride 1, into
|
|
210
|
+
* `dst`) - the shape a SoA particle system feeds straight into `ImageData`.
|
|
211
|
+
*
|
|
212
|
+
* With `useLut=false` the output is bit-for-bit identical to calling
|
|
213
|
+
* {@link packOklchBufferToUint32} n times. With `useLut=true` it uses the 4k
|
|
214
|
+
* transfer LUT: near-exact (~1 LSB) at close to fast-packer throughput, with a
|
|
215
|
+
* single one-time table allocation and no per-color pow(). The branch is hoisted
|
|
216
|
+
* out of the loop so each variant stays monomorphic.
|
|
217
|
+
*
|
|
218
|
+
* @param {Float32Array} src - Source OKLCH buffer (stride 3)
|
|
219
|
+
* @param {number} offSrc - Base offset of the first triplet in src
|
|
220
|
+
* @param {Uint32Array} dst - Destination packed-color buffer (stride 1)
|
|
221
|
+
* @param {number} offDst - Base offset of the first packed color in dst
|
|
222
|
+
* @param {number} n - Number of colors to pack (n <= 0 is a no-op)
|
|
223
|
+
* @param {number} [alpha=1.0] - Shared alpha for all n colors
|
|
224
|
+
* @param {boolean} [useLut=false] - Opt in to the 4k LUT transfer (near-exact, faster)
|
|
225
|
+
* @returns {void}
|
|
226
|
+
*/
|
|
227
|
+
export const packOklchBufferToUint32IntoN = (src, offSrc, dst, offDst, n, alpha = 1.0, useLut = false) => {
|
|
228
|
+
if (n <= 0) return;
|
|
229
|
+
const a8 = alpha <= 0 ? 0 : (alpha >= 1 ? 255 : (alpha * 255 + 0.5) | 0);
|
|
230
|
+
const aHi = a8 << 24;
|
|
231
|
+
|
|
232
|
+
if (useLut) {
|
|
233
|
+
for (let i = 0; i < n; i++) {
|
|
234
|
+
const io = offSrc + i * 3;
|
|
235
|
+
oklchToLinearSrgbClamped(src[io], src[io + 1], src[io + 2], _scratchRgb);
|
|
236
|
+
const r8 = linearToSrgbByteLut(_scratchRgb[0]);
|
|
237
|
+
const g8 = linearToSrgbByteLut(_scratchRgb[1]);
|
|
238
|
+
const b8 = linearToSrgbByteLut(_scratchRgb[2]);
|
|
239
|
+
dst[offDst + i] = (aHi | (b8 << 16) | (g8 << 8) | r8) >>> 0;
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
for (let i = 0; i < n; i++) {
|
|
243
|
+
const io = offSrc + i * 3;
|
|
244
|
+
oklchToLinearSrgbClamped(src[io], src[io + 1], src[io + 2], _scratchRgb);
|
|
245
|
+
const r8 = linearToSrgbByte(_scratchRgb[0]);
|
|
246
|
+
const g8 = linearToSrgbByte(_scratchRgb[1]);
|
|
247
|
+
const b8 = linearToSrgbByte(_scratchRgb[2]);
|
|
248
|
+
dst[offDst + i] = (aHi | (b8 << 16) | (g8 << 8) | r8) >>> 0;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Internal helper: OKLCH -> linear Display P3 with hard clamp to [0, 1].
|
|
255
|
+
* Mirrors oklchToLinearSrgbClamped but targets the wider P3 gamut.
|
|
141
256
|
* @internal
|
|
142
257
|
*/
|
|
143
258
|
const oklchToLinearP3Clamped = (l, c, h, outRgb) => {
|