@zakkster/lite-color-engine 1.0.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/LICENSE.md ADDED
@@ -0,0 +1,19 @@
1
+ @zakkster/lite-tween-pro
2
+ Copyright (c) 2026 Zahary Shinikchiev. All Rights Reserved.
3
+
4
+ This software is commercial and proprietary. Your use of this software is
5
+ governed exclusively by the End User License Agreement (EULA) provided at
6
+ the time of purchase.
7
+
8
+ You may modify the source code for use within your own licensed projects.
9
+ However, you may not distribute, sublicense, sell, rent, lease, or publicly
10
+ host this software, its source code, or any modified derivatives, in whole
11
+ or in part, without explicit written permission from the copyright holder.
12
+
13
+ Access to updates, source code, and private repositories is granted only to
14
+ licensed users and only for the duration specified in the purchased license.
15
+
16
+ **Full EULA:** https://cdpn.io/pen/debug/LERgMOq
17
+
18
+ For licensing terms, purchasing options, and support, visit:
19
+ [Your Gumroad / Website Link]
package/README.md ADDED
@@ -0,0 +1,337 @@
1
+ # @zakkster/lite-color-engine
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@zakkster/lite-color-engine.svg?style=for-the-badge&color=latest)](https://www.npmjs.com/package/@zakkster/lite-color-engine)
4
+ [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@zakkster/lite-color-engine?style=for-the-badge)](https://bundlephobia.com/result?p=@zakkster/lite-color-engine)
5
+ [![npm downloads](https://img.shields.io/npm/dm/@zakkster/lite-color-engine?style=for-the-badge&color=blue)](https://www.npmjs.com/package/@zakkster/lite-color-engine)
6
+ ![TypeScript](https://img.shields.io/badge/TypeScript-Types-informational)
7
+ ![Dependencies](https://img.shields.io/badge/dependencies-1-brightgreen)
8
+ [![License](https://img.shields.io/badge/License-See_LICENSE-yellow.svg?style=for-the-badge)](./LICENSE.txt)
9
+
10
+ ## 🎨 What is lite-color-engine?
11
+
12
+ **[β†’ Demo](https://cdpn.io/pen/debug/RNoaNmQ)**
13
+
14
+ `@zakkster/lite-color-engine` is the **OKLCH color pipeline** for the Lite ecosystem. You hand it any CSS color string β€” `#ff0000`, `rgb(255 0 0)`, `oklch(60% 0.15 250)`, `rebeccapurple` β€” and you get back a perceptually-uniform `Float32Array` triplet you can interpolate, bake into a LUT, or pack to `Uint32` for direct `ImageData` / WebGL upload.
15
+
16
+ The library splits the work along the only axis that matters for hot-path rendering: **when**.
17
+
18
+ - 🟒 **Authoring layer** β€” `parseCSSColor()` and friends. Runs once at init. Allocations are fine here. CSS Color Level 4 permissive. Output is always a packed `[L, C, H]` triplet at an offset you control.
19
+ - πŸ”΅ **LUT layer** β€” `bakeGradientToUint32()`. Runs once at gradient compile. Multi-stop OKLCH input, optional easing, fixed-resolution `Uint32Array` output ready for `texImage2D` or a `Uint32Array` view of `ImageData`.
20
+ - πŸ”΄ **Runtime layer** β€” `lerpOklchBuffer()`, `packOklchBufferToUint32()`, `sampleColorLUT()`. Zero allocations on the hot path. Zero closures. Branch-clamped, SoA-friendly, V8-monomorphic.
21
+
22
+ The split is the whole point. Every existing color library hands you an object per call. **Sub-2KB min+gzip. One peer dependency** (`@zakkster/lite-lerp`).
23
+
24
+ ## 🧬 Where it fits
25
+
26
+ `lite-color-engine` is the **color pipeline kernel** of the zero-GC rendering stack β€” the bridge between human-authored CSS colors and per-frame `Uint32` pixel writes:
27
+
28
+ ```mermaid
29
+ flowchart LR
30
+ A[Designer / CSS<br/><sub>any CSS color string</sub>]:::input --> B[parseCSSColor<br/><sub>authoring</sub>]:::author
31
+ B --> C[OKLCH Float32 buffer<br/><sub>L, C, H triplets</sub>]:::buffer
32
+ C --> D[lerpOklchBuffer<br/><sub>shortest-path hue</sub>]:::runtime
33
+ C --> E[bakeGradientToUint32<br/><sub>LUT baking</sub>]:::lut
34
+ E --> F[Uint32Array LUT<br/><sub>RGBA-LE</sub>]:::buffer
35
+ F --> G[sampleColorLUT<br/><sub>zero-GC sampler</sub>]:::runtime
36
+ D --> H[packOklchBufferToUint32<br/><sub>sRGB transfer</sub>]:::runtime
37
+ G --> I[Canvas ImageData<br/>WebGL texImage2D]:::render
38
+ H --> I
39
+
40
+ classDef input fill:#fef3c7,stroke:#d97706,color:#78350f
41
+ classDef author fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e
42
+ classDef buffer fill:#f3f4f6,stroke:#6b7280,color:#1f2937
43
+ classDef lut fill:#dcfce7,stroke:#16a34a,color:#14532d
44
+ classDef runtime fill:#ede9fe,stroke:#7c3aed,color:#4c1d95
45
+ classDef render fill:#fecaca,stroke:#dc2626,color:#7f1d1d
46
+ ```
47
+
48
+ Every layer is independent. You can use just the parser. You can hand-author OKLCH buffers and skip the parser entirely. You can sample baked LUTs without ever touching `lerpOklchBuffer`. No framework lock-in, no global state.
49
+
50
+ ## πŸš€ Install
51
+
52
+ ```bash
53
+ npm install @zakkster/lite-color-engine
54
+ ```
55
+
56
+ ## πŸ•ΉοΈ Quick Start
57
+
58
+ ### Authoring β€” parse once, store forever
59
+
60
+ ```js
61
+ import { parseCSSColor } from '@zakkster/lite-color-engine';
62
+
63
+ // Pre-allocate the entire palette as one contiguous buffer.
64
+ // Each color is 3 floats: [L, C, H].
65
+ const palette = new Float32Array(3 * 4);
66
+ const alphas = new Float32Array(4);
67
+
68
+ alphas[0] = parseCSSColor('#ff0000', palette, 0); // hex
69
+ alphas[1] = parseCSSColor('rgba(0, 200, 100, 0.5)', palette, 3); // rgb
70
+ alphas[2] = parseCSSColor('oklch(60% 0.15 250)', palette, 6); // oklch
71
+ alphas[3] = parseCSSColor('rebeccapurple', palette, 9); // named
72
+ ```
73
+
74
+ That `Float32Array` is now your color storage for the rest of the program. No object headers, no GC pressure, perfectly cache-friendly.
75
+
76
+ ### Runtime β€” interpolate and pack with zero allocations
77
+
78
+ ```js
79
+ import { lerpOklchBuffer, packOklchBufferToUint32 } from '@zakkster/lite-color-engine';
80
+
81
+ const tempColor = new Float32Array(3); // single scratch buffer for the program lifetime
82
+
83
+ // Tween color 0 β†’ color 2 over time, packing for ImageData.
84
+ function tick(t) {
85
+ lerpOklchBuffer(palette, 0, palette, 6, t, tempColor, 0);
86
+ return packOklchBufferToUint32(tempColor, 0, 1.0);
87
+ }
88
+
89
+ // Inside your render loop:
90
+ const u32 = tick(progress);
91
+ imageDataU32View[pixelIndex] = u32; // direct write β€” no string parsing, no Color object
92
+ ```
93
+
94
+ ### LUT β€” bake a gradient once, sample forever
95
+
96
+ ```js
97
+ import { bakeGradientToUint32, sampleColorLUT } from '@zakkster/lite-color-engine';
98
+
99
+ // Three-stop gradient: red β†’ gold β†’ blue
100
+ const stops = new Float32Array(9);
101
+ parseCSSColor('#ff0000', stops, 0);
102
+ parseCSSColor('#ffd700', stops, 3);
103
+ parseCSSColor('#0000ff', stops, 6);
104
+
105
+ const lut = bakeGradientToUint32(stops, 3, 256); // 256-entry Uint32Array
106
+
107
+ // Hot path: zero allocations, zero allocations, zero allocations.
108
+ function colorAt(t) {
109
+ return sampleColorLUT(lut, t);
110
+ }
111
+ ```
112
+
113
+ ## 🧠 Why this exists
114
+
115
+ CSS color is a string-first API. Every existing color library (`color`, `colord`, `culori`, `chroma-js`) treats this seriously: it gives you an object you call methods on. That's correct for tooling. It's wrong for **rendering**.
116
+
117
+ A 60fps loop with 10,000 particles allocates 600,000 color objects per second if each particle calls `.mix()` or `.toRgb()`. Even if each object is 100 bytes, you've handed the GC 60MB/sec of churn. The frame time isn't the math β€” it's the major GC pause.
118
+
119
+ `lite-color-engine` solves this by **separating the parse from the use**:
120
+
121
+ 1. **Authoring time** (once, off the critical path): expand CSS strings into a flat `Float32Array` of OKLCH triplets. Allocate the buffer yourself, at the size you want. Done.
122
+ 2. **Runtime** (every frame, every particle): pure scalar math against that buffer. Lerp two triplets into a third. Pack a triplet to `Uint32`. Sample a baked LUT by `t`. Zero allocations, zero closures, zero indirection.
123
+
124
+ The buffer-and-offset API is the contract. It looks like a C-style API in JavaScript because that's what zero-GC actually means β€” you control the memory, the library reads and writes it.
125
+
126
+ ### Why OKLCH
127
+
128
+ Linear RGB interpolation is fast but **wrong**: red β†’ blue passes through muddy purple-gray at the midpoint because RGB isn't perceptually uniform. HSL/HSV are perceptually uniform in name only β€” the saturation axis interacts non-linearly with lightness. OKLCH (BjΓΆrn Ottosson, 2020) is the modern fix: hue, chroma, and lightness are independently meaningful, and linear interpolation between any two OKLCH points stays on a perceptually smooth curve.
129
+
130
+ The library uses OKLCH as its **internal representation** so every interpolation and every gradient bake is perceptually correct by construction. Pack-to-RGBA happens only at the moment of pixel write.
131
+
132
+ ## πŸ”₯ Algorithm
133
+
134
+ The end-to-end pipeline:
135
+
136
+ ```mermaid
137
+ flowchart TD
138
+ A[CSS string<br/>#ff0000]:::start --> B{prefix?}
139
+ B -->|named| N[NAMED_COLORS<br/>lookup]:::lut
140
+ B -->|#| H[HEX_REGEX]:::regex
141
+ B -->|rgb| R[RGB_REGEX]:::regex
142
+ B -->|hsl| S[HSL_REGEX]:::regex
143
+ B -->|oklch| K[OKLCH_REGEX<br/>direct]:::regex
144
+ B -->|oklab| L[OKLAB_REGEX<br/>+ polar]:::regex
145
+ N --> H
146
+ H --> M[sRGB byte 0-255]:::buf
147
+ R --> M
148
+ S --> M
149
+ M --> O[linearize<br/><sub>pow 2.4</sub>]:::math
150
+ O --> P[matrix β†’ LMS]:::math
151
+ P --> Q[cbrt]:::math
152
+ Q --> T[matrix β†’ OKLab]:::math
153
+ T --> U[atan2 / hypot<br/>β†’ OKLCH]:::math
154
+ U --> V[outBuf L, C, H]:::buf
155
+ K --> V
156
+ L --> V
157
+
158
+ V --> W[lerpOklchBuffer<br/><sub>shortest-arc hue</sub>]:::run
159
+ W --> X[OKLCH β†’ linear sRGB<br/><sub>cube + matrix</sub>]:::math
160
+ X --> Y[hard gamut clamp]:::math
161
+ Y --> Z[sRGB transfer<br/><sub>pow 1/2.4</sub>]:::math
162
+ Z --> AA[byte pack<br/><sub>A&lt;&lt;24 | B&lt;&lt;16 | G&lt;&lt;8 | R</sub>]:::math
163
+ AA --> AB[Uint32 RGBA-LE]:::buf
164
+
165
+ classDef start fill:#e0f2fe,stroke:#0284c7
166
+ classDef regex fill:#fef3c7,stroke:#d97706,color:#78350f
167
+ classDef lut fill:#dcfce7,stroke:#16a34a,color:#14532d
168
+ classDef math fill:#ede9fe,stroke:#7c3aed,color:#4c1d95
169
+ classDef buf fill:#f3f4f6,stroke:#6b7280,color:#1f2937
170
+ classDef run fill:#fecaca,stroke:#dc2626,color:#7f1d1d
171
+ ```
172
+
173
+ **Key invariants:**
174
+
175
+ - Every parser writes exactly **3 contiguous Float32 entries** at the offset you supply: `[L, C, H]`. Alpha is returned by value, not stored in the buffer β€” this keeps the OKLCH stride at 3, which divides cleanly into SoA layouts.
176
+ - Hue is **always** in `[0, 360)` after a write β€” every parser, every lerp, every conversion canonicalizes.
177
+ - Lightness is **always** clamped to `[0, 1]` after a write.
178
+ - Chroma is **always** clamped to `[0, +∞)` β€” never negative.
179
+ - LUT output is **always** little-endian RGBA byte order β€” drop straight into a `Uint32Array` view of `ImageData.data.buffer`. No byte-swapping. No platform check (browsers are universally LE).
180
+
181
+ ## πŸ—οΈ Buffer layout
182
+
183
+ The library is uncompromising about one thing: **every color is exactly 3 contiguous Float32 entries.** No fourth slot for alpha, no padding, no per-color object. This makes the layout naturally SoA-friendly:
184
+
185
+ ```
186
+ palette : [ L0 C0 H0 | L1 C1 H1 | L2 C2 H2 | ... ] // float32 stride = 3
187
+ alphas : [ a0 | a1 | a2 | ... ] // float32 stride = 1
188
+ ```
189
+
190
+ Why a separate alpha array? Because **most colors are opaque**, and packing alpha into the OKLCH stride wastes 25% of every cache line on a `1.0` constant. A separate `Float32Array` (or even `Uint8Array`) lets opaque colors skip alpha entirely, and lets you swap alpha layouts (per-color vs per-instance) without changing the color storage.
191
+
192
+ If you genuinely want premultiplied OKLCHa as a stride-4, you can β€” the API doesn't stop you, you just write `outBuf[offset + 3] = alpha`. The library functions only touch the first three slots.
193
+
194
+ ## πŸ“Š Comparison
195
+
196
+ | | **lite-color-engine** | culori | colord | chroma-js | hand-rolled |
197
+ |---|---|---|---|---|---|
198
+ | Bundle (min+gzip) | **<2 KB** | ~14 KB | ~7 KB | ~14 KB | depends |
199
+ | Hot-path allocations | **0** | several / call | 1 / call | several / call | usually 0 |
200
+ | OKLCH-native runtime | **yes** | yes (multi-space) | via plugin | yes | manual |
201
+ | Float32Array I/O | **yes** | no (objects) | no (objects) | no (objects) | yes |
202
+ | Bake-to-Uint32 LUT | **yes** | no | no | no | manual |
203
+ | Direct `ImageData.data.buffer` write | **yes** (LE-RGBA) | no | no | no | manual |
204
+ | CSS Color Level 4 parsing | **yes** | yes | yes | partial | manual |
205
+ | Canonical hue + lightness clamps | **yes** | yes | partial | partial | manual |
206
+ | SoA / ECS friendly | **yes** | no | no | no | yes |
207
+ | TypeScript types | **yes (full)** | yes | yes | yes | n/a |
208
+ | Framework lock-in | **none** | none | none | none | none |
209
+
210
+ `lite-color-engine` is **not** a replacement for `culori` or `colord`. It does not handle every color space, gamut mapping algorithm, or color difference metric. It does **one** thing: turn a CSS string into a per-frame allocation-free pixel writer.
211
+
212
+ ## βš™οΈ API
213
+
214
+ ### Authoring
215
+
216
+ #### `parseCSSColor(str, outBuf, offset): number`
217
+
218
+ Universal parser. Dispatches by string prefix to the appropriate format-specific parser. Writes `[L, C, H]` at `outBuf[offset..offset+2]`.
219
+
220
+ | Param | Type | Description |
221
+ |---|---|---|
222
+ | `str` | `string` | Any CSS color: hex, named, rgb/rgba, hsl/hsla, oklch, oklab. |
223
+ | `outBuf` | `Float32Array` | Pre-allocated destination. Must have length β‰₯ `offset + 3`. |
224
+ | `offset` | `number` | Start index of the L, C, H triplet. |
225
+ | **Returns** | `number` | Parsed alpha in `[0, 1]`. Defaults to `1.0` when omitted in the string. |
226
+
227
+ **Throws** if `str` is not a recognized format.
228
+
229
+ Format-specific parsers (`parseHexToBuffer`, `parseRgbToBuffer`, `parseHslToBuffer`, `parseOklchToBuffer`, `parseOklabToBuffer`) have identical signatures and skip the dispatch β€” call directly when you know the format.
230
+
231
+ ### Convert
232
+
233
+ #### `sRgbToOklchBuffer(r, g, b, outBuf, outOffset): void`
234
+
235
+ Raw sRGB-byte β†’ OKLCH conversion. The kernel underneath the parsers. Use when you have integer RGB triplets (e.g. from canvas pixel reads) and want to skip the regex layer.
236
+
237
+ ### Runtime
238
+
239
+ #### `lerpOklchBuffer(bufA, offsetA, bufB, offsetB, t, outBuf, outOffset): void`
240
+
241
+ Zero-GC OKLCH interpolation. Hue uses **shortest-path** so `350Β° β†’ 10Β°` correctly passes through `0Β°` instead of taking the long way around. Source and destination buffers may alias.
242
+
243
+ | Param | Type | Description |
244
+ |---|---|---|
245
+ | `t` | `number` | Interpolation factor. Values outside `[0, 1]` extrapolate, then clamp. `NaN` propagates. |
246
+
247
+ #### `packOklchBufferToUint32(buf, offset, alpha?): number`
248
+
249
+ Encodes an OKLCH triplet to a 32-bit unsigned integer in **little-endian RGBA** byte order. Uses the proper sRGB transfer function (`pow(c, 1/2.4)`). Round-tripping `sRGB β†’ OKLCH β†’ here` recovers the original byte exactly (within rounding).
250
+
251
+ #### `packOklchBufferToUint32Fast(buf, offset, alpha?): number`
252
+
253
+ Faster, less-accurate sibling. Substitutes `Math.sqrt(c)` for the proper sRGB transfer. ~2Γ— throughput on V8, with these documented costs:
254
+
255
+ - mid-gray (`#808080`) round-trips to about `#767676` (~10/255 darker)
256
+ - warm midtones (browns, golds) shift toward black
257
+
258
+ Use for ephemeral pixels (particle trails, alpha-blended sprite tints) where the round-trip identity isn't observable. **Avoid** for UI tokens, palette previews, or anywhere a designer compares the output to the input.
259
+
260
+ #### `sampleColorLUT(lut, t): number`
261
+
262
+ Zero-GC LUT sampler. Inline `t`-clamp + bitwise-truncated index β€” no allocations, no function calls beyond this one. Use inside particle systems, fragment-shader-style canvas loops, etc.
263
+
264
+ ### LUT
265
+
266
+ #### `bakeGradientToUint32(keyframesBuf, numStops, resolution?, easeFn?, packer?): Uint32Array`
267
+
268
+ Bakes a multi-stop OKLCH gradient into a `Uint32Array` of packed RGBA-LE colors.
269
+
270
+ | Param | Type | Description |
271
+ |---|---|---|
272
+ | `keyframesBuf` | `Float32Array` | Contiguous `[L0, C0, H0, L1, C1, H1, ...]`. |
273
+ | `numStops` | `number` | Stop count; must be `β‰₯ 2`. |
274
+ | `resolution` | `number` | LUT entry count. Default `256`. Must be `β‰₯ 2`. |
275
+ | `easeFn` | `(t) => number` | Optional. Warps `t` before stop selection. Outputs outside `[0, 1]` are clamped. |
276
+ | `packer` | `OklchPackerFn` | Optional. Defaults to the accurate packer. Pass `packOklchBufferToUint32Fast` for ~2Γ— bake throughput. |
277
+
278
+ **Throws** if `numStops < 2` or `resolution < 2`.
279
+
280
+ ## ⚑ Performance characteristics
281
+
282
+ | Operation | Cost |
283
+ |---|---|
284
+ | `parseCSSColor()` — named color | regex + map lookup + sRGB→OKLCH (~30 multiplies, 3 cbrt) |
285
+ | `parseCSSColor()` β€” oklch direct | regex + 3 number parses (no color math) |
286
+ | `lerpOklchBuffer()` | 3 lerps, 1 modulo, 4 comparisons. Zero allocations. |
287
+ | `packOklchBufferToUint32()` | ~30 multiplies, 1 cos, 1 sin, 3 cubes, 3 `pow(_, 1/2.4)`. Zero allocations. |
288
+ | `packOklchBufferToUint32Fast()` | Same minus the 3 `pow`s, plus 3 `sqrt`. Zero allocations. |
289
+ | `sampleColorLUT()` | 2 comparisons, 1 multiply, 1 bitwise truncate, 1 array load. Zero allocations. |
290
+ | `bakeGradientToUint32()` | `resolution Γ— (lerp + pack)`; ~25k ops for a default 256-entry LUT. **One** allocation (the output Uint32Array). |
291
+ | Hot-path allocations | **zero** across the entire runtime API |
292
+ | Module-load allocations | one `Float32Array(3)` scratch (single instance, shared) |
293
+
294
+ ## πŸ›‘οΈ Validation
295
+
296
+ The hot path trusts its inputs. There is no runtime validation in `lerpOklchBuffer`, `packOklchBufferToUint32`, or `sampleColorLUT` β€” bad data produces predictable bad output, not exceptions.
297
+
298
+ | Input | Behavior |
299
+ |---|---|
300
+ | `t < 0` or `t > 1` in lerp | Extrapolates, then output is clamped (lightness β†’ `[0, 1]`, chroma β†’ `[0, +∞)`) |
301
+ | `t === NaN` | NaN propagates through math; output may be `0` after clamps |
302
+ | Out-of-range OKLCH values | Pack clamps r/g/b channels to `[0, 1]` before encoding |
303
+ | Wrong-length `outBuf` in parser | Throws (`out of bounds`) only if offset is invalid; otherwise writes garbage past the buffer |
304
+ | Invalid CSS string in parser | **Throws** with a descriptive `lite-color-engine: Invalid X` error |
305
+
306
+ The asymmetry is deliberate: the parsers run at init time where exceptions are useful for catching authoring mistakes; the runtime functions run thousands of times per frame where every branch is a tax.
307
+
308
+ ## πŸ“¦ TypeScript
309
+
310
+ Full TypeScript declarations in `index.d.ts`. Every public function has a single overload β€” no union types in the API surface, no inference puzzles.
311
+
312
+ ```ts
313
+ import {
314
+ parseCSSColor,
315
+ lerpOklchBuffer,
316
+ packOklchBufferToUint32,
317
+ bakeGradientToUint32,
318
+ } from '@zakkster/lite-color-engine';
319
+ import type { OklchPackerFn } from '@zakkster/lite-color-engine';
320
+
321
+ const palette = new Float32Array(12);
322
+ const alpha: number = parseCSSColor('oklch(0.7 0.2 250)', palette, 0);
323
+ ```
324
+
325
+ ## πŸ“š LLM-friendly documentation
326
+
327
+ See `llms.txt` for a structured reference designed for AI coding assistants. Public surface, buffer layout, integration patterns, and common pitfalls in one parseable document.
328
+
329
+ ## 🧩 Pairs well with
330
+
331
+ - [`@zakkster/lite-lerp`](https://www.npmjs.com/package/@zakkster/lite-lerp) β€” peer dependency. Provides `lerp`, `lerpAngle`, and the rest of the math primitives the runtime layer relies on.
332
+ - [`@zakkster/lite-cubic-bezier`](https://www.npmjs.com/package/@zakkster/lite-cubic-bezier) β€” supplies `cubicBezier(...)` easing functions you can hand directly to `bakeGradientToUint32`.
333
+ - [`@zakkster/lite-ease`](https://www.npmjs.com/package/@zakkster/lite-ease) β€” the Penner library. Same compatibility β€” pass any of the 30 functions as the `easeFn` argument.
334
+
335
+ ## License
336
+
337
+ See [`LICENSE.txt`](./LICENSE.txt). Β© Zahary Shinikchiev
package/index.d.ts ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @zakkster/lite-color-engine
3
+ *
4
+ * Zero-GC, data-oriented OKLCH color engine for WebGL/Canvas pipelines.
5
+ *
6
+ * Buffer layout: every color is 3 contiguous Float32 entries `[L, C, H]`.
7
+ * - L (Lightness): [0, 1]
8
+ * - C (Chroma): [0, ~0.4] in practice, unbounded mathematically
9
+ * - H (Hue): [0, 360) degrees
10
+ *
11
+ * Pack output is a 32-bit unsigned integer in **little-endian RGBA** byte
12
+ * order β€” drop straight into `new Uint32Array(imageData.data.buffer)`.
13
+ */
14
+
15
+ // ============================================================================
16
+ // Authoring (CSS Parsing β†’ OKLCH Buffer)
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Parses CSS hex (`#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`) into the OKLCH
21
+ * triplet at `outBuf[offset..offset+2]`.
22
+ * @returns Parsed alpha in `[0, 1]`; defaults to `1.0` when omitted.
23
+ * @throws If the string is not a valid hex literal.
24
+ */
25
+ export function parseHexToBuffer(str: string, outBuf: Float32Array, offset: number): number;
26
+
27
+ /**
28
+ * Parses a CSS `oklch(...)` string directly into the buffer (no color-space
29
+ * conversion). Hue accepts `deg` (default), `rad`, and `turn` units.
30
+ * @returns Parsed alpha in `[0, 1]`; defaults to `1.0` when omitted.
31
+ * @throws If the string is not a valid `oklch(...)` literal.
32
+ */
33
+ export function parseOklchToBuffer(str: string, outBuf: Float32Array, offset: number): number;
34
+
35
+ /**
36
+ * Parses a CSS `oklab(...)` string and converts (a, b) β†’ polar (C, H) before
37
+ * writing the OKLCH triplet.
38
+ * @returns Parsed alpha in `[0, 1]`; defaults to `1.0` when omitted.
39
+ * @throws If the string is not a valid `oklab(...)` literal.
40
+ */
41
+ export function parseOklabToBuffer(str: string, outBuf: Float32Array, offset: number): number;
42
+
43
+ /**
44
+ * Parses a CSS `rgb(...)` / `rgba(...)` string and converts to OKLCH via the
45
+ * proper sRGB linearization. Accepts comma-separated and slash-alpha forms,
46
+ * `0-255` or `%` channels.
47
+ * @returns Parsed alpha in `[0, 1]`; defaults to `1.0` when omitted.
48
+ * @throws If the string is not a valid `rgb(...)` / `rgba(...)` literal.
49
+ */
50
+ export function parseRgbToBuffer(str: string, outBuf: Float32Array, offset: number): number;
51
+
52
+ /**
53
+ * Parses a CSS `hsl(...)` / `hsla(...)` string and converts to OKLCH via sRGB.
54
+ * @returns Parsed alpha in `[0, 1]`; defaults to `1.0` when omitted.
55
+ * @throws If the string is not a valid `hsl(...)` / `hsla(...)` literal.
56
+ */
57
+ export function parseHslToBuffer(str: string, outBuf: Float32Array, offset: number): number;
58
+
59
+ /**
60
+ * Universal CSS color parser. Dispatches by string prefix to the appropriate
61
+ * format-specific parser.
62
+ *
63
+ * Supports: 148 named colors, `#RGB[A]`, `#RRGGBB[AA]`, `rgb()`, `rgba()`,
64
+ * `hsl()`, `hsla()`, `oklch()`, `oklab()`.
65
+ *
66
+ * Intended for the **authoring/init phase** of a render pipeline. After
67
+ * compilation, work with the resulting `Float32Array` buffers directly via
68
+ * {@link lerpOklchBuffer} and {@link packOklchBufferToUint32}.
69
+ *
70
+ * @returns Parsed alpha in `[0, 1]`.
71
+ * @throws If the string is not a recognized format.
72
+ */
73
+ export function parseCSSColor(str: string, outBuf: Float32Array, offset: number): number;
74
+
75
+ // ============================================================================
76
+ // Convert (Raw Math)
77
+ // ============================================================================
78
+
79
+ /**
80
+ * Converts standard sRGB (0-255 byte channels) into a flat OKLCH buffer at
81
+ * `outBuf[outOffset..outOffset+2]`.
82
+ *
83
+ * Implements BjΓΆrn Ottosson's OKLab (2020) algorithm with defenses against
84
+ * NaN-propagating negative cube-root inputs and a strict lightness clamp.
85
+ */
86
+ export function sRgbToOklchBuffer(
87
+ r: number,
88
+ g: number,
89
+ b: number,
90
+ outBuf: Float32Array,
91
+ outOffset: number
92
+ ): void;
93
+
94
+ // ============================================================================
95
+ // Runtime (Zero-GC Hot Path)
96
+ // ============================================================================
97
+
98
+ /**
99
+ * Zero-GC, cache-friendly OKLCH buffer interpolation.
100
+ *
101
+ * Hue uses **shortest-path** interpolation (`lerpAngle`) so gradients never
102
+ * 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)`.
104
+ *
105
+ * Source and destination buffers may alias (same buffer, different offsets).
106
+ *
107
+ * @param t Interpolation factor; values outside `[0, 1]` extrapolate then clamp.
108
+ */
109
+ export function lerpOklchBuffer(
110
+ bufA: Float32Array,
111
+ offsetA: number,
112
+ bufB: Float32Array,
113
+ offsetB: number,
114
+ t: number,
115
+ outBuf: Float32Array,
116
+ outOffset: number
117
+ ): void;
118
+
119
+ /**
120
+ * 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
122
+ * a `Uint32Array` view.
123
+ *
124
+ * Uses the proper sRGB transfer function (`pow(c, 1/2.4)` branch).
125
+ * Round-tripping `sRGB β†’ OKLCH β†’ here` recovers the original byte exactly
126
+ * (within rounding).
127
+ *
128
+ * For a faster `Math.sqrt`-approximated sibling (~2x throughput, ~10/255
129
+ * mid-tone error), see {@link packOklchBufferToUint32Fast}.
130
+ *
131
+ * @param alpha Alpha in `[0, 1]`. Values outside the range are clamped. Default `1.0`.
132
+ * @returns 32-bit unsigned integer (high bit always positive thanks to `>>> 0`).
133
+ */
134
+ export function packOklchBufferToUint32(
135
+ buf: Float32Array,
136
+ offset: number,
137
+ alpha?: number
138
+ ): number;
139
+
140
+ /**
141
+ * Faster, less accurate variant of {@link packOklchBufferToUint32}.
142
+ *
143
+ * Substitutes `Math.sqrt(c)` for the proper sRGB transfer. ~2x throughput on
144
+ * V8 in tight loops at the cost of:
145
+ * - mid-gray (`#808080`) round-trips to about `#767676` (~10/255 darker)
146
+ * - warm midtones (browns, golds) shift toward black
147
+ *
148
+ * Use for ephemeral pixels (particle trails, alpha-blended sprite tints)
149
+ * where the round-trip identity isn't observable.
150
+ *
151
+ * @param alpha Alpha in `[0, 1]`. Default `1.0`.
152
+ * @returns 32-bit unsigned integer in little-endian RGBA byte order.
153
+ */
154
+ export function packOklchBufferToUint32Fast(
155
+ buf: Float32Array,
156
+ offset: number,
157
+ alpha?: number
158
+ ): number;
159
+
160
+ /**
161
+ * Zero-GC sampler for a baked LUT. Inline `t`-clamp + bitwise-truncated index.
162
+ *
163
+ * @param lut LUT produced by {@link bakeGradientToUint32}.
164
+ * @param t Sample position; values outside `[0, 1]` are clamped.
165
+ * @returns 32-bit packed color in little-endian RGBA byte order.
166
+ */
167
+ export function sampleColorLUT(lut: Uint32Array, t: number): number;
168
+
169
+ // ============================================================================
170
+ // LUT (Gradient Baking)
171
+ // ============================================================================
172
+
173
+ /** Signature of a packer compatible with {@link bakeGradientToUint32}. */
174
+ export type OklchPackerFn = (buf: Float32Array, offset: number, alpha?: number) => number;
175
+
176
+ /**
177
+ * Bakes a multi-stop OKLCH gradient into a ready-to-render `Uint32Array`.
178
+ *
179
+ * Output bytes are little-endian RGBA β€” drop into a `Uint32Array` view of
180
+ * `Canvas ImageData`, or upload as `RGBA / UNSIGNED_BYTE` via `texImage2D`.
181
+ *
182
+ * Stops are evenly distributed: stop _i_ is at `i / (numStops - 1)`. The
183
+ * optional `easeFn` warps the parametric position **before** stop selection.
184
+ * Easing outputs outside `[0, 1]` are clamped (the LUT is fixed-resolution
185
+ * and cannot represent overshoot).
186
+ *
187
+ * @param keyframesBuf Contiguous buffer of `[L0, C0, H0, L1, C1, H1, ...]`.
188
+ * @param numStops Stop count; must be `>= 2`.
189
+ * @param resolution LUT entry count. Defaults to `256`. Must be `>= 2`.
190
+ * @param easeFn Optional easing applied to `t` before stop selection.
191
+ * @param packer Optional packer override. Defaults to the accurate
192
+ * {@link packOklchBufferToUint32}. Pass
193
+ * {@link packOklchBufferToUint32Fast} for ~2x bake throughput.
194
+ * @throws If `numStops < 2` or `resolution < 2`.
195
+ */
196
+ export function bakeGradientToUint32(
197
+ keyframesBuf: Float32Array,
198
+ numStops: number,
199
+ resolution?: number,
200
+ easeFn?: (t: number) => number,
201
+ packer?: OklchPackerFn
202
+ ): Uint32Array;
package/index.js ADDED
@@ -0,0 +1,19 @@
1
+ export {
2
+ parseHexToBuffer,
3
+ parseCSSColor,
4
+ parseHslToBuffer,
5
+ parseOklabToBuffer,
6
+ parseOklchToBuffer,
7
+ parseRgbToBuffer
8
+ } from './src/authoring.js';
9
+
10
+ export { bakeGradientToUint32 } from './src/lut.js';
11
+
12
+ export {
13
+ lerpOklchBuffer,
14
+ packOklchBufferToUint32,
15
+ packOklchBufferToUint32Fast,
16
+ sampleColorLUT
17
+ } from './src/runtime.js';
18
+
19
+ export { sRgbToOklchBuffer } from './src/convert.js';