shardlight 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShardLight contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,328 @@
1
+ # shardlight
2
+
3
+ [![CI](https://github.com/jukrapopk/shardlight/actions/workflows/ci.yml/badge.svg)](https://github.com/jukrapopk/shardlight/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/shardlight.svg)](https://www.npmjs.com/package/shardlight)
5
+ [![license](https://img.shields.io/npm/l/shardlight.svg)](./LICENSE)
6
+ [![demo](https://img.shields.io/badge/demo-live-ffb454)](https://jukrapopk.github.io/shardlight/)
7
+
8
+ Layered **shard** lens-flare lights that render the same in normal 2D React (DOM),
9
+ [three.js](https://threejs.org) and [React Three Fiber](https://docs.pmnd.rs/react-three-fiber).
10
+
11
+ A light is an ordered list of _shards_ (glows, rays, streak bundles, halos). Shards that
12
+ share a motion channel are baked into one image, then animated — scale, opacity and rotation —
13
+ with no re-rendering.
14
+
15
+ ![shardlight presets: star, sun, sparkle, starburst, ember](https://raw.githubusercontent.com/jukrapopk/shardlight/main/assets/demo.png)
16
+
17
+ - **One light, three targets.** The same config renders to `<img>`, to three.js planes, and
18
+ to R3F meshes, and looks identical everywhere.
19
+ - **Easy by default.** `<ShardLight preset="star" />` draws a light; swap the preset, or start
20
+ from nothing.
21
+ - **Tunable at every level.** Preset → shards → per-shard motion → channel effects → per-frame
22
+ values → CSS variables. Change one shard, or drive the whole light.
23
+ - **Declarative or imperative.** Describe lights as JSX / JSON, or drive them per frame from state,
24
+ a ref, or CSS — without re-baking.
25
+ - **Extensible without forking.** New shard kinds, effects and presets plug in through public
26
+ registries (and augmentable types). The built-ins are registered the same way.
27
+ - **Data first.** Every light is a plain, versioned JSON config; JSX compiles to it.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ npm install shardlight
33
+ ```
34
+
35
+ `react`, `react-dom`, `three` and `@react-three/fiber` are **optional peers** — install only
36
+ the ones a target needs. The DOM entry never loads three.js; the three.js entry never loads
37
+ React.
38
+
39
+ ## Usage
40
+
41
+ ### 2D React (DOM) — `shardlight/react`
42
+
43
+ ```tsx
44
+ import { ShardLight, Shard, Glow, Rays, Halo } from 'shardlight/react';
45
+
46
+ <ShardLight preset="star" size={320} />
47
+ <ShardLight preset="sun" size={320} />
48
+
49
+ <ShardLight preset="star" size={320} flicker>
50
+ <Shard id="beam" strength={0.9} size={1.1} /> {/* tweak a preset shard */}
51
+ <Shard id="ring" visible={false} /> {/* hide one */}
52
+ <Shard id="glint" kind="fan" count={5} size={0.25} channel="glint" /> {/* add one */}
53
+ </ShardLight>
54
+ ```
55
+
56
+ `flicker` and `collapse` are shorthands for those effects. Omit `preset` (or pass `preset={null}`)
57
+ to start empty and build a light purely from `<Shard>` children.
58
+
59
+ Animate from CSS variables, or imperatively through a ref — never through React props:
60
+
61
+ ```tsx
62
+ const ref = useRef<ShardLightHandle>(null);
63
+ ref.current?.set({ channels: { rays: { scale: 1.3, rotation: 20 } } });
64
+ ref.current?.setChannel('rays', { scale: 1.3 });
65
+ ```
66
+
67
+ See [Control](#control) for every level you can change.
68
+
69
+ #### Effects
70
+
71
+ Five built-in effects:
72
+
73
+ | Effect | Params (defaults) | Does |
74
+ |---|---|---|
75
+ | `pulse` | `amount` 0.1, `speed` 1, `channels` `['main']` | Scales a channel in and out |
76
+ | `flicker` | `amount` 0.15, `speed` 1, `channels` all | Wobbles a channel's brightness |
77
+ | `spin` | `speed` 0.1 turns/s, `phase` 0, `channels` `['main']` | Turns a channel |
78
+ | `hover` | `scale` 1.15, `opacity` 1, `rotate` 0, `spin` 0, `channels` `['main']` | Reacts while hovered |
79
+ | `collapse` | `scale` 0, `opacity` 1, `channels` all, `trigger` click | Implodes on click (`click` toggles, `once` stays, `none` = manual) |
80
+
81
+ `flicker` and `collapse` are also shorthand props:
82
+
83
+ ```tsx
84
+ <ShardLight preset="ember" size={320} flicker={{ amount: 0.35 }} />
85
+ <ShardLight preset="star" size={320} collapse /> {/* click toggles */}
86
+ <ShardLight preset="star" size={320} collapse={{ trigger: 'once' }} /> {/* click collapses once */}
87
+ ```
88
+
89
+ `collapse={{ trigger: 'none' }}` leaves the light's click alone so you can drive it yourself —
90
+ `ref.current?.setCollapsed(true)`, `toggleCollapsed()`, or `set({ collapse: 0..1 })` — and tie it to
91
+ your own state. `once` collapses and stays; `click` (the default) toggles.
92
+
93
+ `spin`, `hover` and `collapse` also sit directly on a shard, so a preset can carry its own defaults:
94
+
95
+ ```tsx
96
+ <ShardLight preset="star" size={320}>
97
+ <Shard id="beam" spin={0.08} /> {/* turns on its own */}
98
+ <Shard id="hotspot" hover={{ scale: 1.6, opacity: 1.4 }} /> {/* reacts to pointer-over */}
99
+ <Shard id="ring" hover={{ spin: 0.3 }} /> {/* spins up while hovered */}
100
+ <Shard id="glint" kind="fan" count={5} collapse /> {/* implodes on click */}
101
+ </ShardLight>
102
+ ```
103
+
104
+ A shard with `spin`, `hover` or `collapse` is moved to its own channel, so it never drags the
105
+ shards it was baked with. `spin` is `turns/second` (negative reverses). `hover` scales / fades /
106
+ turns the shard as the pointer sits over the light, eased in and out; add `spin` to keep turning
107
+ while hovered and hold that angle after. `collapse` scales / fades it away when the light is
108
+ clicked, eased in and out. Both are whole-light; drive them by hand with `ref.current?.setHover(…)`
109
+ and `ref.current?.setCollapsed(…)`.
110
+
111
+ Every effect is also plain config, so a preset (or you) can list them on any channel:
112
+
113
+ ```ts
114
+ effects={[
115
+ { type: 'spin', channels: ['rays'], speed: 0.05 },
116
+ { type: 'hover', channels: ['body'], scale: 1.2, spin: 0.1 },
117
+ { type: 'flicker', amount: 0.2, channels: ['*'] },
118
+ { type: 'collapse', scale: 0.2, opacity: 0, channels: ['*'] },
119
+ ]}
120
+ ```
121
+
122
+ The five built-in presets ship effect-free, so a preset stays still until you give it motion.
123
+
124
+ ### three.js — `shardlight/three`
125
+
126
+ ```ts
127
+ import { createShardLight } from 'shardlight/three';
128
+
129
+ const light = createShardLight({ preset: 'sun', size: 0.6 });
130
+ scene.add(light.object);
131
+
132
+ // per frame: no re-bake, just transforms and opacity
133
+ light.set({ channels: { rays: { scale: 1.2 } }, opacity: 0.8 });
134
+ light.setHover(true); // eases the light's `hover` shards in
135
+ light.toggleCollapsed(); // or setCollapsed(true) to implode
136
+
137
+ light.update({ shards: [{ id: 'beam', strength: 1 }] }); // re-bakes only what changed
138
+ light.dispose();
139
+ ```
140
+
141
+ ### React Three Fiber — `shardlight/r3f`
142
+
143
+ ```tsx
144
+ import { ShardLightMesh, Shard } from 'shardlight/r3f';
145
+
146
+ <ShardLightMesh ref={light} preset="sun" size={0.4} position={[0, 1, -2]} flicker>
147
+ <Shard id="ring" visible={false} />
148
+ </ShardLightMesh>;
149
+
150
+ // pointer-over eases `hover` shards in and clicks collapse — automatically
151
+ light.current?.setHover(false);
152
+ light.current?.setCollapsed(true);
153
+ ```
154
+
155
+ ### Headless core — `shardlight`
156
+
157
+ Framework-free: config, kinds, presets, effects, baking and cache. Every adapter wraps
158
+ `createLightModel()`.
159
+
160
+ ```ts
161
+ import { createLightModel, resolveConfig, migrateConfig, prewarm } from 'shardlight';
162
+
163
+ const model = createLightModel({
164
+ preset: 'star', // a name, a config, or `null` (start empty)
165
+ config, // optional partial ShardLightConfig merged over the preset
166
+ shards, // optional per-shard overrides
167
+ effects, // optional effects
168
+ resolution: 1024,
169
+ rayScale: 1, // thins every ray
170
+ accepts: ['bitmap'], // 'url' | 'bitmap' | 'canvas'
171
+ reducedMotion: false, // pauses auto effects
172
+ hoverEase: 0.25, // seconds; 0 snaps
173
+ collapseEase: 0.3,
174
+ });
175
+
176
+ model.subscribe((layers) => {
177
+ /* one image per layer: add / replace / remove host objects */
178
+ });
179
+ model.onFrame((values) => {
180
+ /* channel scale / opacity / rotation, plus the eased `hover` and `collapse` */
181
+ });
182
+
183
+ model.set({ channels: { rays: { scale: 1.2 } }, opacity: 0.8 }); // per frame
184
+ model.setChannel('rays', { rotation: 15 });
185
+ model.setHover(true);
186
+ model.setCollapsed(false);
187
+ model.toggleCollapsed();
188
+
189
+ await model.ready; // every layer baked
190
+ model.update({ preset: 'sun' }); // re-resolves, re-bakes only what changed
191
+ model.resolved; // the current ResolvedLight
192
+ model.layers; // the current host layers
193
+ model.dispose();
194
+ ```
195
+
196
+ ## Control
197
+
198
+ Everything below animates without re-baking. Only changing a shard — its kind, params or colour —
199
+ bakes again, and only the layers that changed.
200
+
201
+ **Presets.** Start from a built-in, your own config object, or nothing:
202
+
203
+ ```tsx
204
+ <ShardLight preset="sun" />
205
+ <ShardLight preset={myConfig} />
206
+ <ShardLight /> {/* empty: build it from <Shard> children */}
207
+ ```
208
+
209
+ **Shards.** Override, hide or add any shard by `id` — no deep merge:
210
+
211
+ ```tsx
212
+ <ShardLight preset="star">
213
+ <Shard id="beam" strength={0.9} size={1.1} /> {/* tweak a preset shard */}
214
+ <Shard id="ring" visible={false} /> {/* hide one */}
215
+ <Shard id="glint" kind="fan" count={5} channel="glint" /> {/* add one */}
216
+ </ShardLight>
217
+ ```
218
+
219
+ **Per-shard motion.** `spin`, `hover` and `collapse` sit on a shard (see [Effects](#effects)); each
220
+ gets its own channel so it moves independently of what it was baked with.
221
+
222
+ **Channel values.** Scale / opacity / rotation per channel, statically or per frame:
223
+
224
+ ```tsx
225
+ <ShardLight channels={{ rays: { scale: 1.2, rotation: 15 } }} />
226
+ ref.current?.setChannel('rays', { opacity: 0.5 });
227
+ ```
228
+
229
+ **Imperative / per-frame.** The DOM and R3F refs, and the three controller, expose:
230
+
231
+ ```ts
232
+ set({ channels, opacity, hover, collapse }); // per-frame values
233
+ setChannel(channel, { scale, opacity, rotation });
234
+ setHover(bool); setCollapsed(bool); toggleCollapsed();
235
+ ready; // Promise that resolves once every layer is baked
236
+ model; // the headless LightModel
237
+ ```
238
+
239
+ **CSS variables (DOM).** The light writes these on its root, so CSS, WAAPI, GSAP or Framer Motion
240
+ can drive it with no JS per frame:
241
+
242
+ ```
243
+ --shardlight-<channel>-scale (default 1)
244
+ --shardlight-<channel>-opacity (default 1)
245
+ --shardlight-<channel>-rotation (default 0deg)
246
+ --shardlight-opacity (whole light)
247
+ ```
248
+
249
+ ```css
250
+ .badge:hover { --shardlight-rays-scale: 1.3; --shardlight-rays-rotation: 15deg; }
251
+ ```
252
+
253
+ **Render options.** `resolution` (`'auto'` = rendered size × DPR) and `rayScale` at the adapter;
254
+ `baker`, `accepts`, `reducedMotion`, `hoverEase` and `collapseEase` on the model.
255
+
256
+ **Data first.** Every light is a versioned `ShardLightConfig`: save and load it as JSON,
257
+ `migrateConfig()` old saves, and `prewarm()` a config's layers before first paint.
258
+
259
+ ## Extending
260
+
261
+ New kinds, effects and presets register like the built-ins, so they work in every target:
262
+
263
+ ```ts
264
+ import {
265
+ star,
266
+ defineShardKind, registerShardKind,
267
+ defineEffect, registerEffect,
268
+ definePreset, registerPreset,
269
+ } from 'shardlight';
270
+ import { createShardComponent } from 'shardlight/react';
271
+
272
+ // a new shard kind — `params` drives defaults, validation and typed props
273
+ const dots = defineShardKind({
274
+ kind: 'dots',
275
+ label: 'Ring of dots',
276
+ params: { count: { type: 'number', default: 12, min: 1, max: 64, step: 1 } },
277
+ draw(ctx, p, env) { /* the context is centred, rotated and coloured for you */ },
278
+ });
279
+ registerShardKind(dots);
280
+
281
+ // a new effect — a pure function of time over the frame values
282
+ const sway = defineEffect({
283
+ name: 'sway',
284
+ params: { amount: { type: 'number', default: 0.4, min: 0, max: 1 } },
285
+ apply(t, p, out) { out.channel('rays').rotation += p.amount * 30 * Math.sin(t); },
286
+ });
287
+ registerEffect(sway);
288
+
289
+ // a preset is just a config, and can carry its own motion
290
+ registerPreset('star-spin', {
291
+ ...star,
292
+ effects: [{ type: 'spin', channels: ['rays'], speed: 0.05 }],
293
+ });
294
+
295
+ // typed components for a kind, in either React entry
296
+ const Dots = createShardComponent(dots);
297
+ <ShardLight preset="star"><Dots id="crown" count={8} /></ShardLight>
298
+ ```
299
+
300
+ `ShardKinds`, `ShardPresets` and `ChannelValues` are augmentable, so downstream kinds and presets
301
+ are typed too:
302
+
303
+ ```ts
304
+ declare module 'shardlight' {
305
+ interface ShardKinds { dots: ParamsOf<typeof dots> }
306
+ }
307
+ ```
308
+
309
+ ## Adapter contract
310
+
311
+ `shardlight/testing` exports the contract suite (`runAdapterContract`, `createFakeBaker`,
312
+ `waitFor`) that any adapter — including a third-party one — can run against itself.
313
+
314
+ ## Testing
315
+
316
+ ```bash
317
+ pnpm test # Vitest unit tests (config, model, effects, three adapter)
318
+ pnpm test:visual # Playwright: golden PNGs + cross-target parity (Chromium)
319
+ ```
320
+
321
+ The visual tests render every preset in real headless Chromium through all three targets,
322
+ compare against golden PNGs in `tests/visual/__screenshots__`, and check parity between
323
+ `<ShardLight>`, `createShardLight()` and `<ShardLightMesh>`. Regenerate goldens with
324
+ `pnpm test:visual:update` after an intentional look change.
325
+
326
+ ## License
327
+
328
+ MIT
@@ -0,0 +1,290 @@
1
+ /**
2
+ * The config model. A ShardLight is an ordered list of shards, each a
3
+ * flat set of that kind's parameters. Flat means no nested `shape: {}` objects,
4
+ * so overriding one field never needs a deep merge, and config keys match the
5
+ * component props one to one.
6
+ *
7
+ * The `Base*` interfaces are the built-ins. The public, augmentable interfaces
8
+ * (`ShardKinds`, `ShardPresets`, `ChannelValues`) are declared on the package
9
+ * entry so downstream code can extend them with `declare module 'shardlight'`.
10
+ */
11
+ interface BlobParams {
12
+ /** Peak brightness. Above 1 saturates the middle. */
13
+ strength: number;
14
+ /** Fade-out radius, as a fraction of the light's half-edge. */
15
+ size: number;
16
+ /** Share of the radius held at full brightness before the falloff. */
17
+ hardness: number;
18
+ /** Curve past `hardness`. */
19
+ falloff: number;
20
+ /** Horizontal ÷ vertical stretch. */
21
+ aspect: number;
22
+ /** Direction of the stretch, degrees. */
23
+ angle: number;
24
+ /** Composite blur, px at a 1024 bake. */
25
+ softness: number;
26
+ /** Uneven brightness around the centre. */
27
+ variance: number;
28
+ }
29
+ interface FanParams {
30
+ strength: number;
31
+ /** Ray length, from `inner` out. */
32
+ size: number;
33
+ /** Number of rays; 2 = one line through the centre. */
34
+ count: number;
35
+ /** First ray's direction, degrees. */
36
+ angle: number;
37
+ /** Measure `angle` from that fan shard's `angle` instead of from 0. */
38
+ relativeTo?: string;
39
+ /** Angular wander per ray, as a share of half the gap between rays. */
40
+ jitter: number;
41
+ /** Where rays start, from the centre. */
42
+ inner: number;
43
+ /** Per-ray length / brightness / width randomness. */
44
+ variance: number;
45
+ /** Width at the root, px at a 1024 bake (ray-scaled). */
46
+ width: number;
47
+ /** 0 = constant width, 1 = pointed tip. */
48
+ taper: number;
49
+ /** Brightness along the length. */
50
+ falloff: number;
51
+ /** Distance from the root over which the ray ramps in. */
52
+ fadeIn: number;
53
+ /** Composite blur, px (ray-scaled). */
54
+ softness: number;
55
+ }
56
+ interface ClusterParams {
57
+ /** Number of bundles. */
58
+ clusters: number;
59
+ /** Rays per bundle. */
60
+ perCluster: number;
61
+ /** Angular width of a bundle, degrees. */
62
+ spread: number;
63
+ /** Where rays start; each ray also varies by ±variance/2. */
64
+ inner: number;
65
+ strength: number;
66
+ size: number;
67
+ angle: number;
68
+ relativeTo?: string;
69
+ variance: number;
70
+ width: number;
71
+ taper: number;
72
+ falloff: number;
73
+ fadeIn: number;
74
+ softness: number;
75
+ }
76
+ interface HaloParams {
77
+ strength: number;
78
+ /** Radius of the brightest line. */
79
+ size: number;
80
+ /** Ring thickness, as a fraction of the half-edge. */
81
+ width: number;
82
+ /** Edge curve across the thickness. */
83
+ falloff: number;
84
+ /** Composite blur, px. */
85
+ softness: number;
86
+ /** Uneven brightness around the ring. */
87
+ variance: number;
88
+ }
89
+ /** Built-in kinds. Extend the public `ShardKinds` on the entry to add one. */
90
+ interface BaseShardKinds {
91
+ blob: BlobParams;
92
+ fan: FanParams;
93
+ clusters: ClusterParams;
94
+ halo: HaloParams;
95
+ }
96
+ /** Built-in presets. Extend the public `ShardPresets` on the entry to add one. */
97
+ interface BaseShardPresets {
98
+ star: true;
99
+ sun: true;
100
+ sparkle: true;
101
+ starburst: true;
102
+ ember: true;
103
+ }
104
+ type ShardBlend = 'add' | 'screen';
105
+ interface ShardSpin {
106
+ /** Turns per second; negative reverses. */
107
+ speed: number;
108
+ /** Starting angle, degrees. Default 0. */
109
+ phase?: number;
110
+ }
111
+ interface ShardHover {
112
+ /** Scale multiplier at full hover. Default 1 (no change). */
113
+ scale?: number;
114
+ /** Opacity multiplier at full hover. Default 1 (no change). */
115
+ opacity?: number;
116
+ /** Degrees added at full hover. Default 0. */
117
+ rotate?: number;
118
+ /** Turns/second banked while hovered; negative reverses. Default 0. */
119
+ spin?: number;
120
+ }
121
+ interface ShardCollapse {
122
+ /** Scale at full collapse. Default 0 (implodes). */
123
+ scale?: number;
124
+ /** Opacity at full collapse. Default 1 (keeps its brightness). */
125
+ opacity?: number;
126
+ }
127
+ interface ShardBase {
128
+ /** Stable name: seeds its randomness, target of overrides. */
129
+ id: string;
130
+ /** Any registered kind. */
131
+ kind: string;
132
+ /** Default true. */
133
+ visible?: boolean;
134
+ /** Motion channel it animates with; default 'main'. */
135
+ channel?: string;
136
+ /**
137
+ * Turns this shard continuously. `number` = turns/second; object = `{ speed, phase }`.
138
+ * A shard that spins is given its own animation channel, so it turns
139
+ * independently of the shards baked with it.
140
+ */
141
+ spin?: number | ShardSpin;
142
+ /**
143
+ * Reacts while the pointer is over the light. `true` = a small default bump;
144
+ * object = `{ scale, opacity, rotate }` at full hover. Eased in and out.
145
+ */
146
+ hover?: boolean | ShardHover;
147
+ /**
148
+ * Collapses when the light is clicked. `true` = implodes to nothing;
149
+ * object = `{ scale, opacity }` at full collapse. Click again to expand.
150
+ */
151
+ collapse?: boolean | ShardCollapse;
152
+ /** Default 'add'. */
153
+ blend?: ShardBlend;
154
+ /** Overrides the light's colour for this shard only. */
155
+ color?: string;
156
+ /** Overrides the light's seed for this shard only. */
157
+ seed?: number;
158
+ }
159
+ /** A shard's spin, with defaults filled in. */
160
+ interface ResolvedSpin {
161
+ speed: number;
162
+ phase: number;
163
+ }
164
+ /** A shard's hover response, with defaults filled in. */
165
+ interface ResolvedHover {
166
+ scale: number;
167
+ opacity: number;
168
+ rotate: number;
169
+ spin: number;
170
+ }
171
+ /** A shard's collapse response, with defaults filled in. */
172
+ interface ResolvedCollapse {
173
+ scale: number;
174
+ opacity: number;
175
+ }
176
+ /** A full shard: `kind` required, params optional (kind defaults fill the rest). */
177
+ type ShardConfig = {
178
+ [K in keyof BaseShardKinds]: ShardBase & {
179
+ kind: K;
180
+ } & Partial<BaseShardKinds[K]>;
181
+ }[keyof BaseShardKinds] & {
182
+ relativeTo?: string;
183
+ };
184
+ /**
185
+ * What overrides accept (`<Shard>` props, the `shards` option,
186
+ * `config.shards`): either a full shard, or a partial one whose `id` names a
187
+ * shard already in the preset. A partial one omits `kind` and inherits it.
188
+ */
189
+ type ShardInput = ShardConfig | ({
190
+ id: string;
191
+ kind?: string;
192
+ } & Partial<Omit<ShardBase, 'id' | 'kind'>> & Record<string, unknown>);
193
+ interface EffectConfig {
194
+ type: string;
195
+ [param: string]: unknown;
196
+ }
197
+ interface ShardLightConfig {
198
+ /** Schema version. */
199
+ version: 1;
200
+ /** Default '#FFFFFF'. */
201
+ color: string;
202
+ /** Degrees; turns the whole light. Default 0. */
203
+ rotation: number;
204
+ /** Default 1. */
205
+ seed: number;
206
+ /** Drawn in order (additive, so order only matters for 'screen'). */
207
+ shards: ShardConfig[];
208
+ /** Optional animations. */
209
+ effects?: EffectConfig[];
210
+ }
211
+ /** A preset is a plain ShardLightConfig. Built-ins: star, sun, sparkle, starburst, ember. */
212
+ type BuiltInPresetName = keyof BaseShardPresets;
213
+ type Preset = string | ShardLightConfig;
214
+ interface ResolvedShard {
215
+ id: string;
216
+ kind: string;
217
+ visible: boolean;
218
+ channel: string;
219
+ blend: ShardBlend;
220
+ /** Resolved colour (shard override or the light's). */
221
+ color: string;
222
+ /** Resolved seed (shard override or the light's). */
223
+ seed: number;
224
+ /** Present when the shard turns. */
225
+ spin?: ResolvedSpin;
226
+ /** Present when the shard reacts to hover. */
227
+ hover?: ResolvedHover;
228
+ /** Present when the shard collapses on click. */
229
+ collapse?: ResolvedCollapse;
230
+ /** Kind defaults filled in and clamped. */
231
+ params: Record<string, unknown>;
232
+ }
233
+ interface ResolvedLight {
234
+ version: 1;
235
+ color: string;
236
+ rotation: number;
237
+ seed: number;
238
+ shards: ResolvedShard[];
239
+ effects: EffectConfig[];
240
+ }
241
+ interface ResolvedLayer {
242
+ /** Content hash of the resolved shards plus light-wide bake inputs. */
243
+ key: string;
244
+ /** Stable while the layer exists: `${channel}|${blend}`. */
245
+ id: string;
246
+ channel: string;
247
+ blend: ShardBlend;
248
+ shards: ResolvedShard[];
249
+ color: string;
250
+ rotation: number;
251
+ seed: number;
252
+ }
253
+
254
+ /**
255
+ * What a baked layer can become. Adapters say which types they
256
+ * accept, and the model asks the baker for a compatible one: DOM takes `url`,
257
+ * three takes `bitmap` or `canvas`.
258
+ */
259
+ type LayerSource = {
260
+ type: 'url';
261
+ url: string;
262
+ } | {
263
+ type: 'bitmap';
264
+ bitmap: ImageBitmap;
265
+ } | {
266
+ type: 'canvas';
267
+ canvas: HTMLCanvasElement | OffscreenCanvas;
268
+ };
269
+ interface BakeOptions {
270
+ resolution: number;
271
+ rayScale: number;
272
+ accept: LayerSource['type'][];
273
+ }
274
+ /**
275
+ * Baking sits behind this interface, so where and how it happens can change
276
+ * without touching kinds or adapters. The baker's `id` is part of the cache key.
277
+ */
278
+ interface Baker {
279
+ id: string;
280
+ bake(layer: ResolvedLayer, opts: BakeOptions, signal: AbortSignal): Promise<LayerSource>;
281
+ }
282
+ /**
283
+ * Thrown when a 2D context is unavailable (canvas memory cap). Treat it as a
284
+ * non-fatal "try again later": the result is left uncached.
285
+ */
286
+ declare class NullContextError extends Error {
287
+ constructor();
288
+ }
289
+
290
+ export { type Baker as B, type ClusterParams as C, type EffectConfig as E, type FanParams as F, type HaloParams as H, type LayerSource as L, NullContextError as N, type Preset as P, type ResolvedLight as R, type ShardInput as S, type ShardLightConfig as a, type ShardConfig as b, type ShardBase as c, type ResolvedLayer as d, type BaseShardKinds as e, type BaseShardPresets as f, type BakeOptions as g, type BlobParams as h, type BuiltInPresetName as i, type ResolvedCollapse as j, type ResolvedHover as k, type ResolvedShard as l, type ResolvedSpin as m, type ShardBlend as n, type ShardCollapse as o, type ShardHover as p, type ShardSpin as q };