@zakkster/lite-gradient-studio 1.0.1 → 1.1.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 +103 -0
- package/README.md +47 -0
- package/llms.txt +41 -0
- package/package.json +21 -9
- package/src/color-convert.js +18 -3
- package/src/index.d.ts +39 -0
- package/src/index.js +1 -1
- package/src/mesh.js +133 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,109 @@ All notable changes to `@zakkster/lite-gradient-studio` are documented here.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
This library follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0] — 2026-07-03
|
|
9
|
+
|
|
10
|
+
### Added — Monochrome mesh
|
|
11
|
+
|
|
12
|
+
Mesh-level analogue of the `monochromeGradient` factory that shipped in
|
|
13
|
+
`@zakkster/lite-gradient` v1.1.0. Both together form a coordinated
|
|
14
|
+
monochrome capability across the ecosystem — 1D continuous gradients in
|
|
15
|
+
lite-gradient, 2D deformable meshes in lite-gradient-studio.
|
|
16
|
+
|
|
17
|
+
- **`monochromeMesh(base, cols, rows, opts?)`** — factory returning a
|
|
18
|
+
`MeshGradient` where every control point shares `base.c` and `base.h`
|
|
19
|
+
(or `c=0` if `mode: 'grayscale'`); only L varies according to
|
|
20
|
+
`direction`. Post-construction, the returned mesh behaves like any
|
|
21
|
+
other `MeshGradient` — `setPointPosition(...)` to warp the L
|
|
22
|
+
distribution off-grid, `rasterizeTo(...)` / `rasterizeDeformedTo(...)`
|
|
23
|
+
to render.
|
|
24
|
+
|
|
25
|
+
- **Options:**
|
|
26
|
+
- `mode`: `'tinted'` (default) | `'grayscale'`
|
|
27
|
+
- `range`: `[lo, hi]` with `0 <= lo < hi <= 1`, default `[0, 1]`
|
|
28
|
+
- `direction`: `'horizontal'` | `'vertical'` | `'diagonal'` (default) | `'radial'`
|
|
29
|
+
|
|
30
|
+
- **Directions:**
|
|
31
|
+
- `'horizontal'` — L varies left-to-right, uniform per row (equivalent
|
|
32
|
+
to a linear gradient, but with mesh deformability post-hoc).
|
|
33
|
+
- `'vertical'` — L varies top-to-bottom, uniform per column.
|
|
34
|
+
- `'diagonal'` (default) — top-left corner (lo) to bottom-right
|
|
35
|
+
corner (hi). Uses both axes meaningfully — the most versatile default.
|
|
36
|
+
- `'radial'` — center (lo) outward to corners (hi). Atmospheric,
|
|
37
|
+
"premium background" feel.
|
|
38
|
+
|
|
39
|
+
- **Type declarations:** `MonoMode`, `MonoMeshDirection`,
|
|
40
|
+
`MonochromeMeshOptions`, plus the factory signature in
|
|
41
|
+
`src/index.d.ts`.
|
|
42
|
+
|
|
43
|
+
- **Re-exports flow through:** `monochromeGradient`, `gradientMonoWarm`,
|
|
44
|
+
`gradientMonoCool` from `@zakkster/lite-gradient` v1.1.0 are already
|
|
45
|
+
visible via the existing `export * from '@zakkster/lite-gradient'`
|
|
46
|
+
in `src/index.js`. No new studio-level 1D monochrome factory needed;
|
|
47
|
+
users import from either package interchangeably.
|
|
48
|
+
|
|
49
|
+
### Why this matters
|
|
50
|
+
|
|
51
|
+
Designer feedback on the mesh-gradient positioning (the original
|
|
52
|
+
mesh-gradient authoring focus) surfaced two problems: (1) AI image
|
|
53
|
+
generators can produce arbitrary decorative gradients on demand, eating
|
|
54
|
+
the "generate a wild gradient" use case at the low end; (2) designers
|
|
55
|
+
doing client work don't have creative freedom to ship wild mesh
|
|
56
|
+
gradients — they're constrained to brand palettes.
|
|
57
|
+
|
|
58
|
+
Monochrome mesh dodges both problems. The palette is fixed to a single
|
|
59
|
+
brand tone (nothing "wild" or "random-looking"); the mesh capability
|
|
60
|
+
still gives designers organic-feeling backgrounds that flat 1D
|
|
61
|
+
gradients can't — subtle asymmetry via `setPointPosition`, off-center
|
|
62
|
+
radial gradients, non-linear L distributions. Same authoring surface,
|
|
63
|
+
narrower creative space that lands in the client-work zone.
|
|
64
|
+
|
|
65
|
+
### Peer dependency bump
|
|
66
|
+
|
|
67
|
+
- `@zakkster/lite-gradient`: `^1.0.4` → `^1.1.0`
|
|
68
|
+
|
|
69
|
+
Existing consumers using only pre-1.1.0 exports continue to work
|
|
70
|
+
identically. The bump ensures `monochromeGradient` and the two Mono
|
|
71
|
+
presets are guaranteed available via the re-export.
|
|
72
|
+
|
|
73
|
+
### Tests
|
|
74
|
+
|
|
75
|
+
18 new tests across `test/mesh-monochrome.test.js`. Coverage: the four
|
|
76
|
+
directions (verified by inspecting the initialized `stops` array — e.g.
|
|
77
|
+
horizontal makes row 0 == row 1 == row 2; radial makes center = lo and
|
|
78
|
+
all four corners = hi), both modes, custom range, sampling correctness,
|
|
79
|
+
every validation throw path, and base non-mutation.
|
|
80
|
+
|
|
81
|
+
The 4 previously-failing `color-convert.test.js` tests (documented in
|
|
82
|
+
1.0.1 as a docs-vs-implementation mismatch) now pass — see "Fixed"
|
|
83
|
+
below. Total: 218 tests, **203 pass**, 0 fail, 15 skipped.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- **`oklchToLinearSrgb(L, C, H, out?)` and `linearSrgbToOklch(r, g, b, out?)`
|
|
88
|
+
now honor the optional `out` parameter** documented since 1.0.0 in
|
|
89
|
+
`index.d.ts` and covered by tests that had been failing since publish.
|
|
90
|
+
Pass a caller-owned 3-element array (or `{ l, c, h }` object) as the
|
|
91
|
+
final argument; the function writes into it in place and returns the
|
|
92
|
+
same reference — zero allocation on the hot path. Omitting `out` (or
|
|
93
|
+
passing `null` / `undefined`) preserves the existing 3-arg call shape
|
|
94
|
+
and returns a freshly-allocated result.
|
|
95
|
+
|
|
96
|
+
Real impact: `oklchToLinearSrgb` is called per-pixel in `rasterizeTo` /
|
|
97
|
+
`rasterizeDeformedTo` and per-color in every exporter. Callers threading
|
|
98
|
+
a scratch array through that hot loop can now avoid allocating one
|
|
99
|
+
triplet per pixel. Existing 3-arg calls are unchanged.
|
|
100
|
+
|
|
101
|
+
The 1.0.1 CHANGELOG framed this as "docs drifted from implementation"
|
|
102
|
+
and rolled back the docs. That was a stopgap. The correct resolution
|
|
103
|
+
was implementing the feature the docs, types, and tests all documented;
|
|
104
|
+
this ship does that. Non-breaking either way (the 3-arg signature is
|
|
105
|
+
a strict subset of the new 4-arg signature).
|
|
106
|
+
|
|
107
|
+
### Non-breaking
|
|
108
|
+
|
|
109
|
+
Additive only. No existing API surface changed.
|
|
110
|
+
|
|
8
111
|
## [1.0.1] — docs accuracy patch
|
|
9
112
|
|
|
10
113
|
Patch release. No code changes — every export, every behavior is
|
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# @zakkster/lite-gradient-studio
|
|
2
2
|
|
|
3
|
+
> Authoring engine for OKLCH gradients -- linear, radial, conic, and N×M mesh -- with zero-GC rasterization, multi-format export, and chroma-weighted palette extraction.
|
|
4
|
+
|
|
3
5
|
[](https://www.npmjs.com/package/@zakkster/lite-gradient-studio)
|
|
6
|
+
[](https://github.com/sponsors/PeshoVurtoleta)
|
|
7
|
+

|
|
4
8
|
[](https://bundlephobia.com/result?p=@zakkster/lite-gradient-studio)
|
|
5
9
|
[](https://www.npmjs.com/package/@zakkster/lite-gradient-studio)
|
|
10
|
+
[](https://www.npmjs.com/package/@zakkster/lite-gradient-studio)
|
|
11
|
+

|
|
6
12
|
[](https://opensource.org/licenses/MIT)
|
|
7
13
|
|
|
8
14
|
Authoring engine for OKLCH gradients — linear, radial, conic, and N×M mesh — with zero-GC rasterization, multi-format export, and chroma-weighted palette extraction.
|
|
@@ -183,6 +189,47 @@ Each box is one module under `src/` with one test file under `test/`. Modules co
|
|
|
183
189
|
- `'smooth'` — bilinear with smoothstep-eased u/v. Softens cell-border artifacts.
|
|
184
190
|
- `'cubic'` — Catmull-Rom across cell boundaries. Smoothest; ~2.5× the cost of `'bilinear'`.
|
|
185
191
|
|
|
192
|
+
### Monochrome mesh (v1.1.0)
|
|
193
|
+
|
|
194
|
+
Mesh-level analogue of `lite-gradient`'s `monochromeGradient` — chroma and hue held
|
|
195
|
+
constant across every control point; only lightness varies. Client-work-friendly:
|
|
196
|
+
subtle premium backgrounds without the "AI-generated random gradient" look.
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
import { monochromeMesh } from '@zakkster/lite-gradient-studio';
|
|
200
|
+
|
|
201
|
+
// 3×3 mesh, all points share the brand hue, L varies diagonally
|
|
202
|
+
const mesh = monochromeMesh({ l: 0.5, c: 0.06, h: 245 }, 3, 3);
|
|
203
|
+
|
|
204
|
+
// Rasterize to a canvas
|
|
205
|
+
const buf = new Uint32Array(800 * 600);
|
|
206
|
+
mesh.rasterizeTo(buf, 800, 600);
|
|
207
|
+
const img = new ImageData(new Uint8ClampedArray(buf.buffer), 800, 600);
|
|
208
|
+
ctx.putImageData(img, 0, 0);
|
|
209
|
+
|
|
210
|
+
// Or emit CSS (canvas-free rendering)
|
|
211
|
+
const css = formatCssMesh(mesh);
|
|
212
|
+
element.style.background = css;
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**`monochromeMesh(base, cols, rows, opts?)`** → `MeshGradient`
|
|
216
|
+
|
|
217
|
+
| Option | Type | Default | Notes |
|
|
218
|
+
|-------------|---------------------------------------------------------------|----------------|-----------------------------------------------------------------------------|
|
|
219
|
+
| `mode` | `'tinted' \| 'grayscale'` | `'tinted'` | `'tinted'` retains base c/h; `'grayscale'` forces c=0. |
|
|
220
|
+
| `range` | `[number, number]` | `[0, 1]` | L-axis endpoints. Must satisfy `0 ≤ lo < hi ≤ 1`. |
|
|
221
|
+
| `direction` | `'horizontal' \| 'vertical' \| 'diagonal' \| 'radial'` | `'diagonal'` | How L varies across the mesh. See below. |
|
|
222
|
+
|
|
223
|
+
**Directions:**
|
|
224
|
+
- **`'horizontal'`** — L varies left-to-right, uniform across each row. Like a linear gradient, but you can deform post-hoc.
|
|
225
|
+
- **`'vertical'`** — L varies top-to-bottom, uniform across each column.
|
|
226
|
+
- **`'diagonal'`** (default) — top-left corner (lo) to bottom-right corner (hi). Most versatile; uses both axes meaningfully.
|
|
227
|
+
- **`'radial'`** — center (lo) outward to corners (hi). Atmospheric "premium" feel.
|
|
228
|
+
|
|
229
|
+
Post-construction, you can `setPointPosition(...)` to warp the L distribution off-grid
|
|
230
|
+
— something you can't do with a flat 1D gradient. This is what mesh capability buys you
|
|
231
|
+
in the client-work scenario: brand-safe base + designer-controlled organic character.
|
|
232
|
+
|
|
186
233
|
### CSS emitters
|
|
187
234
|
|
|
188
235
|
| Function | Description |
|
package/llms.txt
CHANGED
|
@@ -72,6 +72,47 @@ Mesh interpolation modes (sampleAt's `mode` arg / rasterize's
|
|
|
72
72
|
Legacy boolean accepted for sampleAt: `false` → bilinear, `true` → smooth.
|
|
73
73
|
Rasterize also accepts `opts.smooth: true` as a legacy alias.
|
|
74
74
|
|
|
75
|
+
## Monochrome mesh (v1.1.0)
|
|
76
|
+
|
|
77
|
+
Factory for meshes where every control point shares base c/h (or c=0 for
|
|
78
|
+
grayscale) and only L varies across the grid. Client-work-friendly:
|
|
79
|
+
never looks like an "AI-generated random gradient" because the palette
|
|
80
|
+
is fixed to one brand tone.
|
|
81
|
+
|
|
82
|
+
import { monochromeMesh, formatCssMesh } from '@zakkster/lite-gradient-studio';
|
|
83
|
+
|
|
84
|
+
// Brand-tinted mesh, L varies diagonally
|
|
85
|
+
const m = monochromeMesh({ l: 0.5, c: 0.06, h: 245 }, 3, 3);
|
|
86
|
+
|
|
87
|
+
// Or explicit direction + grayscale + printable-safe range
|
|
88
|
+
const gm = monochromeMesh(
|
|
89
|
+
{ l: 0.5, c: 0, h: 0 },
|
|
90
|
+
4, 4,
|
|
91
|
+
{ mode: 'grayscale', range: [0.05, 0.95], direction: 'radial' }
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Emit CSS without canvas
|
|
95
|
+
element.style.background = formatCssMesh(m);
|
|
96
|
+
|
|
97
|
+
Options:
|
|
98
|
+
- mode: 'tinted' (default) | 'grayscale'
|
|
99
|
+
- range: [lo, hi] with 0 <= lo < hi <= 1 (default [0, 1])
|
|
100
|
+
- direction: 'horizontal' | 'vertical' | 'diagonal' (default) | 'radial'
|
|
101
|
+
|
|
102
|
+
Directions:
|
|
103
|
+
- horizontal: L varies left-to-right, uniform per row
|
|
104
|
+
- vertical: L varies top-to-bottom, uniform per column
|
|
105
|
+
- diagonal: top-left corner (lo) to bottom-right corner (hi)
|
|
106
|
+
- radial: center (lo) outward to corners (hi)
|
|
107
|
+
|
|
108
|
+
Returns a MeshGradient. All post-construction methods (setPointPosition,
|
|
109
|
+
setPoint, sampleAt, rasterizeTo, etc.) work as normal — the factory just
|
|
110
|
+
initializes the color basis. Deform handles off-grid to warp L across
|
|
111
|
+
the surface without disturbing the brand palette.
|
|
112
|
+
|
|
113
|
+
Pairs with @zakkster/lite-gradient's monochromeGradient(base, opts) which
|
|
114
|
+
returns a continuous 1D Gradient; monochromeMesh returns a 2D MeshGradient.
|
|
115
|
+
|
|
75
116
|
## Multi-format export
|
|
76
117
|
|
|
77
118
|
const state = { mode: 'linear', angle: 135, stops };
|
package/package.json
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-gradient-studio",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Authoring engine for OKLCH gradients — linear, radial, conic, and N×M mesh — with zero-GC rasterization, multi-format export (CSS/SCSS/Tailwind/JSON/SVG), and chroma-weighted palette extraction.",
|
|
5
5
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"main": "src/index.js",
|
|
8
|
+
"main": "./src/index.js",
|
|
9
|
+
"module": "./src/index.js",
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
9
11
|
"exports": {
|
|
10
|
-
".":
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./src/index.d.ts",
|
|
14
|
+
"node": "./src/index.js",
|
|
15
|
+
"import": "./src/index.js",
|
|
16
|
+
"default": "./src/index.js"
|
|
17
|
+
}
|
|
11
18
|
},
|
|
12
19
|
"files": [
|
|
13
20
|
"src",
|
|
@@ -17,13 +24,13 @@
|
|
|
17
24
|
"README.md"
|
|
18
25
|
],
|
|
19
26
|
"scripts": {
|
|
20
|
-
"test":
|
|
27
|
+
"test": "node --test --expose-gc test/*.test.js",
|
|
21
28
|
"bench": "node --expose-gc bench/run.mjs"
|
|
22
29
|
},
|
|
23
30
|
"dependencies": {
|
|
24
|
-
"@zakkster/lite-color":
|
|
31
|
+
"@zakkster/lite-color": "^1.0.6",
|
|
25
32
|
"@zakkster/lite-color-engine": "^1.0.0",
|
|
26
|
-
"@zakkster/lite-gradient":
|
|
33
|
+
"@zakkster/lite-gradient": "^1.1.0"
|
|
27
34
|
},
|
|
28
35
|
"keywords": [
|
|
29
36
|
"gradient",
|
|
@@ -42,12 +49,17 @@
|
|
|
42
49
|
"no-build",
|
|
43
50
|
"esm"
|
|
44
51
|
],
|
|
45
|
-
"homepage":
|
|
52
|
+
"homepage": "https://github.com/PeshoVurtoleta/lite-gradient-studio#readme",
|
|
46
53
|
"repository": {
|
|
47
54
|
"type": "git",
|
|
48
|
-
"url":
|
|
55
|
+
"url": "git+https://github.com/PeshoVurtoleta/lite-gradient-studio.git"
|
|
49
56
|
},
|
|
50
57
|
"bugs": {
|
|
51
58
|
"url": "https://github.com/PeshoVurtoleta/lite-gradient-studio/issues"
|
|
52
|
-
}
|
|
59
|
+
},
|
|
60
|
+
"funding": {
|
|
61
|
+
"type": "github",
|
|
62
|
+
"url": "https://github.com/sponsors/PeshoVurtoleta"
|
|
63
|
+
},
|
|
64
|
+
"sideEffects": false
|
|
53
65
|
}
|
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
|
@@ -143,6 +143,45 @@ export class MeshGradient {
|
|
|
143
143
|
destroy(): void;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Monochrome mesh (v1.1.0)
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
export type MonoMode = 'tinted' | 'grayscale';
|
|
151
|
+
export type MonoMeshDirection = 'horizontal' | 'vertical' | 'diagonal' | 'radial';
|
|
152
|
+
|
|
153
|
+
export interface MonochromeMeshOptions {
|
|
154
|
+
/**
|
|
155
|
+
* Chroma handling.
|
|
156
|
+
* - `'tinted'` (default): retain base color's chroma/hue at every point.
|
|
157
|
+
* - `'grayscale'`: force chroma to 0 for pure achromatic tones.
|
|
158
|
+
*/
|
|
159
|
+
mode?: MonoMode;
|
|
160
|
+
/** L-axis endpoints [lo, hi], must satisfy `0 <= lo < hi <= 1`. Default `[0, 1]`. */
|
|
161
|
+
range?: readonly [number, number];
|
|
162
|
+
/**
|
|
163
|
+
* How L varies across the mesh:
|
|
164
|
+
* - `'horizontal'`: left-to-right, uniform per row.
|
|
165
|
+
* - `'vertical'`: top-to-bottom, uniform per column.
|
|
166
|
+
* - `'diagonal'` (default): top-left corner (lo) to bottom-right corner (hi).
|
|
167
|
+
* - `'radial'`: center (lo) outward to corners (hi).
|
|
168
|
+
*/
|
|
169
|
+
direction?: MonoMeshDirection;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build a monochromatic MeshGradient from a single base OKLCH color.
|
|
174
|
+
* Chroma and hue are held constant across every control point; only
|
|
175
|
+
* lightness varies according to `direction`. Client-work-friendly:
|
|
176
|
+
* never looks "AI-generated" because the palette is fixed to one brand tone.
|
|
177
|
+
*/
|
|
178
|
+
export function monochromeMesh(
|
|
179
|
+
base: OklchColor,
|
|
180
|
+
cols: number,
|
|
181
|
+
rows: number,
|
|
182
|
+
opts?: MonochromeMeshOptions
|
|
183
|
+
): MeshGradient;
|
|
184
|
+
|
|
146
185
|
// ---------------------------------------------------------------------------
|
|
147
186
|
// CSS emitters
|
|
148
187
|
// ---------------------------------------------------------------------------
|
package/src/index.js
CHANGED
|
@@ -21,7 +21,7 @@ export {
|
|
|
21
21
|
sampleLut,
|
|
22
22
|
packOklchSingle,
|
|
23
23
|
} from './bake.js';
|
|
24
|
-
export { MeshGradient, defaultMeshColor } from './mesh.js';
|
|
24
|
+
export { MeshGradient, defaultMeshColor, monochromeMesh } from './mesh.js';
|
|
25
25
|
export { formatCssLinear, formatCssRadial, formatCssConic } from './css-emitters.js';
|
|
26
26
|
export { formatCssMesh } from './mesh-css.js';
|
|
27
27
|
export {
|
package/src/mesh.js
CHANGED
|
@@ -678,3 +678,136 @@ export class MeshGradient {
|
|
|
678
678
|
this._scratchBot = null;
|
|
679
679
|
}
|
|
680
680
|
}
|
|
681
|
+
|
|
682
|
+
// ---- Monochrome mesh (v1.1.0) --------------------------------------------
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Build a monochromatic MeshGradient from a single base OKLCH color.
|
|
686
|
+
*
|
|
687
|
+
* Chroma and hue are held constant across every control point in the mesh;
|
|
688
|
+
* only lightness varies according to `direction`. This is the mesh-level
|
|
689
|
+
* analogue to lite-gradient's `monochromeGradient(base, opts)` — a
|
|
690
|
+
* client-work-friendly mesh that never looks like an "AI-generated random
|
|
691
|
+
* gradient" because the palette is fixed to a single brand tone.
|
|
692
|
+
*
|
|
693
|
+
* Typical use: subtle premium backgrounds for cards, hero sections,
|
|
694
|
+
* editorial layouts. The mesh structure means users can post-hoc
|
|
695
|
+
* `setPointPosition(...)` control points off-grid to warp the L
|
|
696
|
+
* distribution — impossible with a flat 1D gradient.
|
|
697
|
+
*
|
|
698
|
+
* @param {{ l: number, c: number, h: number }} base
|
|
699
|
+
* OKLCH base color. `c` and `h` are held constant across the mesh
|
|
700
|
+
* (or c=0 if `mode: 'grayscale'`).
|
|
701
|
+
* @param {number} cols Number of columns (integer >= 2).
|
|
702
|
+
* @param {number} rows Number of rows (integer >= 2).
|
|
703
|
+
* @param {Object} [opts]
|
|
704
|
+
* @param {'tinted' | 'grayscale'} [opts.mode='tinted']
|
|
705
|
+
* @param {[number, number]} [opts.range=[0, 1]]
|
|
706
|
+
* L-axis endpoints. Must satisfy `0 <= lo < hi <= 1`.
|
|
707
|
+
* @param {'horizontal' | 'vertical' | 'diagonal' | 'radial'} [opts.direction='diagonal']
|
|
708
|
+
* How L varies across the mesh:
|
|
709
|
+
* - `'horizontal'`: L varies left-to-right, uniform across each row.
|
|
710
|
+
* - `'vertical'`: L varies top-to-bottom, uniform across each column.
|
|
711
|
+
* - `'diagonal'`: L varies from top-left (lo) to bottom-right (hi).
|
|
712
|
+
* - `'radial'`: L is `lo` at center, `hi` at corners.
|
|
713
|
+
* @returns {MeshGradient}
|
|
714
|
+
*
|
|
715
|
+
* @throws {TypeError} On invalid `base`, `mode`, `direction`, or `range` shape.
|
|
716
|
+
* @throws {RangeError} On invalid `range` values or `cols`/`rows` < 2.
|
|
717
|
+
*
|
|
718
|
+
* @example
|
|
719
|
+
* // Subtle premium background for a brand card
|
|
720
|
+
* const mesh = monochromeMesh({ l: 0.5, c: 0.06, h: 245 }, 3, 3);
|
|
721
|
+
* const buf = new Uint32Array(800 * 600);
|
|
722
|
+
* mesh.rasterizeTo(buf, 800, 600);
|
|
723
|
+
* const img = new ImageData(new Uint8ClampedArray(buf.buffer), 800, 600);
|
|
724
|
+
* ctx.putImageData(img, 0, 0);
|
|
725
|
+
*/
|
|
726
|
+
export function monochromeMesh(base, cols, rows, opts) {
|
|
727
|
+
if (base == null || typeof base !== 'object' ||
|
|
728
|
+
typeof base.l !== 'number' || typeof base.c !== 'number' ||
|
|
729
|
+
typeof base.h !== 'number') {
|
|
730
|
+
throw new TypeError(
|
|
731
|
+
'monochromeMesh: base must be { l, c, h } with numeric fields'
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
if (!Number.isInteger(cols) || cols < 2) {
|
|
735
|
+
throw new RangeError(
|
|
736
|
+
'monochromeMesh: cols must be an integer >= 2, got ' + cols
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
if (!Number.isInteger(rows) || rows < 2) {
|
|
740
|
+
throw new RangeError(
|
|
741
|
+
'monochromeMesh: rows must be an integer >= 2, got ' + rows
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const o = opts || {};
|
|
746
|
+
const mode = o.mode == null ? 'tinted' : o.mode;
|
|
747
|
+
const range = o.range == null ? [0, 1] : o.range;
|
|
748
|
+
const direction = o.direction == null ? 'diagonal' : o.direction;
|
|
749
|
+
|
|
750
|
+
if (mode !== 'tinted' && mode !== 'grayscale') {
|
|
751
|
+
throw new TypeError(
|
|
752
|
+
'monochromeMesh: mode must be "tinted" or "grayscale", got ' + mode
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
if (!Array.isArray(range) || range.length !== 2) {
|
|
756
|
+
throw new TypeError(
|
|
757
|
+
'monochromeMesh: range must be a two-element [lo, hi] array'
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
const lo = range[0];
|
|
761
|
+
const hi = range[1];
|
|
762
|
+
if (typeof lo !== 'number' || typeof hi !== 'number' ||
|
|
763
|
+
!(lo >= 0 && hi <= 1 && lo < hi)) {
|
|
764
|
+
throw new RangeError(
|
|
765
|
+
'monochromeMesh: range must satisfy 0 <= lo < hi <= 1, got [' +
|
|
766
|
+
lo + ', ' + hi + ']'
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
if (direction !== 'horizontal' && direction !== 'vertical' &&
|
|
770
|
+
direction !== 'diagonal' && direction !== 'radial') {
|
|
771
|
+
throw new TypeError(
|
|
772
|
+
'monochromeMesh: direction must be "horizontal", "vertical", ' +
|
|
773
|
+
'"diagonal", or "radial", got ' + direction
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const c = mode === 'grayscale' ? 0 : base.c;
|
|
778
|
+
const h = base.h;
|
|
779
|
+
const span = hi - lo;
|
|
780
|
+
const cx = (cols - 1) / 2;
|
|
781
|
+
const cy = (rows - 1) / 2;
|
|
782
|
+
// Half-diagonal from center to a corner, used to normalize radial t.
|
|
783
|
+
const maxRadial = Math.sqrt(cx * cx + cy * cy);
|
|
784
|
+
// Diagonal length in grid units (top-left to bottom-right corner),
|
|
785
|
+
// used to normalize diagonal t.
|
|
786
|
+
const diagDenom = (cols - 1) + (rows - 1);
|
|
787
|
+
|
|
788
|
+
const total = cols * rows;
|
|
789
|
+
const stops = new Array(total);
|
|
790
|
+
|
|
791
|
+
for (let i = 0; i < total; i++) {
|
|
792
|
+
const col = i % cols;
|
|
793
|
+
const row = (i / cols) | 0;
|
|
794
|
+
|
|
795
|
+
let t;
|
|
796
|
+
if (direction === 'horizontal') {
|
|
797
|
+
t = cols === 1 ? 0 : col / (cols - 1);
|
|
798
|
+
} else if (direction === 'vertical') {
|
|
799
|
+
t = rows === 1 ? 0 : row / (rows - 1);
|
|
800
|
+
} else if (direction === 'diagonal') {
|
|
801
|
+
t = diagDenom === 0 ? 0 : (col + row) / diagDenom;
|
|
802
|
+
} else {
|
|
803
|
+
// radial: 0 at center, 1 at corners
|
|
804
|
+
const dx = col - cx;
|
|
805
|
+
const dy = row - cy;
|
|
806
|
+
t = maxRadial === 0 ? 0 : Math.sqrt(dx * dx + dy * dy) / maxRadial;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
stops[i] = { l: lo + span * t, c, h };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return new MeshGradient(cols, rows, stops);
|
|
813
|
+
}
|