@zakkster/lite-gradient-studio 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/src/index.d.ts ADDED
@@ -0,0 +1,380 @@
1
+ // @zakkster/lite-gradient-studio v1.0.0 -- type definitions.
2
+ //
3
+ // (c) Zahary Shinikchiev. MIT licensed.
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Re-exports from @zakkster/lite-gradient
7
+ // ---------------------------------------------------------------------------
8
+
9
+ export { Gradient } from '@zakkster/lite-gradient';
10
+ export type {
11
+ GradientStop,
12
+ GradientConstructorStop,
13
+ GradientOptions,
14
+ } from '@zakkster/lite-gradient';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Common color shapes
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /** Plain OKLCH color object. Alpha is optional and defaults to 1. */
21
+ export interface OklchColor {
22
+ l: number;
23
+ c: number;
24
+ h: number;
25
+ a?: number;
26
+ }
27
+
28
+ /** OKLCH color with alpha always present (after construction-time defaulting). */
29
+ export interface OklchColorA {
30
+ l: number;
31
+ c: number;
32
+ h: number;
33
+ a: number;
34
+ }
35
+
36
+ /** Mesh stop with optional deformed-grid position. */
37
+ export interface MeshStop extends OklchColor {
38
+ x?: number;
39
+ y?: number;
40
+ }
41
+
42
+ /** Mesh stop fully populated (post-construction): always has x, y, a. */
43
+ export interface MeshStopFull {
44
+ l: number;
45
+ c: number;
46
+ h: number;
47
+ a: number;
48
+ x: number;
49
+ y: number;
50
+ }
51
+
52
+ /** 2D position out-parameter shape. */
53
+ export interface XYOut {
54
+ x: number;
55
+ y: number;
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // MeshGradient
60
+ // ---------------------------------------------------------------------------
61
+
62
+ export type InterpolationMode = 'bilinear' | 'smooth' | 'cubic';
63
+
64
+ export interface MeshRasterizeOptions {
65
+ /** Default 'bilinear'. */
66
+ interpolation?: InterpolationMode;
67
+ /** Legacy boolean: `true` is equivalent to `interpolation: 'smooth'`. */
68
+ smooth?: boolean;
69
+ }
70
+
71
+ /**
72
+ * Procedural default color for a control point at (col, row) of a cols x rows
73
+ * mesh. Pure function. Used internally by MeshGradient when no `stops` array
74
+ * is supplied; re-exported for callers who want to inspect or override the
75
+ * default field.
76
+ */
77
+ export function defaultMeshColor(
78
+ col: number,
79
+ row: number,
80
+ cols: number,
81
+ rows: number,
82
+ ): OklchColor;
83
+
84
+ export class MeshGradient {
85
+ constructor(cols: number, rows: number, stops?: ReadonlyArray<MeshStop>);
86
+
87
+ readonly cols: number;
88
+ readonly rows: number;
89
+ readonly stops: MeshStopFull[];
90
+
91
+ /** Read the (col, row) color into a caller-owned out. Zero-GC. */
92
+ getPoint<T extends Partial<OklchColor>>(col: number, row: number, out: T): T;
93
+ /** Mutate the (col, row) color in place. */
94
+ setPoint(col: number, row: number, l: number, c: number, h: number): void;
95
+
96
+ /** Set the (x, y) deformed-grid position of a control point. */
97
+ setPointPosition(col: number, row: number, x: number, y: number): void;
98
+ /** Read the (x, y) position into a caller-owned `{ x, y }` out. Zero-GC. */
99
+ getPointPosition<T extends XYOut>(col: number, row: number, out: T): T;
100
+ /** Restore all control-point positions to the regular grid. */
101
+ resetPositions(): void;
102
+
103
+ /**
104
+ * Sample at parametric (u, v) in [0, 1]^2 into `out`. Zero-GC.
105
+ *
106
+ * `modeOrSmooth`:
107
+ * - false / undefined -> 'bilinear'
108
+ * - true -> 'smooth' (smoothstep)
109
+ * - 'cubic' -> Catmull-Rom 2D
110
+ * - any of the strings above explicitly
111
+ */
112
+ sampleAt<T extends Partial<OklchColorA>>(
113
+ u: number,
114
+ v: number,
115
+ out: T,
116
+ modeOrSmooth?: boolean | InterpolationMode,
117
+ ): T;
118
+
119
+ /**
120
+ * Rasterize on the REGULAR grid into a packed-RGBA Uint32Array (little-
121
+ * endian byte order; aliasable as Uint8ClampedArray for ImageData).
122
+ */
123
+ rasterizeTo(
124
+ out: Uint32Array,
125
+ width: number,
126
+ height: number,
127
+ opts?: MeshRasterizeOptions,
128
+ ): Uint32Array;
129
+
130
+ /**
131
+ * Rasterize honoring control-point positions (deformable mesh). Pixels
132
+ * outside any quad are LEFT UNTOUCHED -- callers that want a clean
133
+ * canvas must fill or zero `out` first.
134
+ */
135
+ rasterizeDeformedTo(
136
+ out: Uint32Array,
137
+ width: number,
138
+ height: number,
139
+ opts?: MeshRasterizeOptions,
140
+ ): Uint32Array;
141
+
142
+ /** Release internal references for GC. */
143
+ destroy(): void;
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // CSS emitters
148
+ // ---------------------------------------------------------------------------
149
+
150
+ /** Gradient shape accepted by the CSS emitters (a `stops` array of OKLCH+pos). */
151
+ export interface GradientLike {
152
+ stops: ReadonlyArray<OklchColor & { pos: number }>;
153
+ }
154
+
155
+ export interface FormatCssLinearOptions {
156
+ /** Default 90 (left -> right). */
157
+ angle?: number;
158
+ /** Default true. Emits `in oklch` so browsers interpolate perceptually. */
159
+ oklchInterp?: boolean;
160
+ }
161
+
162
+ export interface FormatCssRadialOptions {
163
+ /** Default 'circle'. */
164
+ shape?: 'circle' | 'ellipse';
165
+ /** Default 'farthest-corner'. */
166
+ size?: string;
167
+ /** Default 'center'. Accepts CSS position syntax. */
168
+ position?: string;
169
+ /** Default true. */
170
+ oklchInterp?: boolean;
171
+ }
172
+
173
+ export interface FormatCssConicOptions {
174
+ /** Default 0. Starting angle in degrees. */
175
+ from?: number;
176
+ /** Default 'center'. */
177
+ position?: string;
178
+ /** Default true. */
179
+ oklchInterp?: boolean;
180
+ }
181
+
182
+ export function formatCssLinear(gradient: GradientLike, opts?: FormatCssLinearOptions): string;
183
+ export function formatCssRadial(gradient: GradientLike, opts?: FormatCssRadialOptions): string;
184
+ export function formatCssConic(gradient: GradientLike, opts?: FormatCssConicOptions): string;
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Mesh CSS approximation
188
+ // ---------------------------------------------------------------------------
189
+
190
+ export interface FormatCssMeshOptions {
191
+ /** Layer radius as % of farthest corner. Auto-sized by mesh dims if absent. */
192
+ radiusPct?: number;
193
+ /** Default 'circle'. */
194
+ shape?: 'circle' | 'ellipse';
195
+ /** Default true. */
196
+ oklchInterp?: boolean;
197
+ /** Default true. Append an average-color base layer to fill gaps. */
198
+ includeBase?: boolean;
199
+ }
200
+
201
+ export function formatCssMesh(mesh: MeshGradient, opts?: FormatCssMeshOptions): string;
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Color conversion
205
+ // ---------------------------------------------------------------------------
206
+
207
+ /**
208
+ * OKLCH -> linear sRGB triplet. sRGB-gamut-mapped via 10-iteration binary
209
+ * search on chroma when the requested color is out of gamut.
210
+ *
211
+ * @param out Optional 3-element `[r, g, b]` array. If supplied, written in
212
+ * place and returned (zero allocation). Otherwise a fresh array
213
+ * is allocated.
214
+ */
215
+ export function oklchToLinearSrgb(
216
+ L: number,
217
+ C: number,
218
+ H: number,
219
+ out?: number[],
220
+ ): number[];
221
+
222
+ /**
223
+ * Linear sRGB -> OKLCH.
224
+ * @param out Optional `{l, c, h}` object. If supplied, written in place and
225
+ * returned (zero allocation). The `a` field is left untouched.
226
+ */
227
+ export function linearSrgbToOklch<T extends Partial<OklchColor>>(
228
+ r: number,
229
+ g: number,
230
+ b: number,
231
+ out?: T,
232
+ ): T;
233
+
234
+ /** sRGB transfer (linear -> gamma-encoded). IEC 61966-2-1. */
235
+ export function srgbGamma(x: number): number;
236
+ /** sRGB inverse transfer (gamma-encoded -> linear). */
237
+ export function srgbInverseGamma(x: number): number;
238
+
239
+ /**
240
+ * OKLCH -> hex. Emits '#rrggbb' for opaque colors and '#rrggbbaa' otherwise.
241
+ * Output always lowercase.
242
+ */
243
+ export function toHex(color: OklchColor): string;
244
+
245
+ /**
246
+ * Hex -> OKLCH. Accepts 3, 4, 6, or 8-char hex; '#' optional; case-insensitive.
247
+ * @throws on malformed input.
248
+ */
249
+ export function fromHex(hex: string): OklchColorA;
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Bake / pixel helpers
253
+ // ---------------------------------------------------------------------------
254
+
255
+ /** Flatten `gradient.stops` into a Float32Array of [L, C, H, L, C, H, ...]. */
256
+ export function flattenStopsToBuffer(
257
+ gradient: { stops: ReadonlyArray<OklchColor> },
258
+ out?: Float32Array,
259
+ ): Float32Array;
260
+
261
+ /** Bake a Gradient into a fixed-resolution Uint32Array LUT of packed RGBA. */
262
+ export function bakeGradientToLut(
263
+ gradient: { stops: ReadonlyArray<OklchColor & { pos: number }>; sampleArray(out: Float32Array, n: number): void },
264
+ resolution?: number,
265
+ opts?: {
266
+ easeFn?: (t: number) => number;
267
+ packer?: Function;
268
+ },
269
+ ): Uint32Array;
270
+
271
+ /** Sample a single packed color from a baked LUT at parametric t in [0, 1]. */
272
+ export function sampleLut(lut: Uint32Array, t: number): number;
273
+
274
+ /** Pack a single OKLCH color directly to a 32-bit RGBA value. */
275
+ export function packOklchSingle(l: number, c: number, h: number, alpha?: number): number;
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // Palette extraction
279
+ // ---------------------------------------------------------------------------
280
+
281
+ /**
282
+ * Extract a designer-friendly palette from raw RGBA pixels. Chroma-weighted
283
+ * hue-bucketing with >= 50 deg hue separation between picks. Returns up to
284
+ * `count` colors sorted by L ascending; may return fewer on low-diversity
285
+ * images (e.g. monochrome).
286
+ */
287
+ export function extractPalette(
288
+ pixels: Uint8ClampedArray,
289
+ count?: number,
290
+ ): OklchColor[];
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // CSS parser
294
+ // ---------------------------------------------------------------------------
295
+
296
+ export interface ParsedLinearGradient {
297
+ mode: 'linear';
298
+ angle: number;
299
+ stops: Array<OklchColorA & { stop: number }>;
300
+ }
301
+
302
+ export interface ParsedRadialGradient {
303
+ mode: 'radial';
304
+ radShape: 'circle' | 'ellipse';
305
+ radCx: number;
306
+ radCy: number;
307
+ stops: Array<OklchColorA & { stop: number }>;
308
+ }
309
+
310
+ export interface ParsedConicGradient {
311
+ mode: 'conic';
312
+ conFrom: number;
313
+ conCx: number;
314
+ conCy: number;
315
+ stops: Array<OklchColorA & { stop: number }>;
316
+ }
317
+
318
+ export type ParsedGradient =
319
+ | ParsedLinearGradient
320
+ | ParsedRadialGradient
321
+ | ParsedConicGradient;
322
+
323
+ /** Parse a CSS `linear-gradient`/`radial-gradient`/`conic-gradient` string. */
324
+ export function parseGradientCss(input: string): ParsedGradient;
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // Multi-format exporters
328
+ // ---------------------------------------------------------------------------
329
+
330
+ export type Export1dFormat = 'css' | 'css-var' | 'scss' | 'tailwind' | 'json' | 'svg';
331
+ export type ExportMeshFormat = 'css' | 'css-var' | 'json';
332
+
333
+ export const EXPORT_FORMATS_1D: ReadonlyArray<Export1dFormat>;
334
+ export const EXPORT_FORMATS_MESH: ReadonlyArray<ExportMeshFormat>;
335
+
336
+ export interface FormatMetaEntry {
337
+ label: string;
338
+ hint: string;
339
+ }
340
+
341
+ export const FORMAT_META: Readonly<Record<Export1dFormat, FormatMetaEntry>>;
342
+
343
+ /** 1D state shape consumed by the exporters (matches the editor's serialize/load). */
344
+ export interface State1d {
345
+ mode: 'linear' | 'radial' | 'conic';
346
+ stops: Array<{
347
+ l: number;
348
+ c: number;
349
+ h: number;
350
+ a?: number;
351
+ stop: number;
352
+ }>;
353
+ angle?: number; // linear
354
+ radShape?: 'circle' | 'ellipse';
355
+ radCx?: number; radCy?: number;
356
+ radPos?: string; // legacy keyword form
357
+ conFrom?: number;
358
+ conCx?: number; conCy?: number;
359
+ conPos?: string; // legacy keyword form
360
+ }
361
+
362
+ export interface ExportOptions {
363
+ name?: string;
364
+ width?: number; // svg only
365
+ height?: number; // svg only
366
+ }
367
+
368
+ export function toTokens1d(state: State1d, format: Export1dFormat, opts?: ExportOptions): string;
369
+ export function toTokensMesh(mesh: MeshGradient, format: ExportMeshFormat, opts?: ExportOptions): string;
370
+
371
+ export function toCss1d(state: State1d, opts?: ExportOptions): string;
372
+ export function toCssVar1d(state: State1d, opts?: ExportOptions): string;
373
+ export function toScss1d(state: State1d, opts?: ExportOptions): string;
374
+ export function toTailwind1d(state: State1d, opts?: ExportOptions): string;
375
+ export function toJson1d(state: State1d, opts?: ExportOptions): string;
376
+ export function toSvg1d(state: State1d, opts?: ExportOptions): string;
377
+
378
+ export function toCssMesh(mesh: MeshGradient, opts?: ExportOptions): string;
379
+ export function toCssVarMesh(mesh: MeshGradient, opts?: ExportOptions): string;
380
+ export function toJsonMesh(mesh: MeshGradient, opts?: ExportOptions): string;
package/src/index.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @zakkster/lite-gradient-studio
3
+ *
4
+ * Authoring layer on top of @zakkster/lite-gradient:
5
+ * - High-fidelity CSS emitters (preserve authored stop positions, optional `in oklch` hint)
6
+ * - Conic CSS emitter (missing from lite-gradient)
7
+ * - Mesh gradient kernel — bilinear OKLCH over deformable control grid
8
+ * - Multi-radial mesh → CSS approximation
9
+ * - Multi-format exporters (CSS, SCSS, Tailwind, JSON, SVG)
10
+ * - OKLCH ↔ hex conversion with sRGB-gamut mapping
11
+ * - Pixel-buffer rasterizer for previews + PNG export
12
+ */
13
+
14
+ import { Gradient } from '@zakkster/lite-gradient';
15
+
16
+ export { Gradient };
17
+ export * from '@zakkster/lite-gradient';
18
+ export {
19
+ bakeGradientToLut,
20
+ flattenStopsToBuffer,
21
+ sampleLut,
22
+ packOklchSingle,
23
+ } from './bake.js';
24
+ export { MeshGradient, defaultMeshColor } from './mesh.js';
25
+ export { formatCssLinear, formatCssRadial, formatCssConic } from './css-emitters.js';
26
+ export { formatCssMesh } from './mesh-css.js';
27
+ export {
28
+ toHex, fromHex, oklchToLinearSrgb, srgbGamma, srgbInverseGamma,
29
+ linearSrgbToOklch,
30
+ } from './color-convert.js';
31
+ export { extractPalette } from './palette-extract.js';
32
+ export { parseGradientCss } from './gradient-parse.js';
33
+ export {
34
+ EXPORT_FORMATS_1D, EXPORT_FORMATS_MESH, FORMAT_META,
35
+ toTokens1d, toTokensMesh,
36
+ toCss1d, toCssVar1d, toScss1d, toTailwind1d, toJson1d, toSvg1d,
37
+ toCssMesh, toCssVarMesh, toJsonMesh,
38
+ } from './exporters.js';
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Mesh → CSS approximation.
3
+ *
4
+ * Real mesh gradients don't have a CSS primitive — `mesh-gradient()`
5
+ * isn't standardized. The conventional workaround (Mesher.app, Stripe-
6
+ * style aurora backgrounds, etc.) is to stack one `radial-gradient()`
7
+ * per control point, each fading from the point's color at center to
8
+ * transparent at an outer radius. With sensible overlap the alpha
9
+ * composites approximate the true bilinear field.
10
+ *
11
+ * This is genuinely an approximation, not a faithful reproduction —
12
+ * adjacent control points blend through the alpha layer, not through
13
+ * OKLCH interpolation. The visible result is close enough that most
14
+ * users prefer it to a giant PNG. For pixel-perfect output, use the
15
+ * PNG export path which renders our deformable bilinear kernel direct
16
+ * to a buffer.
17
+ *
18
+ * Output uses the `in oklch` interpolation hint on each radial so the
19
+ * color → transparent fade respects perceptual uniformity in browsers
20
+ * that support it (Chrome 111+, Firefox 113+, Safari 16.4+).
21
+ */
22
+
23
+ import { toCssOklch } from '@zakkster/lite-color';
24
+
25
+ /**
26
+ * @param {object} mesh A MeshGradient instance (uses .stops, .cols, .rows)
27
+ * @param {object} [opts]
28
+ * @param {number} [opts.radiusPct] Layer radius as % of farthest-corner.
29
+ * Defaults to a formula scaled by mesh size.
30
+ * @param {string} [opts.shape='circle'] 'circle' or 'ellipse'
31
+ * @param {boolean} [opts.oklchInterp=true] Emit `in oklch` per layer.
32
+ * @param {boolean} [opts.includeBase=true] Append a base color layer at end so
33
+ * gaps between radials are filled.
34
+ * @returns {string} A CSS `background:` value (no `background:` keyword, no
35
+ * trailing semicolon — caller decides how to wrap it).
36
+ */
37
+ export function formatCssMesh(mesh, opts = {}) {
38
+ const {
39
+ radiusPct = autoRadius(mesh),
40
+ shape = 'circle',
41
+ oklchInterp = true,
42
+ includeBase = true,
43
+ } = opts;
44
+
45
+ const scratch = { l: 0, c: 0, h: 0, a: 1 };
46
+ const layers = [];
47
+ const interp = oklchInterp ? ' in oklch' : '';
48
+
49
+ for (const s of mesh.stops) {
50
+ scratch.l = s.l; scratch.c = s.c; scratch.h = s.h;
51
+ scratch.a = s.a === undefined ? 1 : s.a;
52
+ const color = toCssOklch(scratch);
53
+ const x = (s.x * 100).toFixed(1);
54
+ const y = (s.y * 100).toFixed(1);
55
+ // `transparent` resolves to oklch(0 0 0 / 0) — same hue interpolation.
56
+ layers.push(
57
+ `radial-gradient(${shape} at ${x}% ${y}%${interp}, ` +
58
+ `${color}, transparent ${radiusPct.toFixed(0)}%)`
59
+ );
60
+ }
61
+
62
+ if (includeBase) {
63
+ // Base layer = average of all stops. Picks a sensible "background"
64
+ // tone so gaps between radials don't show as transparent.
65
+ const avg = averageColor(mesh.stops);
66
+ scratch.l = avg.l; scratch.c = avg.c; scratch.h = avg.h;
67
+ scratch.a = avg.a;
68
+ layers.push(toCssOklch(scratch));
69
+ }
70
+
71
+ return layers.join(',\n ');
72
+ }
73
+
74
+ /**
75
+ * Heuristic radius. Larger meshes → smaller per-layer radii (each
76
+ * layer covers less canvas, so seams between adjacent points stay
77
+ * tight; otherwise the alpha stack reduces to a muddy average).
78
+ *
79
+ * Tuned against 2×2 → 5×5: the multiplier (1.2×) gives ~20% overlap
80
+ * between neighbouring layers — enough that they blend, not enough
81
+ * that the field collapses toward the average. The base layer beneath
82
+ * fills any remaining gaps.
83
+ */
84
+ function autoRadius(mesh) {
85
+ const n = Math.max(mesh.cols, mesh.rows);
86
+ if (n <= 2) return 70;
87
+ return Math.round(100 / (n - 1) * 1.2);
88
+ }
89
+
90
+ /**
91
+ * Average L/C and circular-mean H of the stops. Used as the base layer
92
+ * so the visible field has a sensible color where layers don't reach.
93
+ *
94
+ * Hue is averaged via vector mean (sum unit vectors at each hue, take
95
+ * angle of the resultant) so that a 350°/10° pair averages to ~0°/360°,
96
+ * not 180°. Standard circular-statistics trick.
97
+ */
98
+ function averageColor(stops) {
99
+ let lSum = 0, cSum = 0, aSum = 0;
100
+ let hxSum = 0, hySum = 0;
101
+ const n = stops.length;
102
+ for (const s of stops) {
103
+ lSum += s.l;
104
+ cSum += s.c;
105
+ aSum += s.a === undefined ? 1 : s.a;
106
+ const rad = s.h * Math.PI / 180;
107
+ hxSum += Math.cos(rad);
108
+ hySum += Math.sin(rad);
109
+ }
110
+ let hAvg = Math.atan2(hySum / n, hxSum / n) * 180 / Math.PI;
111
+ if (hAvg < 0) hAvg += 360;
112
+ return { l: lSum / n, c: cSum / n, h: hAvg, a: aSum / n };
113
+ }