@zakkster/lite-gradient-studio 1.0.1 → 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 +334 -0
- package/README.md +147 -0
- package/llms.txt +83 -0
- package/package.json +23 -11
- package/src/bake.js +36 -0
- package/src/color-convert.js +18 -3
- package/src/index.d.ts +122 -1
- package/src/index.js +2 -1
- package/src/mesh.js +407 -74
package/src/bake.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import {
|
|
15
15
|
bakeGradientToUint32,
|
|
16
16
|
packOklchBufferToUint32,
|
|
17
|
+
packOklchBufferToUint32Dithered,
|
|
17
18
|
} from '@zakkster/lite-color-engine';
|
|
18
19
|
|
|
19
20
|
const EVEN_SPACING_EPS = 1e-9;
|
|
@@ -139,3 +140,38 @@ export function packOklchSingle(l, c, h, alpha = 1) {
|
|
|
139
140
|
_packScratch[2] = h;
|
|
140
141
|
return packOklchBufferToUint32(_packScratch, 0, alpha);
|
|
141
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Scalar-arg convenience wrapper around lite-color-engine v1.5's
|
|
146
|
+
* `packOklchBufferToUint32Dithered`. Same output byte order (RGBA-LE),
|
|
147
|
+
* same gamma-encode semantics — the only difference from `packOklchSingle`
|
|
148
|
+
* is a threshold-offset applied at the round step: `byte = (enc*255 + noise01) | 0`
|
|
149
|
+
* instead of `(enc*255 + 0.5) | 0`.
|
|
150
|
+
*
|
|
151
|
+
* Contract from the engine (restated for T2 D8):
|
|
152
|
+
* - The **same** noise value is used for R, G, and B at one pixel —
|
|
153
|
+
* luminance-patterned dither, no chroma speckle.
|
|
154
|
+
* - Alpha is undithered (banding on alpha is rarely visible and would
|
|
155
|
+
* interact badly with premultiplication).
|
|
156
|
+
* - `noise01 = 0.5` reproduces `packOklchSingle` exactly (identity
|
|
157
|
+
* anchor — trivially provable by the +0.5 rounding in the plain
|
|
158
|
+
* packer, which is what the dithered form emits at that noise value).
|
|
159
|
+
*
|
|
160
|
+
* Zero-GC: reuses the module-scoped `_packScratch` Float32Array(3) that
|
|
161
|
+
* `packOklchSingle` uses. Consumers must not call both in reentrant
|
|
162
|
+
* contexts (the studio rasterizers never do — one loop, one packer).
|
|
163
|
+
*
|
|
164
|
+
* @param {number} l OKLCH lightness (0..1)
|
|
165
|
+
* @param {number} c OKLCH chroma (~0..0.4)
|
|
166
|
+
* @param {number} h OKLCH hue in degrees
|
|
167
|
+
* @param {number} alpha (0..1); undithered
|
|
168
|
+
* @param {number} noise01 Threshold offset in [0, 1). Typically `(tile[i] + 0.5) / 256`
|
|
169
|
+
* where `tile` is `getBlueNoise64()` from the engine.
|
|
170
|
+
* @returns {number} Uint32 RGBA-LE pixel.
|
|
171
|
+
*/
|
|
172
|
+
export function packOklchSingleDithered(l, c, h, alpha, noise01) {
|
|
173
|
+
_packScratch[0] = l;
|
|
174
|
+
_packScratch[1] = c;
|
|
175
|
+
_packScratch[2] = h;
|
|
176
|
+
return packOklchBufferToUint32Dithered(_packScratch, 0, alpha, noise01);
|
|
177
|
+
}
|
package/src/color-convert.js
CHANGED
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* OKLCH → linear sRGB triplet, with sRGB-gamut mapping by chroma reduction.
|
|
16
|
-
* Output: [r, g, b] each in [0, 1]
|
|
16
|
+
* Output: `[r, g, b]` each in `[0, 1]`.
|
|
17
|
+
*
|
|
18
|
+
* Zero-GC path: pass a caller-owned 3-element array as `out`. The function
|
|
19
|
+
* writes into it and returns the same reference. Omit `out` (or pass null)
|
|
20
|
+
* to get a fresh allocated array — back-compat with the 3-arg call shape.
|
|
17
21
|
*/
|
|
18
|
-
export function oklchToLinearSrgb(L, C, H) {
|
|
22
|
+
export function oklchToLinearSrgb(L, C, H, out) {
|
|
19
23
|
const hRad = H * Math.PI / 180;
|
|
20
24
|
const cosH = Math.cos(hRad);
|
|
21
25
|
const sinH = Math.sin(hRad);
|
|
@@ -38,6 +42,7 @@ export function oklchToLinearSrgb(L, C, H) {
|
|
|
38
42
|
let [r, g, bb] = getRgb(C);
|
|
39
43
|
|
|
40
44
|
if (r >= 0 && r <= 1 && g >= 0 && g <= 1 && bb >= 0 && bb <= 1) {
|
|
45
|
+
if (out) { out[0] = r; out[1] = g; out[2] = bb; return out; }
|
|
41
46
|
return [r, g, bb];
|
|
42
47
|
}
|
|
43
48
|
|
|
@@ -49,6 +54,7 @@ export function oklchToLinearSrgb(L, C, H) {
|
|
|
49
54
|
if (r < 0) r = 0; else if (r > 1) r = 1;
|
|
50
55
|
if (g < 0) g = 0; else if (g > 1) g = 1;
|
|
51
56
|
if (bb < 0) bb = 0; else if (bb > 1) bb = 1;
|
|
57
|
+
if (out) { out[0] = r; out[1] = g; out[2] = bb; return out; }
|
|
52
58
|
return [r, g, bb];
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -64,6 +70,7 @@ export function oklchToLinearSrgb(L, C, H) {
|
|
|
64
70
|
hi = midC;
|
|
65
71
|
}
|
|
66
72
|
}
|
|
73
|
+
if (out) { out[0] = fitR; out[1] = fitG; out[2] = fitB; return out; }
|
|
67
74
|
return [fitR, fitG, fitB];
|
|
68
75
|
}
|
|
69
76
|
|
|
@@ -111,7 +118,14 @@ function clampByte(v) {
|
|
|
111
118
|
/**
|
|
112
119
|
* Linear sRGB → OKLCH. Used by fromHex and the palette extractor.
|
|
113
120
|
*/
|
|
114
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Linear sRGB → OKLCH. Inverse of `oklchToLinearSrgb`.
|
|
123
|
+
*
|
|
124
|
+
* Zero-GC path: pass a caller-owned `{ l, c, h }` as `out`. The function
|
|
125
|
+
* writes into it and returns the same reference. Omit `out` (or pass null)
|
|
126
|
+
* to get a fresh allocated object — back-compat with the 3-arg call shape.
|
|
127
|
+
*/
|
|
128
|
+
export function linearSrgbToOklch(r, g, b, out) {
|
|
115
129
|
const lLms = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
|
116
130
|
const mLms = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
|
117
131
|
const sLms = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
|
@@ -124,6 +138,7 @@ export function linearSrgbToOklch(r, g, b) {
|
|
|
124
138
|
const C = Math.sqrt(a * a + bb * bb);
|
|
125
139
|
let H = Math.atan2(bb, a) * 180 / Math.PI;
|
|
126
140
|
if (H < 0) H += 360;
|
|
141
|
+
if (out) { out.l = L; out.c = C; out.h = H; return out; }
|
|
127
142
|
return { l: L, c: C, h: H };
|
|
128
143
|
}
|
|
129
144
|
|
package/src/index.d.ts
CHANGED
|
@@ -66,6 +66,23 @@ export interface MeshRasterizeOptions {
|
|
|
66
66
|
interpolation?: InterpolationMode;
|
|
67
67
|
/** Legacy boolean: `true` is equivalent to `interpolation: 'smooth'`. */
|
|
68
68
|
smooth?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* v1.2.0 — blue-noise dithering (D8). When `true`, `rasterizeTo` uses
|
|
71
|
+
* `@zakkster/lite-color-engine`'s dithered packer with per-pixel
|
|
72
|
+
* threshold offsets from the engine's shared 64×64 void-and-cluster
|
|
73
|
+
* tile. Every channel is within ±1 of the undithered output; the
|
|
74
|
+
* same offset is shared across R/G/B at each pixel (no chroma
|
|
75
|
+
* speckle); alpha is undithered.
|
|
76
|
+
*
|
|
77
|
+
* `dither: false` (or absent) walks the same code as v1.2.0 — the
|
|
78
|
+
* byte-parity guarantee for undithered output is preserved.
|
|
79
|
+
*
|
|
80
|
+
* Not yet supported on `rasterizeDeformedTo` — throws
|
|
81
|
+
* `WRAP_DEFORMED_UNSUPPORTED` first if the mesh is wrapped, and
|
|
82
|
+
* silently ignores the flag otherwise (deformed + dither planned
|
|
83
|
+
* alongside the v1.3 ghost-quad seam work).
|
|
84
|
+
*/
|
|
85
|
+
dither?: boolean;
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
/**
|
|
@@ -73,19 +90,58 @@ export interface MeshRasterizeOptions {
|
|
|
73
90
|
* mesh. Pure function. Used internally by MeshGradient when no `stops` array
|
|
74
91
|
* is supplied; re-exported for callers who want to inspect or override the
|
|
75
92
|
* default field.
|
|
93
|
+
*
|
|
94
|
+
* v1.2.0 — trailing `wrapX` / `wrapY` args are optional and default to
|
|
95
|
+
* false. When set, the corresponding axis uses a periodic parameterization
|
|
96
|
+
* (uniform 360/cols hue step for wrapX; sinusoidal L and drift for wrapY)
|
|
97
|
+
* so the default mesh doesn't compress aperiodic sweeps into the final
|
|
98
|
+
* cell. Four-arg calls stay byte-identical to v1.1.0.
|
|
76
99
|
*/
|
|
77
100
|
export function defaultMeshColor(
|
|
78
101
|
col: number,
|
|
79
102
|
row: number,
|
|
80
103
|
cols: number,
|
|
81
104
|
rows: number,
|
|
105
|
+
wrapX?: boolean,
|
|
106
|
+
wrapY?: boolean,
|
|
82
107
|
): OklchColor;
|
|
83
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Constructor options for {@link MeshGradient} (v1.2.0+).
|
|
111
|
+
*
|
|
112
|
+
* Wrap flags are structural — they change the UV period, the cell count,
|
|
113
|
+
* and the default control-point positions. Both flags are independent
|
|
114
|
+
* (torus = both on; cylinder = one on). Non-wrap behaviour is byte-parity
|
|
115
|
+
* with v1.1.0 when opts is absent or both flags are false.
|
|
116
|
+
*/
|
|
117
|
+
export interface MeshGradientOptions {
|
|
118
|
+
/**
|
|
119
|
+
* Treat the X axis as cyclic. `sampleAt` wraps u via `u - Math.floor(u)`,
|
|
120
|
+
* `rasterizeTo` samples the period (`x / width`, not `x / (width - 1)`),
|
|
121
|
+
* cubic mode reads real neighbours across the seam via modulo indexing
|
|
122
|
+
* (C¹ continuity). `rasterizeDeformedTo` throws
|
|
123
|
+
* `WRAP_DEFORMED_UNSUPPORTED` — ghost-quad seam crossing is deferred
|
|
124
|
+
* to v1.3.
|
|
125
|
+
*/
|
|
126
|
+
wrapX?: boolean;
|
|
127
|
+
/** Symmetric to {@link wrapX} on the Y axis. */
|
|
128
|
+
wrapY?: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
84
131
|
export class MeshGradient {
|
|
85
|
-
constructor(
|
|
132
|
+
constructor(
|
|
133
|
+
cols: number,
|
|
134
|
+
rows: number,
|
|
135
|
+
stops?: ReadonlyArray<MeshStop>,
|
|
136
|
+
opts?: MeshGradientOptions,
|
|
137
|
+
);
|
|
86
138
|
|
|
87
139
|
readonly cols: number;
|
|
88
140
|
readonly rows: number;
|
|
141
|
+
/** v1.2.0 — reflects the constructor opts; `false` when opts absent. */
|
|
142
|
+
readonly wrapX: boolean;
|
|
143
|
+
/** v1.2.0 — reflects the constructor opts; `false` when opts absent. */
|
|
144
|
+
readonly wrapY: boolean;
|
|
89
145
|
readonly stops: MeshStopFull[];
|
|
90
146
|
|
|
91
147
|
/** Read the (col, row) color into a caller-owned out. Zero-GC. */
|
|
@@ -108,6 +164,9 @@ export class MeshGradient {
|
|
|
108
164
|
* - true -> 'smooth' (smoothstep)
|
|
109
165
|
* - 'cubic' -> Catmull-Rom 2D
|
|
110
166
|
* - any of the strings above explicitly
|
|
167
|
+
*
|
|
168
|
+
* v1.2.0 — on wrapped axes, u/v accept any float; input is period-wrapped
|
|
169
|
+
* via `u - Math.floor(u)` before mapping to a cell coord.
|
|
111
170
|
*/
|
|
112
171
|
sampleAt<T extends Partial<OklchColorA>>(
|
|
113
172
|
u: number,
|
|
@@ -119,6 +178,10 @@ export class MeshGradient {
|
|
|
119
178
|
/**
|
|
120
179
|
* Rasterize on the REGULAR grid into a packed-RGBA Uint32Array (little-
|
|
121
180
|
* endian byte order; aliasable as Uint8ClampedArray for ImageData).
|
|
181
|
+
*
|
|
182
|
+
* v1.2.0 — on wrapped axes, samples the period (drops the `- 1`
|
|
183
|
+
* divisor) so pixel column 0 of the "next tile" would land at u ≡ 0,
|
|
184
|
+
* enabling seamless `drawImage`-style tiling.
|
|
122
185
|
*/
|
|
123
186
|
rasterizeTo(
|
|
124
187
|
out: Uint32Array,
|
|
@@ -131,6 +194,10 @@ export class MeshGradient {
|
|
|
131
194
|
* Rasterize honoring control-point positions (deformable mesh). Pixels
|
|
132
195
|
* outside any quad are LEFT UNTOUCHED -- callers that want a clean
|
|
133
196
|
* canvas must fill or zero `out` first.
|
|
197
|
+
*
|
|
198
|
+
* v1.2.0 — throws with `err.code = 'WRAP_DEFORMED_UNSUPPORTED'` when
|
|
199
|
+
* called on a wrapped mesh. Ghost-quad seam crossing is planned for
|
|
200
|
+
* v1.3; the current behaviour is loud failure instead of silent seams.
|
|
134
201
|
*/
|
|
135
202
|
rasterizeDeformedTo(
|
|
136
203
|
out: Uint32Array,
|
|
@@ -143,6 +210,45 @@ export class MeshGradient {
|
|
|
143
210
|
destroy(): void;
|
|
144
211
|
}
|
|
145
212
|
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Monochrome mesh (v1.1.0)
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
export type MonoMode = 'tinted' | 'grayscale';
|
|
218
|
+
export type MonoMeshDirection = 'horizontal' | 'vertical' | 'diagonal' | 'radial';
|
|
219
|
+
|
|
220
|
+
export interface MonochromeMeshOptions {
|
|
221
|
+
/**
|
|
222
|
+
* Chroma handling.
|
|
223
|
+
* - `'tinted'` (default): retain base color's chroma/hue at every point.
|
|
224
|
+
* - `'grayscale'`: force chroma to 0 for pure achromatic tones.
|
|
225
|
+
*/
|
|
226
|
+
mode?: MonoMode;
|
|
227
|
+
/** L-axis endpoints [lo, hi], must satisfy `0 <= lo < hi <= 1`. Default `[0, 1]`. */
|
|
228
|
+
range?: readonly [number, number];
|
|
229
|
+
/**
|
|
230
|
+
* How L varies across the mesh:
|
|
231
|
+
* - `'horizontal'`: left-to-right, uniform per row.
|
|
232
|
+
* - `'vertical'`: top-to-bottom, uniform per column.
|
|
233
|
+
* - `'diagonal'` (default): top-left corner (lo) to bottom-right corner (hi).
|
|
234
|
+
* - `'radial'`: center (lo) outward to corners (hi).
|
|
235
|
+
*/
|
|
236
|
+
direction?: MonoMeshDirection;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Build a monochromatic MeshGradient from a single base OKLCH color.
|
|
241
|
+
* Chroma and hue are held constant across every control point; only
|
|
242
|
+
* lightness varies according to `direction`. Client-work-friendly:
|
|
243
|
+
* never looks "AI-generated" because the palette is fixed to one brand tone.
|
|
244
|
+
*/
|
|
245
|
+
export function monochromeMesh(
|
|
246
|
+
base: OklchColor,
|
|
247
|
+
cols: number,
|
|
248
|
+
rows: number,
|
|
249
|
+
opts?: MonochromeMeshOptions
|
|
250
|
+
): MeshGradient;
|
|
251
|
+
|
|
146
252
|
// ---------------------------------------------------------------------------
|
|
147
253
|
// CSS emitters
|
|
148
254
|
// ---------------------------------------------------------------------------
|
|
@@ -274,6 +380,21 @@ export function sampleLut(lut: Uint32Array, t: number): number;
|
|
|
274
380
|
/** Pack a single OKLCH color directly to a 32-bit RGBA value. */
|
|
275
381
|
export function packOklchSingle(l: number, c: number, h: number, alpha?: number): number;
|
|
276
382
|
|
|
383
|
+
/**
|
|
384
|
+
* v1.2.0 — dithered scalar packer. Threshold-offset gamma round via
|
|
385
|
+
* `noise01`: `byte = (encoded * 255 + noise01) | 0`. `noise01 = 0.5`
|
|
386
|
+
* reproduces `packOklchSingle` exactly (identity anchor). Same buffer
|
|
387
|
+
* scratch as `packOklchSingle` — safe to alternate between calls, but
|
|
388
|
+
* do NOT interleave with the plain packer in reentrant contexts.
|
|
389
|
+
*/
|
|
390
|
+
export function packOklchSingleDithered(
|
|
391
|
+
l: number,
|
|
392
|
+
c: number,
|
|
393
|
+
h: number,
|
|
394
|
+
alpha: number,
|
|
395
|
+
noise01: number,
|
|
396
|
+
): number;
|
|
397
|
+
|
|
277
398
|
// ---------------------------------------------------------------------------
|
|
278
399
|
// Palette extraction
|
|
279
400
|
// ---------------------------------------------------------------------------
|
package/src/index.js
CHANGED
|
@@ -20,8 +20,9 @@ export {
|
|
|
20
20
|
flattenStopsToBuffer,
|
|
21
21
|
sampleLut,
|
|
22
22
|
packOklchSingle,
|
|
23
|
+
packOklchSingleDithered,
|
|
23
24
|
} from './bake.js';
|
|
24
|
-
export { MeshGradient, defaultMeshColor } from './mesh.js';
|
|
25
|
+
export { MeshGradient, defaultMeshColor, monochromeMesh } from './mesh.js';
|
|
25
26
|
export { formatCssLinear, formatCssRadial, formatCssConic } from './css-emitters.js';
|
|
26
27
|
export { formatCssMesh } from './mesh-css.js';
|
|
27
28
|
export {
|