@basmilius/sparkle 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/index.d.mts +306 -459
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +1106 -848
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +6 -2
  6. package/src/aurora/index.ts +9 -3
  7. package/src/aurora/layer.ts +57 -29
  8. package/src/balloons/index.ts +9 -3
  9. package/src/balloons/layer.ts +50 -19
  10. package/src/bubbles/index.ts +9 -3
  11. package/src/bubbles/layer.ts +30 -17
  12. package/src/canvas.ts +12 -0
  13. package/src/color.ts +11 -2
  14. package/src/confetti/index.ts +15 -3
  15. package/src/confetti/layer.ts +8 -5
  16. package/src/confetti/particle.ts +12 -11
  17. package/src/donuts/consts.ts +2 -2
  18. package/src/donuts/index.ts +9 -3
  19. package/src/donuts/layer.ts +43 -12
  20. package/src/effect.ts +107 -0
  21. package/src/fade.ts +87 -0
  22. package/src/fireflies/index.ts +9 -3
  23. package/src/fireflies/layer.ts +26 -9
  24. package/src/fireflies/particle.ts +2 -2
  25. package/src/firepit/index.ts +9 -3
  26. package/src/firepit/layer.ts +26 -7
  27. package/src/fireworks/create-explosion.ts +237 -0
  28. package/src/fireworks/explosion.ts +1 -1
  29. package/src/fireworks/index.ts +15 -3
  30. package/src/fireworks/layer.ts +55 -304
  31. package/src/fireworks/spark.ts +2 -2
  32. package/src/fireworks/types.ts +2 -2
  33. package/src/glitter/index.ts +9 -4
  34. package/src/glitter/layer.ts +15 -7
  35. package/src/glitter/types.ts +10 -0
  36. package/src/index.ts +3 -4
  37. package/src/lanterns/index.ts +9 -4
  38. package/src/lanterns/layer.ts +22 -10
  39. package/src/lanterns/types.ts +8 -0
  40. package/src/layer.ts +13 -11
  41. package/src/leaves/index.ts +9 -4
  42. package/src/leaves/layer.ts +21 -14
  43. package/src/leaves/types.ts +9 -0
  44. package/src/lightning/index.ts +9 -4
  45. package/src/lightning/layer.ts +4 -4
  46. package/src/lightning/system.ts +3 -3
  47. package/src/lightning/types.ts +10 -2
  48. package/src/matrix/index.ts +9 -4
  49. package/src/matrix/layer.ts +15 -7
  50. package/src/matrix/types.ts +9 -0
  51. package/src/orbits/index.ts +9 -4
  52. package/src/orbits/layer.ts +51 -21
  53. package/src/orbits/types.ts +12 -1
  54. package/src/particles/index.ts +9 -3
  55. package/src/particles/layer.ts +55 -12
  56. package/src/petals/index.ts +9 -3
  57. package/src/petals/layer.ts +29 -13
  58. package/src/plasma/index.ts +9 -3
  59. package/src/plasma/layer.ts +21 -6
  60. package/src/rain/index.ts +9 -3
  61. package/src/rain/layer.ts +30 -8
  62. package/src/sandstorm/index.ts +9 -3
  63. package/src/sandstorm/layer.ts +26 -9
  64. package/src/scene.ts +201 -0
  65. package/src/shooting-stars/system.ts +26 -24
  66. package/src/shooting-stars/types.ts +2 -1
  67. package/src/simulation-canvas.ts +40 -4
  68. package/src/snow/index.ts +9 -3
  69. package/src/snow/layer.ts +24 -11
  70. package/src/sparklers/index.ts +13 -3
  71. package/src/sparklers/layer.ts +61 -15
  72. package/src/stars/index.ts +9 -3
  73. package/src/stars/layer.ts +28 -22
  74. package/src/streamers/index.ts +9 -3
  75. package/src/streamers/layer.ts +18 -6
  76. package/src/streamers/types.ts +1 -1
  77. package/src/waves/index.ts +9 -3
  78. package/src/waves/layer.ts +42 -45
  79. package/src/waves/types.ts +1 -0
  80. package/src/wormhole/index.ts +9 -3
  81. package/src/wormhole/layer.ts +22 -6
  82. package/src/aurora/simulation.ts +0 -19
  83. package/src/balloons/simulation.ts +0 -19
  84. package/src/bubbles/simulation.ts +0 -20
  85. package/src/confetti/simulation.ts +0 -27
  86. package/src/donuts/simulation.ts +0 -25
  87. package/src/fireflies/simulation.ts +0 -18
  88. package/src/firepit/simulation.ts +0 -17
  89. package/src/fireworks/simulation.ts +0 -18
  90. package/src/glitter/simulation.ts +0 -19
  91. package/src/lanterns/simulation.ts +0 -17
  92. package/src/layered.ts +0 -185
  93. package/src/leaves/simulation.ts +0 -18
  94. package/src/lightning/simulation.ts +0 -17
  95. package/src/matrix/simulation.ts +0 -18
  96. package/src/orbits/simulation.ts +0 -19
  97. package/src/particles/simulation.ts +0 -26
  98. package/src/petals/simulation.ts +0 -18
  99. package/src/plasma/simulation.ts +0 -17
  100. package/src/rain/simulation.ts +0 -21
  101. package/src/sandstorm/simulation.ts +0 -18
  102. package/src/snow/simulation.ts +0 -17
  103. package/src/sparklers/simulation.ts +0 -30
  104. package/src/stars/simulation.ts +0 -22
  105. package/src/streamers/simulation.ts +0 -16
  106. package/src/waves/simulation.ts +0 -18
  107. package/src/wormhole/simulation.ts +0 -17
package/dist/index.mjs CHANGED
@@ -1,132 +1,4 @@
1
1
  import { hexToRGB, mulberry32 } from "@basmilius/utils";
2
- //#region src/layer.ts
3
- var SimulationLayer = class {
4
- fade = null;
5
- onResize(_width, _height) {}
6
- onMount(_canvas) {}
7
- onUnmount(_canvas) {}
8
- withFade(fade) {
9
- this.fade = fade;
10
- return this;
11
- }
12
- };
13
- //#endregion
14
- //#region src/aurora/consts.ts
15
- const MULBERRY$23 = mulberry32(13);
16
- //#endregion
17
- //#region src/aurora/layer.ts
18
- const DEFAULT_COLORS$4 = [
19
- "#9922ff",
20
- "#4455ff",
21
- "#0077ee",
22
- "#00aabb",
23
- "#22ddff"
24
- ];
25
- const TOP_HUE = 265;
26
- var AuroraLayer = class extends SimulationLayer {
27
- #speed;
28
- #intensity;
29
- #waveAmplitude;
30
- #verticalPosition;
31
- #time = 0;
32
- #bands = [];
33
- constructor(config = {}) {
34
- super();
35
- const bandCount = config.bands ?? 5;
36
- const colors = config.colors ?? DEFAULT_COLORS$4;
37
- this.#speed = config.speed ?? 1;
38
- this.#intensity = config.intensity ?? .8;
39
- this.#waveAmplitude = config.waveAmplitude ?? 1;
40
- this.#verticalPosition = config.verticalPosition ?? .68;
41
- const clusterCenters = [.35, .65];
42
- for (let i = 0; i < bandCount; i++) {
43
- const color = colors[i % colors.length];
44
- const [r, g, b] = hexToRGB(color);
45
- const hue = this.#rgbToHue(r, g, b);
46
- const cluster = clusterCenters[i % clusterCenters.length];
47
- this.#bands.push({
48
- x: cluster + (MULBERRY$23.next() - .5) * .22,
49
- baseY: this.#verticalPosition + (MULBERRY$23.next() - .5) * .08,
50
- height: .5 + MULBERRY$23.next() * .3,
51
- sigma: 160 + MULBERRY$23.next() * 110,
52
- phase1: MULBERRY$23.next() * Math.PI * 2,
53
- phase2: MULBERRY$23.next() * Math.PI * 2,
54
- amplitude1: .015 + MULBERRY$23.next() * .025,
55
- frequency1: .003 + MULBERRY$23.next() * .004,
56
- speed: (.4 + MULBERRY$23.next() * .6) * this.#speed,
57
- hue,
58
- opacity: (.5 + MULBERRY$23.next() * .3) * this.#intensity
59
- });
60
- }
61
- }
62
- tick(dt, width, height) {
63
- this.#time += .016 * dt * this.#speed;
64
- for (const band of this.#bands) {
65
- band.phase1 += .005 * band.speed * dt;
66
- band.phase2 += .008 * band.speed * dt;
67
- }
68
- }
69
- draw(ctx, width, height) {
70
- const bg = ctx.createLinearGradient(0, 0, 0, height);
71
- bg.addColorStop(0, "#000000");
72
- bg.addColorStop(.5, "#050012");
73
- bg.addColorStop(1, "#0a0025");
74
- ctx.fillStyle = bg;
75
- ctx.fillRect(0, 0, width, height);
76
- ctx.globalCompositeOperation = "screen";
77
- const step = 4;
78
- const scale = width / 1920;
79
- for (const band of this.#bands) {
80
- const swayX = band.amplitude1 * width * Math.sin(band.phase1);
81
- const cx = band.x * width + swayX;
82
- const baseY = band.baseY * height;
83
- const rayHeight = band.height * height * (height / 800);
84
- const sigma = band.sigma * scale;
85
- const cutoff = sigma * 3.5;
86
- const sigmaSq2 = 2 * sigma * sigma;
87
- const midHue = (band.hue + TOP_HUE) / 2;
88
- const waveRange = height * .035 * this.#waveAmplitude;
89
- const xStart = Math.max(0, Math.floor((cx - cutoff) / step) * step);
90
- const xEnd = Math.min(width, Math.ceil((cx + cutoff) / step) * step);
91
- for (let x = xStart; x < xEnd; x += step) {
92
- const dx = x - cx;
93
- const alpha = Math.exp(-dx * dx / sigmaSq2);
94
- if (alpha < .015) continue;
95
- const colBase = baseY + Math.sin(band.frequency1 * x + band.phase2) * waveRange;
96
- const colTop = colBase - rayHeight;
97
- const fadeBottom = colBase + rayHeight * .1;
98
- const eff = alpha * band.opacity;
99
- const gradient = ctx.createLinearGradient(0, fadeBottom, 0, colTop);
100
- gradient.addColorStop(0, `hsla(${band.hue}, 100%, 90%, 0)`);
101
- gradient.addColorStop(.04, `hsla(${band.hue}, 100%, 90%, ${eff * .55})`);
102
- gradient.addColorStop(.1, `hsla(${band.hue}, 90%, 72%, ${eff})`);
103
- gradient.addColorStop(.32, `hsla(${band.hue}, 85%, 62%, ${eff * .75})`);
104
- gradient.addColorStop(.62, `hsla(${midHue}, 80%, 56%, ${eff * .35})`);
105
- gradient.addColorStop(.86, `hsla(${TOP_HUE}, 75%, 50%, ${eff * .12})`);
106
- gradient.addColorStop(1, `hsla(${TOP_HUE}, 70%, 45%, 0)`);
107
- ctx.fillStyle = gradient;
108
- ctx.fillRect(x, colTop, step, fadeBottom - colTop + 1);
109
- }
110
- }
111
- ctx.globalCompositeOperation = "source-over";
112
- }
113
- #rgbToHue(r, g, b) {
114
- r /= 255;
115
- g /= 255;
116
- b /= 255;
117
- const max = Math.max(r, g, b);
118
- const delta = max - Math.min(r, g, b);
119
- if (delta === 0) return 0;
120
- let hue;
121
- if (max === r) hue = (g - b) / delta % 6;
122
- else if (max === g) hue = (b - r) / delta + 2;
123
- else hue = (r - g) / delta + 4;
124
- hue = Math.round(hue * 60);
125
- if (hue < 0) hue += 360;
126
- return hue;
127
- }
128
- };
129
- //#endregion
130
2
  //#region src/canvas.ts
131
3
  var LimitedFrameRateCanvas = class LimitedFrameRateCanvas {
132
4
  static #globalSpeed = 1;
@@ -217,6 +89,16 @@ var LimitedFrameRateCanvas = class LimitedFrameRateCanvas {
217
89
  this.#isStopped = true;
218
90
  cancelAnimationFrame(this.#frame);
219
91
  }
92
+ pause() {
93
+ this.#isStopped = true;
94
+ cancelAnimationFrame(this.#frame);
95
+ }
96
+ resume() {
97
+ if (this.#isStopped) {
98
+ this.#isStopped = false;
99
+ this.#frame = requestAnimationFrame(this.loop.bind(this));
100
+ }
101
+ }
220
102
  draw() {
221
103
  throw new Error("LimitedFrameRateCanvas::draw() should be overwritten.");
222
104
  }
@@ -245,18 +127,99 @@ var LimitedFrameRateCanvas = class LimitedFrameRateCanvas {
245
127
  }
246
128
  };
247
129
  //#endregion
130
+ //#region src/fade.ts
131
+ function parseSide(side) {
132
+ return typeof side === "number" ? [0, side] : side;
133
+ }
134
+ function applyEdgeFade(ctx, width, height, fade) {
135
+ ctx.globalCompositeOperation = "destination-out";
136
+ if (fade.top !== void 0) {
137
+ const [near, far] = parseSide(fade.top);
138
+ const nearPx = near * height;
139
+ const farPx = far * height;
140
+ if (nearPx > 0) {
141
+ ctx.fillStyle = "rgba(0,0,0,1)";
142
+ ctx.fillRect(0, 0, width, nearPx);
143
+ }
144
+ if (farPx > nearPx) {
145
+ const gradient = ctx.createLinearGradient(0, nearPx, 0, farPx);
146
+ gradient.addColorStop(0, "rgba(0,0,0,1)");
147
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
148
+ ctx.fillStyle = gradient;
149
+ ctx.fillRect(0, nearPx, width, farPx - nearPx);
150
+ }
151
+ }
152
+ if (fade.bottom !== void 0) {
153
+ const [near, far] = parseSide(fade.bottom);
154
+ const nearPx = near * height;
155
+ const farPx = far * height;
156
+ if (nearPx > 0) {
157
+ ctx.fillStyle = "rgba(0,0,0,1)";
158
+ ctx.fillRect(0, height - nearPx, width, nearPx);
159
+ }
160
+ if (farPx > nearPx) {
161
+ const gradient = ctx.createLinearGradient(0, height - farPx, 0, height - nearPx);
162
+ gradient.addColorStop(0, "rgba(0,0,0,0)");
163
+ gradient.addColorStop(1, "rgba(0,0,0,1)");
164
+ ctx.fillStyle = gradient;
165
+ ctx.fillRect(0, height - farPx, width, farPx - nearPx);
166
+ }
167
+ }
168
+ if (fade.left !== void 0) {
169
+ const [near, far] = parseSide(fade.left);
170
+ const nearPx = near * width;
171
+ const farPx = far * width;
172
+ if (nearPx > 0) {
173
+ ctx.fillStyle = "rgba(0,0,0,1)";
174
+ ctx.fillRect(0, 0, nearPx, height);
175
+ }
176
+ if (farPx > nearPx) {
177
+ const gradient = ctx.createLinearGradient(nearPx, 0, farPx, 0);
178
+ gradient.addColorStop(0, "rgba(0,0,0,1)");
179
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
180
+ ctx.fillStyle = gradient;
181
+ ctx.fillRect(nearPx, 0, farPx - nearPx, height);
182
+ }
183
+ }
184
+ if (fade.right !== void 0) {
185
+ const [near, far] = parseSide(fade.right);
186
+ const nearPx = near * width;
187
+ const farPx = far * width;
188
+ if (nearPx > 0) {
189
+ ctx.fillStyle = "rgba(0,0,0,1)";
190
+ ctx.fillRect(width - nearPx, 0, nearPx, height);
191
+ }
192
+ if (farPx > nearPx) {
193
+ const gradient = ctx.createLinearGradient(width - farPx, 0, width - nearPx, 0);
194
+ gradient.addColorStop(0, "rgba(0,0,0,0)");
195
+ gradient.addColorStop(1, "rgba(0,0,0,1)");
196
+ ctx.fillStyle = gradient;
197
+ ctx.fillRect(width - farPx, 0, farPx - nearPx, height);
198
+ }
199
+ }
200
+ ctx.globalCompositeOperation = "source-over";
201
+ }
202
+ //#endregion
248
203
  //#region src/simulation-canvas.ts
249
204
  var SimulationCanvas = class extends LimitedFrameRateCanvas {
250
205
  #simulation;
206
+ #contextOptions;
207
+ #offscreen = null;
208
+ #offscreenCtx = null;
251
209
  constructor(canvas, simulation, frameRate = 60, options = { colorSpace: "display-p3" }) {
252
210
  super(canvas, frameRate, options);
253
211
  this.#simulation = simulation;
212
+ this.#contextOptions = options;
254
213
  canvas.style.position = "absolute";
255
214
  canvas.style.top = "0";
256
215
  canvas.style.left = "0";
257
216
  canvas.style.height = "100%";
258
217
  canvas.style.width = "100%";
259
218
  }
219
+ withFade(fade) {
220
+ this.#simulation.fade = fade;
221
+ return this;
222
+ }
260
223
  start() {
261
224
  this.#simulation.onMount(this.canvas);
262
225
  super.start();
@@ -269,9 +232,17 @@ var SimulationCanvas = class extends LimitedFrameRateCanvas {
269
232
  this.canvas.height = this.height;
270
233
  this.canvas.width = this.width;
271
234
  const ctx = this.context;
272
- ctx.save();
273
- this.#simulation.draw(ctx, this.width, this.height);
274
- ctx.restore();
235
+ if (this.#simulation.fade) {
236
+ const offCtx = this.#getOffscreenCtx(this.width, this.height);
237
+ offCtx.clearRect(0, 0, this.width, this.height);
238
+ this.#simulation.draw(offCtx, this.width, this.height);
239
+ applyEdgeFade(offCtx, this.width, this.height, this.#simulation.fade);
240
+ ctx.drawImage(this.#offscreen, 0, 0);
241
+ } else {
242
+ ctx.save();
243
+ this.#simulation.draw(ctx, this.width, this.height);
244
+ ctx.restore();
245
+ }
275
246
  }
276
247
  tick() {
277
248
  const dt = (this.delta > 0 && this.delta < 200 ? this.delta / (1e3 / 60) : 1) * this.speed * LimitedFrameRateCanvas.globalSpeed;
@@ -279,32 +250,230 @@ var SimulationCanvas = class extends LimitedFrameRateCanvas {
279
250
  }
280
251
  onResize() {
281
252
  super.onResize();
253
+ if (this.#offscreen) {
254
+ this.#offscreen.width = this.width;
255
+ this.#offscreen.height = this.height;
256
+ }
282
257
  this.#simulation.onResize(this.width, this.height);
283
258
  }
259
+ #getOffscreenCtx(width, height) {
260
+ if (!this.#offscreen) {
261
+ this.#offscreen = document.createElement("canvas");
262
+ this.#offscreen.width = width;
263
+ this.#offscreen.height = height;
264
+ this.#offscreenCtx = this.#offscreen.getContext("2d", this.#contextOptions);
265
+ }
266
+ return this.#offscreenCtx;
267
+ }
284
268
  };
285
269
  //#endregion
286
- //#region src/aurora/simulation.ts
287
- var AuroraSimulation = class extends SimulationCanvas {
288
- constructor(canvas, config = {}) {
289
- super(canvas, new AuroraLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
270
+ //#region src/effect.ts
271
+ /**
272
+ * Base class for all visual effects. Implements the internal SimulationLayer interface
273
+ * so that effects can be used both standalone (via mount()) and composed in a Scene.
274
+ *
275
+ * @example Standalone usage
276
+ * const snow = new Snow({ particles: 200 });
277
+ * snow.mount(canvas).start();
278
+ *
279
+ * @example Scene composition
280
+ * const scene = new Scene()
281
+ * .mount(canvas)
282
+ * .layer(new Aurora())
283
+ * .layer(new Snow())
284
+ * .start();
285
+ */
286
+ var Effect = class {
287
+ #canvas = null;
288
+ fade = null;
289
+ configure(_config) {}
290
+ onResize(_width, _height) {}
291
+ onMount(_canvas) {}
292
+ onUnmount(_canvas) {}
293
+ /**
294
+ * Apply an edge fade mask when rendering this effect standalone or in a Scene.
295
+ */
296
+ withFade(fade) {
297
+ this.fade = fade;
298
+ return this;
299
+ }
300
+ /**
301
+ * Mount this effect to a canvas element or CSS selector, creating the render loop.
302
+ * Must be called before start().
303
+ */
304
+ mount(canvas, options = { colorSpace: "display-p3" }) {
305
+ if (typeof canvas === "string") {
306
+ const el = document.querySelector(canvas);
307
+ if (!el) throw new Error(`Effect.mount(): no element found for selector "${canvas}".`);
308
+ canvas = el;
309
+ }
310
+ this.#canvas = new SimulationCanvas(canvas, this, 60, options);
311
+ return this;
312
+ }
313
+ /**
314
+ * Remove this effect from its canvas and clean up the render loop.
315
+ */
316
+ unmount() {
317
+ this.#canvas?.destroy();
318
+ this.#canvas = null;
319
+ return this;
320
+ }
321
+ /**
322
+ * Start the render loop. Call mount() first.
323
+ */
324
+ start() {
325
+ this.#canvas?.start();
326
+ return this;
327
+ }
328
+ /**
329
+ * Pause rendering without destroying state. Use resume() to continue.
330
+ */
331
+ pause() {
332
+ this.#canvas?.pause();
333
+ return this;
334
+ }
335
+ /**
336
+ * Resume rendering after a pause().
337
+ */
338
+ resume() {
339
+ this.#canvas?.resume();
340
+ return this;
341
+ }
342
+ /**
343
+ * Stop rendering and call onUnmount(). Safe to call multiple times.
344
+ */
345
+ destroy() {
346
+ this.unmount();
290
347
  }
291
348
  };
292
349
  //#endregion
293
- //#region src/color.ts
294
- function parseColor(fillStyle) {
295
- const canvas = document.createElement("canvas");
296
- canvas.width = 1;
297
- canvas.height = 1;
298
- const ctx = canvas.getContext("2d");
299
- ctx.fillStyle = fillStyle;
300
- ctx.fillRect(0, 0, 1, 1);
301
- const data = ctx.getImageData(0, 0, 1, 1).data;
302
- return {
303
- r: data[0],
304
- g: data[1],
305
- b: data[2],
306
- a: data[3] / 255
307
- };
350
+ //#region src/aurora/consts.ts
351
+ const MULBERRY$23 = mulberry32(13);
352
+ //#endregion
353
+ //#region src/aurora/layer.ts
354
+ const DEFAULT_COLORS$4 = [
355
+ "#9922ff",
356
+ "#4455ff",
357
+ "#0077ee",
358
+ "#00aabb",
359
+ "#22ddff"
360
+ ];
361
+ const TOP_HUE = 265;
362
+ var Aurora = class extends Effect {
363
+ #speed;
364
+ #intensity;
365
+ #waveAmplitude;
366
+ #verticalPosition;
367
+ #bands = [];
368
+ constructor(config = {}) {
369
+ super();
370
+ const bandCount = config.bands ?? 5;
371
+ const colors = config.colors ?? DEFAULT_COLORS$4;
372
+ this.#speed = config.speed ?? 1;
373
+ this.#intensity = config.intensity ?? .8;
374
+ this.#waveAmplitude = config.waveAmplitude ?? 1;
375
+ this.#verticalPosition = config.verticalPosition ?? .68;
376
+ const clusterCenters = [.35, .65];
377
+ for (let i = 0; i < bandCount; i++) {
378
+ const color = colors[i % colors.length];
379
+ const [r, g, b] = hexToRGB(color);
380
+ const hue = this.#rgbToHue(r, g, b);
381
+ const cluster = clusterCenters[i % clusterCenters.length];
382
+ this.#bands.push({
383
+ x: cluster + (MULBERRY$23.next() - .5) * .22,
384
+ baseY: (MULBERRY$23.next() - .5) * .08,
385
+ height: .5 + MULBERRY$23.next() * .3,
386
+ sigma: 160 + MULBERRY$23.next() * 110,
387
+ phase1: MULBERRY$23.next() * Math.PI * 2,
388
+ phase2: MULBERRY$23.next() * Math.PI * 2,
389
+ amplitude1: .015 + MULBERRY$23.next() * .025,
390
+ frequency1: .003 + MULBERRY$23.next() * .004,
391
+ speed: .4 + MULBERRY$23.next() * .6,
392
+ hue,
393
+ opacity: .5 + MULBERRY$23.next() * .3
394
+ });
395
+ }
396
+ }
397
+ configure(config) {
398
+ if (config.speed !== void 0) this.#speed = config.speed;
399
+ if (config.intensity !== void 0) this.#intensity = config.intensity;
400
+ if (config.waveAmplitude !== void 0) this.#waveAmplitude = config.waveAmplitude;
401
+ if (config.verticalPosition !== void 0) this.#verticalPosition = config.verticalPosition;
402
+ }
403
+ tick(dt, _width, _height) {
404
+ for (const band of this.#bands) {
405
+ band.phase1 += .005 * band.speed * this.#speed * dt;
406
+ band.phase2 += .008 * band.speed * this.#speed * dt;
407
+ }
408
+ }
409
+ draw(ctx, width, height) {
410
+ const bg = ctx.createLinearGradient(0, 0, 0, height);
411
+ bg.addColorStop(0, "#000000");
412
+ bg.addColorStop(.5, "#050012");
413
+ bg.addColorStop(1, "#0a0025");
414
+ ctx.fillStyle = bg;
415
+ ctx.fillRect(0, 0, width, height);
416
+ ctx.globalCompositeOperation = "screen";
417
+ const step = 4;
418
+ const scale = width / 1920;
419
+ for (const band of this.#bands) {
420
+ const swayX = band.amplitude1 * width * Math.sin(band.phase1);
421
+ const cx = band.x * width + swayX;
422
+ const baseY = (this.#verticalPosition + band.baseY) * height;
423
+ const rayHeight = band.height * height * (height / 800);
424
+ const sigma = band.sigma * scale;
425
+ const cutoff = sigma * 3.5;
426
+ const sigmaSq2 = 2 * sigma * sigma;
427
+ const midHue = (band.hue + TOP_HUE) / 2;
428
+ const waveRange = height * .035 * this.#waveAmplitude;
429
+ const xStart = Math.max(0, Math.floor((cx - cutoff) / step) * step);
430
+ const xEnd = Math.min(width, Math.ceil((cx + cutoff) / step) * step);
431
+ const centreBase = baseY + Math.sin(band.frequency1 * cx + band.phase2) * waveRange;
432
+ const centreTop = centreBase - rayHeight;
433
+ const centreFadeBottom = centreBase + rayHeight * .1;
434
+ const gradient = ctx.createLinearGradient(0, centreFadeBottom, 0, centreTop);
435
+ gradient.addColorStop(0, `hsla(${band.hue}, 100%, 90%, 0)`);
436
+ gradient.addColorStop(.04, `hsla(${band.hue}, 100%, 90%, 0.55)`);
437
+ gradient.addColorStop(.1, `hsla(${band.hue}, 90%, 72%, 1)`);
438
+ gradient.addColorStop(.32, `hsla(${band.hue}, 85%, 62%, 0.75)`);
439
+ gradient.addColorStop(.62, `hsla(${midHue}, 80%, 56%, 0.35)`);
440
+ gradient.addColorStop(.86, `hsla(${TOP_HUE}, 75%, 50%, 0.12)`);
441
+ gradient.addColorStop(1, `hsla(${TOP_HUE}, 70%, 45%, 0)`);
442
+ ctx.fillStyle = gradient;
443
+ for (let x = xStart; x < xEnd; x += step) {
444
+ const dx = x - cx;
445
+ const alpha = Math.exp(-dx * dx / sigmaSq2);
446
+ if (alpha < .015) continue;
447
+ const colBase = baseY + Math.sin(band.frequency1 * x + band.phase2) * waveRange;
448
+ const colTop = colBase - rayHeight;
449
+ const fadeBottom = colBase + rayHeight * .1;
450
+ ctx.globalAlpha = alpha * band.opacity * this.#intensity;
451
+ ctx.fillRect(x, colTop, step, fadeBottom - colTop + 1);
452
+ }
453
+ ctx.globalAlpha = 1;
454
+ }
455
+ ctx.globalCompositeOperation = "source-over";
456
+ }
457
+ #rgbToHue(r, g, b) {
458
+ r /= 255;
459
+ g /= 255;
460
+ b /= 255;
461
+ const max = Math.max(r, g, b);
462
+ const delta = max - Math.min(r, g, b);
463
+ if (delta === 0) return 0;
464
+ let hue;
465
+ if (max === r) hue = (g - b) / delta % 6;
466
+ else if (max === g) hue = (b - r) / delta + 2;
467
+ else hue = (r - g) / delta + 4;
468
+ hue = Math.round(hue * 60);
469
+ if (hue < 0) hue += 360;
470
+ return hue;
471
+ }
472
+ };
473
+ //#endregion
474
+ //#region src/aurora/index.ts
475
+ function createAurora(config) {
476
+ return new Aurora(config);
308
477
  }
309
478
  //#endregion
310
479
  //#region src/balloons/consts.ts
@@ -319,7 +488,7 @@ const DEFAULT_COLORS$3 = [
319
488
  "#ff88cc",
320
489
  "#8844ff"
321
490
  ];
322
- var BalloonLayer = class extends SimulationLayer {
491
+ var Balloons = class extends Effect {
323
492
  #scale;
324
493
  #speed;
325
494
  #driftAmount;
@@ -341,6 +510,11 @@ var BalloonLayer = class extends SimulationLayer {
341
510
  if (innerWidth < 991) this.#maxCount = Math.floor(this.#maxCount / 2);
342
511
  for (let i = 0; i < this.#maxCount; ++i) this.#balloons.push(this.#createBalloon(true));
343
512
  }
513
+ configure(config) {
514
+ if (config.speed !== void 0) this.#speed = config.speed;
515
+ if (config.driftAmount !== void 0) this.#driftAmount = config.driftAmount;
516
+ if (config.stringLength !== void 0) this.#stringLengthMul = config.stringLength;
517
+ }
344
518
  tick(dt, width, height) {
345
519
  this.#time += .015 * dt * this.#speed;
346
520
  for (let i = 0; i < this.#balloons.length; i++) {
@@ -359,9 +533,9 @@ var BalloonLayer = class extends SimulationLayer {
359
533
  const rx = balloon.radiusX * this.#scale;
360
534
  const ry = balloon.radiusY * this.#scale;
361
535
  const [r, g, b] = balloon.color;
362
- ctx.save();
363
- ctx.translate(px, py);
364
- ctx.rotate(balloon.rotation);
536
+ const cos = Math.cos(balloon.rotation);
537
+ const sin = Math.sin(balloon.rotation);
538
+ ctx.setTransform(cos, sin, -sin, cos, px, py);
365
539
  const gradient = ctx.createRadialGradient(-rx * .3, -ry * .3, rx * .1, 0, 0, Math.max(rx, ry));
366
540
  gradient.addColorStop(0, `rgba(${Math.min(255, r + 80)}, ${Math.min(255, g + 80)}, ${Math.min(255, b + 80)}, 0.95)`);
367
541
  gradient.addColorStop(.6, `rgba(${r}, ${g}, ${b}, 0.9)`);
@@ -383,15 +557,21 @@ var BalloonLayer = class extends SimulationLayer {
383
557
  ctx.fillStyle = `rgba(${Math.max(0, r - 30)}, ${Math.max(0, g - 30)}, ${Math.max(0, b - 30)}, 0.9)`;
384
558
  ctx.fill();
385
559
  const stringLen = balloon.stringLength * this.#scale * this.#stringLengthMul;
386
- const drift = Math.sin(this.#time * 2 + balloon.driftPhase) * 8 * this.#scale * this.#driftAmount;
560
+ const knotBaseY = knotY + 5 * this.#scale;
561
+ const ph = balloon.driftPhase;
562
+ const fr = balloon.driftFreq;
563
+ const swingAmt = 10 * this.#scale * this.#driftAmount;
564
+ const midSwing = Math.sin(this.#time * fr + ph - .3) * swingAmt * .55;
565
+ const tipSwing = Math.sin(this.#time * fr + ph - .8) * swingAmt;
566
+ const flutter = Math.sin(this.#time * fr * 2.5 + ph * 1.4 + 1.8) * 2.5 * this.#scale;
387
567
  ctx.beginPath();
388
- ctx.moveTo(0, knotY + 5 * this.#scale);
389
- ctx.quadraticCurveTo(drift, knotY + 5 * this.#scale + stringLen * .5, -drift * .5, knotY + 5 * this.#scale + stringLen);
568
+ ctx.moveTo(0, knotBaseY);
569
+ ctx.bezierCurveTo(midSwing * .35, knotBaseY + stringLen * .3, midSwing + flutter * .5, knotBaseY + stringLen * .65, tipSwing + flutter, knotBaseY + stringLen);
390
570
  ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 0.4)`;
391
571
  ctx.lineWidth = 1;
392
572
  ctx.stroke();
393
- ctx.restore();
394
573
  }
574
+ ctx.resetTransform();
395
575
  }
396
576
  #createBalloon(initialSpread) {
397
577
  const colorIndex = Math.floor(MULBERRY$22.next() * this.#colorRGBs.length);
@@ -496,12 +676,32 @@ var BalloonParticle = class {
496
676
  }
497
677
  };
498
678
  //#endregion
499
- //#region src/balloons/simulation.ts
500
- var BalloonSimulation = class extends SimulationCanvas {
501
- constructor(canvas, config = {}) {
502
- super(canvas, new BalloonLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
503
- }
504
- };
679
+ //#region src/balloons/index.ts
680
+ function createBalloons(config) {
681
+ return new Balloons(config);
682
+ }
683
+ //#endregion
684
+ //#region src/color.ts
685
+ const cache = /* @__PURE__ */ new Map();
686
+ function parseColor(fillStyle) {
687
+ const cached = cache.get(fillStyle);
688
+ if (cached) return cached;
689
+ const canvas = document.createElement("canvas");
690
+ canvas.width = 1;
691
+ canvas.height = 1;
692
+ const ctx = canvas.getContext("2d");
693
+ ctx.fillStyle = fillStyle;
694
+ ctx.fillRect(0, 0, 1, 1);
695
+ const data = ctx.getImageData(0, 0, 1, 1).data;
696
+ const result = {
697
+ r: data[0],
698
+ g: data[1],
699
+ b: data[2],
700
+ a: data[3] / 255
701
+ };
702
+ cache.set(fillStyle, result);
703
+ return result;
704
+ }
505
705
  //#endregion
506
706
  //#region src/bubbles/consts.ts
507
707
  const MULBERRY$21 = mulberry32(13);
@@ -512,7 +712,7 @@ const DEFAULT_COLORS$2 = [
512
712
  "#aaddff",
513
713
  "#ccbbff"
514
714
  ];
515
- var BubbleLayer = class extends SimulationLayer {
715
+ var Bubbles = class extends Effect {
516
716
  #scale;
517
717
  #speed;
518
718
  #sizeRange;
@@ -548,6 +748,10 @@ var BubbleLayer = class extends SimulationLayer {
548
748
  canvas.removeEventListener("click", this.#onClickBound);
549
749
  this.#canvas = null;
550
750
  }
751
+ configure(config) {
752
+ if (config.speed !== void 0) this.#speed = config.speed;
753
+ if (config.wobbleAmount !== void 0) this.#wobbleAmount = config.wobbleAmount;
754
+ }
551
755
  tick(dt, width, height) {
552
756
  this.#time += .01 * dt;
553
757
  for (let i = 0; i < this.#bubbles.length; i++) {
@@ -646,16 +850,10 @@ var BubbleLayer = class extends SimulationLayer {
646
850
  }
647
851
  }
648
852
  #colorToHue(color) {
649
- const canvas = document.createElement("canvas");
650
- canvas.width = 1;
651
- canvas.height = 1;
652
- const ctx = canvas.getContext("2d");
653
- ctx.fillStyle = color;
654
- ctx.fillRect(0, 0, 1, 1);
655
- const data = ctx.getImageData(0, 0, 1, 1).data;
656
- let r = data[0] / 255;
657
- let g = data[1] / 255;
658
- let b = data[2] / 255;
853
+ const { r: r255, g: g255, b: b255 } = parseColor(color);
854
+ let r = r255 / 255;
855
+ let g = g255 / 255;
856
+ let b = b255 / 255;
659
857
  const max = Math.max(r, g, b);
660
858
  const delta = max - Math.min(r, g, b);
661
859
  if (delta === 0) return 0;
@@ -669,12 +867,10 @@ var BubbleLayer = class extends SimulationLayer {
669
867
  }
670
868
  };
671
869
  //#endregion
672
- //#region src/bubbles/simulation.ts
673
- var BubbleSimulation = class extends SimulationCanvas {
674
- constructor(canvas, config = {}) {
675
- super(canvas, new BubbleLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
676
- }
677
- };
870
+ //#region src/bubbles/index.ts
871
+ function createBubbles(config) {
872
+ return new Bubbles(config);
873
+ }
678
874
  //#endregion
679
875
  //#region src/confetti/consts.ts
680
876
  const PALETTES = {
@@ -838,7 +1034,7 @@ const SHAPE_PATHS = {
838
1034
  //#endregion
839
1035
  //#region src/confetti/layer.ts
840
1036
  const TWO_PI = Math.PI * 2;
841
- var ConfettiLayer = class extends SimulationLayer {
1037
+ var Confetti = class extends Effect {
842
1038
  #scale;
843
1039
  #particles = [];
844
1040
  #width = 0;
@@ -852,7 +1048,7 @@ var ConfettiLayer = class extends SimulationLayer {
852
1048
  this.#width = width;
853
1049
  this.#height = height;
854
1050
  }
855
- fire(config) {
1051
+ burst(config) {
856
1052
  const width = this.#width;
857
1053
  const height = this.#height;
858
1054
  const resolved = {
@@ -987,28 +1183,28 @@ var ConfettiParticle = class {
987
1183
  const scale = config.scale ?? 1;
988
1184
  const spread = config.spread ?? 45;
989
1185
  const startVelocity = (config.startVelocity ?? 45) * scale;
990
- const launchAngle = -(direction * Math.PI / 180) + .5 * spread * Math.PI / 180 - Math.random() * spread * Math.PI / 180;
991
- const speed = startVelocity * (.5 + Math.random());
992
- const rotAngle = Math.random() * Math.PI * 2;
1186
+ const launchAngle = -(direction * Math.PI / 180) + .5 * spread * Math.PI / 180 - MULBERRY$20.next() * spread * Math.PI / 180;
1187
+ const speed = startVelocity * (.5 + MULBERRY$20.next());
1188
+ const rotAngle = MULBERRY$20.next() * Math.PI * 2;
993
1189
  this.#colorStr = color;
994
1190
  this.#gravity = (config.gravity ?? 1) * scale;
995
1191
  this.#shape = shape;
996
- this.#size = (5 + Math.random() * 5) * scale;
1192
+ this.#size = (5 + MULBERRY$20.next() * 5) * scale;
997
1193
  this.#totalTicks = config.ticks ?? 200;
998
1194
  this.#x = position.x;
999
1195
  this.#y = position.y;
1000
1196
  this.#vx = Math.cos(launchAngle) * speed;
1001
1197
  this.#vy = Math.sin(launchAngle) * speed;
1002
- this.#decay = (config.decay ?? .9) - .05 + Math.random() * .1;
1003
- this.#flipAngle = Math.random() * Math.PI * 2;
1004
- this.#flipSpeed = .03 + Math.random() * .05;
1198
+ this.#decay = (config.decay ?? .9) - .05 + MULBERRY$20.next() * .1;
1199
+ this.#flipAngle = MULBERRY$20.next() * Math.PI * 2;
1200
+ this.#flipSpeed = .03 + MULBERRY$20.next() * .05;
1005
1201
  this.#rotAngle = rotAngle;
1006
1202
  this.#rotCos = Math.cos(rotAngle);
1007
1203
  this.#rotSin = Math.sin(rotAngle);
1008
- this.#rotSpeed = (Math.random() - .5) * .06;
1009
- this.#swing = Math.random() * Math.PI * 2;
1010
- this.#swingAmp = .5 + Math.random() * 1.5;
1011
- this.#swingSpeed = .025 + Math.random() * .035;
1204
+ this.#rotSpeed = (MULBERRY$20.next() - .5) * .06;
1205
+ this.#swing = MULBERRY$20.next() * Math.PI * 2;
1206
+ this.#swingAmp = .5 + MULBERRY$20.next() * 1.5;
1207
+ this.#swingSpeed = .025 + MULBERRY$20.next() * .035;
1012
1208
  }
1013
1209
  draw(ctx) {
1014
1210
  ctx.save();
@@ -1034,20 +1230,10 @@ var ConfettiParticle = class {
1034
1230
  }
1035
1231
  };
1036
1232
  //#endregion
1037
- //#region src/confetti/simulation.ts
1038
- var ConfettiSimulation = class extends SimulationCanvas {
1039
- #layer;
1040
- constructor(canvas, config = {}) {
1041
- const layer = new ConfettiLayer(config);
1042
- super(canvas, layer, 60, config.canvasOptions ?? { colorSpace: "display-p3" });
1043
- this.#layer = layer;
1044
- }
1045
- fire(config) {
1046
- this.onResize();
1047
- this.#layer.fire(config);
1048
- if (!this.isTicking) this.start();
1049
- }
1050
- };
1233
+ //#region src/confetti/index.ts
1234
+ function createConfetti(config) {
1235
+ return new Confetti(config);
1236
+ }
1051
1237
  //#endregion
1052
1238
  //#region src/donuts/consts.ts
1053
1239
  const MULBERRY$19 = mulberry32(13);
@@ -1067,7 +1253,7 @@ const DEFAULT_CONFIG = {
1067
1253
  };
1068
1254
  //#endregion
1069
1255
  //#region src/donuts/layer.ts
1070
- var DonutLayer = class extends SimulationLayer {
1256
+ var Donuts = class extends Effect {
1071
1257
  #background;
1072
1258
  #collisionPadding;
1073
1259
  #colors;
@@ -1128,6 +1314,12 @@ var DonutLayer = class extends SimulationLayer {
1128
1314
  canvas.removeEventListener("mousemove", this.#onMouseMoveBound);
1129
1315
  canvas.removeEventListener("mouseleave", this.#onMouseLeaveBound);
1130
1316
  }
1317
+ configure(config) {
1318
+ if (config.mouseAvoidance !== void 0) this.#mouseAvoidance = config.mouseAvoidance;
1319
+ if (config.mouseAvoidanceRadius !== void 0) this.#mouseAvoidanceRadius = config.mouseAvoidanceRadius;
1320
+ if (config.mouseAvoidanceStrength !== void 0) this.#mouseAvoidanceStrength = config.mouseAvoidanceStrength;
1321
+ if (config.repulsionStrength !== void 0) this.#repulsionStrength = config.repulsionStrength;
1322
+ }
1131
1323
  tick(dt, width, height) {
1132
1324
  this.#width = width;
1133
1325
  this.#height = height;
@@ -1140,17 +1332,17 @@ var DonutLayer = class extends SimulationLayer {
1140
1332
  ctx.fillStyle = this.#background;
1141
1333
  ctx.fillRect(0, 0, width, height);
1142
1334
  for (const donut of this.#donuts) {
1143
- ctx.save();
1144
- ctx.translate(donut.x, donut.y);
1145
- ctx.rotate(donut.angle);
1335
+ const cos = Math.cos(donut.angle);
1336
+ const sin = Math.sin(donut.angle);
1337
+ ctx.setTransform(cos, sin, -sin, cos, donut.x, donut.y);
1146
1338
  ctx.beginPath();
1147
1339
  ctx.arc(0, 0, donut.outerRadius, 0, Math.PI * 2);
1148
1340
  ctx.arc(0, 0, donut.innerRadius, 0, Math.PI * 2, true);
1149
1341
  ctx.closePath();
1150
1342
  ctx.fillStyle = donut.color;
1151
1343
  ctx.fill();
1152
- ctx.restore();
1153
1344
  }
1345
+ ctx.resetTransform();
1154
1346
  }
1155
1347
  #updateDonut(donut, dt) {
1156
1348
  if (Math.sqrt(donut.vx * donut.vx + donut.vy * donut.vy) > donut.speed) {
@@ -1270,12 +1462,10 @@ var DonutLayer = class extends SimulationLayer {
1270
1462
  }
1271
1463
  };
1272
1464
  //#endregion
1273
- //#region src/donuts/simulation.ts
1274
- var DonutSimulation = class extends SimulationCanvas {
1275
- constructor(canvas, config = {}) {
1276
- super(canvas, new DonutLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
1277
- }
1278
- };
1465
+ //#region src/donuts/index.ts
1466
+ function createDonuts(config) {
1467
+ return new Donuts(config);
1468
+ }
1279
1469
  //#endregion
1280
1470
  //#region src/fireflies/consts.ts
1281
1471
  const MULBERRY$18 = mulberry32(13);
@@ -1284,7 +1474,7 @@ const MULBERRY$18 = mulberry32(13);
1284
1474
  const SPRITE_SIZE$1 = 64;
1285
1475
  const SPRITE_CENTER$1 = SPRITE_SIZE$1 / 2;
1286
1476
  const SPRITE_RADIUS$1 = SPRITE_SIZE$1 / 2;
1287
- var FireflyLayer = class extends SimulationLayer {
1477
+ var Fireflies = class extends Effect {
1288
1478
  #scale;
1289
1479
  #size;
1290
1480
  #speed;
@@ -1305,6 +1495,10 @@ var FireflyLayer = class extends SimulationLayer {
1305
1495
  this.#sprite = this.#createSprite(r, g, b);
1306
1496
  for (let i = 0; i < this.#maxCount; ++i) this.#fireflies.push(this.#createFirefly());
1307
1497
  }
1498
+ configure(config) {
1499
+ if (config.speed !== void 0) this.#speed = config.speed;
1500
+ if (config.glowSpeed !== void 0) this.#glowSpeed = config.glowSpeed;
1501
+ }
1308
1502
  tick(dt, _width, _height) {
1309
1503
  this.#time += .02 * dt * this.#speed;
1310
1504
  for (const firefly of this.#fireflies) {
@@ -1478,18 +1672,16 @@ var FireflyParticle = class {
1478
1672
  }
1479
1673
  };
1480
1674
  //#endregion
1481
- //#region src/fireflies/simulation.ts
1482
- var FireflySimulation = class extends SimulationCanvas {
1483
- constructor(canvas, config = {}) {
1484
- super(canvas, new FireflyLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
1485
- }
1486
- };
1675
+ //#region src/fireflies/index.ts
1676
+ function createFireflies(config) {
1677
+ return new Fireflies(config);
1678
+ }
1487
1679
  //#endregion
1488
1680
  //#region src/firepit/consts.ts
1489
1681
  const MULBERRY$17 = mulberry32(13);
1490
1682
  //#endregion
1491
1683
  //#region src/firepit/layer.ts
1492
- var FirepitLayer = class extends SimulationLayer {
1684
+ var Firepit = class extends Effect {
1493
1685
  #scale;
1494
1686
  #flameWidth;
1495
1687
  #flameHeight;
@@ -1515,6 +1707,11 @@ var FirepitLayer = class extends SimulationLayer {
1515
1707
  height: this.#flameHeight * (.7 + MULBERRY$17.next() * .3)
1516
1708
  });
1517
1709
  }
1710
+ configure(config) {
1711
+ if (config.intensity !== void 0) this.#intensity = config.intensity;
1712
+ if (config.flameWidth !== void 0) this.#flameWidth = config.flameWidth;
1713
+ if (config.flameHeight !== void 0) this.#flameHeight = config.flameHeight;
1714
+ }
1518
1715
  tick(dt, _width, _height) {
1519
1716
  this.#time += .03 * dt * this.#intensity;
1520
1717
  if (this.#embers.length < this.#maxEmbers && MULBERRY$17.next() < .3 * this.#intensity * dt) this.#embers.push(this.#createEmber());
@@ -1611,12 +1808,10 @@ var FirepitLayer = class extends SimulationLayer {
1611
1808
  }
1612
1809
  };
1613
1810
  //#endregion
1614
- //#region src/firepit/simulation.ts
1615
- var FirepitSimulation = class extends SimulationCanvas {
1616
- constructor(canvas, config = {}) {
1617
- super(canvas, new FirepitLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
1618
- }
1619
- };
1811
+ //#region src/firepit/index.ts
1812
+ function createFirepit(config) {
1813
+ return new Firepit(config);
1814
+ }
1620
1815
  //#endregion
1621
1816
  //#region src/fireworks/consts.ts
1622
1817
  const MULBERRY$16 = mulberry32(13);
@@ -1923,7 +2118,7 @@ var Explosion = class {
1923
2118
  }
1924
2119
  checkCrackle() {
1925
2120
  if (this.#type !== "crackle" || this.#hasCrackled) return false;
1926
- if (this.#alpha <= this.#decay * 3) {
2121
+ if (this.#alpha <= .4) {
1927
2122
  this.#hasCrackled = true;
1928
2123
  return true;
1929
2124
  }
@@ -2032,6 +2227,158 @@ var Explosion = class {
2032
2227
  }
2033
2228
  };
2034
2229
  //#endregion
2230
+ //#region src/fireworks/create-explosion.ts
2231
+ function between(rng, min, max) {
2232
+ return min + rng() * (max - min);
2233
+ }
2234
+ /**
2235
+ * Creates an array of {@link Explosion} particles for the given firework variant.
2236
+ * Use this to fire a fully formed explosion burst in your own render loop without
2237
+ * needing a {@link Fireworks} instance.
2238
+ *
2239
+ * @param variant - The firework variant to create.
2240
+ * @param position - The center position of the explosion in canvas pixels.
2241
+ * @param hue - Base hue in degrees (0–360).
2242
+ * @param options - Optional overrides for `lineWidth` (default `5`) and `scale` (default `1`).
2243
+ * @param rng - RNG function returning values in [0, 1). Defaults to `Math.random`.
2244
+ */
2245
+ function createExplosion(variant, position, hue, options = {}, rng = Math.random) {
2246
+ const lineWidth = options.lineWidth ?? 5;
2247
+ const scale = options.scale ?? 1;
2248
+ const explosions = [];
2249
+ switch (variant) {
2250
+ case "saturn":
2251
+ createSaturn(explosions, position, hue, lineWidth, scale, rng);
2252
+ break;
2253
+ case "dahlia":
2254
+ createDahlia(explosions, position, hue, lineWidth, scale, rng);
2255
+ break;
2256
+ case "heart":
2257
+ createHeart(explosions, position, hue, lineWidth, scale, rng);
2258
+ break;
2259
+ case "spiral":
2260
+ createSpiral(explosions, position, hue, lineWidth, scale, rng);
2261
+ break;
2262
+ case "flower":
2263
+ createFlower(explosions, position, hue, lineWidth, scale, rng);
2264
+ break;
2265
+ case "concentric":
2266
+ createConcentric(explosions, position, hue, lineWidth, scale, rng);
2267
+ break;
2268
+ default: {
2269
+ const type = variant;
2270
+ const config = EXPLOSION_CONFIGS[type];
2271
+ const count = Math.floor(between(rng, config.particleCount[0], config.particleCount[1]));
2272
+ const effectiveHue = type === "brocade" ? between(rng, 35, 50) : hue;
2273
+ for (let i = 0; i < count; i++) {
2274
+ let angle;
2275
+ let speed;
2276
+ if (type === "ring") {
2277
+ angle = i / count * Math.PI * 2;
2278
+ speed = between(rng, config.speed[0], config.speed[1]) * .5 + config.speed[0] * .5;
2279
+ } else if (type === "palm" || type === "horsetail") {
2280
+ const spread = type === "horsetail" ? Math.PI / 8 : Math.PI / 5;
2281
+ angle = -Math.PI / 2 + between(rng, -spread, spread);
2282
+ }
2283
+ explosions.push(new Explosion(position, effectiveHue, lineWidth, type, scale, angle, speed));
2284
+ }
2285
+ }
2286
+ }
2287
+ return explosions;
2288
+ }
2289
+ function createSaturn(explosions, position, hue, lineWidth, scale, rng) {
2290
+ const velocity = between(rng, 4, 6);
2291
+ const shellCount = Math.floor(between(rng, 25, 35));
2292
+ for (let i = 0; i < shellCount; i++) {
2293
+ const rad = i / shellCount * Math.PI * 2;
2294
+ explosions.push(new Explosion(position, hue, lineWidth, "peony", scale, rad + between(rng, -.05, .05), velocity + between(rng, -.25, .25)));
2295
+ }
2296
+ const fillCount = Math.floor(between(rng, 40, 60));
2297
+ for (let i = 0; i < fillCount; i++) explosions.push(new Explosion(position, hue, lineWidth, "peony", scale, between(rng, 0, Math.PI * 2), velocity * between(rng, 0, 1)));
2298
+ const ringRotation = between(rng, 0, Math.PI * 2);
2299
+ const ringCount = Math.floor(between(rng, 40, 55));
2300
+ const ringVx = velocity * between(rng, 2, 3);
2301
+ const ringVy = velocity * .6;
2302
+ for (let i = 0; i < ringCount; i++) {
2303
+ const rad = i / ringCount * Math.PI * 2;
2304
+ const cx = Math.cos(rad) * ringVx + between(rng, -.25, .25);
2305
+ const cy = Math.sin(rad) * ringVy + between(rng, -.25, .25);
2306
+ const cosR = Math.cos(ringRotation);
2307
+ const sinR = Math.sin(ringRotation);
2308
+ const vx = cx * cosR - cy * sinR;
2309
+ const vy = cx * sinR + cy * cosR;
2310
+ const vz = Math.sin(rad) * velocity * .8;
2311
+ explosions.push(new Explosion(position, hue + 60, lineWidth, "ring", scale, Math.atan2(vy, vx), Math.sqrt(vx * vx + vy * vy), vz));
2312
+ }
2313
+ }
2314
+ function createDahlia(explosions, position, hue, lineWidth, scale, rng) {
2315
+ const petalCount = Math.floor(between(rng, 6, 9));
2316
+ const particlesPerPetal = Math.floor(between(rng, 8, 12));
2317
+ for (let petal = 0; petal < petalCount; petal++) {
2318
+ const baseAngle = petal / petalCount * Math.PI * 2;
2319
+ const petalHue = hue + (petal % 2 === 0 ? 25 : -25);
2320
+ for (let i = 0; i < particlesPerPetal; i++) explosions.push(new Explosion(position, petalHue, lineWidth, "dahlia", scale, baseAngle + between(rng, -.3, .3)));
2321
+ }
2322
+ }
2323
+ function createHeart(explosions, position, hue, lineWidth, scale, rng) {
2324
+ const velocity = between(rng, 3, 5);
2325
+ const count = Math.floor(between(rng, 60, 80));
2326
+ const rotation = between(rng, -.3, .3);
2327
+ const cosR = Math.cos(rotation);
2328
+ const sinR = Math.sin(rotation);
2329
+ for (let i = 0; i < count; i++) {
2330
+ const t = i / count * Math.PI * 2;
2331
+ const hx = 16 * Math.pow(Math.sin(t), 3);
2332
+ const hy = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
2333
+ const s = velocity / 16;
2334
+ const vx = hx * s;
2335
+ const vy = hy * s;
2336
+ const rvx = vx * cosR - vy * sinR;
2337
+ const rvy = vx * sinR + vy * cosR;
2338
+ explosions.push(new Explosion(position, hue, lineWidth, "heart", scale, Math.atan2(rvy, rvx), Math.max(.1, Math.sqrt(rvx * rvx + rvy * rvy) + between(rng, -.15, .15))));
2339
+ }
2340
+ }
2341
+ function createSpiral(explosions, position, hue, lineWidth, scale, rng) {
2342
+ const arms = Math.floor(between(rng, 3, 5));
2343
+ const particlesPerArm = Math.floor(between(rng, 15, 20));
2344
+ const twist = between(rng, 2, 3.5);
2345
+ const baseRotation = between(rng, 0, Math.PI * 2);
2346
+ for (let arm = 0; arm < arms; arm++) {
2347
+ const baseAngle = baseRotation + arm / arms * Math.PI * 2;
2348
+ const armHue = hue + arm * (360 / arms / 3);
2349
+ for (let i = 0; i < particlesPerArm; i++) {
2350
+ const progress = i / particlesPerArm;
2351
+ explosions.push(new Explosion(position, armHue, lineWidth, "spiral", scale, baseAngle + progress * twist, 2 + progress * 8 + between(rng, -.3, .3)));
2352
+ }
2353
+ }
2354
+ }
2355
+ function createFlower(explosions, position, hue, lineWidth, scale, rng) {
2356
+ const velocity = between(rng, 4, 7);
2357
+ const count = Math.floor(between(rng, 70, 90));
2358
+ const petals = Math.floor(between(rng, 2, 4));
2359
+ const rotation = between(rng, 0, Math.PI * 2);
2360
+ for (let i = 0; i < count; i++) {
2361
+ const t = i / count * Math.PI * 2;
2362
+ const speed = velocity * Math.abs(Math.cos(petals * t));
2363
+ if (speed < .3) continue;
2364
+ explosions.push(new Explosion(position, hue + between(rng, -15, 15), lineWidth, "flower", scale, t + rotation, speed + between(rng, -.2, .2)));
2365
+ }
2366
+ }
2367
+ function createConcentric(explosions, position, hue, lineWidth, scale, rng) {
2368
+ const outerCount = Math.floor(between(rng, 35, 50));
2369
+ const outerSpeed = between(rng, 7, 10);
2370
+ for (let i = 0; i < outerCount; i++) {
2371
+ const angle = i / outerCount * Math.PI * 2;
2372
+ explosions.push(new Explosion(position, hue, lineWidth, "ring", scale, angle + between(rng, -.05, .05), outerSpeed + between(rng, -.25, .25)));
2373
+ }
2374
+ const innerCount = Math.floor(between(rng, 25, 35));
2375
+ const innerSpeed = between(rng, 3, 5);
2376
+ for (let i = 0; i < innerCount; i++) {
2377
+ const angle = i / innerCount * Math.PI * 2;
2378
+ explosions.push(new Explosion(position, hue + 120, lineWidth, "ring", scale, angle + between(rng, -.05, .05), innerSpeed + between(rng, -.25, .25)));
2379
+ }
2380
+ }
2381
+ //#endregion
2035
2382
  //#region src/distance.ts
2036
2383
  function distance(a, b) {
2037
2384
  let x = a.x - b.x;
@@ -2047,7 +2394,7 @@ var Spark = class {
2047
2394
  #size;
2048
2395
  #decay;
2049
2396
  #friction = .94;
2050
- #gravity = .6;
2397
+ #gravity = .3;
2051
2398
  #alpha = 1;
2052
2399
  get isDead() {
2053
2400
  return this.#alpha <= 0;
@@ -2062,7 +2409,7 @@ var Spark = class {
2062
2409
  this.#decay = MULBERRY$16.nextBetween(.03, .08);
2063
2410
  this.#velocity = {
2064
2411
  x: velocityX + MULBERRY$16.nextBetween(-1.5, 1.5),
2065
- y: velocityY + MULBERRY$16.nextBetween(-2, .5)
2412
+ y: velocityY + MULBERRY$16.nextBetween(-2, 2)
2066
2413
  };
2067
2414
  }
2068
2415
  draw(ctx) {
@@ -2175,7 +2522,7 @@ var Firework = class extends EventTarget {
2175
2522
  };
2176
2523
  //#endregion
2177
2524
  //#region src/fireworks/layer.ts
2178
- var FireworkLayer = class extends SimulationLayer {
2525
+ var Fireworks = class extends Effect {
2179
2526
  #explosions = [];
2180
2527
  #fireworks = [];
2181
2528
  #sparks = [];
@@ -2183,6 +2530,7 @@ var FireworkLayer = class extends SimulationLayer {
2183
2530
  #spawnTimer = 0;
2184
2531
  #positionRandom = MULBERRY$16.fork();
2185
2532
  #autoSpawn;
2533
+ #variants;
2186
2534
  #baseSize;
2187
2535
  #scale;
2188
2536
  #tailWidth;
@@ -2192,6 +2540,7 @@ var FireworkLayer = class extends SimulationLayer {
2192
2540
  super();
2193
2541
  const scale = config.scale ?? 1;
2194
2542
  this.#autoSpawn = config.autoSpawn ?? true;
2543
+ this.#variants = config.variants?.length ? [...config.variants] : [...FIREWORK_VARIANTS];
2195
2544
  this.#baseSize = 5 * scale;
2196
2545
  this.#scale = scale;
2197
2546
  this.#tailWidth = 2 * scale;
@@ -2200,13 +2549,18 @@ var FireworkLayer = class extends SimulationLayer {
2200
2549
  this.#width = width;
2201
2550
  this.#height = height;
2202
2551
  }
2203
- fireExplosion(variant, position) {
2552
+ launch(variant, position) {
2204
2553
  const pos = position ?? {
2205
2554
  x: this.#width / 2,
2206
2555
  y: this.#height * .4
2207
2556
  };
2208
2557
  this.#hue = MULBERRY$16.nextBetween(0, 360);
2209
- this.#createExplosion(pos, this.#hue, variant);
2558
+ this.#spawnExplosion(pos, this.#hue, variant);
2559
+ }
2560
+ configure(config) {
2561
+ if (config.scale !== void 0) this.#scale = config.scale;
2562
+ if (config.autoSpawn !== void 0) this.#autoSpawn = config.autoSpawn;
2563
+ if (Array.isArray(config.variants) && config.variants.length > 0) this.#variants = [...config.variants];
2210
2564
  }
2211
2565
  tick(dt, width, height) {
2212
2566
  this.#width = width;
@@ -2223,7 +2577,8 @@ var FireworkLayer = class extends SimulationLayer {
2223
2577
  }
2224
2578
  for (const firework of this.#fireworks) {
2225
2579
  firework.tick(dt);
2226
- this.#sparks.push(...firework.collectSparks());
2580
+ const collected = firework.collectSparks();
2581
+ for (let i = 0; i < collected.length; i++) this.#sparks.push(collected[i]);
2227
2582
  }
2228
2583
  for (const explosion of this.#explosions) explosion.tick(dt);
2229
2584
  for (const spark of this.#sparks) spark.tick(dt);
@@ -2234,12 +2589,20 @@ var FireworkLayer = class extends SimulationLayer {
2234
2589
  const angle = explosion.angle + Math.PI / 2 * i + Math.PI / 4;
2235
2590
  newExplosions.push(new Explosion(explosion.position, explosion.hue, this.#baseSize * .6, "peony", this.#scale, angle, MULBERRY$16.nextBetween(3, 6)));
2236
2591
  }
2237
- if (explosion.checkCrackle()) for (let j = 0; j < 8; j++) newSparks.push(new Spark(explosion.position, explosion.hue + MULBERRY$16.nextBetween(-30, 30)));
2592
+ if (explosion.checkCrackle()) for (let j = 0; j < 14; j++) {
2593
+ const angle = MULBERRY$16.nextBetween(0, Math.PI * 2);
2594
+ const speed = MULBERRY$16.nextBetween(3, 8);
2595
+ newSparks.push(new Spark(explosion.position, explosion.hue + MULBERRY$16.nextBetween(-30, 30), Math.cos(angle) * speed, Math.sin(angle) * speed));
2596
+ }
2238
2597
  }
2239
2598
  this.#explosions.push(...newExplosions);
2240
2599
  this.#sparks.push(...newSparks);
2241
- this.#explosions = this.#explosions.filter((e) => !e.isDead);
2242
- this.#sparks = this.#sparks.filter((s) => !s.isDead);
2600
+ let aliveExplosions = 0;
2601
+ for (let i = 0; i < this.#explosions.length; i++) if (!this.#explosions[i].isDead) this.#explosions[aliveExplosions++] = this.#explosions[i];
2602
+ this.#explosions.length = aliveExplosions;
2603
+ let aliveSparks = 0;
2604
+ for (let i = 0; i < this.#sparks.length; i++) if (!this.#sparks[i].isDead) this.#sparks[aliveSparks++] = this.#sparks[i];
2605
+ this.#sparks.length = aliveSparks;
2243
2606
  }
2244
2607
  draw(ctx, width, height) {
2245
2608
  if (ctx.canvas.width !== width || ctx.canvas.height !== height) {
@@ -2252,153 +2615,13 @@ var FireworkLayer = class extends SimulationLayer {
2252
2615
  for (const firework of this.#fireworks) firework.draw(ctx);
2253
2616
  ctx.globalCompositeOperation = "source-over";
2254
2617
  }
2255
- #createExplosion(position, hue, variant) {
2618
+ #spawnExplosion(position, hue, variant) {
2256
2619
  const selected = variant ?? this.#pickVariant();
2257
- if (selected === "saturn") {
2258
- this.#createSaturnExplosion(position, hue);
2259
- return;
2260
- }
2261
- if (selected === "dahlia") {
2262
- this.#createDahliaExplosion(position, hue);
2263
- return;
2264
- }
2265
- if (selected === "heart") {
2266
- this.#createHeartExplosion(position, hue);
2267
- return;
2268
- }
2269
- if (selected === "spiral") {
2270
- this.#createSpiralExplosion(position, hue);
2271
- return;
2272
- }
2273
- if (selected === "flower") {
2274
- this.#createFlowerExplosion(position, hue);
2275
- return;
2276
- }
2277
- if (selected === "concentric") {
2278
- this.#createConcentricExplosion(position, hue);
2279
- return;
2280
- }
2281
- const type = selected;
2282
- const config = EXPLOSION_CONFIGS[type];
2283
- const particleCount = Math.floor(MULBERRY$16.nextBetween(config.particleCount[0], config.particleCount[1]));
2284
- const effectiveHue = type === "brocade" ? MULBERRY$16.nextBetween(35, 50) : hue;
2285
- for (let i = 0; i < particleCount; i++) {
2286
- let angle;
2287
- let speed;
2288
- if (type === "ring") {
2289
- angle = i / particleCount * Math.PI * 2;
2290
- speed = MULBERRY$16.nextBetween(config.speed[0], config.speed[1]) * .5 + config.speed[0] * .5;
2291
- } else if (type === "palm" || type === "horsetail") {
2292
- const spread = type === "horsetail" ? Math.PI / 8 : Math.PI / 5;
2293
- angle = -Math.PI / 2 + MULBERRY$16.nextBetween(-spread, spread);
2294
- }
2295
- this.#explosions.push(new Explosion(position, effectiveHue, this.#baseSize, type, this.#scale, angle, speed));
2296
- }
2297
- }
2298
- #createSaturnExplosion(position, hue) {
2299
- const velocity = MULBERRY$16.nextBetween(4, 6);
2300
- const shellCount = Math.floor(MULBERRY$16.nextBetween(25, 35));
2301
- for (let i = 0; i < shellCount; i++) {
2302
- const rad = i / shellCount * Math.PI * 2;
2303
- this.#explosions.push(new Explosion(position, hue, this.#baseSize, "peony", this.#scale, rad + MULBERRY$16.nextBetween(-.05, .05), velocity + MULBERRY$16.nextBetween(-.25, .25)));
2304
- }
2305
- const fillCount = Math.floor(MULBERRY$16.nextBetween(40, 60));
2306
- for (let i = 0; i < fillCount; i++) {
2307
- const rad = MULBERRY$16.nextBetween(0, Math.PI * 2);
2308
- const speed = velocity * MULBERRY$16.nextBetween(0, 1);
2309
- this.#explosions.push(new Explosion(position, hue, this.#baseSize, "peony", this.#scale, rad, speed));
2310
- }
2311
- const ringRotation = MULBERRY$16.nextBetween(0, Math.PI * 2);
2312
- const ringCount = Math.floor(MULBERRY$16.nextBetween(40, 55));
2313
- const ringVx = velocity * MULBERRY$16.nextBetween(2, 3);
2314
- const ringVy = velocity * .6;
2315
- for (let i = 0; i < ringCount; i++) {
2316
- const rad = i / ringCount * Math.PI * 2;
2317
- const cx = Math.cos(rad) * ringVx + MULBERRY$16.nextBetween(-.25, .25);
2318
- const cy = Math.sin(rad) * ringVy + MULBERRY$16.nextBetween(-.25, .25);
2319
- const cosR = Math.cos(ringRotation);
2320
- const sinR = Math.sin(ringRotation);
2321
- const vx = cx * cosR - cy * sinR;
2322
- const vy = cx * sinR + cy * cosR;
2323
- const screenAngle = Math.atan2(vy, vx);
2324
- const screenSpeed = Math.sqrt(vx * vx + vy * vy);
2325
- const vz = Math.sin(rad) * velocity * .8;
2326
- this.#explosions.push(new Explosion(position, hue + 60, this.#baseSize, "ring", this.#scale, screenAngle, screenSpeed, vz));
2327
- }
2328
- }
2329
- #createDahliaExplosion(position, hue) {
2330
- const petalCount = Math.floor(MULBERRY$16.nextBetween(6, 9));
2331
- const particlesPerPetal = Math.floor(MULBERRY$16.nextBetween(8, 12));
2332
- for (let petal = 0; petal < petalCount; petal++) {
2333
- const baseAngle = petal / petalCount * Math.PI * 2;
2334
- const petalHue = hue + (petal % 2 === 0 ? 25 : -25);
2335
- for (let i = 0; i < particlesPerPetal; i++) {
2336
- const angle = baseAngle + MULBERRY$16.nextBetween(-.3, .3);
2337
- this.#explosions.push(new Explosion(position, petalHue, this.#baseSize, "dahlia", this.#scale, angle));
2338
- }
2339
- }
2340
- }
2341
- #createHeartExplosion(position, hue) {
2342
- const velocity = MULBERRY$16.nextBetween(3, 5);
2343
- const count = Math.floor(MULBERRY$16.nextBetween(60, 80));
2344
- const rotation = MULBERRY$16.nextBetween(-.3, .3);
2345
- for (let i = 0; i < count; i++) {
2346
- const t = i / count * Math.PI * 2;
2347
- const hx = 16 * Math.pow(Math.sin(t), 3);
2348
- const hy = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
2349
- const scale = velocity / 16;
2350
- const vx = hx * scale;
2351
- const vy = hy * scale;
2352
- const cosR = Math.cos(rotation);
2353
- const sinR = Math.sin(rotation);
2354
- const rvx = vx * cosR - vy * sinR;
2355
- const rvy = vx * sinR + vy * cosR;
2356
- const angle = Math.atan2(rvy, rvx);
2357
- const speed = Math.sqrt(rvx * rvx + rvy * rvy);
2358
- this.#explosions.push(new Explosion(position, hue, this.#baseSize, "heart", this.#scale, angle, Math.max(.1, speed + MULBERRY$16.nextBetween(-.15, .15))));
2359
- }
2360
- }
2361
- #createSpiralExplosion(position, hue) {
2362
- const arms = Math.floor(MULBERRY$16.nextBetween(3, 5));
2363
- const particlesPerArm = Math.floor(MULBERRY$16.nextBetween(15, 20));
2364
- const twist = MULBERRY$16.nextBetween(2, 3.5);
2365
- const baseRotation = MULBERRY$16.nextBetween(0, Math.PI * 2);
2366
- for (let arm = 0; arm < arms; arm++) {
2367
- const baseAngle = baseRotation + arm / arms * Math.PI * 2;
2368
- const armHue = hue + arm * (360 / arms / 3);
2369
- for (let i = 0; i < particlesPerArm; i++) {
2370
- const progress = i / particlesPerArm;
2371
- const angle = baseAngle + progress * twist;
2372
- const speed = 2 + progress * 8;
2373
- this.#explosions.push(new Explosion(position, armHue, this.#baseSize, "spiral", this.#scale, angle, speed + MULBERRY$16.nextBetween(-.3, .3)));
2374
- }
2375
- }
2376
- }
2377
- #createFlowerExplosion(position, hue) {
2378
- const velocity = MULBERRY$16.nextBetween(4, 7);
2379
- const count = Math.floor(MULBERRY$16.nextBetween(70, 90));
2380
- const petals = Math.floor(MULBERRY$16.nextBetween(2, 4));
2381
- const rotation = MULBERRY$16.nextBetween(0, Math.PI * 2);
2382
- for (let i = 0; i < count; i++) {
2383
- const t = i / count * Math.PI * 2;
2384
- const speed = velocity * Math.abs(Math.cos(petals * t));
2385
- if (speed < .3) continue;
2386
- this.#explosions.push(new Explosion(position, hue + MULBERRY$16.nextBetween(-15, 15), this.#baseSize, "flower", this.#scale, t + rotation, speed + MULBERRY$16.nextBetween(-.2, .2)));
2387
- }
2388
- }
2389
- #createConcentricExplosion(position, hue) {
2390
- const outerCount = Math.floor(MULBERRY$16.nextBetween(35, 50));
2391
- const outerSpeed = MULBERRY$16.nextBetween(7, 10);
2392
- for (let i = 0; i < outerCount; i++) {
2393
- const angle = i / outerCount * Math.PI * 2;
2394
- this.#explosions.push(new Explosion(position, hue, this.#baseSize, "ring", this.#scale, angle + MULBERRY$16.nextBetween(-.05, .05), outerSpeed + MULBERRY$16.nextBetween(-.25, .25)));
2395
- }
2396
- const innerCount = Math.floor(MULBERRY$16.nextBetween(25, 35));
2397
- const innerSpeed = MULBERRY$16.nextBetween(3, 5);
2398
- for (let i = 0; i < innerCount; i++) {
2399
- const angle = i / innerCount * Math.PI * 2;
2400
- this.#explosions.push(new Explosion(position, hue + 120, this.#baseSize, "ring", this.#scale, angle + MULBERRY$16.nextBetween(-.05, .05), innerSpeed + MULBERRY$16.nextBetween(-.25, .25)));
2401
- }
2620
+ const rng = () => MULBERRY$16.nextBetween(0, 1);
2621
+ this.#explosions.push(...createExplosion(selected, position, hue, {
2622
+ lineWidth: this.#baseSize,
2623
+ scale: this.#scale
2624
+ }, rng));
2402
2625
  }
2403
2626
  #createFirework(position) {
2404
2627
  const hue = this.#hue;
@@ -2413,43 +2636,20 @@ var FireworkLayer = class extends SimulationLayer {
2413
2636
  }, hue, this.#tailWidth, this.#baseSize);
2414
2637
  firework.addEventListener("remove", () => {
2415
2638
  this.#fireworks.splice(this.#fireworks.indexOf(firework), 1);
2416
- this.#createExplosion(firework.position, hue);
2639
+ this.#spawnExplosion(firework.position, hue);
2417
2640
  }, { once: true });
2418
2641
  this.#fireworks.push(firework);
2419
2642
  }
2420
2643
  #pickVariant() {
2421
- const roll = MULBERRY$16.nextBetween(0, 100);
2422
- if (roll < 12) return "peony";
2423
- if (roll < 22) return "chrysanthemum";
2424
- if (roll < 29) return "willow";
2425
- if (roll < 34) return "ring";
2426
- if (roll < 39) return "palm";
2427
- if (roll < 44) return "crackle";
2428
- if (roll < 48) return "crossette";
2429
- if (roll < 55) return "saturn";
2430
- if (roll < 62) return "dahlia";
2431
- if (roll < 67) return "brocade";
2432
- if (roll < 71) return "horsetail";
2433
- if (roll < 75) return "strobe";
2434
- if (roll < 82) return "heart";
2435
- if (roll < 89) return "spiral";
2436
- if (roll < 94) return "flower";
2437
- return "concentric";
2644
+ const index = Math.floor(MULBERRY$16.nextBetween(0, this.#variants.length));
2645
+ return this.#variants[index];
2438
2646
  }
2439
2647
  };
2440
2648
  //#endregion
2441
- //#region src/fireworks/simulation.ts
2442
- var FireworkSimulation = class extends SimulationCanvas {
2443
- #layer;
2444
- constructor(canvas, config = {}) {
2445
- const layer = new FireworkLayer(config);
2446
- super(canvas, layer, 60, config.canvasOptions ?? { colorSpace: "display-p3" });
2447
- this.#layer = layer;
2448
- }
2449
- fireExplosion(variant, position) {
2450
- this.#layer.fireExplosion(variant, position);
2451
- }
2452
- };
2649
+ //#region src/fireworks/index.ts
2650
+ function createFireworks(config) {
2651
+ return new Fireworks(config);
2652
+ }
2453
2653
  //#endregion
2454
2654
  //#region src/glitter/consts.ts
2455
2655
  const MULBERRY$15 = mulberry32(13);
@@ -2464,7 +2664,7 @@ const GLITTER_COLORS = [
2464
2664
  ];
2465
2665
  //#endregion
2466
2666
  //#region src/glitter/layer.ts
2467
- var GlitterLayer = class extends SimulationLayer {
2667
+ var Glitter = class extends Effect {
2468
2668
  #scale;
2469
2669
  #size;
2470
2670
  #speed;
@@ -2487,6 +2687,10 @@ var GlitterLayer = class extends SimulationLayer {
2487
2687
  if (innerWidth < 991) this.#maxCount = Math.floor(this.#maxCount / 2);
2488
2688
  for (let i = 0; i < this.#maxCount; ++i) this.#falling.push(this.#createFallingPiece(true));
2489
2689
  }
2690
+ configure(config) {
2691
+ if (config.speed !== void 0) this.#speed = config.speed;
2692
+ if (config.groundLevel !== void 0) this.#groundLevel = config.groundLevel;
2693
+ }
2490
2694
  tick(dt, _width, _height) {
2491
2695
  this.#time += .03 * dt;
2492
2696
  let alive = 0;
@@ -2586,12 +2790,10 @@ var GlitterLayer = class extends SimulationLayer {
2586
2790
  }
2587
2791
  };
2588
2792
  //#endregion
2589
- //#region src/glitter/simulation.ts
2590
- var GlitterSimulation = class extends SimulationCanvas {
2591
- constructor(canvas, config = {}) {
2592
- super(canvas, new GlitterLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
2593
- }
2594
- };
2793
+ //#region src/glitter/index.ts
2794
+ function createGlitter(config) {
2795
+ return new Glitter(config);
2796
+ }
2595
2797
  //#endregion
2596
2798
  //#region src/lanterns/consts.ts
2597
2799
  const MULBERRY$14 = mulberry32(13);
@@ -2606,7 +2808,7 @@ const LANTERN_COLORS = [
2606
2808
  ];
2607
2809
  //#endregion
2608
2810
  //#region src/lanterns/layer.ts
2609
- var LanternLayer = class extends SimulationLayer {
2811
+ var Lanterns = class extends Effect {
2610
2812
  #scale;
2611
2813
  #speed;
2612
2814
  #size;
@@ -2614,6 +2816,8 @@ var LanternLayer = class extends SimulationLayer {
2614
2816
  #maxCount;
2615
2817
  #time = 0;
2616
2818
  #lanterns = [];
2819
+ #sortedLanterns = [];
2820
+ #sortDirty = true;
2617
2821
  constructor(config = {}) {
2618
2822
  super();
2619
2823
  this.#scale = config.scale ?? 1;
@@ -2624,6 +2828,9 @@ var LanternLayer = class extends SimulationLayer {
2624
2828
  if (innerWidth < 991) this.#maxCount = Math.floor(this.#maxCount / 2);
2625
2829
  for (let i = 0; i < this.#maxCount; ++i) this.#lanterns.push(this.#createLantern(true));
2626
2830
  }
2831
+ configure(config) {
2832
+ if (config.speed !== void 0) this.#speed = config.speed;
2833
+ }
2627
2834
  tick(dt, width, height) {
2628
2835
  this.#time += .02 * dt * this.#speed;
2629
2836
  for (let i = 0; i < this.#lanterns.length; i++) {
@@ -2631,11 +2838,18 @@ var LanternLayer = class extends SimulationLayer {
2631
2838
  lantern.y -= lantern.vy * this.#speed * dt / (height * 1.5);
2632
2839
  const sway = Math.sin(this.#time * lantern.swaySpeed + lantern.swayPhase) * lantern.swayAmplitude;
2633
2840
  lantern.x += sway * dt / (width * 8);
2634
- if (lantern.y < -.15) this.#lanterns[i] = this.#createLantern(false);
2841
+ if (lantern.y < -.15) {
2842
+ this.#lanterns[i] = this.#createLantern(false);
2843
+ this.#sortDirty = true;
2844
+ }
2635
2845
  }
2636
2846
  }
2637
2847
  draw(ctx, width, height) {
2638
- const sorted = [...this.#lanterns].sort((a, b) => a.size - b.size);
2848
+ if (this.#sortDirty) {
2849
+ this.#sortedLanterns = [...this.#lanterns].sort((a, b) => a.size - b.size);
2850
+ this.#sortDirty = false;
2851
+ }
2852
+ const sorted = this.#sortedLanterns;
2639
2853
  for (const lantern of sorted) {
2640
2854
  const px = lantern.x * width;
2641
2855
  const py = lantern.y * height;
@@ -2653,8 +2867,7 @@ var LanternLayer = class extends SimulationLayer {
2653
2867
  ctx.beginPath();
2654
2868
  ctx.arc(px, py, glowRadius, 0, Math.PI * 2);
2655
2869
  ctx.fill();
2656
- ctx.save();
2657
- ctx.translate(px, py);
2870
+ ctx.setTransform(1, 0, 0, 1, px, py);
2658
2871
  const bodyW = size * .8;
2659
2872
  const bodyH = size;
2660
2873
  const topW = bodyW * .6;
@@ -2664,210 +2877,69 @@ var LanternLayer = class extends SimulationLayer {
2664
2877
  ctx.lineTo(bodyW * .7, bodyH * .5);
2665
2878
  ctx.quadraticCurveTo(bodyW, 0, topW, -bodyH * .5);
2666
2879
  ctx.closePath();
2667
- const bodyGradient = ctx.createLinearGradient(0, -bodyH * .5, 0, bodyH * .5);
2668
- bodyGradient.addColorStop(0, `rgba(${Math.min(255, r + 60)}, ${Math.min(255, g + 60)}, ${Math.min(255, b + 30)}, ${alpha * .9})`);
2669
- bodyGradient.addColorStop(.5, `rgba(${r}, ${g}, ${b}, ${alpha * .85})`);
2670
- bodyGradient.addColorStop(1, `rgba(${Math.max(0, r - 30)}, ${Math.max(0, g - 30)}, ${Math.max(0, b - 20)}, ${alpha * .8})`);
2671
- ctx.fillStyle = bodyGradient;
2672
- ctx.fill();
2673
- ctx.beginPath();
2674
- ctx.moveTo(-topW * .7, -bodyH * .55);
2675
- ctx.lineTo(topW * .7, -bodyH * .55);
2676
- ctx.lineWidth = size * .06;
2677
- ctx.strokeStyle = `rgba(${Math.max(0, r - 40)}, ${Math.max(0, g - 40)}, ${Math.max(0, b - 40)}, ${alpha * .7})`;
2678
- ctx.stroke();
2679
- const flameH = bodyH * .3;
2680
- const flameW = bodyW * .15;
2681
- const flameFlicker = Math.sin(this.#time * 8 + lantern.glowPhase) * flameW * .3;
2682
- const flameGradient = ctx.createRadialGradient(flameFlicker, -flameH * .1, 0, flameFlicker, -flameH * .1, flameH);
2683
- flameGradient.addColorStop(0, `rgba(255, 255, 200, ${alpha * .95})`);
2684
- flameGradient.addColorStop(.3, `rgba(255, 200, 80, ${alpha * .7})`);
2685
- flameGradient.addColorStop(.7, `rgba(255, 140, 40, ${alpha * .3})`);
2686
- flameGradient.addColorStop(1, `rgba(255, 100, 20, 0)`);
2687
- ctx.beginPath();
2688
- ctx.moveTo(-flameW + flameFlicker, flameH * .2);
2689
- ctx.quadraticCurveTo(-flameW * .5 + flameFlicker, -flameH * .3, flameFlicker, -flameH);
2690
- ctx.quadraticCurveTo(flameW * .5 + flameFlicker, -flameH * .3, flameW + flameFlicker, flameH * .2);
2691
- ctx.closePath();
2692
- ctx.fillStyle = flameGradient;
2693
- ctx.fill();
2694
- const stringLen = size * .6;
2695
- const stringDrift = Math.sin(this.#time * 1.5 + lantern.swayPhase) * size * .1;
2696
- ctx.beginPath();
2697
- ctx.moveTo(0, bodyH * .5);
2698
- ctx.quadraticCurveTo(stringDrift, bodyH * .5 + stringLen * .5, -stringDrift * .5, bodyH * .5 + stringLen);
2699
- ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha * .4})`;
2700
- ctx.lineWidth = size * .04;
2701
- ctx.stroke();
2702
- ctx.restore();
2703
- }
2704
- }
2705
- #createLantern(initialSpread) {
2706
- const colorIndex = Math.floor(MULBERRY$14.next() * this.#colorRGBs.length);
2707
- const sizeVariation = .6 + MULBERRY$14.next() * .8;
2708
- return {
2709
- x: .05 + MULBERRY$14.next() * .9,
2710
- y: initialSpread ? MULBERRY$14.next() * 1.3 : 1.15 + MULBERRY$14.next() * .2,
2711
- vx: 0,
2712
- vy: .2 + MULBERRY$14.next() * .6,
2713
- size: this.#size * sizeVariation,
2714
- glowPhase: MULBERRY$14.next() * Math.PI * 2,
2715
- glowSpeed: .8 + MULBERRY$14.next() * 1.2,
2716
- swayPhase: MULBERRY$14.next() * Math.PI * 2,
2717
- swaySpeed: .4 + MULBERRY$14.next() * .8,
2718
- swayAmplitude: .3 + MULBERRY$14.next() * .7,
2719
- colorIndex,
2720
- opacity: .7 + MULBERRY$14.next() * .3
2721
- };
2722
- }
2723
- };
2724
- //#endregion
2725
- //#region src/lanterns/simulation.ts
2726
- var LanternSimulation = class extends SimulationCanvas {
2727
- constructor(canvas, config = {}) {
2728
- super(canvas, new LanternLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
2729
- }
2730
- };
2731
- //#endregion
2732
- //#region src/layered.ts
2733
- function parseSide(side) {
2734
- return typeof side === "number" ? [0, side] : side;
2735
- }
2736
- function applyEdgeFade(ctx, width, height, fade) {
2737
- ctx.globalCompositeOperation = "destination-out";
2738
- if (fade.top !== void 0) {
2739
- const [near, far] = parseSide(fade.top);
2740
- const nearPx = near * height;
2741
- const farPx = far * height;
2742
- if (nearPx > 0) {
2743
- ctx.fillStyle = "rgba(0,0,0,1)";
2744
- ctx.fillRect(0, 0, width, nearPx);
2745
- }
2746
- if (farPx > nearPx) {
2747
- const gradient = ctx.createLinearGradient(0, nearPx, 0, farPx);
2748
- gradient.addColorStop(0, "rgba(0,0,0,1)");
2749
- gradient.addColorStop(1, "rgba(0,0,0,0)");
2750
- ctx.fillStyle = gradient;
2751
- ctx.fillRect(0, nearPx, width, farPx - nearPx);
2752
- }
2753
- }
2754
- if (fade.bottom !== void 0) {
2755
- const [near, far] = parseSide(fade.bottom);
2756
- const nearPx = near * height;
2757
- const farPx = far * height;
2758
- if (nearPx > 0) {
2759
- ctx.fillStyle = "rgba(0,0,0,1)";
2760
- ctx.fillRect(0, height - nearPx, width, nearPx);
2761
- }
2762
- if (farPx > nearPx) {
2763
- const gradient = ctx.createLinearGradient(0, height - farPx, 0, height - nearPx);
2764
- gradient.addColorStop(0, "rgba(0,0,0,0)");
2765
- gradient.addColorStop(1, "rgba(0,0,0,1)");
2766
- ctx.fillStyle = gradient;
2767
- ctx.fillRect(0, height - farPx, width, farPx - nearPx);
2768
- }
2769
- }
2770
- if (fade.left !== void 0) {
2771
- const [near, far] = parseSide(fade.left);
2772
- const nearPx = near * width;
2773
- const farPx = far * width;
2774
- if (nearPx > 0) {
2775
- ctx.fillStyle = "rgba(0,0,0,1)";
2776
- ctx.fillRect(0, 0, nearPx, height);
2777
- }
2778
- if (farPx > nearPx) {
2779
- const gradient = ctx.createLinearGradient(nearPx, 0, farPx, 0);
2780
- gradient.addColorStop(0, "rgba(0,0,0,1)");
2781
- gradient.addColorStop(1, "rgba(0,0,0,0)");
2782
- ctx.fillStyle = gradient;
2783
- ctx.fillRect(nearPx, 0, farPx - nearPx, height);
2784
- }
2785
- }
2786
- if (fade.right !== void 0) {
2787
- const [near, far] = parseSide(fade.right);
2788
- const nearPx = near * width;
2789
- const farPx = far * width;
2790
- if (nearPx > 0) {
2791
- ctx.fillStyle = "rgba(0,0,0,1)";
2792
- ctx.fillRect(width - nearPx, 0, nearPx, height);
2793
- }
2794
- if (farPx > nearPx) {
2795
- const gradient = ctx.createLinearGradient(width - farPx, 0, width - nearPx, 0);
2796
- gradient.addColorStop(0, "rgba(0,0,0,0)");
2797
- gradient.addColorStop(1, "rgba(0,0,0,1)");
2798
- ctx.fillStyle = gradient;
2799
- ctx.fillRect(width - farPx, 0, farPx - nearPx, height);
2800
- }
2801
- }
2802
- ctx.globalCompositeOperation = "source-over";
2803
- }
2804
- var LayeredSimulation = class extends LimitedFrameRateCanvas {
2805
- #layers = [];
2806
- #contextOptions;
2807
- #offscreen = null;
2808
- #offscreenCtx = null;
2809
- constructor(canvas, frameRate = 60, options = { colorSpace: "display-p3" }) {
2810
- super(canvas, frameRate, options);
2811
- this.#contextOptions = options;
2812
- canvas.style.position = "absolute";
2813
- canvas.style.top = "0";
2814
- canvas.style.left = "0";
2815
- canvas.style.height = "100%";
2816
- canvas.style.width = "100%";
2817
- }
2818
- add(layer) {
2819
- this.#layers.push(layer);
2820
- if (this.isTicking) layer.onMount(this.canvas);
2821
- return this;
2822
- }
2823
- start() {
2824
- for (const layer of this.#layers) layer.onMount(this.canvas);
2825
- super.start();
2826
- }
2827
- destroy() {
2828
- for (const layer of this.#layers) layer.onUnmount(this.canvas);
2829
- super.destroy();
2830
- }
2831
- draw() {
2832
- this.canvas.height = this.height;
2833
- this.canvas.width = this.width;
2834
- const ctx = this.context;
2835
- ctx.clearRect(0, 0, this.width, this.height);
2836
- for (const layer of this.#layers) if (layer.fade) {
2837
- const offCtx = this.#getOffscreenCtx(this.width, this.height);
2838
- offCtx.clearRect(0, 0, this.width, this.height);
2839
- layer.draw(offCtx, this.width, this.height);
2840
- applyEdgeFade(offCtx, this.width, this.height, layer.fade);
2841
- ctx.drawImage(this.#offscreen, 0, 0);
2842
- } else {
2843
- ctx.save();
2844
- layer.draw(ctx, this.width, this.height);
2845
- ctx.restore();
2846
- }
2847
- }
2848
- tick() {
2849
- const dt = (this.delta > 0 && this.delta < 200 ? this.delta / (1e3 / 60) : 1) * this.speed * LimitedFrameRateCanvas.globalSpeed;
2850
- for (const layer of this.#layers) layer.tick(dt, this.width, this.height);
2851
- }
2852
- onResize() {
2853
- super.onResize();
2854
- if (this.#offscreen) {
2855
- this.#offscreen.width = this.width;
2856
- this.#offscreen.height = this.height;
2880
+ const bodyGradient = ctx.createLinearGradient(0, -bodyH * .5, 0, bodyH * .5);
2881
+ bodyGradient.addColorStop(0, `rgba(${Math.min(255, r + 60)}, ${Math.min(255, g + 60)}, ${Math.min(255, b + 30)}, ${alpha * .9})`);
2882
+ bodyGradient.addColorStop(.5, `rgba(${r}, ${g}, ${b}, ${alpha * .85})`);
2883
+ bodyGradient.addColorStop(1, `rgba(${Math.max(0, r - 30)}, ${Math.max(0, g - 30)}, ${Math.max(0, b - 20)}, ${alpha * .8})`);
2884
+ ctx.fillStyle = bodyGradient;
2885
+ ctx.fill();
2886
+ ctx.beginPath();
2887
+ ctx.moveTo(-topW * .7, -bodyH * .55);
2888
+ ctx.lineTo(topW * .7, -bodyH * .55);
2889
+ ctx.lineWidth = size * .06;
2890
+ ctx.strokeStyle = `rgba(${Math.max(0, r - 40)}, ${Math.max(0, g - 40)}, ${Math.max(0, b - 40)}, ${alpha * .7})`;
2891
+ ctx.stroke();
2892
+ const flameH = bodyH * .3;
2893
+ const flameW = bodyW * .15;
2894
+ const flameFlicker = Math.sin(this.#time * 8 + lantern.glowPhase) * flameW * .3;
2895
+ const flameGradient = ctx.createRadialGradient(flameFlicker, -flameH * .1, 0, flameFlicker, -flameH * .1, flameH);
2896
+ flameGradient.addColorStop(0, `rgba(255, 255, 200, ${alpha * .95})`);
2897
+ flameGradient.addColorStop(.3, `rgba(255, 200, 80, ${alpha * .7})`);
2898
+ flameGradient.addColorStop(.7, `rgba(255, 140, 40, ${alpha * .3})`);
2899
+ flameGradient.addColorStop(1, `rgba(255, 100, 20, 0)`);
2900
+ ctx.beginPath();
2901
+ ctx.moveTo(-flameW + flameFlicker, flameH * .2);
2902
+ ctx.quadraticCurveTo(-flameW * .5 + flameFlicker, -flameH * .3, flameFlicker, -flameH);
2903
+ ctx.quadraticCurveTo(flameW * .5 + flameFlicker, -flameH * .3, flameW + flameFlicker, flameH * .2);
2904
+ ctx.closePath();
2905
+ ctx.fillStyle = flameGradient;
2906
+ ctx.fill();
2907
+ const stringLen = size * .6;
2908
+ const stringDrift = Math.sin(this.#time * 1.5 + lantern.swayPhase) * size * .1;
2909
+ ctx.beginPath();
2910
+ ctx.moveTo(0, bodyH * .5);
2911
+ ctx.quadraticCurveTo(stringDrift, bodyH * .5 + stringLen * .5, -stringDrift * .5, bodyH * .5 + stringLen);
2912
+ ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha * .4})`;
2913
+ ctx.lineWidth = size * .04;
2914
+ ctx.stroke();
2915
+ ctx.resetTransform();
2857
2916
  }
2858
- for (const layer of this.#layers) layer.onResize(this.width, this.height);
2859
2917
  }
2860
- #getOffscreenCtx(width, height) {
2861
- if (!this.#offscreen) {
2862
- this.#offscreen = document.createElement("canvas");
2863
- this.#offscreen.width = width;
2864
- this.#offscreen.height = height;
2865
- this.#offscreenCtx = this.#offscreen.getContext("2d", this.#contextOptions);
2866
- }
2867
- return this.#offscreenCtx;
2918
+ #createLantern(initialSpread) {
2919
+ const colorIndex = Math.floor(MULBERRY$14.next() * this.#colorRGBs.length);
2920
+ const sizeVariation = .6 + MULBERRY$14.next() * .8;
2921
+ return {
2922
+ x: .05 + MULBERRY$14.next() * .9,
2923
+ y: initialSpread ? MULBERRY$14.next() * 1.3 : 1.15 + MULBERRY$14.next() * .2,
2924
+ vx: 0,
2925
+ vy: .2 + MULBERRY$14.next() * .6,
2926
+ size: this.#size * sizeVariation,
2927
+ glowPhase: MULBERRY$14.next() * Math.PI * 2,
2928
+ glowSpeed: .8 + MULBERRY$14.next() * 1.2,
2929
+ swayPhase: MULBERRY$14.next() * Math.PI * 2,
2930
+ swaySpeed: .4 + MULBERRY$14.next() * .8,
2931
+ swayAmplitude: .3 + MULBERRY$14.next() * .7,
2932
+ colorIndex,
2933
+ opacity: .7 + MULBERRY$14.next() * .3
2934
+ };
2868
2935
  }
2869
2936
  };
2870
2937
  //#endregion
2938
+ //#region src/lanterns/index.ts
2939
+ function createLanterns(config) {
2940
+ return new Lanterns(config);
2941
+ }
2942
+ //#endregion
2871
2943
  //#region src/leaves/consts.ts
2872
2944
  const MULBERRY$13 = mulberry32(13);
2873
2945
  const LEAF_COLORS = [
@@ -2884,7 +2956,7 @@ const LEAF_COLORS = [
2884
2956
  ];
2885
2957
  //#endregion
2886
2958
  //#region src/leaves/layer.ts
2887
- var LeafLayer = class extends SimulationLayer {
2959
+ var Leaves = class extends Effect {
2888
2960
  #scale;
2889
2961
  #size;
2890
2962
  #speed;
@@ -2910,6 +2982,10 @@ var LeafLayer = class extends SimulationLayer {
2910
2982
  onResize(_width, height) {
2911
2983
  this.#height = height;
2912
2984
  }
2985
+ configure(config) {
2986
+ if (config.speed !== void 0) this.#speed = config.speed;
2987
+ if (config.wind !== void 0) this.#wind = config.wind;
2988
+ }
2913
2989
  tick(dt, _width, height) {
2914
2990
  this.#height = height;
2915
2991
  const speedFactor = height / 540 / this.#speed;
@@ -2944,14 +3020,13 @@ var LeafLayer = class extends SimulationLayer {
2944
3020
  const py = leaf.y * height;
2945
3021
  const displaySize = leaf.size * leaf.depth;
2946
3022
  const scaleX = Math.cos(leaf.flipAngle);
2947
- ctx.save();
2948
- ctx.translate(px, py);
2949
- ctx.rotate(leaf.rotation);
2950
- ctx.scale(scaleX, 1);
3023
+ const cos = Math.cos(leaf.rotation);
3024
+ const sin = Math.sin(leaf.rotation);
3025
+ ctx.setTransform(cos * scaleX, sin * scaleX, -sin, cos, px, py);
2951
3026
  ctx.globalAlpha = .3 + leaf.depth * .7;
2952
3027
  ctx.drawImage(this.#sprites[leaf.colorIndex % this.#sprites.length], -displaySize / 2, -displaySize / 2, displaySize, displaySize);
2953
- ctx.restore();
2954
3028
  }
3029
+ ctx.resetTransform();
2955
3030
  ctx.globalAlpha = 1;
2956
3031
  }
2957
3032
  #createSprites() {
@@ -3080,12 +3155,10 @@ var LeafLayer = class extends SimulationLayer {
3080
3155
  }
3081
3156
  };
3082
3157
  //#endregion
3083
- //#region src/leaves/simulation.ts
3084
- var LeafSimulation = class extends SimulationCanvas {
3085
- constructor(canvas, config = {}) {
3086
- super(canvas, new LeafLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
3087
- }
3088
- };
3158
+ //#region src/leaves/index.ts
3159
+ function createLeaves(config) {
3160
+ return new Leaves(config);
3161
+ }
3089
3162
  //#endregion
3090
3163
  //#region src/lightning/consts.ts
3091
3164
  const MULBERRY$12 = mulberry32(13);
@@ -3233,7 +3306,7 @@ var LightningSystem = class {
3233
3306
  };
3234
3307
  //#endregion
3235
3308
  //#region src/lightning/layer.ts
3236
- var LightningLayer = class extends SimulationLayer {
3309
+ var Lightning = class extends Effect {
3237
3310
  #system;
3238
3311
  #enableFlash;
3239
3312
  constructor(config = {}) {
@@ -3264,19 +3337,17 @@ var LightningLayer = class extends SimulationLayer {
3264
3337
  }
3265
3338
  };
3266
3339
  //#endregion
3267
- //#region src/lightning/simulation.ts
3268
- var LightningSimulation = class extends SimulationCanvas {
3269
- constructor(canvas, config = {}) {
3270
- super(canvas, new LightningLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
3271
- }
3272
- };
3340
+ //#region src/lightning/index.ts
3341
+ function createLightning(config) {
3342
+ return new Lightning(config);
3343
+ }
3273
3344
  //#endregion
3274
3345
  //#region src/matrix/consts.ts
3275
3346
  const MULBERRY$11 = mulberry32(13);
3276
3347
  const MATRIX_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
3277
3348
  //#endregion
3278
3349
  //#region src/matrix/layer.ts
3279
- var MatrixLayer = class extends SimulationLayer {
3350
+ var Matrix = class extends Effect {
3280
3351
  #scale;
3281
3352
  #speed;
3282
3353
  #fontSize;
@@ -3315,6 +3386,10 @@ var MatrixLayer = class extends SimulationLayer {
3315
3386
  }
3316
3387
  }
3317
3388
  }
3389
+ configure(config) {
3390
+ if (config.speed !== void 0) this.#speed = config.speed;
3391
+ if (config.trailLength !== void 0) this.#trailLength = config.trailLength;
3392
+ }
3318
3393
  tick(dt, width, height) {
3319
3394
  this.#width = width;
3320
3395
  this.#height = height;
@@ -3366,12 +3441,10 @@ var MatrixLayer = class extends SimulationLayer {
3366
3441
  }
3367
3442
  };
3368
3443
  //#endregion
3369
- //#region src/matrix/simulation.ts
3370
- var MatrixSimulation = class extends SimulationCanvas {
3371
- constructor(canvas, config = {}) {
3372
- super(canvas, new MatrixLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
3373
- }
3374
- };
3444
+ //#region src/matrix/index.ts
3445
+ function createMatrix(config) {
3446
+ return new Matrix(config);
3447
+ }
3375
3448
  //#endregion
3376
3449
  //#region src/orbits/consts.ts
3377
3450
  const MULBERRY$10 = mulberry32(13);
@@ -3386,7 +3459,7 @@ const ORBIT_COLORS = [
3386
3459
  ];
3387
3460
  //#endregion
3388
3461
  //#region src/orbits/layer.ts
3389
- var OrbitLayer = class extends SimulationLayer {
3462
+ var Orbits = class extends Effect {
3390
3463
  #centerCount;
3391
3464
  #orbitersPerCenter;
3392
3465
  #speed;
@@ -3421,6 +3494,12 @@ var OrbitLayer = class extends SimulationLayer {
3421
3494
  for (let ci = 0; ci < this.#centers.length; ci++) for (let oi = 0; oi < count; oi++) this.#orbiters.push(this.#createOrbiter(ci));
3422
3495
  }
3423
3496
  }
3497
+ configure(config) {
3498
+ if (config.speed !== void 0) this.#speed = config.speed;
3499
+ if (config.trailLength !== void 0) this.#trailLength = config.trailLength;
3500
+ if (config.showCenters !== void 0) this.#showCenters = config.showCenters;
3501
+ if (config.scale !== void 0) this.#scale = config.scale;
3502
+ }
3424
3503
  tick(dt, width, height) {
3425
3504
  this.#time += .01 * dt * this.#speed;
3426
3505
  for (const orbiter of this.#orbiters) {
@@ -3437,11 +3516,20 @@ var OrbitLayer = class extends SimulationLayer {
3437
3516
  const rotatedY = localX * Math.sin(orbiter.tilt * .3) + localY * Math.cos(orbiter.tilt * .3);
3438
3517
  const px = cx + rotatedX;
3439
3518
  const py = cy + rotatedY;
3440
- orbiter.trail.push({
3441
- x: px,
3442
- y: py
3443
- });
3444
- if (orbiter.trail.length > this.#trailLength) orbiter.trail.shift();
3519
+ const trail = orbiter.trail;
3520
+ const maxLen = this.#trailLength;
3521
+ if (trail.length < maxLen) {
3522
+ trail.push({
3523
+ x: px,
3524
+ y: py
3525
+ });
3526
+ orbiter.trailHead = trail.length - 1;
3527
+ } else {
3528
+ const next = (orbiter.trailHead + 1) % maxLen;
3529
+ trail[next].x = px;
3530
+ trail[next].y = py;
3531
+ orbiter.trailHead = next;
3532
+ }
3445
3533
  }
3446
3534
  }
3447
3535
  draw(ctx, width, height) {
@@ -3466,21 +3554,28 @@ var OrbitLayer = class extends SimulationLayer {
3466
3554
  ctx.globalCompositeOperation = "lighter";
3467
3555
  for (const orbiter of this.#orbiters) {
3468
3556
  const [cr, cg, cb] = hexToRGB(orbiter.color);
3469
- if (orbiter.trail.length > 1) for (let ti = 0; ti < orbiter.trail.length - 1; ti++) {
3470
- const progress = (ti + 1) / orbiter.trail.length;
3471
- const trailAlpha = progress * .5;
3472
- const trailWidth = orbiter.size * progress * this.#scale;
3473
- if (trailAlpha < .01) continue;
3474
- ctx.globalAlpha = trailAlpha;
3475
- ctx.strokeStyle = `rgb(${cr}, ${cg}, ${cb})`;
3476
- ctx.lineWidth = trailWidth;
3477
- ctx.beginPath();
3478
- ctx.moveTo(orbiter.trail[ti].x, orbiter.trail[ti].y);
3479
- ctx.lineTo(orbiter.trail[ti + 1].x, orbiter.trail[ti + 1].y);
3480
- ctx.stroke();
3557
+ const trail = orbiter.trail;
3558
+ const trailLen = trail.length;
3559
+ if (trailLen > 1) {
3560
+ const oldest = trailLen === this.#trailLength ? (orbiter.trailHead + 1) % trailLen : 0;
3561
+ for (let ti = 0; ti < trailLen - 1; ti++) {
3562
+ const progress = (ti + 1) / trailLen;
3563
+ const trailAlpha = progress * .5;
3564
+ const trailWidth = orbiter.size * progress * this.#scale;
3565
+ if (trailAlpha < .01) continue;
3566
+ const idx0 = (oldest + ti) % trailLen;
3567
+ const idx1 = (oldest + ti + 1) % trailLen;
3568
+ ctx.globalAlpha = trailAlpha;
3569
+ ctx.strokeStyle = `rgb(${cr}, ${cg}, ${cb})`;
3570
+ ctx.lineWidth = trailWidth;
3571
+ ctx.beginPath();
3572
+ ctx.moveTo(trail[idx0].x, trail[idx0].y);
3573
+ ctx.lineTo(trail[idx1].x, trail[idx1].y);
3574
+ ctx.stroke();
3575
+ }
3481
3576
  }
3482
- if (orbiter.trail.length > 0) {
3483
- const head = orbiter.trail[orbiter.trail.length - 1];
3577
+ if (trailLen > 0) {
3578
+ const head = trail[orbiter.trailHead];
3484
3579
  const headSize = orbiter.size * this.#scale;
3485
3580
  const glow = ctx.createRadialGradient(head.x, head.y, 0, head.x, head.y, headSize * 3);
3486
3581
  glow.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, 0.9)`);
@@ -3512,23 +3607,22 @@ var OrbitLayer = class extends SimulationLayer {
3512
3607
  tilt: MULBERRY$10.next() * Math.PI,
3513
3608
  size: 1.5 + MULBERRY$10.next() * 2.5,
3514
3609
  color: this.#colors[Math.floor(MULBERRY$10.next() * this.#colors.length)],
3515
- trail: []
3610
+ trail: [],
3611
+ trailHead: 0
3516
3612
  };
3517
3613
  }
3518
3614
  };
3519
3615
  //#endregion
3520
- //#region src/orbits/simulation.ts
3521
- var OrbitSimulation = class extends SimulationCanvas {
3522
- constructor(canvas, config = {}) {
3523
- super(canvas, new OrbitLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
3524
- }
3525
- };
3616
+ //#region src/orbits/index.ts
3617
+ function createOrbits(config) {
3618
+ return new Orbits(config);
3619
+ }
3526
3620
  //#endregion
3527
3621
  //#region src/particles/consts.ts
3528
3622
  const MULBERRY$9 = mulberry32(13);
3529
3623
  //#endregion
3530
3624
  //#region src/particles/layer.ts
3531
- var ParticleLayer = class extends SimulationLayer {
3625
+ var Particles = class extends Effect {
3532
3626
  #scale;
3533
3627
  #connectionDistance;
3534
3628
  #lineWidth;
@@ -3575,6 +3669,16 @@ var ParticleLayer = class extends SimulationLayer {
3575
3669
  this.#onMouseMoveBound = this.#onMouseMove.bind(this);
3576
3670
  this.#onMouseLeaveBound = this.#onMouseLeave.bind(this);
3577
3671
  }
3672
+ configure(config) {
3673
+ if (config.scale !== void 0) this.#scale = config.scale;
3674
+ if (config.connectionDistance !== void 0) this.#connectionDistance = config.connectionDistance * this.#scale;
3675
+ if (config.lineWidth !== void 0) this.#lineWidth = config.lineWidth;
3676
+ if (config.mouseMode !== void 0) this.#mouseMode = config.mouseMode;
3677
+ if (config.mouseRadius !== void 0) this.#mouseRadius = config.mouseRadius * this.#scale;
3678
+ if (config.mouseStrength !== void 0) this.#mouseStrength = config.mouseStrength;
3679
+ if (config.particleForces !== void 0) this.#particleForces = config.particleForces;
3680
+ if (config.glow !== void 0) this.#glow = config.glow;
3681
+ }
3578
3682
  onResize(width, height) {
3579
3683
  this.#width = width;
3580
3684
  this.#height = height;
@@ -3757,12 +3861,10 @@ var ParticleLayer = class extends SimulationLayer {
3757
3861
  }
3758
3862
  };
3759
3863
  //#endregion
3760
- //#region src/particles/simulation.ts
3761
- var ParticleSimulation = class extends SimulationCanvas {
3762
- constructor(canvas, config = {}) {
3763
- super(canvas, new ParticleLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
3764
- }
3765
- };
3864
+ //#region src/particles/index.ts
3865
+ function createParticles(config) {
3866
+ return new Particles(config);
3867
+ }
3766
3868
  //#endregion
3767
3869
  //#region src/petals/consts.ts
3768
3870
  const MULBERRY$8 = mulberry32(13);
@@ -3777,7 +3879,7 @@ const PETAL_COLORS = [
3777
3879
  ];
3778
3880
  //#endregion
3779
3881
  //#region src/petals/layer.ts
3780
- var PetalLayer = class extends SimulationLayer {
3882
+ var Petals = class extends Effect {
3781
3883
  #scale;
3782
3884
  #size;
3783
3885
  #speed;
@@ -3799,6 +3901,10 @@ var PetalLayer = class extends SimulationLayer {
3799
3901
  this.#sprites = this.#createSprites();
3800
3902
  for (let i = 0; i < this.#maxCount; ++i) this.#petals.push(this.#createPetal(true));
3801
3903
  }
3904
+ configure(config) {
3905
+ if (config.speed !== void 0) this.#speed = config.speed;
3906
+ if (config.wind !== void 0) this.#wind = config.wind;
3907
+ }
3802
3908
  tick(dt, _width, height) {
3803
3909
  const speedFactor = height / 540 / this.#speed;
3804
3910
  this.#time += .012 * dt;
@@ -3829,14 +3935,13 @@ var PetalLayer = class extends SimulationLayer {
3829
3935
  const py = petal.y * height;
3830
3936
  const displaySize = petal.size * petal.depth;
3831
3937
  const scaleX = Math.cos(petal.flipAngle);
3832
- ctx.save();
3833
- ctx.translate(px, py);
3834
- ctx.rotate(petal.rotation);
3835
- ctx.scale(scaleX, 1);
3938
+ const cos = Math.cos(petal.rotation);
3939
+ const sin = Math.sin(petal.rotation);
3940
+ ctx.setTransform(cos * scaleX, sin * scaleX, -sin, cos, px, py);
3836
3941
  ctx.globalAlpha = .4 + petal.depth * .6;
3837
3942
  ctx.drawImage(this.#sprites[petal.colorIndex % this.#sprites.length], -displaySize / 2, -displaySize / 2, displaySize, displaySize);
3838
- ctx.restore();
3839
3943
  }
3944
+ ctx.resetTransform();
3840
3945
  ctx.globalAlpha = 1;
3841
3946
  }
3842
3947
  #createSprites() {
@@ -3888,12 +3993,10 @@ var PetalLayer = class extends SimulationLayer {
3888
3993
  }
3889
3994
  };
3890
3995
  //#endregion
3891
- //#region src/petals/simulation.ts
3892
- var PetalSimulation = class extends SimulationCanvas {
3893
- constructor(canvas, config = {}) {
3894
- super(canvas, new PetalLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
3895
- }
3896
- };
3996
+ //#region src/petals/index.ts
3997
+ function createPetals(config) {
3998
+ return new Petals(config);
3999
+ }
3897
4000
  //#endregion
3898
4001
  //#region src/plasma/layer.ts
3899
4002
  const DEFAULT_PALETTE = [
@@ -3923,7 +4026,7 @@ const DEFAULT_PALETTE = [
3923
4026
  b: 100
3924
4027
  }
3925
4028
  ];
3926
- var PlasmaLayer = class extends SimulationLayer {
4029
+ var Plasma = class extends Effect {
3927
4030
  #speed;
3928
4031
  #scale;
3929
4032
  #resolution;
@@ -3939,6 +4042,10 @@ var PlasmaLayer = class extends SimulationLayer {
3939
4042
  this.#resolution = config.resolution ?? 4;
3940
4043
  this.#palette = config.palette ?? DEFAULT_PALETTE;
3941
4044
  }
4045
+ configure(config) {
4046
+ if (config.speed !== void 0) this.#speed = config.speed;
4047
+ if (config.scale !== void 0) this.#scale = config.scale;
4048
+ }
3942
4049
  tick(dt, _width, _height) {
3943
4050
  this.#time += .02 * dt * this.#speed;
3944
4051
  }
@@ -3983,12 +4090,10 @@ var PlasmaLayer = class extends SimulationLayer {
3983
4090
  }
3984
4091
  };
3985
4092
  //#endregion
3986
- //#region src/plasma/simulation.ts
3987
- var PlasmaSimulation = class extends SimulationCanvas {
3988
- constructor(canvas, config = {}) {
3989
- super(canvas, new PlasmaLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
3990
- }
3991
- };
4093
+ //#region src/plasma/index.ts
4094
+ function createPlasma(config) {
4095
+ return new Plasma(config);
4096
+ }
3992
4097
  //#endregion
3993
4098
  //#region src/rain/consts.ts
3994
4099
  const MULBERRY$7 = mulberry32(13);
@@ -4014,7 +4119,7 @@ const VARIANT_PRESETS = {
4014
4119
  splashes: true
4015
4120
  }
4016
4121
  };
4017
- var RainLayer = class extends SimulationLayer {
4122
+ var Rain = class extends Effect {
4018
4123
  #scale;
4019
4124
  #speed;
4020
4125
  #wind;
@@ -4042,6 +4147,11 @@ var RainLayer = class extends SimulationLayer {
4042
4147
  if (innerWidth < 991) this.#maxDrops = Math.floor(this.#maxDrops / 2);
4043
4148
  for (let i = 0; i < this.#maxDrops; ++i) this.#drops.push(this.#createDrop(true));
4044
4149
  }
4150
+ configure(config) {
4151
+ if (config.speed !== void 0) this.#speed = config.speed;
4152
+ if (config.wind !== void 0) this.#wind = config.wind;
4153
+ if (config.splashes !== void 0) this.#enableSplashes = config.splashes;
4154
+ }
4045
4155
  tick(dt, width, height) {
4046
4156
  let aliveDrops = 0;
4047
4157
  for (let i = 0; i < this.#drops.length; i++) {
@@ -4228,18 +4338,16 @@ var SplashParticle = class SplashParticle {
4228
4338
  }
4229
4339
  };
4230
4340
  //#endregion
4231
- //#region src/rain/simulation.ts
4232
- var RainSimulation = class extends SimulationCanvas {
4233
- constructor(canvas, config = {}) {
4234
- super(canvas, new RainLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
4235
- }
4236
- };
4341
+ //#region src/rain/index.ts
4342
+ function createRain(config) {
4343
+ return new Rain(config);
4344
+ }
4237
4345
  //#endregion
4238
4346
  //#region src/sandstorm/consts.ts
4239
4347
  const MULBERRY$6 = mulberry32(13);
4240
4348
  //#endregion
4241
4349
  //#region src/sandstorm/layer.ts
4242
- var SandstormLayer = class extends SimulationLayer {
4350
+ var Sandstorm = class extends Effect {
4243
4351
  #scale;
4244
4352
  #wind;
4245
4353
  #turbulence;
@@ -4264,6 +4372,10 @@ var SandstormLayer = class extends SimulationLayer {
4264
4372
  if (innerWidth < 991) this.#maxCount = Math.floor(this.#maxCount / 2);
4265
4373
  for (let i = 0; i < this.#maxCount; ++i) this.#grains.push(this.#createGrain(true));
4266
4374
  }
4375
+ configure(config) {
4376
+ if (config.wind !== void 0) this.#wind = config.wind;
4377
+ if (config.turbulence !== void 0) this.#turbulence = config.turbulence;
4378
+ }
4267
4379
  tick(dt, width, height) {
4268
4380
  this.#time += .02 * dt;
4269
4381
  const gustX = Math.sin(this.#time * .3) * .5 + Math.sin(this.#time * .8 + 1) * .3 + Math.sin(this.#time * 2.1) * .2;
@@ -4338,12 +4450,162 @@ var SandstormLayer = class extends SimulationLayer {
4338
4450
  }
4339
4451
  };
4340
4452
  //#endregion
4341
- //#region src/sandstorm/simulation.ts
4342
- var SandstormSimulation = class extends SimulationCanvas {
4343
- constructor(canvas, config = {}) {
4344
- super(canvas, new SandstormLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
4453
+ //#region src/sandstorm/index.ts
4454
+ function createSandstorm(config) {
4455
+ return new Sandstorm(config);
4456
+ }
4457
+ //#endregion
4458
+ //#region src/scene.ts
4459
+ /**
4460
+ * Internal canvas runner that drives all layers in a Scene.
4461
+ */
4462
+ var SceneCanvas = class extends LimitedFrameRateCanvas {
4463
+ #layers;
4464
+ #contextOptions;
4465
+ #offscreen = null;
4466
+ #offscreenCtx = null;
4467
+ constructor(canvas, layers, frameRate, options) {
4468
+ super(canvas, frameRate, options);
4469
+ this.#layers = layers;
4470
+ this.#contextOptions = options;
4471
+ canvas.style.position = "absolute";
4472
+ canvas.style.top = "0";
4473
+ canvas.style.left = "0";
4474
+ canvas.style.height = "100%";
4475
+ canvas.style.width = "100%";
4476
+ }
4477
+ start() {
4478
+ for (const layer of this.#layers) layer.onMount(this.canvas);
4479
+ super.start();
4480
+ }
4481
+ destroy() {
4482
+ for (const layer of this.#layers) layer.onUnmount(this.canvas);
4483
+ super.destroy();
4484
+ }
4485
+ draw() {
4486
+ this.canvas.height = this.height;
4487
+ this.canvas.width = this.width;
4488
+ const ctx = this.context;
4489
+ ctx.clearRect(0, 0, this.width, this.height);
4490
+ for (const layer of this.#layers) if (layer.fade) {
4491
+ const offCtx = this.#getOffscreenCtx(this.width, this.height);
4492
+ offCtx.clearRect(0, 0, this.width, this.height);
4493
+ layer.draw(offCtx, this.width, this.height);
4494
+ applyEdgeFade(offCtx, this.width, this.height, layer.fade);
4495
+ ctx.drawImage(this.#offscreen, 0, 0);
4496
+ } else {
4497
+ ctx.save();
4498
+ layer.draw(ctx, this.width, this.height);
4499
+ ctx.restore();
4500
+ }
4501
+ }
4502
+ tick() {
4503
+ const dt = (this.delta > 0 && this.delta < 200 ? this.delta / (1e3 / 60) : 1) * this.speed * LimitedFrameRateCanvas.globalSpeed;
4504
+ for (const layer of this.#layers) layer.tick(dt, this.width, this.height);
4505
+ }
4506
+ onResize() {
4507
+ super.onResize();
4508
+ if (this.#offscreen) {
4509
+ this.#offscreen.width = this.width;
4510
+ this.#offscreen.height = this.height;
4511
+ }
4512
+ for (const layer of this.#layers) layer.onResize(this.width, this.height);
4513
+ }
4514
+ #getOffscreenCtx(width, height) {
4515
+ if (!this.#offscreen) {
4516
+ this.#offscreen = document.createElement("canvas");
4517
+ this.#offscreen.width = width;
4518
+ this.#offscreen.height = height;
4519
+ this.#offscreenCtx = this.#offscreen.getContext("2d", this.#contextOptions);
4520
+ }
4521
+ return this.#offscreenCtx;
4522
+ }
4523
+ };
4524
+ /**
4525
+ * Composable canvas that renders multiple Effect layers in order (first = bottom, last = top).
4526
+ *
4527
+ * @example
4528
+ * const scene = new Scene()
4529
+ * .mount(canvas)
4530
+ * .layer(new Aurora({ bands: 5 }))
4531
+ * .layer(new Stars().withFade({ bottom: 0.4 }))
4532
+ * .start();
4533
+ */
4534
+ var Scene = class {
4535
+ #layers = [];
4536
+ #frameRate;
4537
+ #defaultOptions;
4538
+ #runner = null;
4539
+ constructor(frameRate = 60, options = { colorSpace: "display-p3" }) {
4540
+ this.#frameRate = frameRate;
4541
+ this.#defaultOptions = options;
4542
+ }
4543
+ /**
4544
+ * Mount the scene to a canvas element or CSS selector.
4545
+ */
4546
+ mount(canvas, options) {
4547
+ if (typeof canvas === "string") {
4548
+ const el = document.querySelector(canvas);
4549
+ if (!el) throw new Error(`Scene.mount(): no element found for selector "${canvas}".`);
4550
+ canvas = el;
4551
+ }
4552
+ this.#runner?.destroy();
4553
+ this.#runner = new SceneCanvas(canvas, this.#layers, this.#frameRate, options ?? this.#defaultOptions);
4554
+ return this;
4555
+ }
4556
+ /**
4557
+ * Add an effect layer. Layers are rendered in the order they are added.
4558
+ * If the scene is already running, the layer is mounted immediately.
4559
+ */
4560
+ layer(effect) {
4561
+ this.#layers.push(effect);
4562
+ if (this.#runner?.isTicking) effect.onMount(this.#runner.canvas);
4563
+ return this;
4564
+ }
4565
+ /**
4566
+ * Start the render loop.
4567
+ */
4568
+ start() {
4569
+ this.#runner?.start();
4570
+ return this;
4571
+ }
4572
+ /**
4573
+ * Pause rendering without destroying state. Use resume() to continue.
4574
+ */
4575
+ pause() {
4576
+ this.#runner?.pause();
4577
+ return this;
4578
+ }
4579
+ /**
4580
+ * Resume rendering after pause().
4581
+ */
4582
+ resume() {
4583
+ this.#runner?.resume();
4584
+ return this;
4585
+ }
4586
+ /**
4587
+ * Stop and destroy all layers.
4588
+ */
4589
+ destroy() {
4590
+ this.#runner?.destroy();
4591
+ this.#runner = null;
4592
+ }
4593
+ get speed() {
4594
+ return this.#runner?.speed ?? 1;
4595
+ }
4596
+ set speed(value) {
4597
+ if (this.#runner) this.#runner.speed = value;
4598
+ }
4599
+ get isTicking() {
4600
+ return this.#runner?.isTicking ?? false;
4345
4601
  }
4346
4602
  };
4603
+ /**
4604
+ * Factory alternative to `new Scene()`. Call .mount() and .layer() on the returned instance.
4605
+ */
4606
+ function createScene(frameRate, options) {
4607
+ return new Scene(frameRate, options);
4608
+ }
4347
4609
  //#endregion
4348
4610
  //#region src/shooting-stars/system.ts
4349
4611
  var ShootingStarSystem = class {
@@ -4357,10 +4619,8 @@ var ShootingStarSystem = class {
4357
4619
  #alphaRange;
4358
4620
  #decayMin;
4359
4621
  #decayRange;
4360
- #verticalFade;
4361
4622
  #rng;
4362
4623
  #cooldown;
4363
- #height = 0;
4364
4624
  #stars = [];
4365
4625
  constructor(config, rng) {
4366
4626
  this.#interval = config.interval;
@@ -4377,12 +4637,10 @@ var ShootingStarSystem = class {
4377
4637
  this.#alphaRange = config.alphaRange ?? .3;
4378
4638
  this.#decayMin = config.decayMin ?? .008;
4379
4639
  this.#decayRange = config.decayRange ?? .01;
4380
- this.#verticalFade = config.verticalFade ?? null;
4381
4640
  this.#rng = rng;
4382
4641
  this.#cooldown = this.#interval[0] + this.#rng() * (this.#interval[1] - this.#interval[0]);
4383
4642
  }
4384
4643
  tick(dt, width, height) {
4385
- this.#height = height;
4386
4644
  this.#cooldown -= dt;
4387
4645
  if (this.#cooldown <= 0) {
4388
4646
  this.#stars.push(this.#create(width, height));
@@ -4391,17 +4649,24 @@ var ShootingStarSystem = class {
4391
4649
  let alive = 0;
4392
4650
  for (let i = 0; i < this.#stars.length; i++) {
4393
4651
  const star = this.#stars[i];
4394
- star.trail.push({
4395
- x: star.x,
4396
- y: star.y
4397
- });
4398
- if (star.trail.length > this.#trailLength) star.trail.shift();
4652
+ const trail = star.trail;
4653
+ const maxLen = this.#trailLength;
4654
+ if (trail.length < maxLen) {
4655
+ trail.push({
4656
+ x: star.x,
4657
+ y: star.y
4658
+ });
4659
+ star.trailHead = trail.length - 1;
4660
+ } else {
4661
+ const next = (star.trailHead + 1) % maxLen;
4662
+ trail[next].x = star.x;
4663
+ trail[next].y = star.y;
4664
+ star.trailHead = next;
4665
+ }
4399
4666
  star.x += star.vx * this.#speed * dt;
4400
4667
  star.y += star.vy * this.#speed * dt;
4401
4668
  star.alpha -= star.decay * dt;
4402
- const inBounds = star.alpha > 0 && star.x > -50 && star.x < width + 50 && star.y < height + 50;
4403
- const fullyFaded = this.#verticalFade !== null && star.y / height >= this.#verticalFade[1];
4404
- if (inBounds && !fullyFaded) this.#stars[alive++] = star;
4669
+ if (star.alpha > 0 && star.x > -50 && star.x < width + 50 && star.y < height + 50) this.#stars[alive++] = star;
4405
4670
  }
4406
4671
  this.#stars.length = alive;
4407
4672
  }
@@ -4409,23 +4674,22 @@ var ShootingStarSystem = class {
4409
4674
  const [cr, cg, cb] = this.#color;
4410
4675
  ctx.globalCompositeOperation = "lighter";
4411
4676
  for (const star of this.#stars) {
4412
- let fadeFactor = 1;
4413
- if (this.#verticalFade && this.#height > 0) {
4414
- const [fadeStart, fadeEnd] = this.#verticalFade;
4415
- fadeFactor = 1 - Math.max(0, Math.min(1, (star.y / this.#height - fadeStart) / (fadeEnd - fadeStart)));
4416
- }
4417
- for (let t = 0; t < star.trail.length; t++) {
4418
- const progress = t / star.trail.length;
4419
- const trailAlpha = star.alpha * progress * this.#trailAlphaFactor * fadeFactor;
4677
+ const trail = star.trail;
4678
+ const trailLen = trail.length;
4679
+ const oldest = trailLen === this.#trailLength ? (star.trailHead + 1) % trailLen : 0;
4680
+ for (let t = 0; t < trailLen; t++) {
4681
+ const progress = t / trailLen;
4682
+ const trailAlpha = star.alpha * progress * this.#trailAlphaFactor;
4420
4683
  const trailSize = star.size * progress * this.#scale;
4421
4684
  if (trailAlpha < .01) continue;
4685
+ const idx = (oldest + t) % trailLen;
4422
4686
  ctx.globalAlpha = trailAlpha;
4423
4687
  ctx.beginPath();
4424
- ctx.arc(star.trail[t].x, star.trail[t].y, trailSize, 0, Math.PI * 2);
4688
+ ctx.arc(trail[idx].x, trail[idx].y, trailSize, 0, Math.PI * 2);
4425
4689
  ctx.fillStyle = `rgb(${cr}, ${cg}, ${cb})`;
4426
4690
  ctx.fill();
4427
4691
  }
4428
- const alpha = star.alpha * fadeFactor;
4692
+ const alpha = star.alpha;
4429
4693
  const headSize = star.size * 2 * this.#scale;
4430
4694
  const glow = ctx.createRadialGradient(star.x, star.y, 0, star.x, star.y, headSize);
4431
4695
  glow.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, ${alpha})`);
@@ -4453,7 +4717,8 @@ var ShootingStarSystem = class {
4453
4717
  alpha: this.#alphaMin + this.#rng() * this.#alphaRange,
4454
4718
  size: 1.5 + this.#rng() * 2,
4455
4719
  decay: this.#decayMin + this.#rng() * this.#decayRange,
4456
- trail: []
4720
+ trail: [],
4721
+ trailHead: 0
4457
4722
  };
4458
4723
  }
4459
4724
  };
@@ -4465,7 +4730,7 @@ const MULBERRY$5 = mulberry32(13);
4465
4730
  const SPRITE_SIZE = 64;
4466
4731
  const SPRITE_CENTER = SPRITE_SIZE / 2;
4467
4732
  const SPRITE_RADIUS = SPRITE_SIZE / 2;
4468
- var SnowLayer = class extends SimulationLayer {
4733
+ var Snow = class extends Effect {
4469
4734
  #scale;
4470
4735
  #size;
4471
4736
  #speed;
@@ -4488,6 +4753,9 @@ var SnowLayer = class extends SimulationLayer {
4488
4753
  this.#sprites = this.#createSprites(r, g, b);
4489
4754
  for (let i = 0; i < this.#maxParticles; ++i) this.#snowflakes.push(this.#createSnowflake(true));
4490
4755
  }
4756
+ configure(config) {
4757
+ if (config.speed !== void 0) this.#speed = config.speed;
4758
+ }
4491
4759
  onResize(_width, height) {
4492
4760
  this.#height = height;
4493
4761
  }
@@ -4530,11 +4798,11 @@ var SnowLayer = class extends SimulationLayer {
4530
4798
  if (displaySize < .5) continue;
4531
4799
  ctx.globalAlpha = this.#baseOpacity * (.15 + snowflake.depth * .85);
4532
4800
  if (snowflake.spriteIndex === 3) {
4533
- ctx.save();
4534
- ctx.translate(px, py);
4535
- ctx.rotate(snowflake.rotation);
4801
+ const cos = Math.cos(snowflake.rotation);
4802
+ const sin = Math.sin(snowflake.rotation);
4803
+ ctx.setTransform(cos, sin, -sin, cos, px, py);
4536
4804
  ctx.drawImage(this.#sprites[snowflake.spriteIndex], -displayRadius, -displayRadius, displaySize, displaySize);
4537
- ctx.restore();
4805
+ ctx.resetTransform();
4538
4806
  } else ctx.drawImage(this.#sprites[snowflake.spriteIndex], px - displayRadius, py - displayRadius, displaySize, displaySize);
4539
4807
  }
4540
4808
  ctx.globalAlpha = 1;
@@ -4647,12 +4915,10 @@ var SnowLayer = class extends SimulationLayer {
4647
4915
  }
4648
4916
  };
4649
4917
  //#endregion
4650
- //#region src/snow/simulation.ts
4651
- var SnowSimulation = class extends SimulationCanvas {
4652
- constructor(canvas, config = {}) {
4653
- super(canvas, new SnowLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
4654
- }
4655
- };
4918
+ //#region src/snow/index.ts
4919
+ function createSnow(config) {
4920
+ return new Snow(config);
4921
+ }
4656
4922
  //#endregion
4657
4923
  //#region src/sparklers/consts.ts
4658
4924
  const MULBERRY$4 = mulberry32(13);
@@ -4664,7 +4930,7 @@ const DEFAULT_COLORS$1 = [
4664
4930
  "#ffffff",
4665
4931
  "#ffee88"
4666
4932
  ];
4667
- var SparklerLayer = class extends SimulationLayer {
4933
+ var Sparklers = class extends Effect {
4668
4934
  #scale;
4669
4935
  #emitRate;
4670
4936
  #maxSparks;
@@ -4681,6 +4947,8 @@ var SparklerLayer = class extends SimulationLayer {
4681
4947
  #emitY = .5;
4682
4948
  #mouseOnCanvas = false;
4683
4949
  #sparks = [];
4950
+ #mountedCanvas = null;
4951
+ #cachedRect = null;
4684
4952
  constructor(config = {}) {
4685
4953
  super();
4686
4954
  this.#scale = config.scale ?? 1;
@@ -4696,11 +4964,13 @@ var SparklerLayer = class extends SimulationLayer {
4696
4964
  this.#onMouseMoveBound = this.#onMouseMove.bind(this);
4697
4965
  this.#onMouseLeaveBound = this.#onMouseLeave.bind(this);
4698
4966
  }
4699
- setPosition(x, y) {
4967
+ moveTo(x, y) {
4700
4968
  this.#emitX = x;
4701
4969
  this.#emitY = y;
4702
4970
  }
4703
4971
  onMount(canvas) {
4972
+ this.#mountedCanvas = canvas;
4973
+ this.#cachedRect = canvas.getBoundingClientRect();
4704
4974
  if (this.#hoverMode) {
4705
4975
  canvas.addEventListener("mousemove", this.#onMouseMoveBound, { passive: true });
4706
4976
  canvas.addEventListener("mouseleave", this.#onMouseLeaveBound, { passive: true });
@@ -4709,12 +4979,26 @@ var SparklerLayer = class extends SimulationLayer {
4709
4979
  onUnmount(canvas) {
4710
4980
  canvas.removeEventListener("mousemove", this.#onMouseMoveBound);
4711
4981
  canvas.removeEventListener("mouseleave", this.#onMouseLeaveBound);
4982
+ this.#mountedCanvas = null;
4983
+ this.#cachedRect = null;
4984
+ }
4985
+ onResize() {
4986
+ if (this.#mountedCanvas) this.#cachedRect = this.#mountedCanvas.getBoundingClientRect();
4987
+ }
4988
+ configure(config) {
4989
+ if (config.scale !== void 0) this.#scale = config.scale;
4990
+ if (config.emitRate !== void 0) this.#emitRate = config.emitRate;
4991
+ if (config.friction !== void 0) this.#friction = config.friction;
4992
+ if (config.gravity !== void 0) this.#gravity = config.gravity;
4993
+ if (config.trailLength !== void 0) this.#trailLength = config.trailLength;
4994
+ if (config.hoverMode !== void 0) this.#hoverMode = config.hoverMode;
4712
4995
  }
4713
4996
  tick(dt, width, height) {
4714
4997
  if (!this.#hoverMode || this.#mouseOnCanvas) {
4715
4998
  const emitCount = Math.min(this.#emitRate, this.#maxSparks - this.#sparks.length);
4716
4999
  for (let i = 0; i < emitCount; i++) this.#sparks.push(this.#createSpark(width, height));
4717
5000
  }
5001
+ const frictionFactor = Math.pow(this.#friction, dt);
4718
5002
  let alive = 0;
4719
5003
  for (let i = 0; i < this.#sparks.length; i++) {
4720
5004
  const spark = this.#sparks[i];
@@ -4723,8 +5007,8 @@ var SparklerLayer = class extends SimulationLayer {
4723
5007
  y: spark.y
4724
5008
  });
4725
5009
  if (spark.trail.length > this.#trailLength) spark.trail.shift();
4726
- spark.vx *= Math.pow(this.#friction, dt);
4727
- spark.vy *= Math.pow(this.#friction, dt);
5010
+ spark.vx *= frictionFactor;
5011
+ spark.vy *= frictionFactor;
4728
5012
  spark.vy += this.#gravity * this.#scale * dt;
4729
5013
  spark.x += spark.vx * dt;
4730
5014
  spark.y += spark.vy * dt;
@@ -4767,7 +5051,7 @@ var SparklerLayer = class extends SimulationLayer {
4767
5051
  ctx.globalCompositeOperation = "source-over";
4768
5052
  }
4769
5053
  #onMouseMove(evt) {
4770
- const rect = evt.currentTarget.getBoundingClientRect();
5054
+ const rect = this.#cachedRect ?? evt.currentTarget.getBoundingClientRect();
4771
5055
  this.#emitX = (evt.clientX - rect.left) / rect.width;
4772
5056
  this.#emitY = (evt.clientY - rect.top) / rect.height;
4773
5057
  this.#mouseOnCanvas = true;
@@ -4861,29 +5145,20 @@ var SparklerParticle = class {
4861
5145
  }
4862
5146
  };
4863
5147
  //#endregion
4864
- //#region src/sparklers/simulation.ts
4865
- var SparklerSimulation = class extends SimulationCanvas {
4866
- #layer;
4867
- constructor(canvas, config = {}) {
4868
- const layer = new SparklerLayer(config);
4869
- super(canvas, layer, 60, config.canvasOptions ?? { colorSpace: "display-p3" });
4870
- this.#layer = layer;
4871
- }
4872
- setPosition(x, y) {
4873
- this.#layer.setPosition(x, y);
4874
- }
4875
- };
5148
+ //#region src/sparklers/index.ts
5149
+ function createSparklers(config) {
5150
+ return new Sparklers(config);
5151
+ }
4876
5152
  //#endregion
4877
5153
  //#region src/stars/consts.ts
4878
5154
  const MULBERRY$3 = mulberry32(13);
4879
5155
  //#endregion
4880
5156
  //#region src/stars/layer.ts
4881
- var StarLayer = class extends SimulationLayer {
5157
+ var Stars = class extends Effect {
4882
5158
  #mode;
4883
5159
  #twinkleSpeed;
4884
5160
  #colorRGB;
4885
5161
  #scale;
4886
- #verticalFade;
4887
5162
  #shootingStarSystem;
4888
5163
  #starCount;
4889
5164
  #time = 0;
@@ -4894,7 +5169,6 @@ var StarLayer = class extends SimulationLayer {
4894
5169
  this.#starCount = config.starCount ?? 150;
4895
5170
  this.#twinkleSpeed = config.twinkleSpeed ?? 1;
4896
5171
  this.#scale = config.scale ?? 1;
4897
- this.#verticalFade = config.verticalFade ?? null;
4898
5172
  this.#colorRGB = hexToRGB(config.color ?? "#ffffff");
4899
5173
  const shootingColorRGB = hexToRGB(config.shootingColor ?? "#ffffff");
4900
5174
  this.#shootingStarSystem = this.#mode === "shooting" || this.#mode === "both" ? new ShootingStarSystem({
@@ -4907,11 +5181,14 @@ var StarLayer = class extends SimulationLayer {
4907
5181
  alphaMin: .8,
4908
5182
  alphaRange: .2,
4909
5183
  decayMin: .01,
4910
- decayRange: .015,
4911
- verticalFade: this.#verticalFade ?? void 0
5184
+ decayRange: .015
4912
5185
  }, () => MULBERRY$3.next()) : null;
4913
5186
  if (this.#mode === "sky" || this.#mode === "both") for (let i = 0; i < this.#starCount; ++i) this.#stars.push(this.#createStar());
4914
5187
  }
5188
+ configure(config) {
5189
+ if (config.twinkleSpeed !== void 0) this.#twinkleSpeed = config.twinkleSpeed;
5190
+ if (config.scale !== void 0) this.#scale = config.scale;
5191
+ }
4915
5192
  tick(dt, width, height) {
4916
5193
  this.#time += .02 * dt;
4917
5194
  this.#shootingStarSystem?.tick(dt, width, height);
@@ -4923,13 +5200,7 @@ var StarLayer = class extends SimulationLayer {
4923
5200
  for (const star of this.#stars) {
4924
5201
  const px = star.x * width;
4925
5202
  const py = star.y * height;
4926
- let alpha = star.brightness * (.3 + .7 * (.5 + .5 * Math.sin(this.#time * star.twinkleSpeed * this.#twinkleSpeed + star.twinklePhase)));
4927
- if (this.#verticalFade) {
4928
- const [fadeStart, fadeEnd] = this.#verticalFade;
4929
- const fadeFactor = 1 - Math.max(0, Math.min(1, (star.y - fadeStart) / (fadeEnd - fadeStart)));
4930
- alpha *= fadeFactor;
4931
- if (alpha <= 0) continue;
4932
- }
5203
+ const alpha = star.brightness * (.3 + .7 * (.5 + .5 * Math.sin(this.#time * star.twinkleSpeed * this.#twinkleSpeed + star.twinklePhase)));
4933
5204
  const size = star.size * this.#scale;
4934
5205
  ctx.globalAlpha = alpha;
4935
5206
  ctx.beginPath();
@@ -4967,12 +5238,10 @@ var StarLayer = class extends SimulationLayer {
4967
5238
  }
4968
5239
  };
4969
5240
  //#endregion
4970
- //#region src/stars/simulation.ts
4971
- var StarSimulation = class extends SimulationCanvas {
4972
- constructor(canvas, config = {}) {
4973
- super(canvas, new StarLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
4974
- }
4975
- };
5241
+ //#region src/stars/index.ts
5242
+ function createStars(config) {
5243
+ return new Stars(config);
5244
+ }
4976
5245
  //#endregion
4977
5246
  //#region src/streamers/consts.ts
4978
5247
  const MULBERRY$2 = mulberry32(13);
@@ -4988,7 +5257,7 @@ const STREAMER_COLORS = [
4988
5257
  ];
4989
5258
  //#endregion
4990
5259
  //#region src/streamers/layer.ts
4991
- var StreamerLayer = class extends SimulationLayer {
5260
+ var Streamers = class extends Effect {
4992
5261
  #colors;
4993
5262
  #scale;
4994
5263
  #speed;
@@ -5014,6 +5283,9 @@ var StreamerLayer = class extends SimulationLayer {
5014
5283
  for (let i = 0; i < this.#count; i++) this.#streamers.push(this.#createStreamer(true));
5015
5284
  }
5016
5285
  }
5286
+ configure(config) {
5287
+ if (config.speed !== void 0) this.#speed = config.speed;
5288
+ }
5017
5289
  tick(dt, width, height) {
5018
5290
  this.#width = width;
5019
5291
  this.#height = height;
@@ -5128,12 +5400,10 @@ var StreamerLayer = class extends SimulationLayer {
5128
5400
  }
5129
5401
  };
5130
5402
  //#endregion
5131
- //#region src/streamers/simulation.ts
5132
- var StreamerSimulation = class extends SimulationCanvas {
5133
- constructor(canvas, config = {}) {
5134
- super(canvas, new StreamerLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
5135
- }
5136
- };
5403
+ //#region src/streamers/index.ts
5404
+ function createStreamers(config) {
5405
+ return new Streamers(config);
5406
+ }
5137
5407
  //#endregion
5138
5408
  //#region src/trail.ts
5139
5409
  var Trail = class {
@@ -5247,12 +5517,11 @@ const DEFAULT_COLORS = [
5247
5517
  "#3399cc",
5248
5518
  "#66c2e0"
5249
5519
  ];
5250
- var WaveLayer = class extends SimulationLayer {
5520
+ var Waves = class extends Effect {
5251
5521
  #speed;
5252
- #foamColor;
5253
5522
  #foamAmount;
5254
5523
  #scale;
5255
- #time = 0;
5524
+ #foamRGB;
5256
5525
  #waves = [];
5257
5526
  #foamParticles = [];
5258
5527
  #maxFoamParticles;
@@ -5261,10 +5530,10 @@ var WaveLayer = class extends SimulationLayer {
5261
5530
  const layers = config.layers ?? 5;
5262
5531
  const colors = config.colors ?? DEFAULT_COLORS;
5263
5532
  this.#speed = config.speed ?? 1;
5264
- this.#foamColor = config.foamColor ?? "#ffffff";
5265
5533
  this.#foamAmount = config.foamAmount ?? .4;
5266
5534
  this.#scale = config.scale ?? 1;
5267
5535
  this.#maxFoamParticles = 120;
5536
+ this.#foamRGB = hexToRGB(config.foamColor ?? "#ffffff");
5268
5537
  if (innerWidth < 991) this.#maxFoamParticles = Math.floor(this.#maxFoamParticles / 2);
5269
5538
  for (let i = 0; i < layers; i++) {
5270
5539
  const depth = i / Math.max(layers - 1, 1);
@@ -5272,17 +5541,22 @@ var WaveLayer = class extends SimulationLayer {
5272
5541
  this.#waves.push({
5273
5542
  amplitude: (20 + MULBERRY$1.next() * 30) * (1 - depth * .4),
5274
5543
  frequency: .005 + MULBERRY$1.next() * .008 + depth * .002,
5275
- speed: (.4 + MULBERRY$1.next() * .6 + depth * .3) * this.#speed,
5544
+ speed: .4 + MULBERRY$1.next() * .6 + depth * .3,
5276
5545
  phase: MULBERRY$1.next() * Math.PI * 2,
5277
5546
  baseY: .35 + depth * .13,
5278
5547
  color,
5279
- foamThreshold: .6 + MULBERRY$1.next() * .3
5548
+ foamThreshold: .6 + MULBERRY$1.next() * .3,
5549
+ rgb: hexToRGB(color)
5280
5550
  });
5281
5551
  }
5282
5552
  }
5553
+ configure(config) {
5554
+ if (config.speed !== void 0) this.#speed = config.speed;
5555
+ if (config.foamAmount !== void 0) this.#foamAmount = config.foamAmount;
5556
+ if (config.scale !== void 0) this.#scale = config.scale;
5557
+ }
5283
5558
  tick(dt, width, height) {
5284
- this.#time += .02 * dt * this.#speed;
5285
- for (const wave of this.#waves) wave.phase += .015 * wave.speed * dt;
5559
+ for (const wave of this.#waves) wave.phase += .015 * wave.speed * this.#speed * dt;
5286
5560
  let aliveFoam = 0;
5287
5561
  for (let i = 0; i < this.#foamParticles.length; i++) {
5288
5562
  const foam = this.#foamParticles[i];
@@ -5312,6 +5586,7 @@ var WaveLayer = class extends SimulationLayer {
5312
5586
  const step = 2;
5313
5587
  for (let wi = 0; wi < this.#waves.length; wi++) {
5314
5588
  const wave = this.#waves[wi];
5589
+ const [wr, wg, wb] = wave.rgb;
5315
5590
  const centerY = wave.baseY * height;
5316
5591
  ctx.beginPath();
5317
5592
  ctx.moveTo(0, height);
@@ -5325,54 +5600,35 @@ var WaveLayer = class extends SimulationLayer {
5325
5600
  ctx.lineTo(width, height);
5326
5601
  ctx.closePath();
5327
5602
  const gradient = ctx.createLinearGradient(0, centerY - wave.amplitude * this.#scale, 0, height);
5328
- gradient.addColorStop(0, this.#adjustAlpha(wave.color, .85));
5329
- gradient.addColorStop(.4, wave.color);
5330
- gradient.addColorStop(1, this.#darkenColor(wave.color, .6));
5603
+ gradient.addColorStop(0, `rgba(${wr}, ${wg}, ${wb}, 0.85)`);
5604
+ gradient.addColorStop(.4, `rgb(${wr}, ${wg}, ${wb})`);
5605
+ gradient.addColorStop(1, `rgb(${Math.floor(wr * .6)}, ${Math.floor(wg * .6)}, ${Math.floor(wb * .6)})`);
5331
5606
  ctx.fillStyle = gradient;
5332
5607
  ctx.fill();
5333
5608
  }
5334
- if (this.#foamAmount > 0) for (const foam of this.#foamParticles) {
5335
- if (foam.alpha <= 0) continue;
5336
- ctx.beginPath();
5337
- ctx.arc(foam.x, foam.y, foam.size * this.#scale, 0, Math.PI * 2);
5338
- ctx.fillStyle = this.#adjustAlpha(this.#foamColor, foam.alpha * this.#foamAmount);
5339
- ctx.fill();
5609
+ if (this.#foamAmount > 0) {
5610
+ const [fr, fg, fb] = this.#foamRGB;
5611
+ for (const foam of this.#foamParticles) {
5612
+ if (foam.alpha <= 0) continue;
5613
+ ctx.beginPath();
5614
+ ctx.arc(foam.x, foam.y, foam.size * this.#scale, 0, Math.PI * 2);
5615
+ ctx.fillStyle = `rgba(${fr}, ${fg}, ${fb}, ${foam.alpha * this.#foamAmount})`;
5616
+ ctx.fill();
5617
+ }
5340
5618
  }
5341
5619
  }
5342
- #adjustAlpha(color, alpha) {
5343
- const canvas = document.createElement("canvas");
5344
- canvas.width = 1;
5345
- canvas.height = 1;
5346
- const ctx = canvas.getContext("2d");
5347
- ctx.fillStyle = color;
5348
- ctx.fillRect(0, 0, 1, 1);
5349
- const data = ctx.getImageData(0, 0, 1, 1).data;
5350
- return `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${alpha})`;
5351
- }
5352
- #darkenColor(color, factor) {
5353
- const canvas = document.createElement("canvas");
5354
- canvas.width = 1;
5355
- canvas.height = 1;
5356
- const ctx = canvas.getContext("2d");
5357
- ctx.fillStyle = color;
5358
- ctx.fillRect(0, 0, 1, 1);
5359
- const data = ctx.getImageData(0, 0, 1, 1).data;
5360
- return `rgb(${Math.floor(data[0] * factor)}, ${Math.floor(data[1] * factor)}, ${Math.floor(data[2] * factor)})`;
5361
- }
5362
5620
  };
5363
5621
  //#endregion
5364
- //#region src/waves/simulation.ts
5365
- var WaveSimulation = class extends SimulationCanvas {
5366
- constructor(canvas, config = {}) {
5367
- super(canvas, new WaveLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
5368
- }
5369
- };
5622
+ //#region src/waves/index.ts
5623
+ function createWaves(config) {
5624
+ return new Waves(config);
5625
+ }
5370
5626
  //#endregion
5371
5627
  //#region src/wormhole/consts.ts
5372
5628
  const MULBERRY = mulberry32(13);
5373
5629
  //#endregion
5374
5630
  //#region src/wormhole/layer.ts
5375
- var WormholeLayer = class extends SimulationLayer {
5631
+ var Wormhole = class extends Effect {
5376
5632
  #speed;
5377
5633
  #colorRGB;
5378
5634
  #direction;
@@ -5401,6 +5657,10 @@ var WormholeLayer = class extends SimulationLayer {
5401
5657
  for (let i = 0; i < this.#count; ++i) this.#particles.push(this.#createParticle(true));
5402
5658
  }
5403
5659
  }
5660
+ configure(config) {
5661
+ if (config.speed !== void 0) this.#speed = config.speed;
5662
+ if (config.scale !== void 0) this.#scale = config.scale;
5663
+ }
5404
5664
  tick(dt, width, height) {
5405
5665
  this.#width = width;
5406
5666
  this.#height = height;
@@ -5492,13 +5752,11 @@ var WormholeLayer = class extends SimulationLayer {
5492
5752
  }
5493
5753
  };
5494
5754
  //#endregion
5495
- //#region src/wormhole/simulation.ts
5496
- var WormholeSimulation = class extends SimulationCanvas {
5497
- constructor(canvas, config = {}) {
5498
- super(canvas, new WormholeLayer(config), 60, config.canvasOptions ?? { colorSpace: "display-p3" });
5499
- }
5500
- };
5755
+ //#region src/wormhole/index.ts
5756
+ function createWormhole(config) {
5757
+ return new Wormhole(config);
5758
+ }
5501
5759
  //#endregion
5502
- export { AuroraLayer, AuroraSimulation, BalloonLayer, BalloonParticle, BalloonSimulation, BubbleLayer, BubbleSimulation, ConfettiLayer, ConfettiParticle, ConfettiSimulation, DonutLayer, DonutSimulation, EXPLOSION_CONFIGS, Explosion, FIREWORK_VARIANTS, FireflyLayer, FireflyParticle, FireflySimulation, FirepitLayer, FirepitSimulation, Firework, FireworkLayer, FireworkSimulation, GlitterLayer, GlitterSimulation, LanternLayer, LanternSimulation, LayeredSimulation, LeafLayer, LeafSimulation, LightningLayer, LightningSimulation, LightningSystem, LimitedFrameRateCanvas, MatrixLayer, MatrixSimulation, OrbitLayer, OrbitSimulation, PALETTES, ParticleLayer, ParticleSimulation, PetalLayer, PetalSimulation, PlasmaLayer, PlasmaSimulation, RainLayer, RainSimulation, RaindropParticle, SHAPE_PATHS, SandstormLayer, SandstormSimulation, ShootingStarSystem, SimulationCanvas, SimulationLayer, SnowLayer, SnowSimulation, Spark, SparklerLayer, SparklerParticle, SparklerSimulation, SplashParticle, StarLayer, StarSimulation, StreamerLayer, StreamerSimulation, Trail, WaveLayer, WaveSimulation, WormholeLayer, WormholeSimulation, createFireflySprite, parseColor };
5760
+ export { BalloonParticle, ConfettiParticle, EXPLOSION_CONFIGS, Effect, Explosion, FIREWORK_VARIANTS, FireflyParticle, Firework, LightningSystem, LimitedFrameRateCanvas, PALETTES, RaindropParticle, SHAPE_PATHS, Scene, ShootingStarSystem, Spark, SparklerParticle, SplashParticle, Trail, createAurora, createBalloons, createBubbles, createConfetti, createDonuts, createExplosion, createFireflies, createFireflySprite, createFirepit, createFireworks, createGlitter, createLanterns, createLeaves, createLightning, createMatrix, createOrbits, createParticles, createPetals, createPlasma, createRain, createSandstorm, createScene, createSnow, createSparklers, createStars, createStreamers, createWaves, createWormhole, parseColor };
5503
5761
 
5504
5762
  //# sourceMappingURL=index.mjs.map