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.
@@ -0,0 +1,1786 @@
1
+ // src/core/kinds/registry.ts
2
+ var kinds = /* @__PURE__ */ new Map();
3
+ function defineShardKind(def) {
4
+ return def;
5
+ }
6
+ function registerShardKind(def) {
7
+ if (kinds.has(def.kind)) {
8
+ warn(`overwriting already-registered shard kind "${def.kind}"`);
9
+ }
10
+ kinds.set(def.kind, def);
11
+ }
12
+ function getShardKind(kind) {
13
+ return kinds.get(kind);
14
+ }
15
+ function listShardKinds() {
16
+ return [...kinds.values()];
17
+ }
18
+ function unregisterShardKind(kind) {
19
+ kinds.delete(kind);
20
+ }
21
+ function warn(message) {
22
+ if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
23
+ console.warn(`[shardlight] ${message}`);
24
+ }
25
+
26
+ // src/core/kinds/draw.ts
27
+ var GRADIENT_STOPS = 16;
28
+ function applyRayScale(params, schema, rayScale) {
29
+ if (rayScale === 1) return params;
30
+ const out = { ...params };
31
+ for (const [name, def] of Object.entries(schema)) {
32
+ if (def.ray && typeof out[name] === "number") {
33
+ out[name] = out[name] * rayScale;
34
+ }
35
+ }
36
+ return out;
37
+ }
38
+ function drawRay(ctx, env, o) {
39
+ const length = o.tipDist - o.rootDist;
40
+ if (length <= 0 || o.strength <= 0) return;
41
+ ctx.save();
42
+ ctx.rotate(o.angleRad);
43
+ const gradient = ctx.createLinearGradient(o.rootDist, 0, o.tipDist, 0);
44
+ for (let i = 0; i <= GRADIENT_STOPS; i++) {
45
+ const t = i / GRADIENT_STOPS;
46
+ const along = Math.pow(1 - t, o.falloff);
47
+ const ramp = o.fadeIn > 0 ? Math.min(1, t * length / o.fadeIn) : 1;
48
+ gradient.addColorStop(t, env.rgba(o.strength * along * ramp));
49
+ }
50
+ ctx.fillStyle = gradient;
51
+ ctx.beginPath();
52
+ ctx.moveTo(o.rootDist, -o.rootHalf);
53
+ ctx.lineTo(o.tipDist, -o.tipHalf);
54
+ ctx.lineTo(o.tipDist, o.tipHalf);
55
+ ctx.lineTo(o.rootDist, o.rootHalf);
56
+ ctx.closePath();
57
+ ctx.fill();
58
+ ctx.restore();
59
+ }
60
+ function conicVarianceMask(ctx, params, env, size) {
61
+ const variance = params.variance ?? 0;
62
+ if (variance <= 0) return;
63
+ const createConic = ctx.createConicGradient;
64
+ if (typeof createConic !== "function") return;
65
+ const center = size / 2;
66
+ const gradient = createConic.call(ctx, 0, center, center);
67
+ const stops = 12;
68
+ const alphas = [];
69
+ for (let i = 0; i < stops; i++) alphas.push(1 - variance * env.rng());
70
+ for (let i = 0; i < stops; i++) {
71
+ gradient.addColorStop(i / stops, env.rgba(alphas[i]));
72
+ }
73
+ gradient.addColorStop(1, env.rgba(alphas[0]));
74
+ ctx.save();
75
+ ctx.globalCompositeOperation = "destination-in";
76
+ ctx.fillStyle = gradient;
77
+ ctx.fillRect(0, 0, size, size);
78
+ ctx.restore();
79
+ }
80
+
81
+ // src/core/kinds/blob.ts
82
+ var blob = defineShardKind({
83
+ kind: "blob",
84
+ label: "Glow",
85
+ params: {
86
+ strength: { type: "number", default: 1, min: 0, max: 2 },
87
+ size: { type: "number", default: 0.5, min: 0, max: 2 },
88
+ hardness: { type: "number", default: 0, min: 0, max: 0.999 },
89
+ falloff: { type: "number", default: 2, min: 0.1, max: 8 },
90
+ aspect: { type: "number", default: 1, min: 0.1, max: 10 },
91
+ angle: { type: "angle", default: 0 },
92
+ softness: { type: "number", default: 0, min: 0, max: 50, unit: "px" },
93
+ variance: { type: "number", default: 0, min: 0, max: 1 }
94
+ },
95
+ draw(ctx, p, env) {
96
+ const radius = p.size * env.unit;
97
+ if (radius <= 0 || p.strength <= 0) return;
98
+ ctx.save();
99
+ ctx.rotate(p.angle * Math.PI / 180);
100
+ ctx.scale(p.aspect, 1);
101
+ const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radius);
102
+ for (let i = 0; i <= GRADIENT_STOPS; i++) {
103
+ const t = i / GRADIENT_STOPS;
104
+ let a;
105
+ if (p.hardness >= 1) {
106
+ a = t < 1 ? 1 : 0;
107
+ } else if (p.hardness > 0 && t <= p.hardness) {
108
+ a = 1;
109
+ } else {
110
+ a = Math.pow((1 - t) / (1 - p.hardness), p.falloff);
111
+ }
112
+ gradient.addColorStop(t, env.rgba(Math.max(0, a) * p.strength));
113
+ }
114
+ ctx.fillStyle = gradient;
115
+ ctx.beginPath();
116
+ ctx.arc(0, 0, radius, 0, Math.PI * 2);
117
+ ctx.fill();
118
+ ctx.restore();
119
+ },
120
+ mask: conicVarianceMask
121
+ });
122
+
123
+ // src/core/kinds/fan.ts
124
+ var fan = defineShardKind({
125
+ kind: "fan",
126
+ label: "Rays",
127
+ params: {
128
+ strength: { type: "number", default: 1, min: 0, max: 2 },
129
+ size: { type: "number", default: 1, min: 0, max: 2 },
130
+ count: { type: "number", default: 4, min: 1, max: 256, step: 1 },
131
+ angle: { type: "angle", default: 0 },
132
+ jitter: { type: "number", default: 0, min: 0, max: 1 },
133
+ inner: { type: "number", default: 0, min: 0, max: 1 },
134
+ variance: { type: "number", default: 0, min: 0, max: 1 },
135
+ width: { type: "number", default: 4, min: 0, max: 100, unit: "px", ray: true },
136
+ taper: { type: "number", default: 1, min: 0, max: 1 },
137
+ falloff: { type: "number", default: 1, min: 0, max: 8 },
138
+ fadeIn: { type: "number", default: 0, min: 0, max: 1 },
139
+ softness: { type: "number", default: 1, min: 0, max: 50, unit: "px", ray: true }
140
+ },
141
+ draw(ctx, p, env) {
142
+ const count = Math.max(1, Math.round(p.count));
143
+ const gap = Math.PI * 2 / count;
144
+ const rootDist = p.inner * env.unit;
145
+ const lengthPx = p.size * env.unit;
146
+ const rootHalf = p.width * env.px / 2;
147
+ for (let i = 0; i < count; i++) {
148
+ const jitterR = env.rng();
149
+ const varianceR = env.rng();
150
+ const base = p.angle * Math.PI / 180 + i * gap;
151
+ const angle = base + (jitterR * 2 - 1) * p.jitter * (gap / 2);
152
+ const lengthMul = 1 - p.variance * varianceR;
153
+ const brightMul = 1 - p.variance * varianceR;
154
+ const widthMul = 1 - 0.5 * p.variance * varianceR;
155
+ const half = rootHalf * widthMul;
156
+ drawRay(ctx, env, {
157
+ angleRad: angle,
158
+ rootDist,
159
+ tipDist: rootDist + lengthPx * lengthMul,
160
+ rootHalf: half,
161
+ tipHalf: half * (1 - p.taper),
162
+ strength: p.strength * brightMul,
163
+ falloff: p.falloff,
164
+ fadeIn: p.fadeIn * env.unit
165
+ });
166
+ }
167
+ }
168
+ });
169
+
170
+ // src/core/kinds/clusters.ts
171
+ var clusters = defineShardKind({
172
+ kind: "clusters",
173
+ label: "Streaks",
174
+ params: {
175
+ clusters: { type: "number", default: 4, min: 1, max: 64, step: 1 },
176
+ perCluster: { type: "number", default: 5, min: 1, max: 64, step: 1 },
177
+ spread: { type: "number", default: 20, min: 0, max: 360 },
178
+ inner: { type: "number", default: 0.1, min: 0, max: 1 },
179
+ strength: { type: "number", default: 0.5, min: 0, max: 2 },
180
+ size: { type: "number", default: 0.5, min: 0, max: 2 },
181
+ angle: { type: "angle", default: 0 },
182
+ variance: { type: "number", default: 0.5, min: 0, max: 1 },
183
+ width: { type: "number", default: 2, min: 0, max: 100, unit: "px", ray: true },
184
+ taper: { type: "number", default: 0.5, min: 0, max: 1 },
185
+ falloff: { type: "number", default: 1, min: 0, max: 8 },
186
+ fadeIn: { type: "number", default: 0.1, min: 0, max: 1 },
187
+ softness: { type: "number", default: 1, min: 0, max: 50, unit: "px", ray: true }
188
+ },
189
+ draw(ctx, p, env) {
190
+ const clusterCount = Math.max(1, Math.round(p.clusters));
191
+ const perCluster = Math.max(1, Math.round(p.perCluster));
192
+ const gap = Math.PI * 2 / clusterCount;
193
+ const spreadRad = p.spread * Math.PI / 180;
194
+ const lengthPx = p.size * env.unit;
195
+ const rootHalf = p.width * env.px / 2;
196
+ for (let c = 0; c < clusterCount; c++) {
197
+ const center = p.angle * Math.PI / 180 + c * gap;
198
+ for (let i = 0; i < perCluster; i++) {
199
+ const dirR = env.rng();
200
+ const innerR = env.rng();
201
+ const varianceR = env.rng();
202
+ const angle = center + (dirR - 0.5) * spreadRad;
203
+ const inner = Math.max(0, p.inner + (innerR - 0.5) * p.variance);
204
+ const rootDist = inner * env.unit;
205
+ const lengthMul = 1 - p.variance * varianceR;
206
+ const brightMul = 1 - p.variance * varianceR;
207
+ const widthMul = 1 - 0.5 * p.variance * varianceR;
208
+ const half = rootHalf * widthMul;
209
+ drawRay(ctx, env, {
210
+ angleRad: angle,
211
+ rootDist,
212
+ tipDist: rootDist + lengthPx * lengthMul,
213
+ rootHalf: half,
214
+ tipHalf: half * (1 - p.taper),
215
+ strength: p.strength * brightMul,
216
+ falloff: p.falloff,
217
+ fadeIn: p.fadeIn * env.unit
218
+ });
219
+ }
220
+ }
221
+ }
222
+ });
223
+
224
+ // src/core/kinds/halo.ts
225
+ var halo = defineShardKind({
226
+ kind: "halo",
227
+ label: "Halo",
228
+ params: {
229
+ strength: { type: "number", default: 0.1, min: 0, max: 2 },
230
+ size: { type: "number", default: 0.5, min: 0, max: 2 },
231
+ width: { type: "number", default: 0.05, min: 0, max: 2 },
232
+ falloff: { type: "number", default: 1, min: 0, max: 8 },
233
+ softness: { type: "number", default: 0, min: 0, max: 50, unit: "px" },
234
+ variance: { type: "number", default: 0, min: 0, max: 1 }
235
+ },
236
+ draw(ctx, p, env) {
237
+ const inner = Math.max(0, p.size - p.width / 2) * env.unit;
238
+ const outer = Math.max(inner, (p.size + p.width / 2) * env.unit);
239
+ if (outer <= 0 || p.strength <= 0) return;
240
+ const gradient = ctx.createRadialGradient(0, 0, inner, 0, 0, outer);
241
+ for (let i = 0; i <= GRADIENT_STOPS; i++) {
242
+ const t = i / GRADIENT_STOPS;
243
+ const across = 1 - Math.abs(2 * t - 1);
244
+ gradient.addColorStop(t, env.rgba(Math.pow(across, p.falloff) * p.strength));
245
+ }
246
+ ctx.fillStyle = gradient;
247
+ ctx.beginPath();
248
+ ctx.arc(0, 0, outer, 0, Math.PI * 2);
249
+ ctx.fill();
250
+ },
251
+ mask: conicVarianceMask
252
+ });
253
+
254
+ // src/core/kinds/index.ts
255
+ function registerBuiltInKinds() {
256
+ registerShardKind(blob);
257
+ registerShardKind(fan);
258
+ registerShardKind(clusters);
259
+ registerShardKind(halo);
260
+ }
261
+ registerBuiltInKinds();
262
+
263
+ // src/core/effects/registry.ts
264
+ var effects = /* @__PURE__ */ new Map();
265
+ function defineEffect(def) {
266
+ return def;
267
+ }
268
+ function registerEffect(def) {
269
+ if (effects.has(def.name)) {
270
+ warn2(`overwriting already-registered effect "${def.name}"`);
271
+ }
272
+ effects.set(def.name, def);
273
+ }
274
+ function getEffect(name) {
275
+ return effects.get(name);
276
+ }
277
+ function listEffects() {
278
+ return [...effects.values()];
279
+ }
280
+ function unregisterEffect(name) {
281
+ effects.delete(name);
282
+ }
283
+ function warn2(message) {
284
+ if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
285
+ console.warn(`[shardlight] ${message}`);
286
+ }
287
+
288
+ // src/core/effects/pulse.ts
289
+ var pulse = defineEffect({
290
+ name: "pulse",
291
+ label: "Pulse",
292
+ params: {
293
+ channels: { type: "channels", default: ["main"] },
294
+ speed: { type: "number", default: 1, min: 0, max: 10, step: 0.01 },
295
+ amount: { type: "number", default: 0.1, min: 0, max: 1 }
296
+ },
297
+ apply(t, p, out) {
298
+ const k = 1 + Math.sin(t * p.speed * Math.PI * 2) * p.amount;
299
+ for (const channel of p.channels) out.channel(channel).scale *= k;
300
+ }
301
+ });
302
+
303
+ // src/core/effects/flicker.ts
304
+ var flicker = defineEffect({
305
+ name: "flicker",
306
+ label: "Flicker",
307
+ params: {
308
+ amount: { type: "number", default: 0.15, min: 0, max: 1 },
309
+ speed: { type: "number", default: 1, min: 0, max: 10, step: 0.01 },
310
+ channels: { type: "channels", default: ["*"] }
311
+ },
312
+ apply(t, p, out) {
313
+ const s = t * p.speed;
314
+ const factor = 1 - p.amount * (0.5 + 0.25 * Math.sin(11.3 * s) + 0.25 * Math.sin(17.9 * s + 1.7));
315
+ const targets = p.channels.includes("*") ? out.channels() : p.channels;
316
+ for (const channel of targets) out.channel(channel).opacity *= factor;
317
+ }
318
+ });
319
+
320
+ // src/core/effects/spin.ts
321
+ var spin = defineEffect({
322
+ name: "spin",
323
+ label: "Spin",
324
+ params: {
325
+ channels: { type: "channels", default: ["main"] },
326
+ speed: { type: "number", default: 0.1, min: -10, max: 10, step: 0.01, unit: "turns/s" },
327
+ phase: { type: "angle", default: 0 }
328
+ },
329
+ apply(t, p, out) {
330
+ const rotation = p.phase + p.speed * 360 * t;
331
+ for (const channel of p.channels) out.channel(channel).rotation += rotation;
332
+ }
333
+ });
334
+
335
+ // src/core/effects/hover.ts
336
+ var hover = defineEffect({
337
+ name: "hover",
338
+ label: "Hover",
339
+ params: {
340
+ channels: { type: "channels", default: ["main"] },
341
+ scale: { type: "number", default: 1.15, min: 0, max: 4, step: 0.01 },
342
+ opacity: { type: "number", default: 1, min: 0, max: 4, step: 0.01 },
343
+ rotate: { type: "angle", default: 0 },
344
+ spin: { type: "number", default: 0, min: -10, max: 10, step: 0.01, unit: "turns/s" }
345
+ },
346
+ reducedMotion: "keep",
347
+ apply(_t, p, out) {
348
+ const h = out.hover;
349
+ for (const channel of p.channels) {
350
+ const values = out.channel(channel);
351
+ values.scale *= 1 + (p.scale - 1) * h;
352
+ values.opacity *= 1 + (p.opacity - 1) * h;
353
+ values.rotation += p.rotate * h + p.spin * 360 * out.hoverTime;
354
+ }
355
+ }
356
+ });
357
+
358
+ // src/core/effects/collapse.ts
359
+ var collapse = defineEffect({
360
+ name: "collapse",
361
+ label: "Collapse",
362
+ params: {
363
+ channels: { type: "channels", default: ["*"] },
364
+ scale: { type: "number", default: 0, min: 0, max: 4, step: 0.01 },
365
+ opacity: { type: "number", default: 1, min: 0, max: 4, step: 0.01 },
366
+ trigger: { type: "enum", default: "click", values: ["click", "once", "none"] }
367
+ },
368
+ reducedMotion: "keep",
369
+ apply(_t, p, out) {
370
+ const amount = out.collapse;
371
+ if (amount <= 0) return;
372
+ const targets = p.channels.includes("*") ? out.channels() : p.channels;
373
+ for (const channel of targets) {
374
+ const values = out.channel(channel);
375
+ values.scale *= 1 + (p.scale - 1) * amount;
376
+ values.opacity *= 1 + (p.opacity - 1) * amount;
377
+ }
378
+ }
379
+ });
380
+ function collapseTrigger(effects2) {
381
+ let has = false;
382
+ let once = false;
383
+ for (const effect of effects2) {
384
+ if (effect.type !== "collapse") continue;
385
+ has = true;
386
+ if (effect.trigger === "none") return "none";
387
+ if (effect.trigger === "once") once = true;
388
+ }
389
+ if (!has) return "none";
390
+ return once ? "once" : "click";
391
+ }
392
+
393
+ // src/core/effects/index.ts
394
+ function registerBuiltInEffects() {
395
+ registerEffect(pulse);
396
+ registerEffect(flicker);
397
+ registerEffect(spin);
398
+ registerEffect(hover);
399
+ registerEffect(collapse);
400
+ }
401
+ registerBuiltInEffects();
402
+
403
+ // src/core/presets/registry.ts
404
+ var presets = /* @__PURE__ */ new Map();
405
+ function definePreset(config) {
406
+ return config;
407
+ }
408
+ function registerPreset(name, config) {
409
+ if (presets.has(name)) {
410
+ warn3(`overwriting already-registered preset "${name}"`);
411
+ }
412
+ presets.set(name, config);
413
+ }
414
+ function getPreset(name) {
415
+ return presets.get(name);
416
+ }
417
+ function listPresets() {
418
+ return [...presets.keys()];
419
+ }
420
+ function unregisterPreset(name) {
421
+ presets.delete(name);
422
+ }
423
+ function warn3(message) {
424
+ if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
425
+ console.warn(`[shardlight] ${message}`);
426
+ }
427
+
428
+ // src/core/presets/star.ts
429
+ var star = definePreset({
430
+ version: 1,
431
+ color: "#FFF4E0",
432
+ rotation: 45,
433
+ seed: 1,
434
+ shards: [
435
+ {
436
+ id: "bloom",
437
+ kind: "blob",
438
+ channel: "body",
439
+ strength: 0.4,
440
+ size: 0.85,
441
+ hardness: 0,
442
+ falloff: 3,
443
+ softness: 3
444
+ },
445
+ {
446
+ id: "hotspot",
447
+ kind: "blob",
448
+ channel: "body",
449
+ strength: 1,
450
+ size: 0.13,
451
+ hardness: 0.4,
452
+ falloff: 2.2
453
+ },
454
+ {
455
+ id: "beam",
456
+ kind: "fan",
457
+ channel: "rays",
458
+ strength: 0.9,
459
+ size: 1,
460
+ count: 4,
461
+ variance: 0.08,
462
+ width: 5,
463
+ taper: 1,
464
+ falloff: 2,
465
+ softness: 2
466
+ },
467
+ {
468
+ id: "ring",
469
+ kind: "halo",
470
+ channel: "rays",
471
+ strength: 0.06,
472
+ size: 0.62,
473
+ width: 0.05,
474
+ falloff: 2,
475
+ softness: 1.5,
476
+ variance: 0.35
477
+ }
478
+ ]
479
+ });
480
+
481
+ // src/core/presets/sun.ts
482
+ var sun = definePreset({
483
+ version: 1,
484
+ color: "#FFE9B8",
485
+ rotation: 0,
486
+ seed: 1,
487
+ shards: [
488
+ {
489
+ id: "bloom",
490
+ kind: "blob",
491
+ channel: "body",
492
+ strength: 0.55,
493
+ size: 1.15,
494
+ hardness: 0,
495
+ falloff: 2.6,
496
+ softness: 5
497
+ },
498
+ {
499
+ id: "hotspot",
500
+ kind: "blob",
501
+ channel: "body",
502
+ strength: 1,
503
+ size: 0.22,
504
+ hardness: 0.3,
505
+ falloff: 2
506
+ },
507
+ {
508
+ id: "rays",
509
+ kind: "fan",
510
+ channel: "rays",
511
+ strength: 0.45,
512
+ size: 1,
513
+ count: 32,
514
+ variance: 0.7,
515
+ jitter: 0.55,
516
+ inner: 0.06,
517
+ width: 2,
518
+ taper: 1,
519
+ falloff: 1.6,
520
+ fadeIn: 0.05,
521
+ softness: 1.5
522
+ },
523
+ {
524
+ id: "ring",
525
+ kind: "halo",
526
+ channel: "rays",
527
+ strength: 0.04,
528
+ size: 0.8,
529
+ width: 0.07,
530
+ falloff: 2,
531
+ softness: 2.5,
532
+ variance: 0.4
533
+ }
534
+ ]
535
+ });
536
+
537
+ // src/core/presets/sparkle.ts
538
+ var sparkle = definePreset({
539
+ version: 1,
540
+ color: "#FFFFFF",
541
+ rotation: 0,
542
+ seed: 4,
543
+ shards: [
544
+ {
545
+ id: "bloom",
546
+ kind: "blob",
547
+ channel: "body",
548
+ strength: 0.25,
549
+ size: 0.28,
550
+ hardness: 0,
551
+ falloff: 3,
552
+ softness: 2
553
+ },
554
+ {
555
+ id: "hotspot",
556
+ kind: "blob",
557
+ channel: "body",
558
+ strength: 1,
559
+ size: 0.08,
560
+ hardness: 0.5,
561
+ falloff: 2
562
+ },
563
+ {
564
+ id: "spikes",
565
+ kind: "fan",
566
+ channel: "rays",
567
+ strength: 1,
568
+ size: 0.42,
569
+ count: 6,
570
+ width: 3,
571
+ taper: 1,
572
+ falloff: 2,
573
+ softness: 1
574
+ },
575
+ {
576
+ id: "glint",
577
+ kind: "fan",
578
+ channel: "glint",
579
+ strength: 0.6,
580
+ size: 0.26,
581
+ count: 6,
582
+ angle: 30,
583
+ width: 2,
584
+ taper: 1,
585
+ falloff: 2,
586
+ softness: 1
587
+ }
588
+ ]
589
+ });
590
+
591
+ // src/core/presets/starburst.ts
592
+ var starburst = definePreset({
593
+ version: 1,
594
+ color: "#FFFFFF",
595
+ rotation: 0,
596
+ seed: 7,
597
+ shards: [
598
+ {
599
+ id: "glow",
600
+ kind: "blob",
601
+ channel: "body",
602
+ strength: 0.3,
603
+ size: 0.5,
604
+ hardness: 0,
605
+ falloff: 3,
606
+ softness: 4
607
+ },
608
+ {
609
+ id: "core",
610
+ kind: "blob",
611
+ channel: "body",
612
+ strength: 1,
613
+ size: 0.1,
614
+ hardness: 0.5,
615
+ falloff: 2
616
+ },
617
+ {
618
+ id: "rays",
619
+ kind: "fan",
620
+ channel: "rays",
621
+ strength: 0.7,
622
+ size: 1.1,
623
+ count: 24,
624
+ variance: 0.45,
625
+ width: 2,
626
+ taper: 1,
627
+ falloff: 1.6,
628
+ fadeIn: 0.02,
629
+ softness: 1
630
+ }
631
+ ]
632
+ });
633
+
634
+ // src/core/presets/ember.ts
635
+ var ember = definePreset({
636
+ version: 1,
637
+ color: "#FF9A4D",
638
+ rotation: 0,
639
+ seed: 6,
640
+ shards: [
641
+ {
642
+ id: "bloom",
643
+ kind: "blob",
644
+ channel: "body",
645
+ strength: 0.5,
646
+ size: 1,
647
+ hardness: 0,
648
+ falloff: 2.2,
649
+ softness: 6
650
+ },
651
+ {
652
+ id: "hotspot",
653
+ kind: "blob",
654
+ channel: "body",
655
+ strength: 1,
656
+ size: 0.18,
657
+ hardness: 0.25,
658
+ falloff: 2.4
659
+ },
660
+ {
661
+ id: "sparks",
662
+ kind: "fan",
663
+ channel: "rays",
664
+ strength: 0.3,
665
+ size: 0.85,
666
+ count: 6,
667
+ variance: 0.8,
668
+ jitter: 0.4,
669
+ inner: 0.15,
670
+ width: 2,
671
+ taper: 1,
672
+ falloff: 2,
673
+ fadeIn: 0.15,
674
+ softness: 1
675
+ }
676
+ ]
677
+ });
678
+
679
+ // src/core/presets/index.ts
680
+ function registerBuiltInPresets() {
681
+ registerPreset("star", star);
682
+ registerPreset("sun", sun);
683
+ registerPreset("sparkle", sparkle);
684
+ registerPreset("starburst", starburst);
685
+ registerPreset("ember", ember);
686
+ }
687
+ registerBuiltInPresets();
688
+
689
+ // src/core/util/color.ts
690
+ var HEX = /^#?([0-9a-f]{3,8})$/i;
691
+ var RGB_FN = /^rgba?\(\s*([^)]+)\)$/i;
692
+ function clampByte(n) {
693
+ return Math.max(0, Math.min(255, Math.round(n)));
694
+ }
695
+ function parseColor(input) {
696
+ const value = (input ?? "").trim();
697
+ const hex = HEX.exec(value);
698
+ if (hex) {
699
+ const h = hex[1];
700
+ if (h.length === 3 || h.length === 4) {
701
+ const r = parseInt(h[0] + h[0], 16);
702
+ const g = parseInt(h[1] + h[1], 16);
703
+ const b = parseInt(h[2] + h[2], 16);
704
+ const a = h.length === 4 ? parseInt(h[3] + h[3], 16) / 255 : 1;
705
+ return { r, g, b, a };
706
+ }
707
+ if (h.length === 6 || h.length === 8) {
708
+ const r = parseInt(h.slice(0, 2), 16);
709
+ const g = parseInt(h.slice(2, 4), 16);
710
+ const b = parseInt(h.slice(4, 6), 16);
711
+ const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
712
+ return { r, g, b, a };
713
+ }
714
+ }
715
+ const fn = RGB_FN.exec(value);
716
+ if (fn) {
717
+ const parts = fn[1].split(/[\s,/]+/).filter(Boolean).map(Number);
718
+ if (parts.length >= 3 && parts.every((n) => Number.isFinite(n))) {
719
+ return {
720
+ r: clampByte(parts[0]),
721
+ g: clampByte(parts[1]),
722
+ b: clampByte(parts[2]),
723
+ a: parts.length >= 4 ? Math.max(0, Math.min(1, parts[3])) : 1
724
+ };
725
+ }
726
+ }
727
+ return { r: 255, g: 255, b: 255, a: 1 };
728
+ }
729
+ function makeRgba(color) {
730
+ const { r, g, b, a } = parseColor(color);
731
+ return (alpha) => {
732
+ const clamped = Math.max(0, Math.min(1, alpha)) * a;
733
+ return `rgba(${r},${g},${b},${clamped})`;
734
+ };
735
+ }
736
+ function isCssIdentifier(name) {
737
+ return /^-?[_a-zA-Z][-_a-zA-Z0-9]*$/.test(name);
738
+ }
739
+
740
+ // src/core/config/resolve.ts
741
+ var DEFAULT_COLOR = "#FFFFFF";
742
+ function resolveConfig(input = {}, options = {}) {
743
+ const warn4 = makeWarn(options.warn);
744
+ const presetConfig = resolvePreset(input.preset, options.defaultPreset ?? null, warn4);
745
+ const config = input.config ?? void 0;
746
+ const color = input.color ?? config?.color ?? presetConfig?.color ?? DEFAULT_COLOR;
747
+ const rotation = input.rotation ?? config?.rotation ?? presetConfig?.rotation ?? 0;
748
+ const seed = input.seed ?? config?.seed ?? presetConfig?.seed ?? 1;
749
+ let list = presetConfig ? presetConfig.shards.map((s) => ({ ...s })) : [];
750
+ if (config?.shards) list = mergeShardList(list, config.shards, warn4);
751
+ if (input.shards) list = mergeShardList(list, input.shards, warn4);
752
+ const effects2 = input.effects ?? config?.effects ?? presetConfig?.effects ?? [];
753
+ const shards = [];
754
+ for (const shard of list) {
755
+ const resolved = resolveShard(shard, color, seed, warn4);
756
+ if (resolved) shards.push(resolved);
757
+ }
758
+ resolveRelativeAngles(shards);
759
+ const motionEffects = applyShardMotion(shards, warn4);
760
+ return {
761
+ version: 1,
762
+ color,
763
+ rotation,
764
+ seed,
765
+ shards,
766
+ effects: [...resolveEffects(effects2, warn4), ...motionEffects]
767
+ };
768
+ }
769
+ function resolvePreset(preset, defaultName, warn4) {
770
+ if (preset === void 0) preset = defaultName ?? null;
771
+ if (preset === null) return void 0;
772
+ if (typeof preset === "string") {
773
+ const found = getPreset(preset);
774
+ if (!found) {
775
+ warn4(`unknown preset "${preset}"`);
776
+ return defaultName ? getPreset(defaultName) : void 0;
777
+ }
778
+ return found;
779
+ }
780
+ return preset;
781
+ }
782
+ function mergeShardList(base, overrides, warn4) {
783
+ const list = base.map((s) => ({ ...s }));
784
+ const indexById = new Map(list.map((s, i) => [s.id, i]));
785
+ for (const override of overrides) {
786
+ const id = override.id;
787
+ if (typeof id !== "string") {
788
+ warn4("a shard override without a string `id` was skipped");
789
+ continue;
790
+ }
791
+ const index = indexById.get(id);
792
+ if (index === void 0) {
793
+ if (override.kind === void 0) {
794
+ warn4(`override for unknown shard "${id}" skipped (no \`kind\` to add it)`);
795
+ continue;
796
+ }
797
+ list.push({ ...override });
798
+ indexById.set(id, list.length - 1);
799
+ } else {
800
+ list[index] = { ...list[index], ...override };
801
+ }
802
+ }
803
+ return list;
804
+ }
805
+ function resolveShard(shard, lightColor, lightSeed, warn4) {
806
+ const kindDef = getShardKind(shard.kind);
807
+ if (!kindDef) {
808
+ warn4(`unknown shard kind "${shard.kind}" (shard "${shard.id}") skipped`);
809
+ return null;
810
+ }
811
+ const params = {};
812
+ for (const [name, schema] of Object.entries(kindDef.params)) {
813
+ const raw = shard[name];
814
+ params[name] = coerce(schema, raw, `${shard.id}.${name}`, warn4);
815
+ }
816
+ const relativeTo = shard.relativeTo;
817
+ if (typeof relativeTo === "string") params.relativeTo = relativeTo;
818
+ const channel = shard.channel ?? "main";
819
+ if (!isCssIdentifier(channel)) {
820
+ warn4(`shard "${shard.id}" channel "${channel}" is not a valid CSS identifier`);
821
+ }
822
+ return {
823
+ id: shard.id,
824
+ kind: shard.kind,
825
+ visible: shard.visible ?? true,
826
+ channel,
827
+ blend: shard.blend ?? "add",
828
+ color: shard.color ?? lightColor,
829
+ seed: shard.seed ?? lightSeed,
830
+ spin: resolveSpin(shard.spin, shard.id, warn4),
831
+ hover: resolveHover(shard.hover, shard.id, warn4),
832
+ collapse: resolveCollapse(shard.collapse, shard.id, warn4),
833
+ params
834
+ };
835
+ }
836
+ var DEFAULT_SPIN_SPEED = 0.1;
837
+ var HOVER_DEFAULTS = { scale: 1.15, opacity: 1, rotate: 0, spin: 0 };
838
+ var COLLAPSE_DEFAULTS = { scale: 0, opacity: 1 };
839
+ function resolveSpin(value, id, warn4) {
840
+ if (value === void 0 || value === null || value === false) return void 0;
841
+ if (value === true) return { speed: DEFAULT_SPIN_SPEED, phase: 0 };
842
+ if (typeof value === "number") {
843
+ if (!Number.isFinite(value) || value === 0) return void 0;
844
+ return { speed: value, phase: 0 };
845
+ }
846
+ if (typeof value === "object") {
847
+ const speed = Number(value.speed);
848
+ if (!Number.isFinite(speed)) {
849
+ warn4(`shard "${id}" spin.speed is not a number; spin ignored`);
850
+ return void 0;
851
+ }
852
+ if (speed === 0) return void 0;
853
+ const phase = Number(value.phase ?? 0);
854
+ return { speed, phase: Number.isFinite(phase) ? phase : 0 };
855
+ }
856
+ warn4(`shard "${id}" spin must be a number, boolean or object; spin ignored`);
857
+ return void 0;
858
+ }
859
+ function resolveHover(value, id, warn4) {
860
+ if (value === void 0 || value === null || value === false) return void 0;
861
+ if (value === true) return { ...HOVER_DEFAULTS };
862
+ if (typeof value === "object") {
863
+ return {
864
+ scale: finiteOr(value.scale, HOVER_DEFAULTS.scale),
865
+ opacity: finiteOr(value.opacity, HOVER_DEFAULTS.opacity),
866
+ rotate: finiteOr(value.rotate, HOVER_DEFAULTS.rotate),
867
+ spin: finiteOr(value.spin, HOVER_DEFAULTS.spin)
868
+ };
869
+ }
870
+ warn4(`shard "${id}" hover must be a boolean or object; hover ignored`);
871
+ return void 0;
872
+ }
873
+ function resolveCollapse(value, id, warn4) {
874
+ if (value === void 0 || value === null || value === false) return void 0;
875
+ if (value === true) return { ...COLLAPSE_DEFAULTS };
876
+ if (typeof value === "object") {
877
+ return {
878
+ scale: finiteOr(value.scale, COLLAPSE_DEFAULTS.scale),
879
+ opacity: finiteOr(value.opacity, COLLAPSE_DEFAULTS.opacity)
880
+ };
881
+ }
882
+ warn4(`shard "${id}" collapse must be a boolean or object; collapse ignored`);
883
+ return void 0;
884
+ }
885
+ function finiteOr(value, fallback) {
886
+ const n = Number(value);
887
+ return Number.isFinite(n) ? n : fallback;
888
+ }
889
+ function applyShardMotion(shards, warn4) {
890
+ const effects2 = [];
891
+ const originalChannels = shards.map((shard) => shard.channel);
892
+ const taken = new Set(originalChannels);
893
+ for (const shard of shards) {
894
+ if (!shard.spin && !shard.hover && !shard.collapse) continue;
895
+ const shared = originalChannels.filter((channel) => channel === shard.channel).length > 1;
896
+ if (shared) {
897
+ const name = uniqueChannel(shard.id, taken);
898
+ shard.channel = name;
899
+ taken.add(name);
900
+ if (!isCssIdentifier(name)) {
901
+ warn4(`shard "${shard.id}" got an invalid motion channel "${name}"`);
902
+ }
903
+ }
904
+ if (shard.spin) {
905
+ effects2.push({
906
+ type: "spin",
907
+ channels: [shard.channel],
908
+ speed: shard.spin.speed,
909
+ phase: shard.spin.phase
910
+ });
911
+ }
912
+ if (shard.hover) {
913
+ effects2.push({
914
+ type: "hover",
915
+ channels: [shard.channel],
916
+ scale: shard.hover.scale,
917
+ opacity: shard.hover.opacity,
918
+ rotate: shard.hover.rotate,
919
+ spin: shard.hover.spin
920
+ });
921
+ }
922
+ if (shard.collapse) {
923
+ effects2.push({
924
+ type: "collapse",
925
+ channels: [shard.channel],
926
+ scale: shard.collapse.scale,
927
+ opacity: shard.collapse.opacity
928
+ });
929
+ }
930
+ }
931
+ return effects2;
932
+ }
933
+ function uniqueChannel(id, taken) {
934
+ const base = isCssIdentifier(id) ? id : `shard-${id.replace(/[^_a-zA-Z0-9-]/g, "-").replace(/^-+/, "")}`;
935
+ let name = base;
936
+ let i = 2;
937
+ while (taken.has(name)) {
938
+ name = `${base}-${i}`;
939
+ i += 1;
940
+ }
941
+ return name;
942
+ }
943
+ function resolveRelativeAngles(shards) {
944
+ const byId = new Map(shards.map((s) => [s.id, s]));
945
+ for (const shard of shards) {
946
+ const relativeTo = shard.params.relativeTo;
947
+ if (typeof relativeTo !== "string") continue;
948
+ const target = byId.get(relativeTo);
949
+ if (!target) continue;
950
+ if (typeof target.params.angle === "number" && typeof shard.params.angle === "number") {
951
+ shard.params.angle += target.params.angle;
952
+ }
953
+ }
954
+ }
955
+ function resolveEffects(effects2, warn4) {
956
+ const out = [];
957
+ for (const effect of effects2) {
958
+ const def = getEffect(effect.type);
959
+ if (!def) {
960
+ warn4(`unknown effect "${effect.type}" skipped`);
961
+ continue;
962
+ }
963
+ const params = { type: effect.type };
964
+ for (const [name, schema] of Object.entries(def.params)) {
965
+ params[name] = coerce(
966
+ schema,
967
+ effect[name],
968
+ `effect ${effect.type}.${name}`,
969
+ warn4
970
+ );
971
+ }
972
+ out.push(params);
973
+ }
974
+ return out;
975
+ }
976
+ function coerce(schema, raw, label, warn4) {
977
+ switch (schema.type) {
978
+ case "number":
979
+ case "angle": {
980
+ let value = raw === void 0 || raw === null ? schema.default : Number(raw);
981
+ if (!Number.isFinite(value)) {
982
+ warn4(`${label} is not a number; using default ${schema.default}`);
983
+ value = schema.default;
984
+ }
985
+ if (schema.min !== void 0 && value < schema.min) {
986
+ warn4(`${label} (${value}) clamped to min ${schema.min}`);
987
+ value = schema.min;
988
+ }
989
+ if (schema.max !== void 0 && value > schema.max) {
990
+ warn4(`${label} (${value}) clamped to max ${schema.max}`);
991
+ value = schema.max;
992
+ }
993
+ return value;
994
+ }
995
+ case "boolean":
996
+ return raw === void 0 || raw === null ? schema.default : Boolean(raw);
997
+ case "color":
998
+ return raw === void 0 || raw === null ? schema.default : String(raw);
999
+ case "enum": {
1000
+ const value = raw === void 0 || raw === null ? schema.default : String(raw);
1001
+ if (!schema.values.includes(value)) {
1002
+ warn4(`${label} "${value}" is not one of ${schema.values.join(", ")}; using default`);
1003
+ return schema.default;
1004
+ }
1005
+ return value;
1006
+ }
1007
+ case "channels": {
1008
+ if (raw === void 0 || raw === null) return [...schema.default];
1009
+ if (!Array.isArray(raw)) {
1010
+ warn4(`${label} is not an array; using default`);
1011
+ return [...schema.default];
1012
+ }
1013
+ return raw.map((v) => String(v));
1014
+ }
1015
+ default:
1016
+ return raw;
1017
+ }
1018
+ }
1019
+ function makeWarn(enabled) {
1020
+ const on = enabled ?? (typeof process !== "undefined" ? process.env?.NODE_ENV !== "production" : true);
1021
+ if (!on) return () => {
1022
+ };
1023
+ return (message) => {
1024
+ console.warn(`[shardlight] ${message}`);
1025
+ };
1026
+ }
1027
+
1028
+ // src/core/config/migrate.ts
1029
+ var CURRENT_VERSION = 1;
1030
+ function migrateConfig(input) {
1031
+ if (!input || typeof input !== "object") {
1032
+ throw new Error("shardlight: migrateConfig expected a config object");
1033
+ }
1034
+ let config = { ...input };
1035
+ let version = typeof config.version === "number" ? config.version : CURRENT_VERSION;
1036
+ if (version > CURRENT_VERSION) {
1037
+ return config;
1038
+ }
1039
+ while (version < CURRENT_VERSION) {
1040
+ version += 1;
1041
+ config = { ...config, version };
1042
+ }
1043
+ if (config.version !== CURRENT_VERSION) config = { ...config, version: CURRENT_VERSION };
1044
+ return config;
1045
+ }
1046
+
1047
+ // src/core/effects/frame-values.ts
1048
+ function computeFrame(base, effects2, t, channelNames = []) {
1049
+ const map = /* @__PURE__ */ new Map();
1050
+ for (const name of channelNames) map.set(name, { scale: 1, opacity: 1, rotation: 0 });
1051
+ if (base.channels) {
1052
+ for (const [name, values] of Object.entries(base.channels)) {
1053
+ const current = map.get(name) ?? { scale: 1, opacity: 1, rotation: 0 };
1054
+ map.set(name, {
1055
+ scale: values.scale ?? current.scale,
1056
+ opacity: values.opacity ?? current.opacity,
1057
+ rotation: values.rotation ?? current.rotation
1058
+ });
1059
+ }
1060
+ }
1061
+ const out = {
1062
+ opacity: base.opacity ?? 1,
1063
+ hover: base.hover ?? 0,
1064
+ hoverTime: base.hoverTime ?? 0,
1065
+ collapse: base.collapse ?? 0,
1066
+ channel(name) {
1067
+ let channel = map.get(name);
1068
+ if (!channel) {
1069
+ channel = { scale: 1, opacity: 1, rotation: 0 };
1070
+ map.set(name, channel);
1071
+ }
1072
+ return channel;
1073
+ },
1074
+ channels() {
1075
+ return [...map.keys()];
1076
+ }
1077
+ };
1078
+ for (const effect of effects2) {
1079
+ const def = getEffect(effect.type);
1080
+ if (!def) continue;
1081
+ def.apply(t, effect, out);
1082
+ }
1083
+ return out;
1084
+ }
1085
+
1086
+ // src/core/bake/baker.ts
1087
+ var NullContextError = class extends Error {
1088
+ constructor() {
1089
+ super("shardlight: 2D context unavailable (canvas memory cap?)");
1090
+ this.name = "NullContextError";
1091
+ }
1092
+ };
1093
+
1094
+ // src/core/util/hash.ts
1095
+ function hashString(input) {
1096
+ let h = 2166136261;
1097
+ for (let i = 0; i < input.length; i++) {
1098
+ h ^= input.charCodeAt(i);
1099
+ h = Math.imul(h, 16777619);
1100
+ }
1101
+ return h >>> 0;
1102
+ }
1103
+
1104
+ // src/core/util/rng.ts
1105
+ function mulberry32(seed) {
1106
+ let a = seed >>> 0;
1107
+ return function next() {
1108
+ a = a + 1831565813 | 0;
1109
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
1110
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
1111
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
1112
+ };
1113
+ }
1114
+ function shardSeed(id, seed) {
1115
+ return (hashString(id) ^ seed >>> 0) >>> 0;
1116
+ }
1117
+
1118
+ // src/core/util/dom.ts
1119
+ function isBrowser() {
1120
+ return typeof window !== "undefined" && typeof document !== "undefined";
1121
+ }
1122
+ function createCanvas(width, height) {
1123
+ if (typeof OffscreenCanvas !== "undefined") {
1124
+ return new OffscreenCanvas(width, height);
1125
+ }
1126
+ if (typeof document !== "undefined") {
1127
+ const canvas = document.createElement("canvas");
1128
+ canvas.width = width;
1129
+ canvas.height = height;
1130
+ return canvas;
1131
+ }
1132
+ throw new Error("shardlight: no canvas implementation available in this environment");
1133
+ }
1134
+ function get2dContext(canvas) {
1135
+ try {
1136
+ return canvas.getContext("2d");
1137
+ } catch {
1138
+ return null;
1139
+ }
1140
+ }
1141
+ function freeCanvas(canvas) {
1142
+ try {
1143
+ canvas.width = 0;
1144
+ canvas.height = 0;
1145
+ } catch {
1146
+ }
1147
+ }
1148
+ function canvasToBlob(canvas) {
1149
+ if (typeof canvas.convertToBlob === "function") {
1150
+ return canvas.convertToBlob({ type: "image/png" });
1151
+ }
1152
+ const el = canvas;
1153
+ return new Promise((resolve, reject) => {
1154
+ el.toBlob((blob2) => {
1155
+ if (blob2) resolve(blob2);
1156
+ else reject(new Error("shardlight: canvas.toBlob returned null"));
1157
+ }, "image/png");
1158
+ });
1159
+ }
1160
+ function isLowMemoryDevice() {
1161
+ if (typeof navigator === "undefined") return false;
1162
+ const nav = navigator;
1163
+ if (typeof nav.deviceMemory === "number") return nav.deviceMemory <= 2;
1164
+ return false;
1165
+ }
1166
+
1167
+ // src/core/bake/canvas2d.ts
1168
+ var canvas2dBaker = {
1169
+ id: "canvas2d",
1170
+ async bake(layer, opts, signal) {
1171
+ throwIfAborted(signal);
1172
+ const resolution = Math.max(1, Math.floor(opts.resolution));
1173
+ const canvas = createCanvas(resolution, resolution);
1174
+ const ctx = get2dContext(canvas);
1175
+ if (!ctx) {
1176
+ freeCanvas(canvas);
1177
+ throw new NullContextError();
1178
+ }
1179
+ ctx.clearRect(0, 0, resolution, resolution);
1180
+ const unit = resolution / 2;
1181
+ const px = resolution / 1024;
1182
+ for (const shard of layer.shards) {
1183
+ throwIfAborted(signal);
1184
+ if (!shard.visible) continue;
1185
+ const def = getShardKind(shard.kind);
1186
+ if (!def) continue;
1187
+ const scratch = createCanvas(resolution, resolution);
1188
+ const scratchCtx = get2dContext(scratch);
1189
+ if (!scratchCtx) {
1190
+ freeCanvas(scratch);
1191
+ continue;
1192
+ }
1193
+ const params = applyRayScale(
1194
+ shard.params,
1195
+ def.params,
1196
+ opts.rayScale
1197
+ );
1198
+ const env = {
1199
+ unit,
1200
+ px,
1201
+ color: shard.color,
1202
+ rayScale: opts.rayScale,
1203
+ size: resolution,
1204
+ rng: mulberry32(shardSeed(shard.id, shard.seed)),
1205
+ rgba: makeRgba(shard.color)
1206
+ };
1207
+ scratchCtx.save();
1208
+ scratchCtx.translate(unit, unit);
1209
+ scratchCtx.rotate(layer.rotation * Math.PI / 180);
1210
+ try {
1211
+ def.draw(scratchCtx, params, env);
1212
+ } catch {
1213
+ }
1214
+ scratchCtx.restore();
1215
+ if (def.mask) {
1216
+ try {
1217
+ def.mask(scratchCtx, params, env, resolution);
1218
+ } catch {
1219
+ }
1220
+ }
1221
+ let blur = 0;
1222
+ if (def.blur) blur = def.blur(params, env);
1223
+ else if (typeof params.softness === "number") blur = params.softness;
1224
+ ctx.save();
1225
+ ctx.globalCompositeOperation = "lighter";
1226
+ if (blur > 0 && typeof ctx.filter === "string") ctx.filter = `blur(${blur}px)`;
1227
+ ctx.drawImage(scratch, 0, 0);
1228
+ ctx.restore();
1229
+ freeCanvas(scratch);
1230
+ }
1231
+ return encode(canvas, opts, signal);
1232
+ }
1233
+ };
1234
+ async function encode(canvas, opts, signal) {
1235
+ const accept = opts.accept;
1236
+ if (accept.includes("url")) {
1237
+ const blob3 = await canvasToBlob(canvas);
1238
+ freeCanvas(canvas);
1239
+ throwIfAborted(signal);
1240
+ return { type: "url", url: URL.createObjectURL(blob3) };
1241
+ }
1242
+ if (accept.includes("bitmap") && typeof createImageBitmap === "function") {
1243
+ try {
1244
+ const bitmap = await createImageBitmap(canvas, {
1245
+ premultiplyAlpha: "none",
1246
+ colorSpaceConversion: "none"
1247
+ });
1248
+ throwIfAborted(signal);
1249
+ freeCanvas(canvas);
1250
+ return { type: "bitmap", bitmap };
1251
+ } catch (error) {
1252
+ if (isAbort(error)) throw error;
1253
+ }
1254
+ }
1255
+ if (accept.includes("canvas") || accept.includes("bitmap")) {
1256
+ return { type: "canvas", canvas };
1257
+ }
1258
+ const blob2 = await canvasToBlob(canvas);
1259
+ freeCanvas(canvas);
1260
+ throwIfAborted(signal);
1261
+ return { type: "url", url: URL.createObjectURL(blob2) };
1262
+ }
1263
+ function throwIfAborted(signal) {
1264
+ if (signal.aborted) {
1265
+ const error = new Error("Aborted");
1266
+ error.name = "AbortError";
1267
+ throw error;
1268
+ }
1269
+ }
1270
+ function isAbort(error) {
1271
+ return error instanceof Error && error.name === "AbortError";
1272
+ }
1273
+
1274
+ // src/core/bake/cache.ts
1275
+ var sources = /* @__PURE__ */ new Map();
1276
+ var refs = /* @__PURE__ */ new Map();
1277
+ var pending = /* @__PURE__ */ new Map();
1278
+ function acquireSource(key, start) {
1279
+ refs.set(key, (refs.get(key) ?? 0) + 1);
1280
+ const existing = sources.get(key);
1281
+ if (existing) return Promise.resolve(existing);
1282
+ const inflight = pending.get(key);
1283
+ if (inflight) return inflight.promise;
1284
+ const controller = new AbortController();
1285
+ const entry = { controller, promise: void 0 };
1286
+ entry.promise = start(controller.signal).then(
1287
+ (source) => {
1288
+ if (pending.get(key) === entry) pending.delete(key);
1289
+ sources.set(key, source);
1290
+ if (!refs.has(key)) {
1291
+ sources.delete(key);
1292
+ disposeSource(source);
1293
+ }
1294
+ return source;
1295
+ },
1296
+ (error) => {
1297
+ if (pending.get(key) === entry) pending.delete(key);
1298
+ throw error;
1299
+ }
1300
+ );
1301
+ pending.set(key, entry);
1302
+ return entry.promise;
1303
+ }
1304
+ function releaseSource(key) {
1305
+ const next = (refs.get(key) ?? 0) - 1;
1306
+ if (next > 0) {
1307
+ refs.set(key, next);
1308
+ return;
1309
+ }
1310
+ refs.delete(key);
1311
+ const source = sources.get(key);
1312
+ if (source) {
1313
+ sources.delete(key);
1314
+ disposeSource(source);
1315
+ return;
1316
+ }
1317
+ const inflight = pending.get(key);
1318
+ if (inflight) {
1319
+ pending.delete(key);
1320
+ inflight.controller.abort();
1321
+ }
1322
+ }
1323
+ function disposeSource(source) {
1324
+ if (source.type === "url") {
1325
+ if (typeof URL !== "undefined" && typeof URL.revokeObjectURL === "function") {
1326
+ URL.revokeObjectURL(source.url);
1327
+ }
1328
+ } else if (source.type === "bitmap") {
1329
+ try {
1330
+ source.bitmap.close();
1331
+ } catch {
1332
+ }
1333
+ } else if (source.type === "canvas") {
1334
+ try {
1335
+ source.canvas.width = 0;
1336
+ source.canvas.height = 0;
1337
+ } catch {
1338
+ }
1339
+ }
1340
+ }
1341
+ function cacheSize() {
1342
+ return sources.size;
1343
+ }
1344
+ function clearCache() {
1345
+ for (const source of sources.values()) disposeSource(source);
1346
+ sources.clear();
1347
+ refs.clear();
1348
+ for (const inflight of pending.values()) inflight.controller.abort();
1349
+ pending.clear();
1350
+ }
1351
+
1352
+ // src/core/bake/scheduler.ts
1353
+ function createScheduler(options = {}) {
1354
+ const delay = options.yieldDelay ?? 0;
1355
+ const queue = [];
1356
+ let running = false;
1357
+ async function pump() {
1358
+ if (running) return;
1359
+ running = true;
1360
+ try {
1361
+ while (queue.length > 0) {
1362
+ const item = queue.shift();
1363
+ if (item.signal?.aborted) {
1364
+ item.reject(abortError());
1365
+ continue;
1366
+ }
1367
+ try {
1368
+ item.resolve(await item.task());
1369
+ } catch (error) {
1370
+ item.reject(error);
1371
+ }
1372
+ if (queue.length > 0) await sleep(delay);
1373
+ }
1374
+ } finally {
1375
+ running = false;
1376
+ }
1377
+ }
1378
+ return {
1379
+ run(task, signal) {
1380
+ return new Promise((resolve, reject) => {
1381
+ queue.push({
1382
+ task,
1383
+ resolve,
1384
+ reject,
1385
+ signal
1386
+ });
1387
+ void pump();
1388
+ });
1389
+ },
1390
+ clear() {
1391
+ queue.length = 0;
1392
+ }
1393
+ };
1394
+ }
1395
+ function abortError() {
1396
+ if (typeof DOMException !== "undefined") {
1397
+ return new DOMException("Aborted", "AbortError");
1398
+ }
1399
+ const error = new Error("Aborted");
1400
+ error.name = "AbortError";
1401
+ return error;
1402
+ }
1403
+ function isAbortError(error) {
1404
+ return error instanceof Error && error.name === "AbortError" || typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError";
1405
+ }
1406
+ function sleep(ms) {
1407
+ return new Promise((resolve) => setTimeout(resolve, ms));
1408
+ }
1409
+
1410
+ // src/core/util/stringify.ts
1411
+ function stableStringify(value) {
1412
+ return JSON.stringify(sortValue(value));
1413
+ }
1414
+ function sortValue(value) {
1415
+ if (Array.isArray(value)) return value.map(sortValue);
1416
+ if (value && typeof value === "object") {
1417
+ const obj = value;
1418
+ const out = {};
1419
+ for (const key of Object.keys(obj).sort()) out[key] = sortValue(obj[key]);
1420
+ return out;
1421
+ }
1422
+ if (typeof value === "number") {
1423
+ if (Object.is(value, -0)) return 0;
1424
+ if (Number.isNaN(value)) return "NaN";
1425
+ }
1426
+ return value;
1427
+ }
1428
+
1429
+ // src/core/model/layers.ts
1430
+ function buildLayers(light, inputs) {
1431
+ const groups = /* @__PURE__ */ new Map();
1432
+ for (const shard of light.shards) {
1433
+ if (!shard.visible) continue;
1434
+ const id = layerId(shard.channel, shard.blend);
1435
+ let group = groups.get(id);
1436
+ if (!group) {
1437
+ group = [];
1438
+ groups.set(id, group);
1439
+ }
1440
+ group.push(shard);
1441
+ }
1442
+ const layers = [];
1443
+ for (const [id, shards] of groups) {
1444
+ const [channel, blend] = id.split("|");
1445
+ const content = {
1446
+ shards: shards.map((s) => ({
1447
+ id: s.id,
1448
+ kind: s.kind,
1449
+ params: s.params,
1450
+ color: s.color,
1451
+ seed: s.seed
1452
+ })),
1453
+ color: light.color,
1454
+ rotation: light.rotation,
1455
+ seed: light.seed,
1456
+ resolution: inputs.resolution,
1457
+ rayScale: inputs.rayScale,
1458
+ baker: inputs.baker,
1459
+ accept: inputs.accept
1460
+ };
1461
+ layers.push({
1462
+ key: `sl_${hashString(stableStringify(content)).toString(36)}`,
1463
+ id,
1464
+ channel,
1465
+ blend,
1466
+ shards,
1467
+ color: light.color,
1468
+ rotation: light.rotation,
1469
+ seed: light.seed
1470
+ });
1471
+ }
1472
+ return layers;
1473
+ }
1474
+ function layerId(channel, blend) {
1475
+ return `${channel}|${blend}`;
1476
+ }
1477
+
1478
+ // src/core/bake/prewarm.ts
1479
+ var prewarmed = [];
1480
+ async function prewarm(options = {}) {
1481
+ const baker = options.baker ?? canvas2dBaker;
1482
+ const resolution = options.resolution ?? 1024;
1483
+ const rayScale = options.rayScale ?? 1;
1484
+ const accepts = options.accepts ?? ["url"];
1485
+ const config = options.config ? migrateConfig(options.config) : void 0;
1486
+ const light = resolveConfig(
1487
+ {
1488
+ preset: options.preset,
1489
+ config,
1490
+ shards: options.shards,
1491
+ effects: options.effects,
1492
+ color: options.color,
1493
+ rotation: options.rotation,
1494
+ seed: options.seed
1495
+ },
1496
+ { warn: options.warn }
1497
+ );
1498
+ const layers = buildLayers(light, {
1499
+ resolution,
1500
+ rayScale,
1501
+ baker: baker.id,
1502
+ accept: accepts
1503
+ });
1504
+ const scheduler = createScheduler();
1505
+ await Promise.all(
1506
+ layers.map((layer) => {
1507
+ prewarmed.push(layer.key);
1508
+ return acquireSource(
1509
+ layer.key,
1510
+ (signal) => scheduler.run(
1511
+ () => baker.bake(layer, { resolution, rayScale, accept: accepts }, signal),
1512
+ signal
1513
+ )
1514
+ );
1515
+ })
1516
+ );
1517
+ }
1518
+ function releasePrewarm() {
1519
+ for (const key of prewarmed) releaseSource(key);
1520
+ prewarmed.length = 0;
1521
+ }
1522
+
1523
+ // src/core/util/object.ts
1524
+ function definedOnly(value) {
1525
+ const out = {};
1526
+ for (const key of Object.keys(value)) {
1527
+ if (value[key] !== void 0) out[key] = value[key];
1528
+ }
1529
+ return out;
1530
+ }
1531
+
1532
+ // src/core/model/light-model.ts
1533
+ var DEFAULT_RESOLUTION = 1024;
1534
+ var DEFAULT_ACCEPTS = ["url"];
1535
+ var DEFAULT_HOVER_EASE = 0.25;
1536
+ var DEFAULT_COLLAPSE_EASE = 0.3;
1537
+ function createLightModel(options = {}) {
1538
+ let opts = {
1539
+ resolution: DEFAULT_RESOLUTION,
1540
+ rayScale: 1,
1541
+ baker: canvas2dBaker,
1542
+ accepts: DEFAULT_ACCEPTS,
1543
+ ...definedOnly(options)
1544
+ };
1545
+ const scheduler = createScheduler();
1546
+ const layerListeners = /* @__PURE__ */ new Set();
1547
+ const frameListeners = /* @__PURE__ */ new Set();
1548
+ const acquired = /* @__PURE__ */ new Map();
1549
+ const readyWaiters = [];
1550
+ let resolved = resolve(opts);
1551
+ let layers = [];
1552
+ let manual = {};
1553
+ let time = 0;
1554
+ let hover2 = 0;
1555
+ let hoverTarget = 0;
1556
+ let hoverTime = 0;
1557
+ let collapse2 = 0;
1558
+ let collapseTarget = 0;
1559
+ let disposed = false;
1560
+ function resolve(current) {
1561
+ const config = current.config ? migrateConfig(current.config) : void 0;
1562
+ return resolveConfig(
1563
+ {
1564
+ preset: current.preset,
1565
+ config,
1566
+ shards: current.shards,
1567
+ effects: current.effects,
1568
+ color: current.color,
1569
+ rotation: current.rotation,
1570
+ seed: current.seed
1571
+ },
1572
+ { warn: current.warn }
1573
+ );
1574
+ }
1575
+ function activeEffects() {
1576
+ if (!opts.reducedMotion) return resolved.effects;
1577
+ return resolved.effects.filter((effect) => getEffect(effect.type)?.reducedMotion === "keep");
1578
+ }
1579
+ function channelNames() {
1580
+ return [...new Set(layers.map((layer) => layer.channel))];
1581
+ }
1582
+ function hasEffect(type) {
1583
+ return resolved.effects.some((effect) => effect.type === type);
1584
+ }
1585
+ function emitLayers() {
1586
+ const snapshot = layers.slice();
1587
+ for (const listener of layerListeners) listener(snapshot);
1588
+ }
1589
+ function frame() {
1590
+ return computeFrame(
1591
+ { ...manual, hover: hover2, hoverTime, collapse: collapse2 },
1592
+ activeEffects(),
1593
+ time,
1594
+ channelNames()
1595
+ );
1596
+ }
1597
+ function emitFrame() {
1598
+ const values = frame();
1599
+ for (const listener of frameListeners) listener(values);
1600
+ }
1601
+ function pendingCount() {
1602
+ return layers.reduce((count, layer) => count + (layer.source ? 0 : 1), 0);
1603
+ }
1604
+ function flushReady() {
1605
+ if (pendingCount() !== 0) return;
1606
+ while (readyWaiters.length > 0) readyWaiters.shift()();
1607
+ }
1608
+ function bake(resolvedLayer, view) {
1609
+ acquireSource(
1610
+ view.key,
1611
+ (signal) => scheduler.run(
1612
+ () => opts.baker.bake(
1613
+ resolvedLayer,
1614
+ {
1615
+ resolution: opts.resolution ?? DEFAULT_RESOLUTION,
1616
+ rayScale: opts.rayScale ?? 1,
1617
+ accept: opts.accepts ?? DEFAULT_ACCEPTS
1618
+ },
1619
+ signal
1620
+ ),
1621
+ signal
1622
+ )
1623
+ ).then((source) => {
1624
+ if (disposed) return;
1625
+ const current = currentView(view);
1626
+ if (!current) return;
1627
+ current.source = source;
1628
+ emitLayers();
1629
+ flushReady();
1630
+ }).catch((error) => {
1631
+ if (disposed || isAbortError(error)) return;
1632
+ opts.onError?.(error);
1633
+ });
1634
+ }
1635
+ function currentView(view) {
1636
+ return layers.find((layer) => layer.id === view.id && layer.key === view.key);
1637
+ }
1638
+ function sync() {
1639
+ const desired = buildLayers(resolved, {
1640
+ resolution: opts.resolution ?? DEFAULT_RESOLUTION,
1641
+ rayScale: opts.rayScale ?? 1,
1642
+ baker: opts.baker.id,
1643
+ accept: opts.accepts ?? DEFAULT_ACCEPTS
1644
+ });
1645
+ const desiredKeys = new Set(desired.map((layer) => layer.key));
1646
+ for (const key of [...acquired.keys()]) {
1647
+ if (!desiredKeys.has(key)) {
1648
+ releaseSource(key);
1649
+ acquired.delete(key);
1650
+ }
1651
+ }
1652
+ const previous = new Map(layers.map((layer) => [layer.id, layer]));
1653
+ layers = desired.map((layer) => {
1654
+ const prev = previous.get(layer.id);
1655
+ return {
1656
+ id: layer.id,
1657
+ key: layer.key,
1658
+ channel: layer.channel,
1659
+ blend: layer.blend,
1660
+ source: prev && prev.key === layer.key ? prev.source : null
1661
+ };
1662
+ });
1663
+ emitLayers();
1664
+ for (let i = 0; i < desired.length; i++) {
1665
+ const layer = layers[i];
1666
+ if (acquired.has(layer.key)) continue;
1667
+ acquired.set(layer.key, 1);
1668
+ bake(desired[i], layer);
1669
+ }
1670
+ flushReady();
1671
+ }
1672
+ const model = {
1673
+ subscribe(listener) {
1674
+ layerListeners.add(listener);
1675
+ listener(layers.slice());
1676
+ return () => layerListeners.delete(listener);
1677
+ },
1678
+ onFrame(listener) {
1679
+ frameListeners.add(listener);
1680
+ listener(frame());
1681
+ return () => frameListeners.delete(listener);
1682
+ },
1683
+ update(patch) {
1684
+ if (disposed) return;
1685
+ opts = { ...opts, ...definedOnly(patch) };
1686
+ resolved = resolve(opts);
1687
+ sync();
1688
+ emitFrame();
1689
+ },
1690
+ set(values) {
1691
+ if (values.hover !== void 0) {
1692
+ hover2 = clamp01(values.hover);
1693
+ hoverTarget = hover2;
1694
+ }
1695
+ if (values.collapse !== void 0) {
1696
+ collapse2 = clamp01(values.collapse);
1697
+ collapseTarget = collapse2;
1698
+ }
1699
+ manual = {
1700
+ ...manual,
1701
+ ...values,
1702
+ channels: values.channels ? { ...manual.channels, ...values.channels } : manual.channels
1703
+ };
1704
+ emitFrame();
1705
+ },
1706
+ setChannel(channel, values) {
1707
+ model.set({ channels: { [channel]: values } });
1708
+ },
1709
+ setHover(hovered) {
1710
+ if (disposed) return;
1711
+ if (!hasEffect("hover")) return;
1712
+ hoverTarget = hovered ? 1 : 0;
1713
+ if ((opts.hoverEase ?? DEFAULT_HOVER_EASE) <= 0) {
1714
+ hover2 = hoverTarget;
1715
+ emitFrame();
1716
+ }
1717
+ },
1718
+ setCollapsed(collapsed) {
1719
+ if (disposed) return;
1720
+ if (!hasEffect("collapse")) return;
1721
+ collapseTarget = collapsed ? 1 : 0;
1722
+ if ((opts.collapseEase ?? DEFAULT_COLLAPSE_EASE) <= 0) {
1723
+ collapse2 = collapseTarget;
1724
+ emitFrame();
1725
+ }
1726
+ },
1727
+ toggleCollapsed() {
1728
+ model.setCollapsed(collapseTarget < 0.5);
1729
+ },
1730
+ tick(dt) {
1731
+ if (disposed) return;
1732
+ const idle = activeEffects().length === 0;
1733
+ if (idle && hover2 === hoverTarget && collapse2 === collapseTarget) return;
1734
+ time += dt;
1735
+ if (hover2 !== hoverTarget) {
1736
+ const ease = opts.hoverEase ?? DEFAULT_HOVER_EASE;
1737
+ if (ease <= 0) {
1738
+ hover2 = hoverTarget;
1739
+ } else {
1740
+ hover2 += (hoverTarget - hover2) * (1 - Math.exp(-dt / ease));
1741
+ if (Math.abs(hoverTarget - hover2) < 1e-3) hover2 = hoverTarget;
1742
+ }
1743
+ }
1744
+ if (collapse2 !== collapseTarget) {
1745
+ const ease = opts.collapseEase ?? DEFAULT_COLLAPSE_EASE;
1746
+ if (ease <= 0) {
1747
+ collapse2 = collapseTarget;
1748
+ } else {
1749
+ collapse2 += (collapseTarget - collapse2) * (1 - Math.exp(-dt / ease));
1750
+ if (Math.abs(collapseTarget - collapse2) < 1e-3) collapse2 = collapseTarget;
1751
+ }
1752
+ }
1753
+ hoverTime += hover2 * dt;
1754
+ emitFrame();
1755
+ },
1756
+ dispose() {
1757
+ if (disposed) return;
1758
+ disposed = true;
1759
+ scheduler.clear();
1760
+ for (const key of acquired.keys()) releaseSource(key);
1761
+ acquired.clear();
1762
+ layerListeners.clear();
1763
+ frameListeners.clear();
1764
+ flushReady();
1765
+ },
1766
+ get layers() {
1767
+ return layers;
1768
+ },
1769
+ get resolved() {
1770
+ return resolved;
1771
+ },
1772
+ get ready() {
1773
+ if (pendingCount() === 0) return Promise.resolve();
1774
+ return new Promise((resolveReady) => readyWaiters.push(resolveReady));
1775
+ }
1776
+ };
1777
+ sync();
1778
+ return model;
1779
+ }
1780
+ function clamp01(value) {
1781
+ return Math.max(0, Math.min(1, value));
1782
+ }
1783
+
1784
+ export { CURRENT_VERSION, NullContextError, abortError, acquireSource, blob, buildLayers, cacheSize, canvas2dBaker, canvasToBlob, clearCache, clusters, collapse, collapseTrigger, computeFrame, createCanvas, createLightModel, createScheduler, defineEffect, definePreset, defineShardKind, definedOnly, disposeSource, ember, fan, flicker, freeCanvas, get2dContext, getEffect, getPreset, getShardKind, halo, hashString, hover, isAbortError, isBrowser, isCssIdentifier, isLowMemoryDevice, layerId, listEffects, listPresets, listShardKinds, makeRgba, migrateConfig, mulberry32, parseColor, prewarm, pulse, registerBuiltInEffects, registerBuiltInKinds, registerBuiltInPresets, registerEffect, registerPreset, registerShardKind, releasePrewarm, releaseSource, resolveConfig, shardSeed, sparkle, spin, stableStringify, star, starburst, sun, unregisterEffect, unregisterPreset, unregisterShardKind };
1785
+ //# sourceMappingURL=chunk-ZWPHIC3K.js.map
1786
+ //# sourceMappingURL=chunk-ZWPHIC3K.js.map