@basmilius/sparkle 2.1.0 → 2.3.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 (108) hide show
  1. package/dist/index.d.mts +317 -459
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +1258 -949
  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 +92 -2
  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/confetti/shapes.ts +84 -97
  18. package/src/donuts/consts.ts +2 -2
  19. package/src/donuts/index.ts +9 -3
  20. package/src/donuts/layer.ts +43 -12
  21. package/src/effect.ts +107 -0
  22. package/src/fade.ts +87 -0
  23. package/src/fireflies/index.ts +9 -3
  24. package/src/fireflies/layer.ts +26 -9
  25. package/src/fireflies/particle.ts +2 -2
  26. package/src/firepit/index.ts +9 -3
  27. package/src/firepit/layer.ts +26 -7
  28. package/src/fireworks/create-explosion.ts +237 -0
  29. package/src/fireworks/explosion.ts +1 -1
  30. package/src/fireworks/index.ts +15 -3
  31. package/src/fireworks/layer.ts +55 -304
  32. package/src/fireworks/spark.ts +2 -2
  33. package/src/fireworks/types.ts +2 -2
  34. package/src/glitter/index.ts +9 -4
  35. package/src/glitter/layer.ts +15 -7
  36. package/src/glitter/types.ts +10 -0
  37. package/src/index.ts +3 -4
  38. package/src/lanterns/index.ts +9 -4
  39. package/src/lanterns/layer.ts +22 -10
  40. package/src/lanterns/types.ts +8 -0
  41. package/src/layer.ts +13 -11
  42. package/src/leaves/index.ts +9 -4
  43. package/src/leaves/layer.ts +21 -14
  44. package/src/leaves/types.ts +9 -0
  45. package/src/lightning/index.ts +9 -4
  46. package/src/lightning/layer.ts +4 -4
  47. package/src/lightning/system.ts +3 -3
  48. package/src/lightning/types.ts +10 -2
  49. package/src/matrix/index.ts +9 -4
  50. package/src/matrix/layer.ts +15 -7
  51. package/src/matrix/types.ts +9 -0
  52. package/src/orbits/index.ts +9 -4
  53. package/src/orbits/layer.ts +51 -21
  54. package/src/orbits/types.ts +12 -1
  55. package/src/particles/index.ts +9 -3
  56. package/src/particles/layer.ts +55 -12
  57. package/src/petals/index.ts +9 -3
  58. package/src/petals/layer.ts +29 -13
  59. package/src/plasma/index.ts +9 -3
  60. package/src/plasma/layer.ts +21 -6
  61. package/src/rain/index.ts +9 -3
  62. package/src/rain/layer.ts +30 -8
  63. package/src/sandstorm/index.ts +9 -3
  64. package/src/sandstorm/layer.ts +26 -9
  65. package/src/scene.ts +204 -0
  66. package/src/shooting-stars/system.ts +26 -24
  67. package/src/shooting-stars/types.ts +2 -1
  68. package/src/simulation-canvas.ts +45 -6
  69. package/src/snow/index.ts +9 -3
  70. package/src/snow/layer.ts +24 -11
  71. package/src/sparklers/index.ts +13 -3
  72. package/src/sparklers/layer.ts +61 -15
  73. package/src/stars/index.ts +9 -3
  74. package/src/stars/layer.ts +28 -22
  75. package/src/streamers/index.ts +9 -3
  76. package/src/streamers/layer.ts +18 -6
  77. package/src/streamers/types.ts +1 -1
  78. package/src/waves/index.ts +9 -3
  79. package/src/waves/layer.ts +42 -45
  80. package/src/waves/types.ts +1 -0
  81. package/src/wormhole/index.ts +9 -3
  82. package/src/wormhole/layer.ts +22 -6
  83. package/src/aurora/simulation.ts +0 -19
  84. package/src/balloons/simulation.ts +0 -19
  85. package/src/bubbles/simulation.ts +0 -20
  86. package/src/confetti/simulation.ts +0 -27
  87. package/src/donuts/simulation.ts +0 -25
  88. package/src/fireflies/simulation.ts +0 -18
  89. package/src/firepit/simulation.ts +0 -17
  90. package/src/fireworks/simulation.ts +0 -18
  91. package/src/glitter/simulation.ts +0 -19
  92. package/src/lanterns/simulation.ts +0 -17
  93. package/src/layered.ts +0 -185
  94. package/src/leaves/simulation.ts +0 -18
  95. package/src/lightning/simulation.ts +0 -17
  96. package/src/matrix/simulation.ts +0 -18
  97. package/src/orbits/simulation.ts +0 -19
  98. package/src/particles/simulation.ts +0 -26
  99. package/src/petals/simulation.ts +0 -18
  100. package/src/plasma/simulation.ts +0 -17
  101. package/src/rain/simulation.ts +0 -21
  102. package/src/sandstorm/simulation.ts +0 -18
  103. package/src/snow/simulation.ts +0 -17
  104. package/src/sparklers/simulation.ts +0 -30
  105. package/src/stars/simulation.ts +0 -22
  106. package/src/streamers/simulation.ts +0 -16
  107. package/src/waves/simulation.ts +0 -18
  108. package/src/wormhole/simulation.ts +0 -17
package/dist/index.mjs CHANGED
@@ -1,141 +1,33 @@
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;
5
+ static #globalFrameRate = null;
6
+ static #showFps = false;
133
7
  static get globalSpeed() {
134
8
  return LimitedFrameRateCanvas.#globalSpeed;
135
9
  }
136
10
  static set globalSpeed(value) {
137
11
  LimitedFrameRateCanvas.#globalSpeed = value;
138
12
  }
13
+ /**
14
+ * Global frame rate override for all canvas instances.
15
+ * null = use each instance's own frame rate.
16
+ * 0 = unlimited (render as fast as the browser allows).
17
+ * Any positive number = cap at that many frames per second.
18
+ */
19
+ static get globalFrameRate() {
20
+ return LimitedFrameRateCanvas.#globalFrameRate;
21
+ }
22
+ static set globalFrameRate(value) {
23
+ LimitedFrameRateCanvas.#globalFrameRate = value;
24
+ }
25
+ static get showFps() {
26
+ return LimitedFrameRateCanvas.#showFps;
27
+ }
28
+ static set showFps(value) {
29
+ LimitedFrameRateCanvas.#showFps = value;
30
+ }
139
31
  #canvas;
140
32
  #context;
141
33
  #frameRate;
@@ -150,6 +42,9 @@ var LimitedFrameRateCanvas = class LimitedFrameRateCanvas {
150
42
  #isStopped = true;
151
43
  #height = 540;
152
44
  #width = 960;
45
+ #fps = "0.0";
46
+ #fpsFrames = 0;
47
+ #fpsTime = 0;
153
48
  get canvas() {
154
49
  return this.#canvas;
155
50
  }
@@ -168,6 +63,9 @@ var LimitedFrameRateCanvas = class LimitedFrameRateCanvas {
168
63
  set speed(value) {
169
64
  this.#speed = value;
170
65
  }
66
+ get dpr() {
67
+ return devicePixelRatio;
68
+ }
171
69
  get frameRate() {
172
70
  return this.#frameRate;
173
71
  }
@@ -190,7 +88,7 @@ var LimitedFrameRateCanvas = class LimitedFrameRateCanvas {
190
88
  this.#canvas = canvas;
191
89
  this.#context = canvas.getContext("2d", options);
192
90
  this.#frameRate = frameRate;
193
- this.#target = 1e3 / frameRate;
91
+ this.#target = frameRate > 0 ? 1e3 / frameRate : 0;
194
92
  this.onVisibilityChange = this.onVisibilityChange.bind(this);
195
93
  this.onResize = this.onResize.bind(this);
196
94
  document.addEventListener("visibilitychange", this.onVisibilityChange, { passive: true });
@@ -200,12 +98,27 @@ var LimitedFrameRateCanvas = class LimitedFrameRateCanvas {
200
98
  if (this.#isStopped) return;
201
99
  this.#current = Date.now();
202
100
  this.#frame = requestAnimationFrame(this.loop.bind(this));
203
- if (this.#then > 0 && this.#current - this.#then + 1 < this.#target) return;
101
+ const globalRate = LimitedFrameRateCanvas.#globalFrameRate;
102
+ const effectiveTarget = globalRate !== null ? globalRate > 0 ? 1e3 / globalRate : 0 : this.#target;
103
+ if (effectiveTarget > 0 && this.#then > 0 && this.#current - this.#then + 1 < effectiveTarget) return;
204
104
  this.#now = this.#current;
205
105
  this.#delta = this.#now - this.#then;
206
106
  ++this.#ticks;
207
107
  this.tick();
208
108
  this.draw();
109
+ if (LimitedFrameRateCanvas.#showFps) {
110
+ ++this.#fpsFrames;
111
+ if (this.#fpsTime === 0) this.#fpsTime = this.#current;
112
+ else {
113
+ const elapsed = this.#current - this.#fpsTime;
114
+ if (elapsed >= 1e3) {
115
+ this.#fps = (Math.round(this.#fpsFrames * 1e4 / elapsed) / 10).toFixed(1);
116
+ this.#fpsFrames = 0;
117
+ this.#fpsTime = this.#current;
118
+ }
119
+ }
120
+ this.#drawFps();
121
+ }
209
122
  this.#then = this.#now;
210
123
  }
211
124
  start() {
@@ -217,6 +130,36 @@ var LimitedFrameRateCanvas = class LimitedFrameRateCanvas {
217
130
  this.#isStopped = true;
218
131
  cancelAnimationFrame(this.#frame);
219
132
  }
133
+ pause() {
134
+ this.#isStopped = true;
135
+ cancelAnimationFrame(this.#frame);
136
+ }
137
+ resume() {
138
+ if (this.#isStopped) {
139
+ this.#isStopped = false;
140
+ this.#frame = requestAnimationFrame(this.loop.bind(this));
141
+ }
142
+ }
143
+ #drawFps() {
144
+ const ctx = this.#context;
145
+ const text = `${this.#fps} FPS`;
146
+ const x = 9;
147
+ const y = 9;
148
+ const paddingX = 6;
149
+ const paddingY = 4;
150
+ ctx.save();
151
+ ctx.font = "700 10px ui-monospace, monospace";
152
+ const boxWidth = ctx.measureText(text).width + paddingX * 2;
153
+ const boxHeight = 11 + paddingY * 2;
154
+ ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
155
+ ctx.beginPath();
156
+ ctx.roundRect(x, y, boxWidth, boxHeight, 3);
157
+ ctx.fill();
158
+ ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
159
+ ctx.textBaseline = "middle";
160
+ ctx.fillText(text, x + paddingX, y + boxHeight / 1.9);
161
+ ctx.restore();
162
+ }
220
163
  draw() {
221
164
  throw new Error("LimitedFrameRateCanvas::draw() should be overwritten.");
222
165
  }
@@ -245,18 +188,99 @@ var LimitedFrameRateCanvas = class LimitedFrameRateCanvas {
245
188
  }
246
189
  };
247
190
  //#endregion
191
+ //#region src/fade.ts
192
+ function parseSide(side) {
193
+ return typeof side === "number" ? [0, side] : side;
194
+ }
195
+ function applyEdgeFade(ctx, width, height, fade) {
196
+ ctx.globalCompositeOperation = "destination-out";
197
+ if (fade.top !== void 0) {
198
+ const [near, far] = parseSide(fade.top);
199
+ const nearPx = near * height;
200
+ const farPx = far * height;
201
+ if (nearPx > 0) {
202
+ ctx.fillStyle = "rgba(0,0,0,1)";
203
+ ctx.fillRect(0, 0, width, nearPx);
204
+ }
205
+ if (farPx > nearPx) {
206
+ const gradient = ctx.createLinearGradient(0, nearPx, 0, farPx);
207
+ gradient.addColorStop(0, "rgba(0,0,0,1)");
208
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
209
+ ctx.fillStyle = gradient;
210
+ ctx.fillRect(0, nearPx, width, farPx - nearPx);
211
+ }
212
+ }
213
+ if (fade.bottom !== void 0) {
214
+ const [near, far] = parseSide(fade.bottom);
215
+ const nearPx = near * height;
216
+ const farPx = far * height;
217
+ if (nearPx > 0) {
218
+ ctx.fillStyle = "rgba(0,0,0,1)";
219
+ ctx.fillRect(0, height - nearPx, width, nearPx);
220
+ }
221
+ if (farPx > nearPx) {
222
+ const gradient = ctx.createLinearGradient(0, height - farPx, 0, height - nearPx);
223
+ gradient.addColorStop(0, "rgba(0,0,0,0)");
224
+ gradient.addColorStop(1, "rgba(0,0,0,1)");
225
+ ctx.fillStyle = gradient;
226
+ ctx.fillRect(0, height - farPx, width, farPx - nearPx);
227
+ }
228
+ }
229
+ if (fade.left !== void 0) {
230
+ const [near, far] = parseSide(fade.left);
231
+ const nearPx = near * width;
232
+ const farPx = far * width;
233
+ if (nearPx > 0) {
234
+ ctx.fillStyle = "rgba(0,0,0,1)";
235
+ ctx.fillRect(0, 0, nearPx, height);
236
+ }
237
+ if (farPx > nearPx) {
238
+ const gradient = ctx.createLinearGradient(nearPx, 0, farPx, 0);
239
+ gradient.addColorStop(0, "rgba(0,0,0,1)");
240
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
241
+ ctx.fillStyle = gradient;
242
+ ctx.fillRect(nearPx, 0, farPx - nearPx, height);
243
+ }
244
+ }
245
+ if (fade.right !== void 0) {
246
+ const [near, far] = parseSide(fade.right);
247
+ const nearPx = near * width;
248
+ const farPx = far * width;
249
+ if (nearPx > 0) {
250
+ ctx.fillStyle = "rgba(0,0,0,1)";
251
+ ctx.fillRect(width - nearPx, 0, nearPx, height);
252
+ }
253
+ if (farPx > nearPx) {
254
+ const gradient = ctx.createLinearGradient(width - farPx, 0, width - nearPx, 0);
255
+ gradient.addColorStop(0, "rgba(0,0,0,0)");
256
+ gradient.addColorStop(1, "rgba(0,0,0,1)");
257
+ ctx.fillStyle = gradient;
258
+ ctx.fillRect(width - farPx, 0, farPx - nearPx, height);
259
+ }
260
+ }
261
+ ctx.globalCompositeOperation = "source-over";
262
+ }
263
+ //#endregion
248
264
  //#region src/simulation-canvas.ts
249
265
  var SimulationCanvas = class extends LimitedFrameRateCanvas {
250
266
  #simulation;
267
+ #contextOptions;
268
+ #offscreen = null;
269
+ #offscreenCtx = null;
251
270
  constructor(canvas, simulation, frameRate = 60, options = { colorSpace: "display-p3" }) {
252
271
  super(canvas, frameRate, options);
253
272
  this.#simulation = simulation;
273
+ this.#contextOptions = options;
254
274
  canvas.style.position = "absolute";
255
275
  canvas.style.top = "0";
256
276
  canvas.style.left = "0";
257
277
  canvas.style.height = "100%";
258
278
  canvas.style.width = "100%";
259
279
  }
280
+ withFade(fade) {
281
+ this.#simulation.fade = fade;
282
+ return this;
283
+ }
260
284
  start() {
261
285
  this.#simulation.onMount(this.canvas);
262
286
  super.start();
@@ -266,12 +290,23 @@ var SimulationCanvas = class extends LimitedFrameRateCanvas {
266
290
  super.destroy();
267
291
  }
268
292
  draw() {
269
- this.canvas.height = this.height;
270
- this.canvas.width = this.width;
293
+ const dpr = this.dpr;
294
+ this.canvas.height = this.height * dpr;
295
+ this.canvas.width = this.width * dpr;
271
296
  const ctx = this.context;
272
- ctx.save();
273
- this.#simulation.draw(ctx, this.width, this.height);
274
- ctx.restore();
297
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
298
+ if (this.#simulation.fade) {
299
+ const offCtx = this.#getOffscreenCtx(this.width * dpr, this.height * dpr);
300
+ offCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
301
+ offCtx.clearRect(0, 0, this.width, this.height);
302
+ this.#simulation.draw(offCtx, this.width, this.height);
303
+ applyEdgeFade(offCtx, this.width, this.height, this.#simulation.fade);
304
+ ctx.drawImage(this.#offscreen, 0, 0, this.width, this.height);
305
+ } else {
306
+ ctx.save();
307
+ this.#simulation.draw(ctx, this.width, this.height);
308
+ ctx.restore();
309
+ }
275
310
  }
276
311
  tick() {
277
312
  const dt = (this.delta > 0 && this.delta < 200 ? this.delta / (1e3 / 60) : 1) * this.speed * LimitedFrameRateCanvas.globalSpeed;
@@ -279,32 +314,230 @@ var SimulationCanvas = class extends LimitedFrameRateCanvas {
279
314
  }
280
315
  onResize() {
281
316
  super.onResize();
317
+ if (this.#offscreen) {
318
+ this.#offscreen.width = this.width * this.dpr;
319
+ this.#offscreen.height = this.height * this.dpr;
320
+ }
282
321
  this.#simulation.onResize(this.width, this.height);
283
322
  }
323
+ #getOffscreenCtx(width, height) {
324
+ if (!this.#offscreen) {
325
+ this.#offscreen = document.createElement("canvas");
326
+ this.#offscreen.width = width;
327
+ this.#offscreen.height = height;
328
+ this.#offscreenCtx = this.#offscreen.getContext("2d", this.#contextOptions);
329
+ }
330
+ return this.#offscreenCtx;
331
+ }
284
332
  };
285
333
  //#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" });
334
+ //#region src/effect.ts
335
+ /**
336
+ * Base class for all visual effects. Implements the internal SimulationLayer interface
337
+ * so that effects can be used both standalone (via mount()) and composed in a Scene.
338
+ *
339
+ * @example Standalone usage
340
+ * const snow = new Snow({ particles: 200 });
341
+ * snow.mount(canvas).start();
342
+ *
343
+ * @example Scene composition
344
+ * const scene = new Scene()
345
+ * .mount(canvas)
346
+ * .layer(new Aurora())
347
+ * .layer(new Snow())
348
+ * .start();
349
+ */
350
+ var Effect = class {
351
+ #canvas = null;
352
+ fade = null;
353
+ configure(_config) {}
354
+ onResize(_width, _height) {}
355
+ onMount(_canvas) {}
356
+ onUnmount(_canvas) {}
357
+ /**
358
+ * Apply an edge fade mask when rendering this effect standalone or in a Scene.
359
+ */
360
+ withFade(fade) {
361
+ this.fade = fade;
362
+ return this;
363
+ }
364
+ /**
365
+ * Mount this effect to a canvas element or CSS selector, creating the render loop.
366
+ * Must be called before start().
367
+ */
368
+ mount(canvas, options = { colorSpace: "display-p3" }, frameRate = 60) {
369
+ if (typeof canvas === "string") {
370
+ const el = document.querySelector(canvas);
371
+ if (!el) throw new Error(`Effect.mount(): no element found for selector "${canvas}".`);
372
+ canvas = el;
373
+ }
374
+ this.#canvas = new SimulationCanvas(canvas, this, frameRate, options);
375
+ return this;
376
+ }
377
+ /**
378
+ * Remove this effect from its canvas and clean up the render loop.
379
+ */
380
+ unmount() {
381
+ this.#canvas?.destroy();
382
+ this.#canvas = null;
383
+ return this;
384
+ }
385
+ /**
386
+ * Start the render loop. Call mount() first.
387
+ */
388
+ start() {
389
+ this.#canvas?.start();
390
+ return this;
391
+ }
392
+ /**
393
+ * Pause rendering without destroying state. Use resume() to continue.
394
+ */
395
+ pause() {
396
+ this.#canvas?.pause();
397
+ return this;
398
+ }
399
+ /**
400
+ * Resume rendering after a pause().
401
+ */
402
+ resume() {
403
+ this.#canvas?.resume();
404
+ return this;
405
+ }
406
+ /**
407
+ * Stop rendering and call onUnmount(). Safe to call multiple times.
408
+ */
409
+ destroy() {
410
+ this.unmount();
290
411
  }
291
412
  };
292
413
  //#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
- };
414
+ //#region src/aurora/consts.ts
415
+ const MULBERRY$23 = mulberry32(13);
416
+ //#endregion
417
+ //#region src/aurora/layer.ts
418
+ const DEFAULT_COLORS$4 = [
419
+ "#9922ff",
420
+ "#4455ff",
421
+ "#0077ee",
422
+ "#00aabb",
423
+ "#22ddff"
424
+ ];
425
+ const TOP_HUE = 265;
426
+ var Aurora = class extends Effect {
427
+ #speed;
428
+ #intensity;
429
+ #waveAmplitude;
430
+ #verticalPosition;
431
+ #bands = [];
432
+ constructor(config = {}) {
433
+ super();
434
+ const bandCount = config.bands ?? 5;
435
+ const colors = config.colors ?? DEFAULT_COLORS$4;
436
+ this.#speed = config.speed ?? 1;
437
+ this.#intensity = config.intensity ?? .8;
438
+ this.#waveAmplitude = config.waveAmplitude ?? 1;
439
+ this.#verticalPosition = config.verticalPosition ?? .68;
440
+ const clusterCenters = [.35, .65];
441
+ for (let i = 0; i < bandCount; i++) {
442
+ const color = colors[i % colors.length];
443
+ const [r, g, b] = hexToRGB(color);
444
+ const hue = this.#rgbToHue(r, g, b);
445
+ const cluster = clusterCenters[i % clusterCenters.length];
446
+ this.#bands.push({
447
+ x: cluster + (MULBERRY$23.next() - .5) * .22,
448
+ baseY: (MULBERRY$23.next() - .5) * .08,
449
+ height: .5 + MULBERRY$23.next() * .3,
450
+ sigma: 160 + MULBERRY$23.next() * 110,
451
+ phase1: MULBERRY$23.next() * Math.PI * 2,
452
+ phase2: MULBERRY$23.next() * Math.PI * 2,
453
+ amplitude1: .015 + MULBERRY$23.next() * .025,
454
+ frequency1: .003 + MULBERRY$23.next() * .004,
455
+ speed: .4 + MULBERRY$23.next() * .6,
456
+ hue,
457
+ opacity: .5 + MULBERRY$23.next() * .3
458
+ });
459
+ }
460
+ }
461
+ configure(config) {
462
+ if (config.speed !== void 0) this.#speed = config.speed;
463
+ if (config.intensity !== void 0) this.#intensity = config.intensity;
464
+ if (config.waveAmplitude !== void 0) this.#waveAmplitude = config.waveAmplitude;
465
+ if (config.verticalPosition !== void 0) this.#verticalPosition = config.verticalPosition;
466
+ }
467
+ tick(dt, _width, _height) {
468
+ for (const band of this.#bands) {
469
+ band.phase1 += .005 * band.speed * this.#speed * dt;
470
+ band.phase2 += .008 * band.speed * this.#speed * dt;
471
+ }
472
+ }
473
+ draw(ctx, width, height) {
474
+ const bg = ctx.createLinearGradient(0, 0, 0, height);
475
+ bg.addColorStop(0, "#000000");
476
+ bg.addColorStop(.5, "#050012");
477
+ bg.addColorStop(1, "#0a0025");
478
+ ctx.fillStyle = bg;
479
+ ctx.fillRect(0, 0, width, height);
480
+ ctx.globalCompositeOperation = "screen";
481
+ const step = 4;
482
+ const scale = width / 1920;
483
+ for (const band of this.#bands) {
484
+ const swayX = band.amplitude1 * width * Math.sin(band.phase1);
485
+ const cx = band.x * width + swayX;
486
+ const baseY = (this.#verticalPosition + band.baseY) * height;
487
+ const rayHeight = band.height * height * (height / 800);
488
+ const sigma = band.sigma * scale;
489
+ const cutoff = sigma * 3.5;
490
+ const sigmaSq2 = 2 * sigma * sigma;
491
+ const midHue = (band.hue + TOP_HUE) / 2;
492
+ const waveRange = height * .035 * this.#waveAmplitude;
493
+ const xStart = Math.max(0, Math.floor((cx - cutoff) / step) * step);
494
+ const xEnd = Math.min(width, Math.ceil((cx + cutoff) / step) * step);
495
+ const centreBase = baseY + Math.sin(band.frequency1 * cx + band.phase2) * waveRange;
496
+ const centreTop = centreBase - rayHeight;
497
+ const centreFadeBottom = centreBase + rayHeight * .1;
498
+ const gradient = ctx.createLinearGradient(0, centreFadeBottom, 0, centreTop);
499
+ gradient.addColorStop(0, `hsla(${band.hue}, 100%, 90%, 0)`);
500
+ gradient.addColorStop(.04, `hsla(${band.hue}, 100%, 90%, 0.55)`);
501
+ gradient.addColorStop(.1, `hsla(${band.hue}, 90%, 72%, 1)`);
502
+ gradient.addColorStop(.32, `hsla(${band.hue}, 85%, 62%, 0.75)`);
503
+ gradient.addColorStop(.62, `hsla(${midHue}, 80%, 56%, 0.35)`);
504
+ gradient.addColorStop(.86, `hsla(${TOP_HUE}, 75%, 50%, 0.12)`);
505
+ gradient.addColorStop(1, `hsla(${TOP_HUE}, 70%, 45%, 0)`);
506
+ ctx.fillStyle = gradient;
507
+ for (let x = xStart; x < xEnd; x += step) {
508
+ const dx = x - cx;
509
+ const alpha = Math.exp(-dx * dx / sigmaSq2);
510
+ if (alpha < .015) continue;
511
+ const colBase = baseY + Math.sin(band.frequency1 * x + band.phase2) * waveRange;
512
+ const colTop = colBase - rayHeight;
513
+ const fadeBottom = colBase + rayHeight * .1;
514
+ ctx.globalAlpha = alpha * band.opacity * this.#intensity;
515
+ ctx.fillRect(x, colTop, step, fadeBottom - colTop + 1);
516
+ }
517
+ ctx.globalAlpha = 1;
518
+ }
519
+ ctx.globalCompositeOperation = "source-over";
520
+ }
521
+ #rgbToHue(r, g, b) {
522
+ r /= 255;
523
+ g /= 255;
524
+ b /= 255;
525
+ const max = Math.max(r, g, b);
526
+ const delta = max - Math.min(r, g, b);
527
+ if (delta === 0) return 0;
528
+ let hue;
529
+ if (max === r) hue = (g - b) / delta % 6;
530
+ else if (max === g) hue = (b - r) / delta + 2;
531
+ else hue = (r - g) / delta + 4;
532
+ hue = Math.round(hue * 60);
533
+ if (hue < 0) hue += 360;
534
+ return hue;
535
+ }
536
+ };
537
+ //#endregion
538
+ //#region src/aurora/index.ts
539
+ function createAurora(config) {
540
+ return new Aurora(config);
308
541
  }
309
542
  //#endregion
310
543
  //#region src/balloons/consts.ts
@@ -319,7 +552,7 @@ const DEFAULT_COLORS$3 = [
319
552
  "#ff88cc",
320
553
  "#8844ff"
321
554
  ];
322
- var BalloonLayer = class extends SimulationLayer {
555
+ var Balloons = class extends Effect {
323
556
  #scale;
324
557
  #speed;
325
558
  #driftAmount;
@@ -341,6 +574,11 @@ var BalloonLayer = class extends SimulationLayer {
341
574
  if (innerWidth < 991) this.#maxCount = Math.floor(this.#maxCount / 2);
342
575
  for (let i = 0; i < this.#maxCount; ++i) this.#balloons.push(this.#createBalloon(true));
343
576
  }
577
+ configure(config) {
578
+ if (config.speed !== void 0) this.#speed = config.speed;
579
+ if (config.driftAmount !== void 0) this.#driftAmount = config.driftAmount;
580
+ if (config.stringLength !== void 0) this.#stringLengthMul = config.stringLength;
581
+ }
344
582
  tick(dt, width, height) {
345
583
  this.#time += .015 * dt * this.#speed;
346
584
  for (let i = 0; i < this.#balloons.length; i++) {
@@ -359,9 +597,9 @@ var BalloonLayer = class extends SimulationLayer {
359
597
  const rx = balloon.radiusX * this.#scale;
360
598
  const ry = balloon.radiusY * this.#scale;
361
599
  const [r, g, b] = balloon.color;
362
- ctx.save();
363
- ctx.translate(px, py);
364
- ctx.rotate(balloon.rotation);
600
+ const cos = Math.cos(balloon.rotation);
601
+ const sin = Math.sin(balloon.rotation);
602
+ ctx.setTransform(cos, sin, -sin, cos, px, py);
365
603
  const gradient = ctx.createRadialGradient(-rx * .3, -ry * .3, rx * .1, 0, 0, Math.max(rx, ry));
366
604
  gradient.addColorStop(0, `rgba(${Math.min(255, r + 80)}, ${Math.min(255, g + 80)}, ${Math.min(255, b + 80)}, 0.95)`);
367
605
  gradient.addColorStop(.6, `rgba(${r}, ${g}, ${b}, 0.9)`);
@@ -383,15 +621,21 @@ var BalloonLayer = class extends SimulationLayer {
383
621
  ctx.fillStyle = `rgba(${Math.max(0, r - 30)}, ${Math.max(0, g - 30)}, ${Math.max(0, b - 30)}, 0.9)`;
384
622
  ctx.fill();
385
623
  const stringLen = balloon.stringLength * this.#scale * this.#stringLengthMul;
386
- const drift = Math.sin(this.#time * 2 + balloon.driftPhase) * 8 * this.#scale * this.#driftAmount;
624
+ const knotBaseY = knotY + 5 * this.#scale;
625
+ const ph = balloon.driftPhase;
626
+ const fr = balloon.driftFreq;
627
+ const swingAmt = 10 * this.#scale * this.#driftAmount;
628
+ const midSwing = Math.sin(this.#time * fr + ph - .3) * swingAmt * .55;
629
+ const tipSwing = Math.sin(this.#time * fr + ph - .8) * swingAmt;
630
+ const flutter = Math.sin(this.#time * fr * 2.5 + ph * 1.4 + 1.8) * 2.5 * this.#scale;
387
631
  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);
632
+ ctx.moveTo(0, knotBaseY);
633
+ ctx.bezierCurveTo(midSwing * .35, knotBaseY + stringLen * .3, midSwing + flutter * .5, knotBaseY + stringLen * .65, tipSwing + flutter, knotBaseY + stringLen);
390
634
  ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 0.4)`;
391
635
  ctx.lineWidth = 1;
392
636
  ctx.stroke();
393
- ctx.restore();
394
637
  }
638
+ ctx.resetTransform();
395
639
  }
396
640
  #createBalloon(initialSpread) {
397
641
  const colorIndex = Math.floor(MULBERRY$22.next() * this.#colorRGBs.length);
@@ -496,12 +740,32 @@ var BalloonParticle = class {
496
740
  }
497
741
  };
498
742
  //#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
- };
743
+ //#region src/balloons/index.ts
744
+ function createBalloons(config) {
745
+ return new Balloons(config);
746
+ }
747
+ //#endregion
748
+ //#region src/color.ts
749
+ const cache = /* @__PURE__ */ new Map();
750
+ function parseColor(fillStyle) {
751
+ const cached = cache.get(fillStyle);
752
+ if (cached) return cached;
753
+ const canvas = document.createElement("canvas");
754
+ canvas.width = 1;
755
+ canvas.height = 1;
756
+ const ctx = canvas.getContext("2d");
757
+ ctx.fillStyle = fillStyle;
758
+ ctx.fillRect(0, 0, 1, 1);
759
+ const data = ctx.getImageData(0, 0, 1, 1).data;
760
+ const result = {
761
+ r: data[0],
762
+ g: data[1],
763
+ b: data[2],
764
+ a: data[3] / 255
765
+ };
766
+ cache.set(fillStyle, result);
767
+ return result;
768
+ }
505
769
  //#endregion
506
770
  //#region src/bubbles/consts.ts
507
771
  const MULBERRY$21 = mulberry32(13);
@@ -512,7 +776,7 @@ const DEFAULT_COLORS$2 = [
512
776
  "#aaddff",
513
777
  "#ccbbff"
514
778
  ];
515
- var BubbleLayer = class extends SimulationLayer {
779
+ var Bubbles = class extends Effect {
516
780
  #scale;
517
781
  #speed;
518
782
  #sizeRange;
@@ -548,6 +812,10 @@ var BubbleLayer = class extends SimulationLayer {
548
812
  canvas.removeEventListener("click", this.#onClickBound);
549
813
  this.#canvas = null;
550
814
  }
815
+ configure(config) {
816
+ if (config.speed !== void 0) this.#speed = config.speed;
817
+ if (config.wobbleAmount !== void 0) this.#wobbleAmount = config.wobbleAmount;
818
+ }
551
819
  tick(dt, width, height) {
552
820
  this.#time += .01 * dt;
553
821
  for (let i = 0; i < this.#bubbles.length; i++) {
@@ -646,16 +914,10 @@ var BubbleLayer = class extends SimulationLayer {
646
914
  }
647
915
  }
648
916
  #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;
917
+ const { r: r255, g: g255, b: b255 } = parseColor(color);
918
+ let r = r255 / 255;
919
+ let g = g255 / 255;
920
+ let b = b255 / 255;
659
921
  const max = Math.max(r, g, b);
660
922
  const delta = max - Math.min(r, g, b);
661
923
  if (delta === 0) return 0;
@@ -669,12 +931,10 @@ var BubbleLayer = class extends SimulationLayer {
669
931
  }
670
932
  };
671
933
  //#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
- };
934
+ //#region src/bubbles/index.ts
935
+ function createBubbles(config) {
936
+ return new Bubbles(config);
937
+ }
678
938
  //#endregion
679
939
  //#region src/confetti/consts.ts
680
940
  const PALETTES = {
@@ -744,101 +1004,85 @@ const MULBERRY$20 = mulberry32(13);
744
1004
  //#endregion
745
1005
  //#region src/confetti/shapes.ts
746
1006
  const TWO_PI$1 = Math.PI * 2;
747
- const SHAPE_PATHS = {
748
- bowtie: (() => {
749
- const path = new Path2D();
750
- path.moveTo(-1, -.7);
751
- path.lineTo(0, 0);
752
- path.lineTo(-1, .7);
753
- path.closePath();
754
- path.moveTo(1, -.7);
755
- path.lineTo(0, 0);
756
- path.lineTo(1, .7);
757
- path.closePath();
758
- return path;
759
- })(),
760
- circle: (() => {
761
- const path = new Path2D();
762
- path.ellipse(0, 0, .6, 1, 0, 0, TWO_PI$1);
763
- return path;
764
- })(),
765
- crescent: (() => {
766
- const path = new Path2D();
767
- path.arc(0, 0, 1, 0, TWO_PI$1, false);
768
- path.arc(.45, 0, .9, 0, TWO_PI$1, true);
769
- return path;
770
- })(),
771
- diamond: (() => {
772
- const path = new Path2D();
773
- path.moveTo(0, -1);
774
- path.lineTo(.6, 0);
775
- path.lineTo(0, 1);
776
- path.lineTo(-.6, 0);
777
- path.closePath();
778
- return path;
779
- })(),
780
- heart: (() => {
781
- const path = new Path2D();
782
- path.moveTo(0, 1);
783
- path.bezierCurveTo(-.4, .55, -1, .1, -1, -.35);
784
- path.bezierCurveTo(-1, -.8, -.5, -1, 0, -.6);
785
- path.bezierCurveTo(.5, -1, 1, -.8, 1, -.35);
786
- path.bezierCurveTo(1, .1, .4, .55, 0, 1);
787
- path.closePath();
788
- return path;
789
- })(),
790
- hexagon: (() => {
791
- const path = new Path2D();
792
- for (let i = 0; i < 6; i++) {
793
- const angle = i * Math.PI / 3 - Math.PI / 2;
794
- if (i === 0) path.moveTo(Math.cos(angle), Math.sin(angle));
795
- else path.lineTo(Math.cos(angle), Math.sin(angle));
796
- }
797
- path.closePath();
798
- return path;
799
- })(),
800
- ribbon: (() => {
801
- const path = new Path2D();
802
- path.rect(-.2, -1, .4, 2);
803
- return path;
804
- })(),
805
- ring: (() => {
806
- const path = new Path2D();
807
- path.arc(0, 0, 1, 0, TWO_PI$1, false);
808
- path.arc(0, 0, .55, 0, TWO_PI$1, true);
809
- return path;
810
- })(),
811
- square: (() => {
812
- const path = new Path2D();
813
- path.rect(-.7, -.7, 1.4, 1.4);
814
- return path;
815
- })(),
816
- star: (() => {
817
- const path = new Path2D();
818
- for (let i = 0; i < 10; i++) {
819
- const r = i % 2 === 0 ? 1 : .42;
820
- const angle = i * Math.PI / 5 - Math.PI / 2;
821
- if (i === 0) path.moveTo(r * Math.cos(angle), r * Math.sin(angle));
822
- else path.lineTo(r * Math.cos(angle), r * Math.sin(angle));
823
- }
824
- path.closePath();
825
- return path;
826
- })(),
827
- triangle: (() => {
828
- const path = new Path2D();
829
- for (let i = 0; i < 3; i++) {
830
- const angle = i * 2 * Math.PI / 3 - Math.PI / 2;
831
- if (i === 0) path.moveTo(Math.cos(angle), Math.sin(angle));
832
- else path.lineTo(Math.cos(angle), Math.sin(angle));
833
- }
834
- path.closePath();
835
- return path;
836
- })()
837
- };
1007
+ function buildShapePaths() {
1008
+ const bowtie = new Path2D();
1009
+ bowtie.moveTo(-1, -.7);
1010
+ bowtie.lineTo(0, 0);
1011
+ bowtie.lineTo(-1, .7);
1012
+ bowtie.closePath();
1013
+ bowtie.moveTo(1, -.7);
1014
+ bowtie.lineTo(0, 0);
1015
+ bowtie.lineTo(1, .7);
1016
+ bowtie.closePath();
1017
+ const circle = new Path2D();
1018
+ circle.ellipse(0, 0, .6, 1, 0, 0, TWO_PI$1);
1019
+ const crescent = new Path2D();
1020
+ crescent.arc(0, 0, 1, 0, TWO_PI$1, false);
1021
+ crescent.arc(.45, 0, .9, 0, TWO_PI$1, true);
1022
+ const diamond = new Path2D();
1023
+ diamond.moveTo(0, -1);
1024
+ diamond.lineTo(.6, 0);
1025
+ diamond.lineTo(0, 1);
1026
+ diamond.lineTo(-.6, 0);
1027
+ diamond.closePath();
1028
+ const heart = new Path2D();
1029
+ heart.moveTo(0, 1);
1030
+ heart.bezierCurveTo(-.4, .55, -1, .1, -1, -.35);
1031
+ heart.bezierCurveTo(-1, -.8, -.5, -1, 0, -.6);
1032
+ heart.bezierCurveTo(.5, -1, 1, -.8, 1, -.35);
1033
+ heart.bezierCurveTo(1, .1, .4, .55, 0, 1);
1034
+ heart.closePath();
1035
+ const hexagon = new Path2D();
1036
+ for (let i = 0; i < 6; i++) {
1037
+ const angle = i * Math.PI / 3 - Math.PI / 2;
1038
+ if (i === 0) hexagon.moveTo(Math.cos(angle), Math.sin(angle));
1039
+ else hexagon.lineTo(Math.cos(angle), Math.sin(angle));
1040
+ }
1041
+ hexagon.closePath();
1042
+ const ribbon = new Path2D();
1043
+ ribbon.rect(-.2, -1, .4, 2);
1044
+ const ring = new Path2D();
1045
+ ring.arc(0, 0, 1, 0, TWO_PI$1, false);
1046
+ ring.arc(0, 0, .55, 0, TWO_PI$1, true);
1047
+ const square = new Path2D();
1048
+ square.rect(-.7, -.7, 1.4, 1.4);
1049
+ const star = new Path2D();
1050
+ for (let i = 0; i < 10; i++) {
1051
+ const r = i % 2 === 0 ? 1 : .42;
1052
+ const angle = i * Math.PI / 5 - Math.PI / 2;
1053
+ if (i === 0) star.moveTo(r * Math.cos(angle), r * Math.sin(angle));
1054
+ else star.lineTo(r * Math.cos(angle), r * Math.sin(angle));
1055
+ }
1056
+ star.closePath();
1057
+ const triangle = new Path2D();
1058
+ for (let i = 0; i < 3; i++) {
1059
+ const angle = i * 2 * Math.PI / 3 - Math.PI / 2;
1060
+ if (i === 0) triangle.moveTo(Math.cos(angle), Math.sin(angle));
1061
+ else triangle.lineTo(Math.cos(angle), Math.sin(angle));
1062
+ }
1063
+ triangle.closePath();
1064
+ return {
1065
+ bowtie,
1066
+ circle,
1067
+ crescent,
1068
+ diamond,
1069
+ heart,
1070
+ hexagon,
1071
+ ribbon,
1072
+ ring,
1073
+ square,
1074
+ star,
1075
+ triangle
1076
+ };
1077
+ }
1078
+ let _shapePaths = null;
1079
+ const SHAPE_PATHS = new Proxy({}, { get(_, key) {
1080
+ return (_shapePaths ??= buildShapePaths())[key];
1081
+ } });
838
1082
  //#endregion
839
1083
  //#region src/confetti/layer.ts
840
1084
  const TWO_PI = Math.PI * 2;
841
- var ConfettiLayer = class extends SimulationLayer {
1085
+ var Confetti = class extends Effect {
842
1086
  #scale;
843
1087
  #particles = [];
844
1088
  #width = 0;
@@ -852,7 +1096,7 @@ var ConfettiLayer = class extends SimulationLayer {
852
1096
  this.#width = width;
853
1097
  this.#height = height;
854
1098
  }
855
- fire(config) {
1099
+ burst(config) {
856
1100
  const width = this.#width;
857
1101
  const height = this.#height;
858
1102
  const resolved = {
@@ -987,28 +1231,28 @@ var ConfettiParticle = class {
987
1231
  const scale = config.scale ?? 1;
988
1232
  const spread = config.spread ?? 45;
989
1233
  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;
1234
+ const launchAngle = -(direction * Math.PI / 180) + .5 * spread * Math.PI / 180 - MULBERRY$20.next() * spread * Math.PI / 180;
1235
+ const speed = startVelocity * (.5 + MULBERRY$20.next());
1236
+ const rotAngle = MULBERRY$20.next() * Math.PI * 2;
993
1237
  this.#colorStr = color;
994
1238
  this.#gravity = (config.gravity ?? 1) * scale;
995
1239
  this.#shape = shape;
996
- this.#size = (5 + Math.random() * 5) * scale;
1240
+ this.#size = (5 + MULBERRY$20.next() * 5) * scale;
997
1241
  this.#totalTicks = config.ticks ?? 200;
998
1242
  this.#x = position.x;
999
1243
  this.#y = position.y;
1000
1244
  this.#vx = Math.cos(launchAngle) * speed;
1001
1245
  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;
1246
+ this.#decay = (config.decay ?? .9) - .05 + MULBERRY$20.next() * .1;
1247
+ this.#flipAngle = MULBERRY$20.next() * Math.PI * 2;
1248
+ this.#flipSpeed = .03 + MULBERRY$20.next() * .05;
1005
1249
  this.#rotAngle = rotAngle;
1006
1250
  this.#rotCos = Math.cos(rotAngle);
1007
1251
  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;
1252
+ this.#rotSpeed = (MULBERRY$20.next() - .5) * .06;
1253
+ this.#swing = MULBERRY$20.next() * Math.PI * 2;
1254
+ this.#swingAmp = .5 + MULBERRY$20.next() * 1.5;
1255
+ this.#swingSpeed = .025 + MULBERRY$20.next() * .035;
1012
1256
  }
1013
1257
  draw(ctx) {
1014
1258
  ctx.save();
@@ -1034,20 +1278,10 @@ var ConfettiParticle = class {
1034
1278
  }
1035
1279
  };
1036
1280
  //#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
- };
1281
+ //#region src/confetti/index.ts
1282
+ function createConfetti(config) {
1283
+ return new Confetti(config);
1284
+ }
1051
1285
  //#endregion
1052
1286
  //#region src/donuts/consts.ts
1053
1287
  const MULBERRY$19 = mulberry32(13);
@@ -1067,7 +1301,7 @@ const DEFAULT_CONFIG = {
1067
1301
  };
1068
1302
  //#endregion
1069
1303
  //#region src/donuts/layer.ts
1070
- var DonutLayer = class extends SimulationLayer {
1304
+ var Donuts = class extends Effect {
1071
1305
  #background;
1072
1306
  #collisionPadding;
1073
1307
  #colors;
@@ -1128,6 +1362,12 @@ var DonutLayer = class extends SimulationLayer {
1128
1362
  canvas.removeEventListener("mousemove", this.#onMouseMoveBound);
1129
1363
  canvas.removeEventListener("mouseleave", this.#onMouseLeaveBound);
1130
1364
  }
1365
+ configure(config) {
1366
+ if (config.mouseAvoidance !== void 0) this.#mouseAvoidance = config.mouseAvoidance;
1367
+ if (config.mouseAvoidanceRadius !== void 0) this.#mouseAvoidanceRadius = config.mouseAvoidanceRadius;
1368
+ if (config.mouseAvoidanceStrength !== void 0) this.#mouseAvoidanceStrength = config.mouseAvoidanceStrength;
1369
+ if (config.repulsionStrength !== void 0) this.#repulsionStrength = config.repulsionStrength;
1370
+ }
1131
1371
  tick(dt, width, height) {
1132
1372
  this.#width = width;
1133
1373
  this.#height = height;
@@ -1140,17 +1380,17 @@ var DonutLayer = class extends SimulationLayer {
1140
1380
  ctx.fillStyle = this.#background;
1141
1381
  ctx.fillRect(0, 0, width, height);
1142
1382
  for (const donut of this.#donuts) {
1143
- ctx.save();
1144
- ctx.translate(donut.x, donut.y);
1145
- ctx.rotate(donut.angle);
1383
+ const cos = Math.cos(donut.angle);
1384
+ const sin = Math.sin(donut.angle);
1385
+ ctx.setTransform(cos, sin, -sin, cos, donut.x, donut.y);
1146
1386
  ctx.beginPath();
1147
1387
  ctx.arc(0, 0, donut.outerRadius, 0, Math.PI * 2);
1148
1388
  ctx.arc(0, 0, donut.innerRadius, 0, Math.PI * 2, true);
1149
1389
  ctx.closePath();
1150
1390
  ctx.fillStyle = donut.color;
1151
1391
  ctx.fill();
1152
- ctx.restore();
1153
1392
  }
1393
+ ctx.resetTransform();
1154
1394
  }
1155
1395
  #updateDonut(donut, dt) {
1156
1396
  if (Math.sqrt(donut.vx * donut.vx + donut.vy * donut.vy) > donut.speed) {
@@ -1270,12 +1510,10 @@ var DonutLayer = class extends SimulationLayer {
1270
1510
  }
1271
1511
  };
1272
1512
  //#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
- };
1513
+ //#region src/donuts/index.ts
1514
+ function createDonuts(config) {
1515
+ return new Donuts(config);
1516
+ }
1279
1517
  //#endregion
1280
1518
  //#region src/fireflies/consts.ts
1281
1519
  const MULBERRY$18 = mulberry32(13);
@@ -1284,7 +1522,7 @@ const MULBERRY$18 = mulberry32(13);
1284
1522
  const SPRITE_SIZE$1 = 64;
1285
1523
  const SPRITE_CENTER$1 = SPRITE_SIZE$1 / 2;
1286
1524
  const SPRITE_RADIUS$1 = SPRITE_SIZE$1 / 2;
1287
- var FireflyLayer = class extends SimulationLayer {
1525
+ var Fireflies = class extends Effect {
1288
1526
  #scale;
1289
1527
  #size;
1290
1528
  #speed;
@@ -1305,6 +1543,10 @@ var FireflyLayer = class extends SimulationLayer {
1305
1543
  this.#sprite = this.#createSprite(r, g, b);
1306
1544
  for (let i = 0; i < this.#maxCount; ++i) this.#fireflies.push(this.#createFirefly());
1307
1545
  }
1546
+ configure(config) {
1547
+ if (config.speed !== void 0) this.#speed = config.speed;
1548
+ if (config.glowSpeed !== void 0) this.#glowSpeed = config.glowSpeed;
1549
+ }
1308
1550
  tick(dt, _width, _height) {
1309
1551
  this.#time += .02 * dt * this.#speed;
1310
1552
  for (const firefly of this.#fireflies) {
@@ -1478,18 +1720,16 @@ var FireflyParticle = class {
1478
1720
  }
1479
1721
  };
1480
1722
  //#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
- };
1723
+ //#region src/fireflies/index.ts
1724
+ function createFireflies(config) {
1725
+ return new Fireflies(config);
1726
+ }
1487
1727
  //#endregion
1488
1728
  //#region src/firepit/consts.ts
1489
1729
  const MULBERRY$17 = mulberry32(13);
1490
1730
  //#endregion
1491
1731
  //#region src/firepit/layer.ts
1492
- var FirepitLayer = class extends SimulationLayer {
1732
+ var Firepit = class extends Effect {
1493
1733
  #scale;
1494
1734
  #flameWidth;
1495
1735
  #flameHeight;
@@ -1515,6 +1755,11 @@ var FirepitLayer = class extends SimulationLayer {
1515
1755
  height: this.#flameHeight * (.7 + MULBERRY$17.next() * .3)
1516
1756
  });
1517
1757
  }
1758
+ configure(config) {
1759
+ if (config.intensity !== void 0) this.#intensity = config.intensity;
1760
+ if (config.flameWidth !== void 0) this.#flameWidth = config.flameWidth;
1761
+ if (config.flameHeight !== void 0) this.#flameHeight = config.flameHeight;
1762
+ }
1518
1763
  tick(dt, _width, _height) {
1519
1764
  this.#time += .03 * dt * this.#intensity;
1520
1765
  if (this.#embers.length < this.#maxEmbers && MULBERRY$17.next() < .3 * this.#intensity * dt) this.#embers.push(this.#createEmber());
@@ -1611,12 +1856,10 @@ var FirepitLayer = class extends SimulationLayer {
1611
1856
  }
1612
1857
  };
1613
1858
  //#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
- };
1859
+ //#region src/firepit/index.ts
1860
+ function createFirepit(config) {
1861
+ return new Firepit(config);
1862
+ }
1620
1863
  //#endregion
1621
1864
  //#region src/fireworks/consts.ts
1622
1865
  const MULBERRY$16 = mulberry32(13);
@@ -1923,7 +2166,7 @@ var Explosion = class {
1923
2166
  }
1924
2167
  checkCrackle() {
1925
2168
  if (this.#type !== "crackle" || this.#hasCrackled) return false;
1926
- if (this.#alpha <= this.#decay * 3) {
2169
+ if (this.#alpha <= .4) {
1927
2170
  this.#hasCrackled = true;
1928
2171
  return true;
1929
2172
  }
@@ -2032,6 +2275,158 @@ var Explosion = class {
2032
2275
  }
2033
2276
  };
2034
2277
  //#endregion
2278
+ //#region src/fireworks/create-explosion.ts
2279
+ function between(rng, min, max) {
2280
+ return min + rng() * (max - min);
2281
+ }
2282
+ /**
2283
+ * Creates an array of {@link Explosion} particles for the given firework variant.
2284
+ * Use this to fire a fully formed explosion burst in your own render loop without
2285
+ * needing a {@link Fireworks} instance.
2286
+ *
2287
+ * @param variant - The firework variant to create.
2288
+ * @param position - The center position of the explosion in canvas pixels.
2289
+ * @param hue - Base hue in degrees (0–360).
2290
+ * @param options - Optional overrides for `lineWidth` (default `5`) and `scale` (default `1`).
2291
+ * @param rng - RNG function returning values in [0, 1). Defaults to `Math.random`.
2292
+ */
2293
+ function createExplosion(variant, position, hue, options = {}, rng = Math.random) {
2294
+ const lineWidth = options.lineWidth ?? 5;
2295
+ const scale = options.scale ?? 1;
2296
+ const explosions = [];
2297
+ switch (variant) {
2298
+ case "saturn":
2299
+ createSaturn(explosions, position, hue, lineWidth, scale, rng);
2300
+ break;
2301
+ case "dahlia":
2302
+ createDahlia(explosions, position, hue, lineWidth, scale, rng);
2303
+ break;
2304
+ case "heart":
2305
+ createHeart(explosions, position, hue, lineWidth, scale, rng);
2306
+ break;
2307
+ case "spiral":
2308
+ createSpiral(explosions, position, hue, lineWidth, scale, rng);
2309
+ break;
2310
+ case "flower":
2311
+ createFlower(explosions, position, hue, lineWidth, scale, rng);
2312
+ break;
2313
+ case "concentric":
2314
+ createConcentric(explosions, position, hue, lineWidth, scale, rng);
2315
+ break;
2316
+ default: {
2317
+ const type = variant;
2318
+ const config = EXPLOSION_CONFIGS[type];
2319
+ const count = Math.floor(between(rng, config.particleCount[0], config.particleCount[1]));
2320
+ const effectiveHue = type === "brocade" ? between(rng, 35, 50) : hue;
2321
+ for (let i = 0; i < count; i++) {
2322
+ let angle;
2323
+ let speed;
2324
+ if (type === "ring") {
2325
+ angle = i / count * Math.PI * 2;
2326
+ speed = between(rng, config.speed[0], config.speed[1]) * .5 + config.speed[0] * .5;
2327
+ } else if (type === "palm" || type === "horsetail") {
2328
+ const spread = type === "horsetail" ? Math.PI / 8 : Math.PI / 5;
2329
+ angle = -Math.PI / 2 + between(rng, -spread, spread);
2330
+ }
2331
+ explosions.push(new Explosion(position, effectiveHue, lineWidth, type, scale, angle, speed));
2332
+ }
2333
+ }
2334
+ }
2335
+ return explosions;
2336
+ }
2337
+ function createSaturn(explosions, position, hue, lineWidth, scale, rng) {
2338
+ const velocity = between(rng, 4, 6);
2339
+ const shellCount = Math.floor(between(rng, 25, 35));
2340
+ for (let i = 0; i < shellCount; i++) {
2341
+ const rad = i / shellCount * Math.PI * 2;
2342
+ explosions.push(new Explosion(position, hue, lineWidth, "peony", scale, rad + between(rng, -.05, .05), velocity + between(rng, -.25, .25)));
2343
+ }
2344
+ const fillCount = Math.floor(between(rng, 40, 60));
2345
+ 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)));
2346
+ const ringRotation = between(rng, 0, Math.PI * 2);
2347
+ const ringCount = Math.floor(between(rng, 40, 55));
2348
+ const ringVx = velocity * between(rng, 2, 3);
2349
+ const ringVy = velocity * .6;
2350
+ for (let i = 0; i < ringCount; i++) {
2351
+ const rad = i / ringCount * Math.PI * 2;
2352
+ const cx = Math.cos(rad) * ringVx + between(rng, -.25, .25);
2353
+ const cy = Math.sin(rad) * ringVy + between(rng, -.25, .25);
2354
+ const cosR = Math.cos(ringRotation);
2355
+ const sinR = Math.sin(ringRotation);
2356
+ const vx = cx * cosR - cy * sinR;
2357
+ const vy = cx * sinR + cy * cosR;
2358
+ const vz = Math.sin(rad) * velocity * .8;
2359
+ explosions.push(new Explosion(position, hue + 60, lineWidth, "ring", scale, Math.atan2(vy, vx), Math.sqrt(vx * vx + vy * vy), vz));
2360
+ }
2361
+ }
2362
+ function createDahlia(explosions, position, hue, lineWidth, scale, rng) {
2363
+ const petalCount = Math.floor(between(rng, 6, 9));
2364
+ const particlesPerPetal = Math.floor(between(rng, 8, 12));
2365
+ for (let petal = 0; petal < petalCount; petal++) {
2366
+ const baseAngle = petal / petalCount * Math.PI * 2;
2367
+ const petalHue = hue + (petal % 2 === 0 ? 25 : -25);
2368
+ for (let i = 0; i < particlesPerPetal; i++) explosions.push(new Explosion(position, petalHue, lineWidth, "dahlia", scale, baseAngle + between(rng, -.3, .3)));
2369
+ }
2370
+ }
2371
+ function createHeart(explosions, position, hue, lineWidth, scale, rng) {
2372
+ const velocity = between(rng, 3, 5);
2373
+ const count = Math.floor(between(rng, 60, 80));
2374
+ const rotation = between(rng, -.3, .3);
2375
+ const cosR = Math.cos(rotation);
2376
+ const sinR = Math.sin(rotation);
2377
+ for (let i = 0; i < count; i++) {
2378
+ const t = i / count * Math.PI * 2;
2379
+ const hx = 16 * Math.pow(Math.sin(t), 3);
2380
+ const hy = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
2381
+ const s = velocity / 16;
2382
+ const vx = hx * s;
2383
+ const vy = hy * s;
2384
+ const rvx = vx * cosR - vy * sinR;
2385
+ const rvy = vx * sinR + vy * cosR;
2386
+ 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))));
2387
+ }
2388
+ }
2389
+ function createSpiral(explosions, position, hue, lineWidth, scale, rng) {
2390
+ const arms = Math.floor(between(rng, 3, 5));
2391
+ const particlesPerArm = Math.floor(between(rng, 15, 20));
2392
+ const twist = between(rng, 2, 3.5);
2393
+ const baseRotation = between(rng, 0, Math.PI * 2);
2394
+ for (let arm = 0; arm < arms; arm++) {
2395
+ const baseAngle = baseRotation + arm / arms * Math.PI * 2;
2396
+ const armHue = hue + arm * (360 / arms / 3);
2397
+ for (let i = 0; i < particlesPerArm; i++) {
2398
+ const progress = i / particlesPerArm;
2399
+ explosions.push(new Explosion(position, armHue, lineWidth, "spiral", scale, baseAngle + progress * twist, 2 + progress * 8 + between(rng, -.3, .3)));
2400
+ }
2401
+ }
2402
+ }
2403
+ function createFlower(explosions, position, hue, lineWidth, scale, rng) {
2404
+ const velocity = between(rng, 4, 7);
2405
+ const count = Math.floor(between(rng, 70, 90));
2406
+ const petals = Math.floor(between(rng, 2, 4));
2407
+ const rotation = between(rng, 0, Math.PI * 2);
2408
+ for (let i = 0; i < count; i++) {
2409
+ const t = i / count * Math.PI * 2;
2410
+ const speed = velocity * Math.abs(Math.cos(petals * t));
2411
+ if (speed < .3) continue;
2412
+ explosions.push(new Explosion(position, hue + between(rng, -15, 15), lineWidth, "flower", scale, t + rotation, speed + between(rng, -.2, .2)));
2413
+ }
2414
+ }
2415
+ function createConcentric(explosions, position, hue, lineWidth, scale, rng) {
2416
+ const outerCount = Math.floor(between(rng, 35, 50));
2417
+ const outerSpeed = between(rng, 7, 10);
2418
+ for (let i = 0; i < outerCount; i++) {
2419
+ const angle = i / outerCount * Math.PI * 2;
2420
+ explosions.push(new Explosion(position, hue, lineWidth, "ring", scale, angle + between(rng, -.05, .05), outerSpeed + between(rng, -.25, .25)));
2421
+ }
2422
+ const innerCount = Math.floor(between(rng, 25, 35));
2423
+ const innerSpeed = between(rng, 3, 5);
2424
+ for (let i = 0; i < innerCount; i++) {
2425
+ const angle = i / innerCount * Math.PI * 2;
2426
+ explosions.push(new Explosion(position, hue + 120, lineWidth, "ring", scale, angle + between(rng, -.05, .05), innerSpeed + between(rng, -.25, .25)));
2427
+ }
2428
+ }
2429
+ //#endregion
2035
2430
  //#region src/distance.ts
2036
2431
  function distance(a, b) {
2037
2432
  let x = a.x - b.x;
@@ -2047,7 +2442,7 @@ var Spark = class {
2047
2442
  #size;
2048
2443
  #decay;
2049
2444
  #friction = .94;
2050
- #gravity = .6;
2445
+ #gravity = .3;
2051
2446
  #alpha = 1;
2052
2447
  get isDead() {
2053
2448
  return this.#alpha <= 0;
@@ -2062,7 +2457,7 @@ var Spark = class {
2062
2457
  this.#decay = MULBERRY$16.nextBetween(.03, .08);
2063
2458
  this.#velocity = {
2064
2459
  x: velocityX + MULBERRY$16.nextBetween(-1.5, 1.5),
2065
- y: velocityY + MULBERRY$16.nextBetween(-2, .5)
2460
+ y: velocityY + MULBERRY$16.nextBetween(-2, 2)
2066
2461
  };
2067
2462
  }
2068
2463
  draw(ctx) {
@@ -2175,7 +2570,7 @@ var Firework = class extends EventTarget {
2175
2570
  };
2176
2571
  //#endregion
2177
2572
  //#region src/fireworks/layer.ts
2178
- var FireworkLayer = class extends SimulationLayer {
2573
+ var Fireworks = class extends Effect {
2179
2574
  #explosions = [];
2180
2575
  #fireworks = [];
2181
2576
  #sparks = [];
@@ -2183,6 +2578,7 @@ var FireworkLayer = class extends SimulationLayer {
2183
2578
  #spawnTimer = 0;
2184
2579
  #positionRandom = MULBERRY$16.fork();
2185
2580
  #autoSpawn;
2581
+ #variants;
2186
2582
  #baseSize;
2187
2583
  #scale;
2188
2584
  #tailWidth;
@@ -2192,6 +2588,7 @@ var FireworkLayer = class extends SimulationLayer {
2192
2588
  super();
2193
2589
  const scale = config.scale ?? 1;
2194
2590
  this.#autoSpawn = config.autoSpawn ?? true;
2591
+ this.#variants = config.variants?.length ? [...config.variants] : [...FIREWORK_VARIANTS];
2195
2592
  this.#baseSize = 5 * scale;
2196
2593
  this.#scale = scale;
2197
2594
  this.#tailWidth = 2 * scale;
@@ -2200,13 +2597,18 @@ var FireworkLayer = class extends SimulationLayer {
2200
2597
  this.#width = width;
2201
2598
  this.#height = height;
2202
2599
  }
2203
- fireExplosion(variant, position) {
2600
+ launch(variant, position) {
2204
2601
  const pos = position ?? {
2205
2602
  x: this.#width / 2,
2206
2603
  y: this.#height * .4
2207
2604
  };
2208
2605
  this.#hue = MULBERRY$16.nextBetween(0, 360);
2209
- this.#createExplosion(pos, this.#hue, variant);
2606
+ this.#spawnExplosion(pos, this.#hue, variant);
2607
+ }
2608
+ configure(config) {
2609
+ if (config.scale !== void 0) this.#scale = config.scale;
2610
+ if (config.autoSpawn !== void 0) this.#autoSpawn = config.autoSpawn;
2611
+ if (Array.isArray(config.variants) && config.variants.length > 0) this.#variants = [...config.variants];
2210
2612
  }
2211
2613
  tick(dt, width, height) {
2212
2614
  this.#width = width;
@@ -2223,7 +2625,8 @@ var FireworkLayer = class extends SimulationLayer {
2223
2625
  }
2224
2626
  for (const firework of this.#fireworks) {
2225
2627
  firework.tick(dt);
2226
- this.#sparks.push(...firework.collectSparks());
2628
+ const collected = firework.collectSparks();
2629
+ for (let i = 0; i < collected.length; i++) this.#sparks.push(collected[i]);
2227
2630
  }
2228
2631
  for (const explosion of this.#explosions) explosion.tick(dt);
2229
2632
  for (const spark of this.#sparks) spark.tick(dt);
@@ -2234,12 +2637,20 @@ var FireworkLayer = class extends SimulationLayer {
2234
2637
  const angle = explosion.angle + Math.PI / 2 * i + Math.PI / 4;
2235
2638
  newExplosions.push(new Explosion(explosion.position, explosion.hue, this.#baseSize * .6, "peony", this.#scale, angle, MULBERRY$16.nextBetween(3, 6)));
2236
2639
  }
2237
- if (explosion.checkCrackle()) for (let j = 0; j < 8; j++) newSparks.push(new Spark(explosion.position, explosion.hue + MULBERRY$16.nextBetween(-30, 30)));
2640
+ if (explosion.checkCrackle()) for (let j = 0; j < 14; j++) {
2641
+ const angle = MULBERRY$16.nextBetween(0, Math.PI * 2);
2642
+ const speed = MULBERRY$16.nextBetween(3, 8);
2643
+ newSparks.push(new Spark(explosion.position, explosion.hue + MULBERRY$16.nextBetween(-30, 30), Math.cos(angle) * speed, Math.sin(angle) * speed));
2644
+ }
2238
2645
  }
2239
2646
  this.#explosions.push(...newExplosions);
2240
2647
  this.#sparks.push(...newSparks);
2241
- this.#explosions = this.#explosions.filter((e) => !e.isDead);
2242
- this.#sparks = this.#sparks.filter((s) => !s.isDead);
2648
+ let aliveExplosions = 0;
2649
+ for (let i = 0; i < this.#explosions.length; i++) if (!this.#explosions[i].isDead) this.#explosions[aliveExplosions++] = this.#explosions[i];
2650
+ this.#explosions.length = aliveExplosions;
2651
+ let aliveSparks = 0;
2652
+ for (let i = 0; i < this.#sparks.length; i++) if (!this.#sparks[i].isDead) this.#sparks[aliveSparks++] = this.#sparks[i];
2653
+ this.#sparks.length = aliveSparks;
2243
2654
  }
2244
2655
  draw(ctx, width, height) {
2245
2656
  if (ctx.canvas.width !== width || ctx.canvas.height !== height) {
@@ -2252,153 +2663,13 @@ var FireworkLayer = class extends SimulationLayer {
2252
2663
  for (const firework of this.#fireworks) firework.draw(ctx);
2253
2664
  ctx.globalCompositeOperation = "source-over";
2254
2665
  }
2255
- #createExplosion(position, hue, variant) {
2666
+ #spawnExplosion(position, hue, variant) {
2256
2667
  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
- }
2668
+ const rng = () => MULBERRY$16.nextBetween(0, 1);
2669
+ this.#explosions.push(...createExplosion(selected, position, hue, {
2670
+ lineWidth: this.#baseSize,
2671
+ scale: this.#scale
2672
+ }, rng));
2402
2673
  }
2403
2674
  #createFirework(position) {
2404
2675
  const hue = this.#hue;
@@ -2413,43 +2684,20 @@ var FireworkLayer = class extends SimulationLayer {
2413
2684
  }, hue, this.#tailWidth, this.#baseSize);
2414
2685
  firework.addEventListener("remove", () => {
2415
2686
  this.#fireworks.splice(this.#fireworks.indexOf(firework), 1);
2416
- this.#createExplosion(firework.position, hue);
2687
+ this.#spawnExplosion(firework.position, hue);
2417
2688
  }, { once: true });
2418
2689
  this.#fireworks.push(firework);
2419
2690
  }
2420
2691
  #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";
2692
+ const index = Math.floor(MULBERRY$16.nextBetween(0, this.#variants.length));
2693
+ return this.#variants[index];
2438
2694
  }
2439
2695
  };
2440
2696
  //#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
- };
2697
+ //#region src/fireworks/index.ts
2698
+ function createFireworks(config) {
2699
+ return new Fireworks(config);
2700
+ }
2453
2701
  //#endregion
2454
2702
  //#region src/glitter/consts.ts
2455
2703
  const MULBERRY$15 = mulberry32(13);
@@ -2464,7 +2712,7 @@ const GLITTER_COLORS = [
2464
2712
  ];
2465
2713
  //#endregion
2466
2714
  //#region src/glitter/layer.ts
2467
- var GlitterLayer = class extends SimulationLayer {
2715
+ var Glitter = class extends Effect {
2468
2716
  #scale;
2469
2717
  #size;
2470
2718
  #speed;
@@ -2487,6 +2735,10 @@ var GlitterLayer = class extends SimulationLayer {
2487
2735
  if (innerWidth < 991) this.#maxCount = Math.floor(this.#maxCount / 2);
2488
2736
  for (let i = 0; i < this.#maxCount; ++i) this.#falling.push(this.#createFallingPiece(true));
2489
2737
  }
2738
+ configure(config) {
2739
+ if (config.speed !== void 0) this.#speed = config.speed;
2740
+ if (config.groundLevel !== void 0) this.#groundLevel = config.groundLevel;
2741
+ }
2490
2742
  tick(dt, _width, _height) {
2491
2743
  this.#time += .03 * dt;
2492
2744
  let alive = 0;
@@ -2586,12 +2838,10 @@ var GlitterLayer = class extends SimulationLayer {
2586
2838
  }
2587
2839
  };
2588
2840
  //#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
- };
2841
+ //#region src/glitter/index.ts
2842
+ function createGlitter(config) {
2843
+ return new Glitter(config);
2844
+ }
2595
2845
  //#endregion
2596
2846
  //#region src/lanterns/consts.ts
2597
2847
  const MULBERRY$14 = mulberry32(13);
@@ -2606,7 +2856,7 @@ const LANTERN_COLORS = [
2606
2856
  ];
2607
2857
  //#endregion
2608
2858
  //#region src/lanterns/layer.ts
2609
- var LanternLayer = class extends SimulationLayer {
2859
+ var Lanterns = class extends Effect {
2610
2860
  #scale;
2611
2861
  #speed;
2612
2862
  #size;
@@ -2614,6 +2864,8 @@ var LanternLayer = class extends SimulationLayer {
2614
2864
  #maxCount;
2615
2865
  #time = 0;
2616
2866
  #lanterns = [];
2867
+ #sortedLanterns = [];
2868
+ #sortDirty = true;
2617
2869
  constructor(config = {}) {
2618
2870
  super();
2619
2871
  this.#scale = config.scale ?? 1;
@@ -2624,6 +2876,9 @@ var LanternLayer = class extends SimulationLayer {
2624
2876
  if (innerWidth < 991) this.#maxCount = Math.floor(this.#maxCount / 2);
2625
2877
  for (let i = 0; i < this.#maxCount; ++i) this.#lanterns.push(this.#createLantern(true));
2626
2878
  }
2879
+ configure(config) {
2880
+ if (config.speed !== void 0) this.#speed = config.speed;
2881
+ }
2627
2882
  tick(dt, width, height) {
2628
2883
  this.#time += .02 * dt * this.#speed;
2629
2884
  for (let i = 0; i < this.#lanterns.length; i++) {
@@ -2631,11 +2886,18 @@ var LanternLayer = class extends SimulationLayer {
2631
2886
  lantern.y -= lantern.vy * this.#speed * dt / (height * 1.5);
2632
2887
  const sway = Math.sin(this.#time * lantern.swaySpeed + lantern.swayPhase) * lantern.swayAmplitude;
2633
2888
  lantern.x += sway * dt / (width * 8);
2634
- if (lantern.y < -.15) this.#lanterns[i] = this.#createLantern(false);
2889
+ if (lantern.y < -.15) {
2890
+ this.#lanterns[i] = this.#createLantern(false);
2891
+ this.#sortDirty = true;
2892
+ }
2635
2893
  }
2636
2894
  }
2637
2895
  draw(ctx, width, height) {
2638
- const sorted = [...this.#lanterns].sort((a, b) => a.size - b.size);
2896
+ if (this.#sortDirty) {
2897
+ this.#sortedLanterns = [...this.#lanterns].sort((a, b) => a.size - b.size);
2898
+ this.#sortDirty = false;
2899
+ }
2900
+ const sorted = this.#sortedLanterns;
2639
2901
  for (const lantern of sorted) {
2640
2902
  const px = lantern.x * width;
2641
2903
  const py = lantern.y * height;
@@ -2653,221 +2915,79 @@ var LanternLayer = class extends SimulationLayer {
2653
2915
  ctx.beginPath();
2654
2916
  ctx.arc(px, py, glowRadius, 0, Math.PI * 2);
2655
2917
  ctx.fill();
2656
- ctx.save();
2657
- ctx.translate(px, py);
2918
+ ctx.setTransform(1, 0, 0, 1, px, py);
2658
2919
  const bodyW = size * .8;
2659
2920
  const bodyH = size;
2660
2921
  const topW = bodyW * .6;
2661
- ctx.beginPath();
2662
- ctx.moveTo(-topW, -bodyH * .5);
2663
- ctx.quadraticCurveTo(-bodyW, 0, -bodyW * .7, bodyH * .5);
2664
- ctx.lineTo(bodyW * .7, bodyH * .5);
2665
- ctx.quadraticCurveTo(bodyW, 0, topW, -bodyH * .5);
2666
- 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;
2922
+ ctx.beginPath();
2923
+ ctx.moveTo(-topW, -bodyH * .5);
2924
+ ctx.quadraticCurveTo(-bodyW, 0, -bodyW * .7, bodyH * .5);
2925
+ ctx.lineTo(bodyW * .7, bodyH * .5);
2926
+ ctx.quadraticCurveTo(bodyW, 0, topW, -bodyH * .5);
2927
+ ctx.closePath();
2928
+ const bodyGradient = ctx.createLinearGradient(0, -bodyH * .5, 0, bodyH * .5);
2929
+ bodyGradient.addColorStop(0, `rgba(${Math.min(255, r + 60)}, ${Math.min(255, g + 60)}, ${Math.min(255, b + 30)}, ${alpha * .9})`);
2930
+ bodyGradient.addColorStop(.5, `rgba(${r}, ${g}, ${b}, ${alpha * .85})`);
2931
+ bodyGradient.addColorStop(1, `rgba(${Math.max(0, r - 30)}, ${Math.max(0, g - 30)}, ${Math.max(0, b - 20)}, ${alpha * .8})`);
2932
+ ctx.fillStyle = bodyGradient;
2933
+ ctx.fill();
2934
+ ctx.beginPath();
2935
+ ctx.moveTo(-topW * .7, -bodyH * .55);
2936
+ ctx.lineTo(topW * .7, -bodyH * .55);
2937
+ ctx.lineWidth = size * .06;
2938
+ ctx.strokeStyle = `rgba(${Math.max(0, r - 40)}, ${Math.max(0, g - 40)}, ${Math.max(0, b - 40)}, ${alpha * .7})`;
2939
+ ctx.stroke();
2940
+ const flameH = bodyH * .3;
2941
+ const flameW = bodyW * .15;
2942
+ const flameFlicker = Math.sin(this.#time * 8 + lantern.glowPhase) * flameW * .3;
2943
+ const flameGradient = ctx.createRadialGradient(flameFlicker, -flameH * .1, 0, flameFlicker, -flameH * .1, flameH);
2944
+ flameGradient.addColorStop(0, `rgba(255, 255, 200, ${alpha * .95})`);
2945
+ flameGradient.addColorStop(.3, `rgba(255, 200, 80, ${alpha * .7})`);
2946
+ flameGradient.addColorStop(.7, `rgba(255, 140, 40, ${alpha * .3})`);
2947
+ flameGradient.addColorStop(1, `rgba(255, 100, 20, 0)`);
2948
+ ctx.beginPath();
2949
+ ctx.moveTo(-flameW + flameFlicker, flameH * .2);
2950
+ ctx.quadraticCurveTo(-flameW * .5 + flameFlicker, -flameH * .3, flameFlicker, -flameH);
2951
+ ctx.quadraticCurveTo(flameW * .5 + flameFlicker, -flameH * .3, flameW + flameFlicker, flameH * .2);
2952
+ ctx.closePath();
2953
+ ctx.fillStyle = flameGradient;
2954
+ ctx.fill();
2955
+ const stringLen = size * .6;
2956
+ const stringDrift = Math.sin(this.#time * 1.5 + lantern.swayPhase) * size * .1;
2957
+ ctx.beginPath();
2958
+ ctx.moveTo(0, bodyH * .5);
2959
+ ctx.quadraticCurveTo(stringDrift, bodyH * .5 + stringLen * .5, -stringDrift * .5, bodyH * .5 + stringLen);
2960
+ ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha * .4})`;
2961
+ ctx.lineWidth = size * .04;
2962
+ ctx.stroke();
2963
+ ctx.resetTransform();
2857
2964
  }
2858
- for (const layer of this.#layers) layer.onResize(this.width, this.height);
2859
2965
  }
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;
2966
+ #createLantern(initialSpread) {
2967
+ const colorIndex = Math.floor(MULBERRY$14.next() * this.#colorRGBs.length);
2968
+ const sizeVariation = .6 + MULBERRY$14.next() * .8;
2969
+ return {
2970
+ x: .05 + MULBERRY$14.next() * .9,
2971
+ y: initialSpread ? MULBERRY$14.next() * 1.3 : 1.15 + MULBERRY$14.next() * .2,
2972
+ vx: 0,
2973
+ vy: .2 + MULBERRY$14.next() * .6,
2974
+ size: this.#size * sizeVariation,
2975
+ glowPhase: MULBERRY$14.next() * Math.PI * 2,
2976
+ glowSpeed: .8 + MULBERRY$14.next() * 1.2,
2977
+ swayPhase: MULBERRY$14.next() * Math.PI * 2,
2978
+ swaySpeed: .4 + MULBERRY$14.next() * .8,
2979
+ swayAmplitude: .3 + MULBERRY$14.next() * .7,
2980
+ colorIndex,
2981
+ opacity: .7 + MULBERRY$14.next() * .3
2982
+ };
2868
2983
  }
2869
2984
  };
2870
2985
  //#endregion
2986
+ //#region src/lanterns/index.ts
2987
+ function createLanterns(config) {
2988
+ return new Lanterns(config);
2989
+ }
2990
+ //#endregion
2871
2991
  //#region src/leaves/consts.ts
2872
2992
  const MULBERRY$13 = mulberry32(13);
2873
2993
  const LEAF_COLORS = [
@@ -2884,7 +3004,7 @@ const LEAF_COLORS = [
2884
3004
  ];
2885
3005
  //#endregion
2886
3006
  //#region src/leaves/layer.ts
2887
- var LeafLayer = class extends SimulationLayer {
3007
+ var Leaves = class extends Effect {
2888
3008
  #scale;
2889
3009
  #size;
2890
3010
  #speed;
@@ -2910,6 +3030,10 @@ var LeafLayer = class extends SimulationLayer {
2910
3030
  onResize(_width, height) {
2911
3031
  this.#height = height;
2912
3032
  }
3033
+ configure(config) {
3034
+ if (config.speed !== void 0) this.#speed = config.speed;
3035
+ if (config.wind !== void 0) this.#wind = config.wind;
3036
+ }
2913
3037
  tick(dt, _width, height) {
2914
3038
  this.#height = height;
2915
3039
  const speedFactor = height / 540 / this.#speed;
@@ -2944,14 +3068,13 @@ var LeafLayer = class extends SimulationLayer {
2944
3068
  const py = leaf.y * height;
2945
3069
  const displaySize = leaf.size * leaf.depth;
2946
3070
  const scaleX = Math.cos(leaf.flipAngle);
2947
- ctx.save();
2948
- ctx.translate(px, py);
2949
- ctx.rotate(leaf.rotation);
2950
- ctx.scale(scaleX, 1);
3071
+ const cos = Math.cos(leaf.rotation);
3072
+ const sin = Math.sin(leaf.rotation);
3073
+ ctx.setTransform(cos * scaleX, sin * scaleX, -sin, cos, px, py);
2951
3074
  ctx.globalAlpha = .3 + leaf.depth * .7;
2952
3075
  ctx.drawImage(this.#sprites[leaf.colorIndex % this.#sprites.length], -displaySize / 2, -displaySize / 2, displaySize, displaySize);
2953
- ctx.restore();
2954
3076
  }
3077
+ ctx.resetTransform();
2955
3078
  ctx.globalAlpha = 1;
2956
3079
  }
2957
3080
  #createSprites() {
@@ -3080,12 +3203,10 @@ var LeafLayer = class extends SimulationLayer {
3080
3203
  }
3081
3204
  };
3082
3205
  //#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
- };
3206
+ //#region src/leaves/index.ts
3207
+ function createLeaves(config) {
3208
+ return new Leaves(config);
3209
+ }
3089
3210
  //#endregion
3090
3211
  //#region src/lightning/consts.ts
3091
3212
  const MULBERRY$12 = mulberry32(13);
@@ -3233,7 +3354,7 @@ var LightningSystem = class {
3233
3354
  };
3234
3355
  //#endregion
3235
3356
  //#region src/lightning/layer.ts
3236
- var LightningLayer = class extends SimulationLayer {
3357
+ var Lightning = class extends Effect {
3237
3358
  #system;
3238
3359
  #enableFlash;
3239
3360
  constructor(config = {}) {
@@ -3264,19 +3385,17 @@ var LightningLayer = class extends SimulationLayer {
3264
3385
  }
3265
3386
  };
3266
3387
  //#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
- };
3388
+ //#region src/lightning/index.ts
3389
+ function createLightning(config) {
3390
+ return new Lightning(config);
3391
+ }
3273
3392
  //#endregion
3274
3393
  //#region src/matrix/consts.ts
3275
3394
  const MULBERRY$11 = mulberry32(13);
3276
3395
  const MATRIX_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
3277
3396
  //#endregion
3278
3397
  //#region src/matrix/layer.ts
3279
- var MatrixLayer = class extends SimulationLayer {
3398
+ var Matrix = class extends Effect {
3280
3399
  #scale;
3281
3400
  #speed;
3282
3401
  #fontSize;
@@ -3315,6 +3434,10 @@ var MatrixLayer = class extends SimulationLayer {
3315
3434
  }
3316
3435
  }
3317
3436
  }
3437
+ configure(config) {
3438
+ if (config.speed !== void 0) this.#speed = config.speed;
3439
+ if (config.trailLength !== void 0) this.#trailLength = config.trailLength;
3440
+ }
3318
3441
  tick(dt, width, height) {
3319
3442
  this.#width = width;
3320
3443
  this.#height = height;
@@ -3366,12 +3489,10 @@ var MatrixLayer = class extends SimulationLayer {
3366
3489
  }
3367
3490
  };
3368
3491
  //#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
- };
3492
+ //#region src/matrix/index.ts
3493
+ function createMatrix(config) {
3494
+ return new Matrix(config);
3495
+ }
3375
3496
  //#endregion
3376
3497
  //#region src/orbits/consts.ts
3377
3498
  const MULBERRY$10 = mulberry32(13);
@@ -3386,7 +3507,7 @@ const ORBIT_COLORS = [
3386
3507
  ];
3387
3508
  //#endregion
3388
3509
  //#region src/orbits/layer.ts
3389
- var OrbitLayer = class extends SimulationLayer {
3510
+ var Orbits = class extends Effect {
3390
3511
  #centerCount;
3391
3512
  #orbitersPerCenter;
3392
3513
  #speed;
@@ -3421,6 +3542,12 @@ var OrbitLayer = class extends SimulationLayer {
3421
3542
  for (let ci = 0; ci < this.#centers.length; ci++) for (let oi = 0; oi < count; oi++) this.#orbiters.push(this.#createOrbiter(ci));
3422
3543
  }
3423
3544
  }
3545
+ configure(config) {
3546
+ if (config.speed !== void 0) this.#speed = config.speed;
3547
+ if (config.trailLength !== void 0) this.#trailLength = config.trailLength;
3548
+ if (config.showCenters !== void 0) this.#showCenters = config.showCenters;
3549
+ if (config.scale !== void 0) this.#scale = config.scale;
3550
+ }
3424
3551
  tick(dt, width, height) {
3425
3552
  this.#time += .01 * dt * this.#speed;
3426
3553
  for (const orbiter of this.#orbiters) {
@@ -3437,11 +3564,20 @@ var OrbitLayer = class extends SimulationLayer {
3437
3564
  const rotatedY = localX * Math.sin(orbiter.tilt * .3) + localY * Math.cos(orbiter.tilt * .3);
3438
3565
  const px = cx + rotatedX;
3439
3566
  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();
3567
+ const trail = orbiter.trail;
3568
+ const maxLen = this.#trailLength;
3569
+ if (trail.length < maxLen) {
3570
+ trail.push({
3571
+ x: px,
3572
+ y: py
3573
+ });
3574
+ orbiter.trailHead = trail.length - 1;
3575
+ } else {
3576
+ const next = (orbiter.trailHead + 1) % maxLen;
3577
+ trail[next].x = px;
3578
+ trail[next].y = py;
3579
+ orbiter.trailHead = next;
3580
+ }
3445
3581
  }
3446
3582
  }
3447
3583
  draw(ctx, width, height) {
@@ -3466,21 +3602,28 @@ var OrbitLayer = class extends SimulationLayer {
3466
3602
  ctx.globalCompositeOperation = "lighter";
3467
3603
  for (const orbiter of this.#orbiters) {
3468
3604
  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();
3605
+ const trail = orbiter.trail;
3606
+ const trailLen = trail.length;
3607
+ if (trailLen > 1) {
3608
+ const oldest = trailLen === this.#trailLength ? (orbiter.trailHead + 1) % trailLen : 0;
3609
+ for (let ti = 0; ti < trailLen - 1; ti++) {
3610
+ const progress = (ti + 1) / trailLen;
3611
+ const trailAlpha = progress * .5;
3612
+ const trailWidth = orbiter.size * progress * this.#scale;
3613
+ if (trailAlpha < .01) continue;
3614
+ const idx0 = (oldest + ti) % trailLen;
3615
+ const idx1 = (oldest + ti + 1) % trailLen;
3616
+ ctx.globalAlpha = trailAlpha;
3617
+ ctx.strokeStyle = `rgb(${cr}, ${cg}, ${cb})`;
3618
+ ctx.lineWidth = trailWidth;
3619
+ ctx.beginPath();
3620
+ ctx.moveTo(trail[idx0].x, trail[idx0].y);
3621
+ ctx.lineTo(trail[idx1].x, trail[idx1].y);
3622
+ ctx.stroke();
3623
+ }
3481
3624
  }
3482
- if (orbiter.trail.length > 0) {
3483
- const head = orbiter.trail[orbiter.trail.length - 1];
3625
+ if (trailLen > 0) {
3626
+ const head = trail[orbiter.trailHead];
3484
3627
  const headSize = orbiter.size * this.#scale;
3485
3628
  const glow = ctx.createRadialGradient(head.x, head.y, 0, head.x, head.y, headSize * 3);
3486
3629
  glow.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, 0.9)`);
@@ -3512,23 +3655,22 @@ var OrbitLayer = class extends SimulationLayer {
3512
3655
  tilt: MULBERRY$10.next() * Math.PI,
3513
3656
  size: 1.5 + MULBERRY$10.next() * 2.5,
3514
3657
  color: this.#colors[Math.floor(MULBERRY$10.next() * this.#colors.length)],
3515
- trail: []
3658
+ trail: [],
3659
+ trailHead: 0
3516
3660
  };
3517
3661
  }
3518
3662
  };
3519
3663
  //#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
- };
3664
+ //#region src/orbits/index.ts
3665
+ function createOrbits(config) {
3666
+ return new Orbits(config);
3667
+ }
3526
3668
  //#endregion
3527
3669
  //#region src/particles/consts.ts
3528
3670
  const MULBERRY$9 = mulberry32(13);
3529
3671
  //#endregion
3530
3672
  //#region src/particles/layer.ts
3531
- var ParticleLayer = class extends SimulationLayer {
3673
+ var Particles = class extends Effect {
3532
3674
  #scale;
3533
3675
  #connectionDistance;
3534
3676
  #lineWidth;
@@ -3575,6 +3717,16 @@ var ParticleLayer = class extends SimulationLayer {
3575
3717
  this.#onMouseMoveBound = this.#onMouseMove.bind(this);
3576
3718
  this.#onMouseLeaveBound = this.#onMouseLeave.bind(this);
3577
3719
  }
3720
+ configure(config) {
3721
+ if (config.scale !== void 0) this.#scale = config.scale;
3722
+ if (config.connectionDistance !== void 0) this.#connectionDistance = config.connectionDistance * this.#scale;
3723
+ if (config.lineWidth !== void 0) this.#lineWidth = config.lineWidth;
3724
+ if (config.mouseMode !== void 0) this.#mouseMode = config.mouseMode;
3725
+ if (config.mouseRadius !== void 0) this.#mouseRadius = config.mouseRadius * this.#scale;
3726
+ if (config.mouseStrength !== void 0) this.#mouseStrength = config.mouseStrength;
3727
+ if (config.particleForces !== void 0) this.#particleForces = config.particleForces;
3728
+ if (config.glow !== void 0) this.#glow = config.glow;
3729
+ }
3578
3730
  onResize(width, height) {
3579
3731
  this.#width = width;
3580
3732
  this.#height = height;
@@ -3757,12 +3909,10 @@ var ParticleLayer = class extends SimulationLayer {
3757
3909
  }
3758
3910
  };
3759
3911
  //#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
- };
3912
+ //#region src/particles/index.ts
3913
+ function createParticles(config) {
3914
+ return new Particles(config);
3915
+ }
3766
3916
  //#endregion
3767
3917
  //#region src/petals/consts.ts
3768
3918
  const MULBERRY$8 = mulberry32(13);
@@ -3777,7 +3927,7 @@ const PETAL_COLORS = [
3777
3927
  ];
3778
3928
  //#endregion
3779
3929
  //#region src/petals/layer.ts
3780
- var PetalLayer = class extends SimulationLayer {
3930
+ var Petals = class extends Effect {
3781
3931
  #scale;
3782
3932
  #size;
3783
3933
  #speed;
@@ -3799,6 +3949,10 @@ var PetalLayer = class extends SimulationLayer {
3799
3949
  this.#sprites = this.#createSprites();
3800
3950
  for (let i = 0; i < this.#maxCount; ++i) this.#petals.push(this.#createPetal(true));
3801
3951
  }
3952
+ configure(config) {
3953
+ if (config.speed !== void 0) this.#speed = config.speed;
3954
+ if (config.wind !== void 0) this.#wind = config.wind;
3955
+ }
3802
3956
  tick(dt, _width, height) {
3803
3957
  const speedFactor = height / 540 / this.#speed;
3804
3958
  this.#time += .012 * dt;
@@ -3829,14 +3983,13 @@ var PetalLayer = class extends SimulationLayer {
3829
3983
  const py = petal.y * height;
3830
3984
  const displaySize = petal.size * petal.depth;
3831
3985
  const scaleX = Math.cos(petal.flipAngle);
3832
- ctx.save();
3833
- ctx.translate(px, py);
3834
- ctx.rotate(petal.rotation);
3835
- ctx.scale(scaleX, 1);
3986
+ const cos = Math.cos(petal.rotation);
3987
+ const sin = Math.sin(petal.rotation);
3988
+ ctx.setTransform(cos * scaleX, sin * scaleX, -sin, cos, px, py);
3836
3989
  ctx.globalAlpha = .4 + petal.depth * .6;
3837
3990
  ctx.drawImage(this.#sprites[petal.colorIndex % this.#sprites.length], -displaySize / 2, -displaySize / 2, displaySize, displaySize);
3838
- ctx.restore();
3839
3991
  }
3992
+ ctx.resetTransform();
3840
3993
  ctx.globalAlpha = 1;
3841
3994
  }
3842
3995
  #createSprites() {
@@ -3888,12 +4041,10 @@ var PetalLayer = class extends SimulationLayer {
3888
4041
  }
3889
4042
  };
3890
4043
  //#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
- };
4044
+ //#region src/petals/index.ts
4045
+ function createPetals(config) {
4046
+ return new Petals(config);
4047
+ }
3897
4048
  //#endregion
3898
4049
  //#region src/plasma/layer.ts
3899
4050
  const DEFAULT_PALETTE = [
@@ -3923,7 +4074,7 @@ const DEFAULT_PALETTE = [
3923
4074
  b: 100
3924
4075
  }
3925
4076
  ];
3926
- var PlasmaLayer = class extends SimulationLayer {
4077
+ var Plasma = class extends Effect {
3927
4078
  #speed;
3928
4079
  #scale;
3929
4080
  #resolution;
@@ -3939,6 +4090,10 @@ var PlasmaLayer = class extends SimulationLayer {
3939
4090
  this.#resolution = config.resolution ?? 4;
3940
4091
  this.#palette = config.palette ?? DEFAULT_PALETTE;
3941
4092
  }
4093
+ configure(config) {
4094
+ if (config.speed !== void 0) this.#speed = config.speed;
4095
+ if (config.scale !== void 0) this.#scale = config.scale;
4096
+ }
3942
4097
  tick(dt, _width, _height) {
3943
4098
  this.#time += .02 * dt * this.#speed;
3944
4099
  }
@@ -3983,12 +4138,10 @@ var PlasmaLayer = class extends SimulationLayer {
3983
4138
  }
3984
4139
  };
3985
4140
  //#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
- };
4141
+ //#region src/plasma/index.ts
4142
+ function createPlasma(config) {
4143
+ return new Plasma(config);
4144
+ }
3992
4145
  //#endregion
3993
4146
  //#region src/rain/consts.ts
3994
4147
  const MULBERRY$7 = mulberry32(13);
@@ -4014,7 +4167,7 @@ const VARIANT_PRESETS = {
4014
4167
  splashes: true
4015
4168
  }
4016
4169
  };
4017
- var RainLayer = class extends SimulationLayer {
4170
+ var Rain = class extends Effect {
4018
4171
  #scale;
4019
4172
  #speed;
4020
4173
  #wind;
@@ -4042,6 +4195,11 @@ var RainLayer = class extends SimulationLayer {
4042
4195
  if (innerWidth < 991) this.#maxDrops = Math.floor(this.#maxDrops / 2);
4043
4196
  for (let i = 0; i < this.#maxDrops; ++i) this.#drops.push(this.#createDrop(true));
4044
4197
  }
4198
+ configure(config) {
4199
+ if (config.speed !== void 0) this.#speed = config.speed;
4200
+ if (config.wind !== void 0) this.#wind = config.wind;
4201
+ if (config.splashes !== void 0) this.#enableSplashes = config.splashes;
4202
+ }
4045
4203
  tick(dt, width, height) {
4046
4204
  let aliveDrops = 0;
4047
4205
  for (let i = 0; i < this.#drops.length; i++) {
@@ -4228,18 +4386,16 @@ var SplashParticle = class SplashParticle {
4228
4386
  }
4229
4387
  };
4230
4388
  //#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
- };
4389
+ //#region src/rain/index.ts
4390
+ function createRain(config) {
4391
+ return new Rain(config);
4392
+ }
4237
4393
  //#endregion
4238
4394
  //#region src/sandstorm/consts.ts
4239
4395
  const MULBERRY$6 = mulberry32(13);
4240
4396
  //#endregion
4241
4397
  //#region src/sandstorm/layer.ts
4242
- var SandstormLayer = class extends SimulationLayer {
4398
+ var Sandstorm = class extends Effect {
4243
4399
  #scale;
4244
4400
  #wind;
4245
4401
  #turbulence;
@@ -4264,6 +4420,10 @@ var SandstormLayer = class extends SimulationLayer {
4264
4420
  if (innerWidth < 991) this.#maxCount = Math.floor(this.#maxCount / 2);
4265
4421
  for (let i = 0; i < this.#maxCount; ++i) this.#grains.push(this.#createGrain(true));
4266
4422
  }
4423
+ configure(config) {
4424
+ if (config.wind !== void 0) this.#wind = config.wind;
4425
+ if (config.turbulence !== void 0) this.#turbulence = config.turbulence;
4426
+ }
4267
4427
  tick(dt, width, height) {
4268
4428
  this.#time += .02 * dt;
4269
4429
  const gustX = Math.sin(this.#time * .3) * .5 + Math.sin(this.#time * .8 + 1) * .3 + Math.sin(this.#time * 2.1) * .2;
@@ -4338,12 +4498,165 @@ var SandstormLayer = class extends SimulationLayer {
4338
4498
  }
4339
4499
  };
4340
4500
  //#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" });
4501
+ //#region src/sandstorm/index.ts
4502
+ function createSandstorm(config) {
4503
+ return new Sandstorm(config);
4504
+ }
4505
+ //#endregion
4506
+ //#region src/scene.ts
4507
+ /**
4508
+ * Internal canvas runner that drives all layers in a Scene.
4509
+ */
4510
+ var SceneCanvas = class extends LimitedFrameRateCanvas {
4511
+ #layers;
4512
+ #contextOptions;
4513
+ #offscreen = null;
4514
+ #offscreenCtx = null;
4515
+ constructor(canvas, layers, frameRate, options) {
4516
+ super(canvas, frameRate, options);
4517
+ this.#layers = layers;
4518
+ this.#contextOptions = options;
4519
+ canvas.style.position = "absolute";
4520
+ canvas.style.top = "0";
4521
+ canvas.style.left = "0";
4522
+ canvas.style.height = "100%";
4523
+ canvas.style.width = "100%";
4524
+ }
4525
+ start() {
4526
+ for (const layer of this.#layers) layer.onMount(this.canvas);
4527
+ super.start();
4528
+ }
4529
+ destroy() {
4530
+ for (const layer of this.#layers) layer.onUnmount(this.canvas);
4531
+ super.destroy();
4532
+ }
4533
+ draw() {
4534
+ const dpr = this.dpr;
4535
+ this.canvas.height = this.height * dpr;
4536
+ this.canvas.width = this.width * dpr;
4537
+ const ctx = this.context;
4538
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
4539
+ ctx.clearRect(0, 0, this.width, this.height);
4540
+ for (const layer of this.#layers) if (layer.fade) {
4541
+ const offCtx = this.#getOffscreenCtx(this.width * dpr, this.height * dpr);
4542
+ offCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
4543
+ offCtx.clearRect(0, 0, this.width, this.height);
4544
+ layer.draw(offCtx, this.width, this.height);
4545
+ applyEdgeFade(offCtx, this.width, this.height, layer.fade);
4546
+ ctx.drawImage(this.#offscreen, 0, 0, this.width, this.height);
4547
+ } else {
4548
+ ctx.save();
4549
+ layer.draw(ctx, this.width, this.height);
4550
+ ctx.restore();
4551
+ }
4552
+ }
4553
+ tick() {
4554
+ const dt = (this.delta > 0 && this.delta < 200 ? this.delta / (1e3 / 60) : 1) * this.speed * LimitedFrameRateCanvas.globalSpeed;
4555
+ for (const layer of this.#layers) layer.tick(dt, this.width, this.height);
4556
+ }
4557
+ onResize() {
4558
+ super.onResize();
4559
+ if (this.#offscreen) {
4560
+ this.#offscreen.width = this.width * this.dpr;
4561
+ this.#offscreen.height = this.height * this.dpr;
4562
+ }
4563
+ for (const layer of this.#layers) layer.onResize(this.width, this.height);
4564
+ }
4565
+ #getOffscreenCtx(width, height) {
4566
+ if (!this.#offscreen) {
4567
+ this.#offscreen = document.createElement("canvas");
4568
+ this.#offscreen.width = width;
4569
+ this.#offscreen.height = height;
4570
+ this.#offscreenCtx = this.#offscreen.getContext("2d", this.#contextOptions);
4571
+ }
4572
+ return this.#offscreenCtx;
4573
+ }
4574
+ };
4575
+ /**
4576
+ * Composable canvas that renders multiple Effect layers in order (first = bottom, last = top).
4577
+ *
4578
+ * @example
4579
+ * const scene = new Scene()
4580
+ * .mount(canvas)
4581
+ * .layer(new Aurora({ bands: 5 }))
4582
+ * .layer(new Stars().withFade({ bottom: 0.4 }))
4583
+ * .start();
4584
+ */
4585
+ var Scene = class {
4586
+ #layers = [];
4587
+ #frameRate;
4588
+ #defaultOptions;
4589
+ #runner = null;
4590
+ constructor(frameRate = 60, options = { colorSpace: "display-p3" }) {
4591
+ this.#frameRate = frameRate;
4592
+ this.#defaultOptions = options;
4593
+ }
4594
+ /**
4595
+ * Mount the scene to a canvas element or CSS selector.
4596
+ */
4597
+ mount(canvas, options) {
4598
+ if (typeof canvas === "string") {
4599
+ const el = document.querySelector(canvas);
4600
+ if (!el) throw new Error(`Scene.mount(): no element found for selector "${canvas}".`);
4601
+ canvas = el;
4602
+ }
4603
+ this.#runner?.destroy();
4604
+ this.#runner = new SceneCanvas(canvas, this.#layers, this.#frameRate, options ?? this.#defaultOptions);
4605
+ return this;
4606
+ }
4607
+ /**
4608
+ * Add an effect layer. Layers are rendered in the order they are added.
4609
+ * If the scene is already running, the layer is mounted immediately.
4610
+ */
4611
+ layer(effect) {
4612
+ this.#layers.push(effect);
4613
+ if (this.#runner?.isTicking) effect.onMount(this.#runner.canvas);
4614
+ return this;
4615
+ }
4616
+ /**
4617
+ * Start the render loop.
4618
+ */
4619
+ start() {
4620
+ this.#runner?.start();
4621
+ return this;
4622
+ }
4623
+ /**
4624
+ * Pause rendering without destroying state. Use resume() to continue.
4625
+ */
4626
+ pause() {
4627
+ this.#runner?.pause();
4628
+ return this;
4629
+ }
4630
+ /**
4631
+ * Resume rendering after pause().
4632
+ */
4633
+ resume() {
4634
+ this.#runner?.resume();
4635
+ return this;
4636
+ }
4637
+ /**
4638
+ * Stop and destroy all layers.
4639
+ */
4640
+ destroy() {
4641
+ this.#runner?.destroy();
4642
+ this.#runner = null;
4643
+ }
4644
+ get speed() {
4645
+ return this.#runner?.speed ?? 1;
4646
+ }
4647
+ set speed(value) {
4648
+ if (this.#runner) this.#runner.speed = value;
4649
+ }
4650
+ get isTicking() {
4651
+ return this.#runner?.isTicking ?? false;
4345
4652
  }
4346
4653
  };
4654
+ /**
4655
+ * Factory alternative to `new Scene()`. Call .mount() and .layer() on the returned instance.
4656
+ */
4657
+ function createScene(frameRate, options) {
4658
+ return new Scene(frameRate, options);
4659
+ }
4347
4660
  //#endregion
4348
4661
  //#region src/shooting-stars/system.ts
4349
4662
  var ShootingStarSystem = class {
@@ -4357,10 +4670,8 @@ var ShootingStarSystem = class {
4357
4670
  #alphaRange;
4358
4671
  #decayMin;
4359
4672
  #decayRange;
4360
- #verticalFade;
4361
4673
  #rng;
4362
4674
  #cooldown;
4363
- #height = 0;
4364
4675
  #stars = [];
4365
4676
  constructor(config, rng) {
4366
4677
  this.#interval = config.interval;
@@ -4377,12 +4688,10 @@ var ShootingStarSystem = class {
4377
4688
  this.#alphaRange = config.alphaRange ?? .3;
4378
4689
  this.#decayMin = config.decayMin ?? .008;
4379
4690
  this.#decayRange = config.decayRange ?? .01;
4380
- this.#verticalFade = config.verticalFade ?? null;
4381
4691
  this.#rng = rng;
4382
4692
  this.#cooldown = this.#interval[0] + this.#rng() * (this.#interval[1] - this.#interval[0]);
4383
4693
  }
4384
4694
  tick(dt, width, height) {
4385
- this.#height = height;
4386
4695
  this.#cooldown -= dt;
4387
4696
  if (this.#cooldown <= 0) {
4388
4697
  this.#stars.push(this.#create(width, height));
@@ -4391,17 +4700,24 @@ var ShootingStarSystem = class {
4391
4700
  let alive = 0;
4392
4701
  for (let i = 0; i < this.#stars.length; i++) {
4393
4702
  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();
4703
+ const trail = star.trail;
4704
+ const maxLen = this.#trailLength;
4705
+ if (trail.length < maxLen) {
4706
+ trail.push({
4707
+ x: star.x,
4708
+ y: star.y
4709
+ });
4710
+ star.trailHead = trail.length - 1;
4711
+ } else {
4712
+ const next = (star.trailHead + 1) % maxLen;
4713
+ trail[next].x = star.x;
4714
+ trail[next].y = star.y;
4715
+ star.trailHead = next;
4716
+ }
4399
4717
  star.x += star.vx * this.#speed * dt;
4400
4718
  star.y += star.vy * this.#speed * dt;
4401
4719
  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;
4720
+ if (star.alpha > 0 && star.x > -50 && star.x < width + 50 && star.y < height + 50) this.#stars[alive++] = star;
4405
4721
  }
4406
4722
  this.#stars.length = alive;
4407
4723
  }
@@ -4409,23 +4725,22 @@ var ShootingStarSystem = class {
4409
4725
  const [cr, cg, cb] = this.#color;
4410
4726
  ctx.globalCompositeOperation = "lighter";
4411
4727
  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;
4728
+ const trail = star.trail;
4729
+ const trailLen = trail.length;
4730
+ const oldest = trailLen === this.#trailLength ? (star.trailHead + 1) % trailLen : 0;
4731
+ for (let t = 0; t < trailLen; t++) {
4732
+ const progress = t / trailLen;
4733
+ const trailAlpha = star.alpha * progress * this.#trailAlphaFactor;
4420
4734
  const trailSize = star.size * progress * this.#scale;
4421
4735
  if (trailAlpha < .01) continue;
4736
+ const idx = (oldest + t) % trailLen;
4422
4737
  ctx.globalAlpha = trailAlpha;
4423
4738
  ctx.beginPath();
4424
- ctx.arc(star.trail[t].x, star.trail[t].y, trailSize, 0, Math.PI * 2);
4739
+ ctx.arc(trail[idx].x, trail[idx].y, trailSize, 0, Math.PI * 2);
4425
4740
  ctx.fillStyle = `rgb(${cr}, ${cg}, ${cb})`;
4426
4741
  ctx.fill();
4427
4742
  }
4428
- const alpha = star.alpha * fadeFactor;
4743
+ const alpha = star.alpha;
4429
4744
  const headSize = star.size * 2 * this.#scale;
4430
4745
  const glow = ctx.createRadialGradient(star.x, star.y, 0, star.x, star.y, headSize);
4431
4746
  glow.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, ${alpha})`);
@@ -4453,7 +4768,8 @@ var ShootingStarSystem = class {
4453
4768
  alpha: this.#alphaMin + this.#rng() * this.#alphaRange,
4454
4769
  size: 1.5 + this.#rng() * 2,
4455
4770
  decay: this.#decayMin + this.#rng() * this.#decayRange,
4456
- trail: []
4771
+ trail: [],
4772
+ trailHead: 0
4457
4773
  };
4458
4774
  }
4459
4775
  };
@@ -4465,7 +4781,7 @@ const MULBERRY$5 = mulberry32(13);
4465
4781
  const SPRITE_SIZE = 64;
4466
4782
  const SPRITE_CENTER = SPRITE_SIZE / 2;
4467
4783
  const SPRITE_RADIUS = SPRITE_SIZE / 2;
4468
- var SnowLayer = class extends SimulationLayer {
4784
+ var Snow = class extends Effect {
4469
4785
  #scale;
4470
4786
  #size;
4471
4787
  #speed;
@@ -4488,6 +4804,9 @@ var SnowLayer = class extends SimulationLayer {
4488
4804
  this.#sprites = this.#createSprites(r, g, b);
4489
4805
  for (let i = 0; i < this.#maxParticles; ++i) this.#snowflakes.push(this.#createSnowflake(true));
4490
4806
  }
4807
+ configure(config) {
4808
+ if (config.speed !== void 0) this.#speed = config.speed;
4809
+ }
4491
4810
  onResize(_width, height) {
4492
4811
  this.#height = height;
4493
4812
  }
@@ -4530,11 +4849,11 @@ var SnowLayer = class extends SimulationLayer {
4530
4849
  if (displaySize < .5) continue;
4531
4850
  ctx.globalAlpha = this.#baseOpacity * (.15 + snowflake.depth * .85);
4532
4851
  if (snowflake.spriteIndex === 3) {
4533
- ctx.save();
4534
- ctx.translate(px, py);
4535
- ctx.rotate(snowflake.rotation);
4852
+ const cos = Math.cos(snowflake.rotation);
4853
+ const sin = Math.sin(snowflake.rotation);
4854
+ ctx.setTransform(cos, sin, -sin, cos, px, py);
4536
4855
  ctx.drawImage(this.#sprites[snowflake.spriteIndex], -displayRadius, -displayRadius, displaySize, displaySize);
4537
- ctx.restore();
4856
+ ctx.resetTransform();
4538
4857
  } else ctx.drawImage(this.#sprites[snowflake.spriteIndex], px - displayRadius, py - displayRadius, displaySize, displaySize);
4539
4858
  }
4540
4859
  ctx.globalAlpha = 1;
@@ -4647,12 +4966,10 @@ var SnowLayer = class extends SimulationLayer {
4647
4966
  }
4648
4967
  };
4649
4968
  //#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
- };
4969
+ //#region src/snow/index.ts
4970
+ function createSnow(config) {
4971
+ return new Snow(config);
4972
+ }
4656
4973
  //#endregion
4657
4974
  //#region src/sparklers/consts.ts
4658
4975
  const MULBERRY$4 = mulberry32(13);
@@ -4664,7 +4981,7 @@ const DEFAULT_COLORS$1 = [
4664
4981
  "#ffffff",
4665
4982
  "#ffee88"
4666
4983
  ];
4667
- var SparklerLayer = class extends SimulationLayer {
4984
+ var Sparklers = class extends Effect {
4668
4985
  #scale;
4669
4986
  #emitRate;
4670
4987
  #maxSparks;
@@ -4681,6 +4998,8 @@ var SparklerLayer = class extends SimulationLayer {
4681
4998
  #emitY = .5;
4682
4999
  #mouseOnCanvas = false;
4683
5000
  #sparks = [];
5001
+ #mountedCanvas = null;
5002
+ #cachedRect = null;
4684
5003
  constructor(config = {}) {
4685
5004
  super();
4686
5005
  this.#scale = config.scale ?? 1;
@@ -4696,11 +5015,13 @@ var SparklerLayer = class extends SimulationLayer {
4696
5015
  this.#onMouseMoveBound = this.#onMouseMove.bind(this);
4697
5016
  this.#onMouseLeaveBound = this.#onMouseLeave.bind(this);
4698
5017
  }
4699
- setPosition(x, y) {
5018
+ moveTo(x, y) {
4700
5019
  this.#emitX = x;
4701
5020
  this.#emitY = y;
4702
5021
  }
4703
5022
  onMount(canvas) {
5023
+ this.#mountedCanvas = canvas;
5024
+ this.#cachedRect = canvas.getBoundingClientRect();
4704
5025
  if (this.#hoverMode) {
4705
5026
  canvas.addEventListener("mousemove", this.#onMouseMoveBound, { passive: true });
4706
5027
  canvas.addEventListener("mouseleave", this.#onMouseLeaveBound, { passive: true });
@@ -4709,12 +5030,26 @@ var SparklerLayer = class extends SimulationLayer {
4709
5030
  onUnmount(canvas) {
4710
5031
  canvas.removeEventListener("mousemove", this.#onMouseMoveBound);
4711
5032
  canvas.removeEventListener("mouseleave", this.#onMouseLeaveBound);
5033
+ this.#mountedCanvas = null;
5034
+ this.#cachedRect = null;
5035
+ }
5036
+ onResize() {
5037
+ if (this.#mountedCanvas) this.#cachedRect = this.#mountedCanvas.getBoundingClientRect();
5038
+ }
5039
+ configure(config) {
5040
+ if (config.scale !== void 0) this.#scale = config.scale;
5041
+ if (config.emitRate !== void 0) this.#emitRate = config.emitRate;
5042
+ if (config.friction !== void 0) this.#friction = config.friction;
5043
+ if (config.gravity !== void 0) this.#gravity = config.gravity;
5044
+ if (config.trailLength !== void 0) this.#trailLength = config.trailLength;
5045
+ if (config.hoverMode !== void 0) this.#hoverMode = config.hoverMode;
4712
5046
  }
4713
5047
  tick(dt, width, height) {
4714
5048
  if (!this.#hoverMode || this.#mouseOnCanvas) {
4715
5049
  const emitCount = Math.min(this.#emitRate, this.#maxSparks - this.#sparks.length);
4716
5050
  for (let i = 0; i < emitCount; i++) this.#sparks.push(this.#createSpark(width, height));
4717
5051
  }
5052
+ const frictionFactor = Math.pow(this.#friction, dt);
4718
5053
  let alive = 0;
4719
5054
  for (let i = 0; i < this.#sparks.length; i++) {
4720
5055
  const spark = this.#sparks[i];
@@ -4723,8 +5058,8 @@ var SparklerLayer = class extends SimulationLayer {
4723
5058
  y: spark.y
4724
5059
  });
4725
5060
  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);
5061
+ spark.vx *= frictionFactor;
5062
+ spark.vy *= frictionFactor;
4728
5063
  spark.vy += this.#gravity * this.#scale * dt;
4729
5064
  spark.x += spark.vx * dt;
4730
5065
  spark.y += spark.vy * dt;
@@ -4767,7 +5102,7 @@ var SparklerLayer = class extends SimulationLayer {
4767
5102
  ctx.globalCompositeOperation = "source-over";
4768
5103
  }
4769
5104
  #onMouseMove(evt) {
4770
- const rect = evt.currentTarget.getBoundingClientRect();
5105
+ const rect = this.#cachedRect ?? evt.currentTarget.getBoundingClientRect();
4771
5106
  this.#emitX = (evt.clientX - rect.left) / rect.width;
4772
5107
  this.#emitY = (evt.clientY - rect.top) / rect.height;
4773
5108
  this.#mouseOnCanvas = true;
@@ -4861,29 +5196,20 @@ var SparklerParticle = class {
4861
5196
  }
4862
5197
  };
4863
5198
  //#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
- };
5199
+ //#region src/sparklers/index.ts
5200
+ function createSparklers(config) {
5201
+ return new Sparklers(config);
5202
+ }
4876
5203
  //#endregion
4877
5204
  //#region src/stars/consts.ts
4878
5205
  const MULBERRY$3 = mulberry32(13);
4879
5206
  //#endregion
4880
5207
  //#region src/stars/layer.ts
4881
- var StarLayer = class extends SimulationLayer {
5208
+ var Stars = class extends Effect {
4882
5209
  #mode;
4883
5210
  #twinkleSpeed;
4884
5211
  #colorRGB;
4885
5212
  #scale;
4886
- #verticalFade;
4887
5213
  #shootingStarSystem;
4888
5214
  #starCount;
4889
5215
  #time = 0;
@@ -4894,7 +5220,6 @@ var StarLayer = class extends SimulationLayer {
4894
5220
  this.#starCount = config.starCount ?? 150;
4895
5221
  this.#twinkleSpeed = config.twinkleSpeed ?? 1;
4896
5222
  this.#scale = config.scale ?? 1;
4897
- this.#verticalFade = config.verticalFade ?? null;
4898
5223
  this.#colorRGB = hexToRGB(config.color ?? "#ffffff");
4899
5224
  const shootingColorRGB = hexToRGB(config.shootingColor ?? "#ffffff");
4900
5225
  this.#shootingStarSystem = this.#mode === "shooting" || this.#mode === "both" ? new ShootingStarSystem({
@@ -4907,11 +5232,14 @@ var StarLayer = class extends SimulationLayer {
4907
5232
  alphaMin: .8,
4908
5233
  alphaRange: .2,
4909
5234
  decayMin: .01,
4910
- decayRange: .015,
4911
- verticalFade: this.#verticalFade ?? void 0
5235
+ decayRange: .015
4912
5236
  }, () => MULBERRY$3.next()) : null;
4913
5237
  if (this.#mode === "sky" || this.#mode === "both") for (let i = 0; i < this.#starCount; ++i) this.#stars.push(this.#createStar());
4914
5238
  }
5239
+ configure(config) {
5240
+ if (config.twinkleSpeed !== void 0) this.#twinkleSpeed = config.twinkleSpeed;
5241
+ if (config.scale !== void 0) this.#scale = config.scale;
5242
+ }
4915
5243
  tick(dt, width, height) {
4916
5244
  this.#time += .02 * dt;
4917
5245
  this.#shootingStarSystem?.tick(dt, width, height);
@@ -4923,13 +5251,7 @@ var StarLayer = class extends SimulationLayer {
4923
5251
  for (const star of this.#stars) {
4924
5252
  const px = star.x * width;
4925
5253
  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
- }
5254
+ const alpha = star.brightness * (.3 + .7 * (.5 + .5 * Math.sin(this.#time * star.twinkleSpeed * this.#twinkleSpeed + star.twinklePhase)));
4933
5255
  const size = star.size * this.#scale;
4934
5256
  ctx.globalAlpha = alpha;
4935
5257
  ctx.beginPath();
@@ -4967,12 +5289,10 @@ var StarLayer = class extends SimulationLayer {
4967
5289
  }
4968
5290
  };
4969
5291
  //#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
- };
5292
+ //#region src/stars/index.ts
5293
+ function createStars(config) {
5294
+ return new Stars(config);
5295
+ }
4976
5296
  //#endregion
4977
5297
  //#region src/streamers/consts.ts
4978
5298
  const MULBERRY$2 = mulberry32(13);
@@ -4988,7 +5308,7 @@ const STREAMER_COLORS = [
4988
5308
  ];
4989
5309
  //#endregion
4990
5310
  //#region src/streamers/layer.ts
4991
- var StreamerLayer = class extends SimulationLayer {
5311
+ var Streamers = class extends Effect {
4992
5312
  #colors;
4993
5313
  #scale;
4994
5314
  #speed;
@@ -5014,6 +5334,9 @@ var StreamerLayer = class extends SimulationLayer {
5014
5334
  for (let i = 0; i < this.#count; i++) this.#streamers.push(this.#createStreamer(true));
5015
5335
  }
5016
5336
  }
5337
+ configure(config) {
5338
+ if (config.speed !== void 0) this.#speed = config.speed;
5339
+ }
5017
5340
  tick(dt, width, height) {
5018
5341
  this.#width = width;
5019
5342
  this.#height = height;
@@ -5128,12 +5451,10 @@ var StreamerLayer = class extends SimulationLayer {
5128
5451
  }
5129
5452
  };
5130
5453
  //#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
- };
5454
+ //#region src/streamers/index.ts
5455
+ function createStreamers(config) {
5456
+ return new Streamers(config);
5457
+ }
5137
5458
  //#endregion
5138
5459
  //#region src/trail.ts
5139
5460
  var Trail = class {
@@ -5247,12 +5568,11 @@ const DEFAULT_COLORS = [
5247
5568
  "#3399cc",
5248
5569
  "#66c2e0"
5249
5570
  ];
5250
- var WaveLayer = class extends SimulationLayer {
5571
+ var Waves = class extends Effect {
5251
5572
  #speed;
5252
- #foamColor;
5253
5573
  #foamAmount;
5254
5574
  #scale;
5255
- #time = 0;
5575
+ #foamRGB;
5256
5576
  #waves = [];
5257
5577
  #foamParticles = [];
5258
5578
  #maxFoamParticles;
@@ -5261,10 +5581,10 @@ var WaveLayer = class extends SimulationLayer {
5261
5581
  const layers = config.layers ?? 5;
5262
5582
  const colors = config.colors ?? DEFAULT_COLORS;
5263
5583
  this.#speed = config.speed ?? 1;
5264
- this.#foamColor = config.foamColor ?? "#ffffff";
5265
5584
  this.#foamAmount = config.foamAmount ?? .4;
5266
5585
  this.#scale = config.scale ?? 1;
5267
5586
  this.#maxFoamParticles = 120;
5587
+ this.#foamRGB = hexToRGB(config.foamColor ?? "#ffffff");
5268
5588
  if (innerWidth < 991) this.#maxFoamParticles = Math.floor(this.#maxFoamParticles / 2);
5269
5589
  for (let i = 0; i < layers; i++) {
5270
5590
  const depth = i / Math.max(layers - 1, 1);
@@ -5272,17 +5592,22 @@ var WaveLayer = class extends SimulationLayer {
5272
5592
  this.#waves.push({
5273
5593
  amplitude: (20 + MULBERRY$1.next() * 30) * (1 - depth * .4),
5274
5594
  frequency: .005 + MULBERRY$1.next() * .008 + depth * .002,
5275
- speed: (.4 + MULBERRY$1.next() * .6 + depth * .3) * this.#speed,
5595
+ speed: .4 + MULBERRY$1.next() * .6 + depth * .3,
5276
5596
  phase: MULBERRY$1.next() * Math.PI * 2,
5277
5597
  baseY: .35 + depth * .13,
5278
5598
  color,
5279
- foamThreshold: .6 + MULBERRY$1.next() * .3
5599
+ foamThreshold: .6 + MULBERRY$1.next() * .3,
5600
+ rgb: hexToRGB(color)
5280
5601
  });
5281
5602
  }
5282
5603
  }
5604
+ configure(config) {
5605
+ if (config.speed !== void 0) this.#speed = config.speed;
5606
+ if (config.foamAmount !== void 0) this.#foamAmount = config.foamAmount;
5607
+ if (config.scale !== void 0) this.#scale = config.scale;
5608
+ }
5283
5609
  tick(dt, width, height) {
5284
- this.#time += .02 * dt * this.#speed;
5285
- for (const wave of this.#waves) wave.phase += .015 * wave.speed * dt;
5610
+ for (const wave of this.#waves) wave.phase += .015 * wave.speed * this.#speed * dt;
5286
5611
  let aliveFoam = 0;
5287
5612
  for (let i = 0; i < this.#foamParticles.length; i++) {
5288
5613
  const foam = this.#foamParticles[i];
@@ -5312,6 +5637,7 @@ var WaveLayer = class extends SimulationLayer {
5312
5637
  const step = 2;
5313
5638
  for (let wi = 0; wi < this.#waves.length; wi++) {
5314
5639
  const wave = this.#waves[wi];
5640
+ const [wr, wg, wb] = wave.rgb;
5315
5641
  const centerY = wave.baseY * height;
5316
5642
  ctx.beginPath();
5317
5643
  ctx.moveTo(0, height);
@@ -5325,54 +5651,35 @@ var WaveLayer = class extends SimulationLayer {
5325
5651
  ctx.lineTo(width, height);
5326
5652
  ctx.closePath();
5327
5653
  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));
5654
+ gradient.addColorStop(0, `rgba(${wr}, ${wg}, ${wb}, 0.85)`);
5655
+ gradient.addColorStop(.4, `rgb(${wr}, ${wg}, ${wb})`);
5656
+ gradient.addColorStop(1, `rgb(${Math.floor(wr * .6)}, ${Math.floor(wg * .6)}, ${Math.floor(wb * .6)})`);
5331
5657
  ctx.fillStyle = gradient;
5332
5658
  ctx.fill();
5333
5659
  }
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();
5660
+ if (this.#foamAmount > 0) {
5661
+ const [fr, fg, fb] = this.#foamRGB;
5662
+ for (const foam of this.#foamParticles) {
5663
+ if (foam.alpha <= 0) continue;
5664
+ ctx.beginPath();
5665
+ ctx.arc(foam.x, foam.y, foam.size * this.#scale, 0, Math.PI * 2);
5666
+ ctx.fillStyle = `rgba(${fr}, ${fg}, ${fb}, ${foam.alpha * this.#foamAmount})`;
5667
+ ctx.fill();
5668
+ }
5340
5669
  }
5341
5670
  }
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
5671
  };
5363
5672
  //#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
- };
5673
+ //#region src/waves/index.ts
5674
+ function createWaves(config) {
5675
+ return new Waves(config);
5676
+ }
5370
5677
  //#endregion
5371
5678
  //#region src/wormhole/consts.ts
5372
5679
  const MULBERRY = mulberry32(13);
5373
5680
  //#endregion
5374
5681
  //#region src/wormhole/layer.ts
5375
- var WormholeLayer = class extends SimulationLayer {
5682
+ var Wormhole = class extends Effect {
5376
5683
  #speed;
5377
5684
  #colorRGB;
5378
5685
  #direction;
@@ -5401,6 +5708,10 @@ var WormholeLayer = class extends SimulationLayer {
5401
5708
  for (let i = 0; i < this.#count; ++i) this.#particles.push(this.#createParticle(true));
5402
5709
  }
5403
5710
  }
5711
+ configure(config) {
5712
+ if (config.speed !== void 0) this.#speed = config.speed;
5713
+ if (config.scale !== void 0) this.#scale = config.scale;
5714
+ }
5404
5715
  tick(dt, width, height) {
5405
5716
  this.#width = width;
5406
5717
  this.#height = height;
@@ -5492,13 +5803,11 @@ var WormholeLayer = class extends SimulationLayer {
5492
5803
  }
5493
5804
  };
5494
5805
  //#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
- };
5806
+ //#region src/wormhole/index.ts
5807
+ function createWormhole(config) {
5808
+ return new Wormhole(config);
5809
+ }
5501
5810
  //#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 };
5811
+ 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
5812
 
5504
5813
  //# sourceMappingURL=index.mjs.map