@basmilius/sparkle 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1322 @@
1
+ import { hexToRGB, mulberry32 } from "@basmilius/utils";
2
+ //#region src/canvas.ts
3
+ var LimitedFrameRateCanvas = class {
4
+ #canvas;
5
+ #context;
6
+ #frameRate;
7
+ #target;
8
+ #current = 0;
9
+ #delta = 0;
10
+ #frame = 0;
11
+ #now = 0;
12
+ #then = 0;
13
+ #ticks = 0;
14
+ #isStopped = true;
15
+ #height = 540;
16
+ #width = 960;
17
+ get canvas() {
18
+ return this.#canvas;
19
+ }
20
+ get context() {
21
+ return this.#context;
22
+ }
23
+ get delta() {
24
+ return this.#delta;
25
+ }
26
+ get deltaFactor() {
27
+ return this.#then === 0 ? 1 : this.#target / this.#delta;
28
+ }
29
+ get frameRate() {
30
+ return this.#frameRate;
31
+ }
32
+ get isSmall() {
33
+ return innerWidth < 991;
34
+ }
35
+ get isTicking() {
36
+ return !this.#isStopped;
37
+ }
38
+ get ticks() {
39
+ return this.#ticks;
40
+ }
41
+ get height() {
42
+ return this.#height;
43
+ }
44
+ get width() {
45
+ return this.#width;
46
+ }
47
+ constructor(canvas, frameRate, options = { colorSpace: "display-p3" }) {
48
+ this.#canvas = canvas;
49
+ this.#context = canvas.getContext("2d", options);
50
+ this.#frameRate = frameRate;
51
+ this.#target = 1e3 / frameRate;
52
+ this.onVisibilityChange = this.onVisibilityChange.bind(this);
53
+ this.onResize = this.onResize.bind(this);
54
+ document.addEventListener("visibilitychange", this.onVisibilityChange, { passive: true });
55
+ window.addEventListener("resize", this.onResize, { passive: true });
56
+ }
57
+ loop() {
58
+ if (this.#isStopped) return;
59
+ this.#current = Date.now();
60
+ this.#frame = requestAnimationFrame(this.loop.bind(this));
61
+ if (this.#then > 0 && this.#current - this.#then + 1 < this.#target) return;
62
+ this.#now = this.#current;
63
+ this.#delta = this.#now - this.#then;
64
+ ++this.#ticks;
65
+ this.tick();
66
+ this.draw();
67
+ this.#then = this.#now;
68
+ }
69
+ start() {
70
+ this.onResize();
71
+ this.#isStopped = false;
72
+ this.#frame = requestAnimationFrame(this.loop.bind(this));
73
+ }
74
+ stop() {
75
+ this.#isStopped = true;
76
+ cancelAnimationFrame(this.#frame);
77
+ }
78
+ draw() {
79
+ throw new Error("LimitedFrameRateCanvas::draw() should be overwritten.");
80
+ }
81
+ tick() {
82
+ throw new Error("LimitedFrameRateCanvas::tick() should be overwritten.");
83
+ }
84
+ destroy() {
85
+ this.stop();
86
+ document.removeEventListener("visibilitychange", this.onVisibilityChange);
87
+ window.removeEventListener("resize", this.onResize);
88
+ }
89
+ onResize() {
90
+ const { width, height } = this.#canvas.getBoundingClientRect();
91
+ this.#height = height;
92
+ this.#width = width;
93
+ }
94
+ onVisibilityChange() {
95
+ cancelAnimationFrame(this.#frame);
96
+ if (document.visibilityState === "visible") {
97
+ this.#then = 0;
98
+ this.start();
99
+ } else {
100
+ this.#then = 0;
101
+ this.stop();
102
+ }
103
+ }
104
+ };
105
+ //#endregion
106
+ //#region src/confetti/consts.ts
107
+ const DEFAULT_CONFIG = {
108
+ angle: 90,
109
+ colors: [
110
+ "#26ccff",
111
+ "#a25afd",
112
+ "#ff5e7e",
113
+ "#88ff5a",
114
+ "#fcff42",
115
+ "#ffa62d",
116
+ "#ff36ff"
117
+ ],
118
+ decay: .9,
119
+ gravity: 1,
120
+ particles: 50,
121
+ shapes: [
122
+ "circle",
123
+ "diamond",
124
+ "ribbon",
125
+ "square",
126
+ "star",
127
+ "triangle"
128
+ ],
129
+ spread: 45,
130
+ ticks: 200,
131
+ startVelocity: 45,
132
+ x: .5,
133
+ y: .5
134
+ };
135
+ const MULBERRY$2 = mulberry32(13);
136
+ //#endregion
137
+ //#region src/confetti/simulation.ts
138
+ const TWO_PI = Math.PI * 2;
139
+ const SHAPE_PATHS = {
140
+ circle: (() => {
141
+ const path = new Path2D();
142
+ path.ellipse(0, 0, .6, 1, 0, 0, TWO_PI);
143
+ return path;
144
+ })(),
145
+ diamond: (() => {
146
+ const path = new Path2D();
147
+ path.moveTo(0, -1);
148
+ path.lineTo(.6, 0);
149
+ path.lineTo(0, 1);
150
+ path.lineTo(-.6, 0);
151
+ path.closePath();
152
+ return path;
153
+ })(),
154
+ ribbon: (() => {
155
+ const path = new Path2D();
156
+ path.rect(-.2, -1, .4, 2);
157
+ return path;
158
+ })(),
159
+ square: (() => {
160
+ const path = new Path2D();
161
+ path.rect(-.7, -.7, 1.4, 1.4);
162
+ return path;
163
+ })(),
164
+ star: (() => {
165
+ const path = new Path2D();
166
+ for (let i = 0; i < 10; i++) {
167
+ const r = i % 2 === 0 ? 1 : .42;
168
+ const angle = i * Math.PI / 5 - Math.PI / 2;
169
+ if (i === 0) path.moveTo(r * Math.cos(angle), r * Math.sin(angle));
170
+ else path.lineTo(r * Math.cos(angle), r * Math.sin(angle));
171
+ }
172
+ path.closePath();
173
+ return path;
174
+ })(),
175
+ triangle: (() => {
176
+ const path = new Path2D();
177
+ for (let i = 0; i < 3; i++) {
178
+ const angle = i * 2 * Math.PI / 3 - Math.PI / 2;
179
+ if (i === 0) path.moveTo(Math.cos(angle), Math.sin(angle));
180
+ else path.lineTo(Math.cos(angle), Math.sin(angle));
181
+ }
182
+ path.closePath();
183
+ return path;
184
+ })()
185
+ };
186
+ var ConfettiSimulation = class extends LimitedFrameRateCanvas {
187
+ #scale;
188
+ #particles = [];
189
+ constructor(canvas, config = {}) {
190
+ super(canvas, 60, config.canvasOptions ?? { colorSpace: "display-p3" });
191
+ this.#scale = config.scale ?? 1;
192
+ this.canvas.style.position = "absolute";
193
+ this.canvas.style.top = "0";
194
+ this.canvas.style.left = "0";
195
+ this.canvas.style.height = "100%";
196
+ this.canvas.style.width = "100%";
197
+ }
198
+ fire(config) {
199
+ this.onResize();
200
+ this.draw();
201
+ const resolved = {
202
+ ...DEFAULT_CONFIG,
203
+ ...config
204
+ };
205
+ const { angle, colors, decay, gravity, shapes, spread, startVelocity, ticks, x, y } = resolved;
206
+ const numberOfParticles = Math.max(1, resolved.particles);
207
+ for (let i = 0; i < numberOfParticles; i++) {
208
+ const particle = this.#createParticle({
209
+ angle,
210
+ color: hexToRGB(colors[Math.floor(MULBERRY$2.next() * colors.length)]),
211
+ decay,
212
+ gravity: gravity * this.#scale,
213
+ shape: shapes[Math.floor(MULBERRY$2.next() * shapes.length)],
214
+ spread,
215
+ startVelocity: startVelocity * this.#scale,
216
+ ticks,
217
+ x: this.width * x,
218
+ y: this.height * y
219
+ });
220
+ this.#tickParticle(particle);
221
+ this.#particles.push(particle);
222
+ }
223
+ if (!this.isTicking) this.start();
224
+ }
225
+ draw() {
226
+ const { context, width, height } = this;
227
+ context.clearRect(0, 0, width, height);
228
+ const particles = this.#particles;
229
+ for (let i = 0; i < particles.length; i++) {
230
+ const p = particles[i];
231
+ const flipCos = Math.cos(p.flipAngle);
232
+ const size = p.size;
233
+ context.setTransform(p.rotCos * flipCos * size, p.rotSin * flipCos * size, -p.rotSin * size, p.rotCos * size, p.x, p.y);
234
+ context.globalAlpha = 1 - p.tick / p.totalTicks;
235
+ context.fillStyle = p.colorStr;
236
+ context.fill(SHAPE_PATHS[p.shape]);
237
+ }
238
+ context.resetTransform();
239
+ }
240
+ tick() {
241
+ const particles = this.#particles;
242
+ let alive = 0;
243
+ const dt = this.delta > 0 && this.delta < 200 ? this.delta / (1e3 / 60) : 1;
244
+ for (let i = 0; i < particles.length; i++) {
245
+ const p = particles[i];
246
+ if (p.tick < p.totalTicks) {
247
+ this.#tickParticle(p, dt);
248
+ particles[alive++] = p;
249
+ }
250
+ }
251
+ particles.length = alive;
252
+ if (alive === 0) this.stop();
253
+ }
254
+ onResize() {
255
+ super.onResize();
256
+ this.canvas.width = this.width;
257
+ this.canvas.height = this.height;
258
+ }
259
+ #createParticle(config) {
260
+ const launchAngle = -(config.angle * Math.PI / 180) + .5 * config.spread * Math.PI / 180 - MULBERRY$2.next() * config.spread * Math.PI / 180;
261
+ const speed = config.startVelocity * (.5 + MULBERRY$2.next());
262
+ const rotAngle = MULBERRY$2.next() * TWO_PI;
263
+ return {
264
+ colorStr: `rgb(${config.color[0]}, ${config.color[1]}, ${config.color[2]})`,
265
+ decay: config.decay - .05 + MULBERRY$2.next() * .1,
266
+ flipAngle: MULBERRY$2.next() * TWO_PI,
267
+ flipSpeed: .03 + MULBERRY$2.next() * .05,
268
+ gravity: config.gravity,
269
+ rotAngle,
270
+ rotCos: Math.cos(rotAngle),
271
+ rotSin: Math.sin(rotAngle),
272
+ rotSpeed: (MULBERRY$2.next() - .5) * .06,
273
+ shape: config.shape,
274
+ size: (5 + MULBERRY$2.next() * 5) * this.#scale,
275
+ swing: MULBERRY$2.next() * TWO_PI,
276
+ swingAmp: .5 + MULBERRY$2.next() * 1.5,
277
+ swingSpeed: .025 + MULBERRY$2.next() * .035,
278
+ tick: 0,
279
+ totalTicks: config.ticks,
280
+ vx: Math.cos(launchAngle) * speed,
281
+ vy: Math.sin(launchAngle) * speed,
282
+ x: config.x,
283
+ y: config.y
284
+ };
285
+ }
286
+ #tickParticle(particle, dt = 1) {
287
+ const decayFactor = Math.pow(particle.decay, dt);
288
+ particle.vx *= decayFactor;
289
+ particle.vy *= decayFactor;
290
+ particle.vy += particle.gravity * .35 * dt;
291
+ particle.swing += particle.swingSpeed * dt;
292
+ particle.x += (particle.vx + particle.swingAmp * Math.cos(particle.swing)) * dt;
293
+ particle.y += particle.vy * dt;
294
+ particle.rotAngle += particle.rotSpeed * dt;
295
+ particle.rotCos = Math.cos(particle.rotAngle);
296
+ particle.rotSin = Math.sin(particle.rotAngle);
297
+ particle.flipAngle += particle.flipSpeed * dt;
298
+ particle.tick += dt;
299
+ }
300
+ };
301
+ //#endregion
302
+ //#region src/fireworks/consts.ts
303
+ const MULBERRY$1 = mulberry32(13);
304
+ //#endregion
305
+ //#region src/fireworks/types.ts
306
+ const FIREWORK_VARIANTS = [
307
+ "peony",
308
+ "chrysanthemum",
309
+ "willow",
310
+ "ring",
311
+ "palm",
312
+ "crackle",
313
+ "crossette",
314
+ "saturn",
315
+ "dahlia",
316
+ "brocade",
317
+ "horsetail",
318
+ "strobe",
319
+ "heart",
320
+ "spiral",
321
+ "flower",
322
+ "concentric"
323
+ ];
324
+ const EXPLOSION_CONFIGS = {
325
+ peony: {
326
+ particleCount: [50, 70],
327
+ speed: [2, 10],
328
+ friction: .96,
329
+ gravity: .8,
330
+ decay: [.012, .025],
331
+ trailMemory: 3,
332
+ hueVariation: 30,
333
+ brightness: [50, 80],
334
+ lineWidthScale: .8,
335
+ shape: "circle",
336
+ sparkle: false,
337
+ strobe: false,
338
+ spread3d: true,
339
+ glowSize: 12
340
+ },
341
+ chrysanthemum: {
342
+ particleCount: [80, 120],
343
+ speed: [3, 12],
344
+ friction: .975,
345
+ gravity: .5,
346
+ decay: [.006, .012],
347
+ trailMemory: 6,
348
+ hueVariation: 20,
349
+ brightness: [55, 85],
350
+ lineWidthScale: .5,
351
+ shape: "line",
352
+ sparkle: true,
353
+ strobe: false,
354
+ spread3d: true,
355
+ glowSize: 15
356
+ },
357
+ willow: {
358
+ particleCount: [50, 70],
359
+ speed: [3, 10],
360
+ friction: .988,
361
+ gravity: 1.5,
362
+ decay: [.004, .008],
363
+ trailMemory: 10,
364
+ hueVariation: 15,
365
+ brightness: [60, 90],
366
+ lineWidthScale: .4,
367
+ shape: "line",
368
+ sparkle: false,
369
+ strobe: false,
370
+ spread3d: false,
371
+ glowSize: 10
372
+ },
373
+ ring: {
374
+ particleCount: [40, 60],
375
+ speed: [6, 8],
376
+ friction: .96,
377
+ gravity: .4,
378
+ decay: [.012, .022],
379
+ trailMemory: 4,
380
+ hueVariation: 10,
381
+ brightness: [55, 80],
382
+ lineWidthScale: .7,
383
+ shape: "diamond",
384
+ sparkle: false,
385
+ strobe: false,
386
+ spread3d: false,
387
+ glowSize: 14
388
+ },
389
+ palm: {
390
+ particleCount: [20, 30],
391
+ speed: [5, 12],
392
+ friction: .97,
393
+ gravity: 1.2,
394
+ decay: [.006, .014],
395
+ trailMemory: 6,
396
+ hueVariation: 20,
397
+ brightness: [55, 85],
398
+ lineWidthScale: .6,
399
+ shape: "line",
400
+ sparkle: false,
401
+ strobe: false,
402
+ spread3d: false,
403
+ glowSize: 12
404
+ },
405
+ crackle: {
406
+ particleCount: [40, 55],
407
+ speed: [2, 8],
408
+ friction: .955,
409
+ gravity: .8,
410
+ decay: [.012, .025],
411
+ trailMemory: 2,
412
+ hueVariation: 25,
413
+ brightness: [60, 90],
414
+ lineWidthScale: .6,
415
+ shape: "star",
416
+ sparkle: false,
417
+ strobe: false,
418
+ spread3d: true,
419
+ glowSize: 8
420
+ },
421
+ crossette: {
422
+ particleCount: [16, 20],
423
+ speed: [5, 9],
424
+ friction: .965,
425
+ gravity: .6,
426
+ decay: [.006, .014],
427
+ trailMemory: 4,
428
+ hueVariation: 15,
429
+ brightness: [55, 85],
430
+ lineWidthScale: .7,
431
+ shape: "circle",
432
+ sparkle: false,
433
+ strobe: false,
434
+ spread3d: true,
435
+ glowSize: 12
436
+ },
437
+ dahlia: {
438
+ particleCount: [48, 80],
439
+ speed: [3, 9],
440
+ friction: .965,
441
+ gravity: .7,
442
+ decay: [.01, .02],
443
+ trailMemory: 4,
444
+ hueVariation: 5,
445
+ brightness: [55, 85],
446
+ lineWidthScale: .7,
447
+ shape: "circle",
448
+ sparkle: false,
449
+ strobe: false,
450
+ spread3d: true,
451
+ glowSize: 12
452
+ },
453
+ brocade: {
454
+ particleCount: [60, 80],
455
+ speed: [3, 9],
456
+ friction: .98,
457
+ gravity: 1.3,
458
+ decay: [.004, .01],
459
+ trailMemory: 10,
460
+ hueVariation: 10,
461
+ brightness: [60, 90],
462
+ lineWidthScale: .4,
463
+ shape: "line",
464
+ sparkle: true,
465
+ strobe: false,
466
+ spread3d: false,
467
+ glowSize: 10
468
+ },
469
+ horsetail: {
470
+ particleCount: [30, 40],
471
+ speed: [8, 14],
472
+ friction: .975,
473
+ gravity: 2,
474
+ decay: [.004, .01],
475
+ trailMemory: 12,
476
+ hueVariation: 15,
477
+ brightness: [60, 90],
478
+ lineWidthScale: .5,
479
+ shape: "line",
480
+ sparkle: false,
481
+ strobe: false,
482
+ spread3d: false,
483
+ glowSize: 10
484
+ },
485
+ strobe: {
486
+ particleCount: [40, 55],
487
+ speed: [2, 8],
488
+ friction: .96,
489
+ gravity: .7,
490
+ decay: [.01, .02],
491
+ trailMemory: 2,
492
+ hueVariation: 10,
493
+ brightness: [75, 95],
494
+ lineWidthScale: .6,
495
+ shape: "circle",
496
+ sparkle: false,
497
+ strobe: true,
498
+ spread3d: true,
499
+ glowSize: 10
500
+ },
501
+ heart: {
502
+ particleCount: [60, 80],
503
+ speed: [3, 5],
504
+ friction: .965,
505
+ gravity: .3,
506
+ decay: [.008, .016],
507
+ trailMemory: 4,
508
+ hueVariation: 15,
509
+ brightness: [55, 85],
510
+ lineWidthScale: .7,
511
+ shape: "circle",
512
+ sparkle: false,
513
+ strobe: false,
514
+ spread3d: false,
515
+ glowSize: 12
516
+ },
517
+ spiral: {
518
+ particleCount: [45, 60],
519
+ speed: [2, 10],
520
+ friction: .97,
521
+ gravity: .4,
522
+ decay: [.008, .016],
523
+ trailMemory: 5,
524
+ hueVariation: 10,
525
+ brightness: [55, 85],
526
+ lineWidthScale: .6,
527
+ shape: "circle",
528
+ sparkle: false,
529
+ strobe: false,
530
+ spread3d: false,
531
+ glowSize: 12
532
+ },
533
+ flower: {
534
+ particleCount: [70, 90],
535
+ speed: [3, 7],
536
+ friction: .965,
537
+ gravity: .3,
538
+ decay: [.008, .016],
539
+ trailMemory: 4,
540
+ hueVariation: 20,
541
+ brightness: [55, 85],
542
+ lineWidthScale: .7,
543
+ shape: "circle",
544
+ sparkle: false,
545
+ strobe: false,
546
+ spread3d: false,
547
+ glowSize: 12
548
+ }
549
+ };
550
+ //#endregion
551
+ //#region src/fireworks/explosion.ts
552
+ const PERSPECTIVE = 800;
553
+ var Explosion = class {
554
+ #position;
555
+ #angle;
556
+ #brightness;
557
+ #config;
558
+ #decay;
559
+ #hue;
560
+ #lineWidth;
561
+ #shape;
562
+ #trail = [];
563
+ #type;
564
+ #alpha = 1;
565
+ #depthScale = 1;
566
+ #hasCrackled = false;
567
+ #hasSplit = false;
568
+ #speed;
569
+ #sparkleTimer = 0;
570
+ #vz;
571
+ #z = 0;
572
+ get angle() {
573
+ return this.#angle;
574
+ }
575
+ get hue() {
576
+ return this.#hue;
577
+ }
578
+ get isDead() {
579
+ return this.#alpha <= 0;
580
+ }
581
+ get position() {
582
+ return this.#position;
583
+ }
584
+ get type() {
585
+ return this.#type;
586
+ }
587
+ constructor(position, hue, lineWidth, type, scale = 1, angle, speed, vz) {
588
+ const config = EXPLOSION_CONFIGS[type];
589
+ this.#config = config;
590
+ this.#type = type;
591
+ this.#shape = config.shape;
592
+ this.#position = { ...position };
593
+ this.#alpha = 1;
594
+ this.#angle = angle ?? MULBERRY$1.nextBetween(0, Math.PI * 2);
595
+ this.#brightness = MULBERRY$1.nextBetween(config.brightness[0], config.brightness[1]);
596
+ this.#decay = MULBERRY$1.nextBetween(config.decay[0], config.decay[1]);
597
+ this.#hue = hue + MULBERRY$1.nextBetween(-config.hueVariation, config.hueVariation);
598
+ this.#lineWidth = lineWidth * config.lineWidthScale;
599
+ this.#speed = (speed ?? MULBERRY$1.nextBetween(config.speed[0], config.speed[1])) * scale;
600
+ if (vz !== void 0) this.#vz = vz * scale;
601
+ else if (config.spread3d) this.#vz = MULBERRY$1.nextBetween(-this.#speed * .5, this.#speed * .5);
602
+ else this.#vz = 0;
603
+ for (let i = 0; i < config.trailMemory; i++) this.#trail.push({ ...position });
604
+ }
605
+ checkCrackle() {
606
+ if (this.#type !== "crackle" || this.#hasCrackled) return false;
607
+ if (this.#alpha <= this.#decay * 3) {
608
+ this.#hasCrackled = true;
609
+ return true;
610
+ }
611
+ return false;
612
+ }
613
+ checkSplit() {
614
+ if (this.#type !== "crossette" || this.#hasSplit) return false;
615
+ if (this.#alpha < .5) {
616
+ this.#hasSplit = true;
617
+ return true;
618
+ }
619
+ return false;
620
+ }
621
+ draw(ctx) {
622
+ if (this.#config.strobe && this.#sparkleTimer % 6 < 3) return;
623
+ const ds = this.#depthScale;
624
+ const trailEnd = this.#trail[this.#trail.length - 1];
625
+ const effectiveWidth = this.#shape === "line" ? this.#lineWidth * ds : this.#lineWidth * .4 * ds;
626
+ const effectiveAlpha = this.#alpha * Math.min(ds, 1.2);
627
+ ctx.save();
628
+ ctx.lineCap = "round";
629
+ if (this.#trail.length > 2) for (let i = this.#trail.length - 1; i > 0; i--) {
630
+ const progress = i / this.#trail.length;
631
+ const alpha = (1 - progress) * effectiveAlpha * .5;
632
+ const width = effectiveWidth * (1 - progress * .4);
633
+ ctx.beginPath();
634
+ ctx.moveTo(this.#trail[i].x, this.#trail[i].y);
635
+ ctx.lineTo(this.#trail[i - 1].x, this.#trail[i - 1].y);
636
+ ctx.lineWidth = width;
637
+ ctx.strokeStyle = `hsla(${this.#hue}, 100%, ${this.#brightness * .7}%, ${alpha})`;
638
+ ctx.stroke();
639
+ }
640
+ ctx.beginPath();
641
+ ctx.moveTo(trailEnd.x, trailEnd.y);
642
+ ctx.lineTo(this.#position.x, this.#position.y);
643
+ ctx.lineWidth = effectiveWidth;
644
+ ctx.strokeStyle = `hsla(${this.#hue}, 100%, ${this.#brightness}%, ${effectiveAlpha})`;
645
+ ctx.stroke();
646
+ if (this.#shape !== "line") this.#drawShape(ctx, ds, effectiveAlpha);
647
+ if (this.#config.sparkle && this.#sparkleTimer % 4 < 2) {
648
+ ctx.beginPath();
649
+ ctx.arc(this.#position.x, this.#position.y, this.#lineWidth * .8 * ds, 0, Math.PI * 2);
650
+ ctx.fillStyle = `hsla(${this.#hue}, 30%, 95%, ${effectiveAlpha * .9})`;
651
+ ctx.fill();
652
+ }
653
+ ctx.restore();
654
+ }
655
+ tick() {
656
+ this.#trail.pop();
657
+ this.#trail.unshift({ ...this.#position });
658
+ this.#speed *= this.#config.friction;
659
+ this.#vz *= this.#config.friction;
660
+ this.#position.x += Math.cos(this.#angle) * this.#speed;
661
+ this.#position.y += Math.sin(this.#angle) * this.#speed + this.#config.gravity;
662
+ this.#z += this.#vz;
663
+ this.#depthScale = PERSPECTIVE / (PERSPECTIVE + this.#z);
664
+ this.#alpha -= this.#decay;
665
+ this.#sparkleTimer++;
666
+ }
667
+ #drawShape(ctx, ds, alpha) {
668
+ const size = this.#lineWidth * 1.2 * ds;
669
+ const color = `hsla(${this.#hue}, 100%, ${this.#brightness}%, ${alpha})`;
670
+ switch (this.#shape) {
671
+ case "circle":
672
+ ctx.beginPath();
673
+ ctx.arc(this.#position.x, this.#position.y, size * .5, 0, Math.PI * 2);
674
+ ctx.fillStyle = color;
675
+ ctx.fill();
676
+ break;
677
+ case "star":
678
+ this.#drawStarPath(ctx, this.#position.x, this.#position.y, size * .7, 4, this.#sparkleTimer * .15);
679
+ ctx.fillStyle = `hsla(${this.#hue}, 60%, ${Math.min(this.#brightness + 10, 100)}%, ${alpha})`;
680
+ ctx.fill();
681
+ break;
682
+ case "diamond":
683
+ this.#drawDiamondPath(ctx, this.#position.x, this.#position.y, size * .6, this.#angle);
684
+ ctx.fillStyle = color;
685
+ ctx.fill();
686
+ break;
687
+ }
688
+ }
689
+ #drawStarPath(ctx, cx, cy, radius, points, rotation) {
690
+ const innerRadius = radius * .4;
691
+ const totalPoints = points * 2;
692
+ ctx.beginPath();
693
+ for (let i = 0; i < totalPoints; i++) {
694
+ const r = i % 2 === 0 ? radius : innerRadius;
695
+ const angle = i * Math.PI / points + rotation;
696
+ const px = cx + Math.cos(angle) * r;
697
+ const py = cy + Math.sin(angle) * r;
698
+ if (i === 0) ctx.moveTo(px, py);
699
+ else ctx.lineTo(px, py);
700
+ }
701
+ ctx.closePath();
702
+ }
703
+ #drawDiamondPath(ctx, cx, cy, size, rotation) {
704
+ const cos = Math.cos(rotation);
705
+ const sin = Math.sin(rotation);
706
+ const hw = size * .5;
707
+ ctx.beginPath();
708
+ ctx.moveTo(cx + cos * size - sin * 0, cy + sin * size + cos * 0);
709
+ ctx.lineTo(cx + cos * 0 - sin * hw, cy + sin * 0 + cos * hw);
710
+ ctx.lineTo(cx + cos * -size - sin * 0, cy + sin * -size + cos * 0);
711
+ ctx.lineTo(cx + cos * 0 - sin * -hw, cy + sin * 0 + cos * -hw);
712
+ ctx.closePath();
713
+ }
714
+ };
715
+ //#endregion
716
+ //#region src/distance.ts
717
+ function distance(a, b) {
718
+ let x = a.x - b.x;
719
+ let y = a.y - b.y;
720
+ return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
721
+ }
722
+ //#endregion
723
+ //#region src/fireworks/spark.ts
724
+ var Spark = class {
725
+ #position;
726
+ #velocity;
727
+ #hue;
728
+ #size;
729
+ #decay;
730
+ #friction = .94;
731
+ #gravity = .6;
732
+ #alpha = 1;
733
+ get isDead() {
734
+ return this.#alpha <= 0;
735
+ }
736
+ get position() {
737
+ return this.#position;
738
+ }
739
+ constructor(position, hue, velocityX = 0, velocityY = 0) {
740
+ this.#position = { ...position };
741
+ this.#hue = hue + MULBERRY$1.nextBetween(-20, 20);
742
+ this.#size = MULBERRY$1.nextBetween(.5, 1.5);
743
+ this.#decay = MULBERRY$1.nextBetween(.03, .08);
744
+ this.#velocity = {
745
+ x: velocityX + MULBERRY$1.nextBetween(-1.5, 1.5),
746
+ y: velocityY + MULBERRY$1.nextBetween(-2, .5)
747
+ };
748
+ }
749
+ draw(ctx) {
750
+ ctx.beginPath();
751
+ ctx.arc(this.#position.x, this.#position.y, this.#size, 0, Math.PI * 2);
752
+ ctx.fillStyle = `hsla(${this.#hue}, 80%, 70%, ${this.#alpha})`;
753
+ ctx.fill();
754
+ }
755
+ tick() {
756
+ this.#velocity.x *= this.#friction;
757
+ this.#velocity.y *= this.#friction;
758
+ this.#velocity.y += this.#gravity;
759
+ this.#position.x += this.#velocity.x;
760
+ this.#position.y += this.#velocity.y;
761
+ this.#alpha -= this.#decay;
762
+ }
763
+ };
764
+ //#endregion
765
+ //#region src/fireworks/firework.ts
766
+ var Firework = class extends EventTarget {
767
+ #position;
768
+ #startPosition;
769
+ #acceleration = 1.05;
770
+ #angle;
771
+ #baseSize;
772
+ #brightness = MULBERRY$1.nextBetween(55, 75);
773
+ #distance;
774
+ #hue;
775
+ #tailWidth;
776
+ #trail = [];
777
+ #distanceTraveled = 0;
778
+ #speed = 1;
779
+ #sparkTimer = 0;
780
+ #pendingSparks = [];
781
+ get position() {
782
+ return this.#position;
783
+ }
784
+ get hue() {
785
+ return this.#hue;
786
+ }
787
+ constructor(start, target, hue, tailWidth, baseSize) {
788
+ super();
789
+ this.#hue = hue;
790
+ this.#tailWidth = tailWidth;
791
+ this.#baseSize = baseSize;
792
+ this.#position = { ...start };
793
+ this.#startPosition = { ...start };
794
+ this.#angle = Math.atan2(target.y - start.y, target.x - start.x);
795
+ this.#distance = distance(start, target);
796
+ for (let i = 0; i < 6; i++) this.#trail.push({ ...start });
797
+ }
798
+ collectSparks() {
799
+ const sparks = this.#pendingSparks;
800
+ this.#pendingSparks = [];
801
+ return sparks;
802
+ }
803
+ draw(ctx) {
804
+ ctx.save();
805
+ ctx.lineCap = "round";
806
+ for (let i = this.#trail.length - 1; i > 0; i--) {
807
+ const progress = i / this.#trail.length;
808
+ const alpha = (1 - progress) * .8;
809
+ const width = this.#tailWidth * (1 - progress * .5);
810
+ const hue = this.#hue + MULBERRY$1.nextBetween(-15, 15);
811
+ ctx.beginPath();
812
+ ctx.moveTo(this.#trail[i].x, this.#trail[i].y);
813
+ ctx.lineTo(this.#trail[i - 1].x, this.#trail[i - 1].y);
814
+ ctx.lineWidth = width;
815
+ ctx.strokeStyle = `hsla(${hue}, 100%, ${this.#brightness}%, ${alpha})`;
816
+ ctx.stroke();
817
+ }
818
+ ctx.shadowBlur = this.#baseSize * 4;
819
+ ctx.shadowColor = `hsl(${this.#hue}, 100%, 60%)`;
820
+ ctx.beginPath();
821
+ ctx.moveTo(this.#trail[0].x, this.#trail[0].y);
822
+ ctx.lineTo(this.#position.x, this.#position.y);
823
+ ctx.lineWidth = this.#tailWidth;
824
+ ctx.strokeStyle = `hsl(${this.#hue}, 100%, ${this.#brightness}%)`;
825
+ ctx.stroke();
826
+ ctx.shadowBlur = this.#baseSize * 6;
827
+ ctx.shadowColor = `hsl(${this.#hue}, 80%, 80%)`;
828
+ ctx.beginPath();
829
+ ctx.arc(this.#position.x, this.#position.y, this.#tailWidth * .6, 0, Math.PI * 2);
830
+ ctx.fillStyle = `hsl(${this.#hue}, 20%, 92%)`;
831
+ ctx.fill();
832
+ ctx.restore();
833
+ }
834
+ tick() {
835
+ this.#trail.pop();
836
+ this.#trail.unshift({ ...this.#position });
837
+ this.#speed *= this.#acceleration;
838
+ const vx = Math.cos(this.#angle) * this.#speed;
839
+ const vy = Math.sin(this.#angle) * this.#speed;
840
+ this.#distanceTraveled = distance(this.#startPosition, {
841
+ x: this.#position.x + vx,
842
+ y: this.#position.y + vy
843
+ });
844
+ if (this.#distanceTraveled >= this.#distance) {
845
+ this.dispatchEvent(new CustomEvent("remove"));
846
+ return;
847
+ }
848
+ this.#position.x += vx;
849
+ this.#position.y += vy;
850
+ this.#sparkTimer++;
851
+ if (this.#sparkTimer % 3 === 0) this.#pendingSparks.push(new Spark(this.#position, this.#hue, -vx * .1, -vy * .1));
852
+ }
853
+ };
854
+ //#endregion
855
+ //#region src/fireworks/simulation.ts
856
+ var FireworkSimulation = class extends LimitedFrameRateCanvas {
857
+ #explosions = [];
858
+ #fireworks = [];
859
+ #sparks = [];
860
+ #hue = 120;
861
+ #positionRandom = MULBERRY$1.fork();
862
+ #autoSpawn;
863
+ #baseSize;
864
+ #scale;
865
+ #tailWidth;
866
+ constructor(canvas, config = {}) {
867
+ super(canvas, 60, config.canvasOptions ?? { colorSpace: "display-p3" });
868
+ const scale = config.scale ?? 1;
869
+ this.#autoSpawn = config.autoSpawn ?? true;
870
+ this.#baseSize = 5 * scale;
871
+ this.#scale = scale;
872
+ this.#tailWidth = 2 * scale;
873
+ this.canvas.style.position = "absolute";
874
+ this.canvas.style.top = "0";
875
+ this.canvas.style.left = "0";
876
+ this.canvas.style.height = "100%";
877
+ this.canvas.style.width = "100%";
878
+ }
879
+ draw() {
880
+ if (this.canvas.width !== this.width || this.canvas.height !== this.height) {
881
+ this.canvas.width = this.width;
882
+ this.canvas.height = this.height;
883
+ }
884
+ this.context.clearRect(0, 0, this.width, this.height);
885
+ this.context.globalCompositeOperation = "lighter";
886
+ for (const spark of this.#sparks) spark.draw(this.context);
887
+ for (const explosion of this.#explosions) explosion.draw(this.context);
888
+ for (const firework of this.#fireworks) firework.draw(this.context);
889
+ }
890
+ fireExplosion(variant, position) {
891
+ const pos = position ?? {
892
+ x: this.width / 2,
893
+ y: this.height * .4
894
+ };
895
+ this.#hue = MULBERRY$1.nextBetween(0, 360);
896
+ this.#createExplosion(pos, this.#hue, variant);
897
+ }
898
+ tick() {
899
+ if (this.#autoSpawn && this.#fireworks.length < 6 && this.ticks % (this.isSmall ? 60 : 30) === 0) {
900
+ let count = MULBERRY$1.nextBetween(1, 100) < 10 ? 2 : 1;
901
+ while (count--) {
902
+ this.#hue = MULBERRY$1.nextBetween(0, 360);
903
+ this.#createFirework();
904
+ }
905
+ }
906
+ for (const firework of this.#fireworks) {
907
+ firework.tick();
908
+ this.#sparks.push(...firework.collectSparks());
909
+ }
910
+ for (const explosion of this.#explosions) explosion.tick();
911
+ for (const spark of this.#sparks) spark.tick();
912
+ const newExplosions = [];
913
+ const newSparks = [];
914
+ for (const explosion of this.#explosions) {
915
+ if (explosion.checkSplit()) for (let i = 0; i < 4; i++) {
916
+ const angle = explosion.angle + Math.PI / 2 * i + Math.PI / 4;
917
+ newExplosions.push(new Explosion(explosion.position, explosion.hue, this.#baseSize * .6, "peony", this.#scale, angle, MULBERRY$1.nextBetween(3, 6)));
918
+ }
919
+ if (explosion.checkCrackle()) for (let j = 0; j < 8; j++) newSparks.push(new Spark(explosion.position, explosion.hue + MULBERRY$1.nextBetween(-30, 30)));
920
+ }
921
+ this.#explosions.push(...newExplosions);
922
+ this.#sparks.push(...newSparks);
923
+ this.#explosions = this.#explosions.filter((e) => !e.isDead);
924
+ this.#sparks = this.#sparks.filter((s) => !s.isDead);
925
+ }
926
+ #createExplosion(position, hue, variant) {
927
+ const selected = variant ?? this.#pickVariant();
928
+ if (selected === "saturn") {
929
+ this.#createSaturnExplosion(position, hue);
930
+ return;
931
+ }
932
+ if (selected === "dahlia") {
933
+ this.#createDahliaExplosion(position, hue);
934
+ return;
935
+ }
936
+ if (selected === "heart") {
937
+ this.#createHeartExplosion(position, hue);
938
+ return;
939
+ }
940
+ if (selected === "spiral") {
941
+ this.#createSpiralExplosion(position, hue);
942
+ return;
943
+ }
944
+ if (selected === "flower") {
945
+ this.#createFlowerExplosion(position, hue);
946
+ return;
947
+ }
948
+ if (selected === "concentric") {
949
+ this.#createConcentricExplosion(position, hue);
950
+ return;
951
+ }
952
+ const type = selected;
953
+ const config = EXPLOSION_CONFIGS[type];
954
+ const particleCount = Math.floor(MULBERRY$1.nextBetween(config.particleCount[0], config.particleCount[1]));
955
+ const effectiveHue = type === "brocade" ? MULBERRY$1.nextBetween(35, 50) : hue;
956
+ for (let i = 0; i < particleCount; i++) {
957
+ let angle;
958
+ let speed;
959
+ if (type === "ring") {
960
+ angle = i / particleCount * Math.PI * 2;
961
+ speed = MULBERRY$1.nextBetween(config.speed[0], config.speed[1]) * .5 + config.speed[0] * .5;
962
+ } else if (type === "palm" || type === "horsetail") {
963
+ const spread = type === "horsetail" ? Math.PI / 8 : Math.PI / 5;
964
+ angle = -Math.PI / 2 + MULBERRY$1.nextBetween(-spread, spread);
965
+ }
966
+ this.#explosions.push(new Explosion(position, effectiveHue, this.#baseSize, type, this.#scale, angle, speed));
967
+ }
968
+ }
969
+ #createSaturnExplosion(position, hue) {
970
+ const velocity = MULBERRY$1.nextBetween(4, 6);
971
+ const shellCount = Math.floor(MULBERRY$1.nextBetween(25, 35));
972
+ for (let i = 0; i < shellCount; i++) {
973
+ const rad = i / shellCount * Math.PI * 2;
974
+ this.#explosions.push(new Explosion(position, hue, this.#baseSize, "peony", this.#scale, rad + MULBERRY$1.nextBetween(-.05, .05), velocity + MULBERRY$1.nextBetween(-.25, .25)));
975
+ }
976
+ const fillCount = Math.floor(MULBERRY$1.nextBetween(40, 60));
977
+ for (let i = 0; i < fillCount; i++) {
978
+ const rad = MULBERRY$1.nextBetween(0, Math.PI * 2);
979
+ const speed = velocity * MULBERRY$1.nextBetween(0, 1);
980
+ this.#explosions.push(new Explosion(position, hue, this.#baseSize, "peony", this.#scale, rad, speed));
981
+ }
982
+ const ringRotation = MULBERRY$1.nextBetween(0, Math.PI * 2);
983
+ const ringCount = Math.floor(MULBERRY$1.nextBetween(40, 55));
984
+ const ringVx = velocity * MULBERRY$1.nextBetween(2, 3);
985
+ const ringVy = velocity * .6;
986
+ for (let i = 0; i < ringCount; i++) {
987
+ const rad = i / ringCount * Math.PI * 2;
988
+ const cx = Math.cos(rad) * ringVx + MULBERRY$1.nextBetween(-.25, .25);
989
+ const cy = Math.sin(rad) * ringVy + MULBERRY$1.nextBetween(-.25, .25);
990
+ const cosR = Math.cos(ringRotation);
991
+ const sinR = Math.sin(ringRotation);
992
+ const vx = cx * cosR - cy * sinR;
993
+ const vy = cx * sinR + cy * cosR;
994
+ const screenAngle = Math.atan2(vy, vx);
995
+ const screenSpeed = Math.sqrt(vx * vx + vy * vy);
996
+ const vz = Math.sin(rad) * velocity * .8;
997
+ this.#explosions.push(new Explosion(position, hue + 60, this.#baseSize, "ring", this.#scale, screenAngle, screenSpeed, vz));
998
+ }
999
+ }
1000
+ #createDahliaExplosion(position, hue) {
1001
+ const petalCount = Math.floor(MULBERRY$1.nextBetween(6, 9));
1002
+ const particlesPerPetal = Math.floor(MULBERRY$1.nextBetween(8, 12));
1003
+ for (let petal = 0; petal < petalCount; petal++) {
1004
+ const baseAngle = petal / petalCount * Math.PI * 2;
1005
+ const petalHue = hue + (petal % 2 === 0 ? 25 : -25);
1006
+ for (let i = 0; i < particlesPerPetal; i++) {
1007
+ const angle = baseAngle + MULBERRY$1.nextBetween(-.3, .3);
1008
+ this.#explosions.push(new Explosion(position, petalHue, this.#baseSize, "dahlia", this.#scale, angle));
1009
+ }
1010
+ }
1011
+ }
1012
+ #createHeartExplosion(position, hue) {
1013
+ const velocity = MULBERRY$1.nextBetween(3, 5);
1014
+ const count = Math.floor(MULBERRY$1.nextBetween(60, 80));
1015
+ const rotation = MULBERRY$1.nextBetween(-.3, .3);
1016
+ for (let i = 0; i < count; i++) {
1017
+ const t = i / count * Math.PI * 2;
1018
+ const hx = 16 * Math.pow(Math.sin(t), 3);
1019
+ const hy = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
1020
+ const scale = velocity / 16;
1021
+ const vx = hx * scale;
1022
+ const vy = hy * scale;
1023
+ const cosR = Math.cos(rotation);
1024
+ const sinR = Math.sin(rotation);
1025
+ const rvx = vx * cosR - vy * sinR;
1026
+ const rvy = vx * sinR + vy * cosR;
1027
+ const angle = Math.atan2(rvy, rvx);
1028
+ const speed = Math.sqrt(rvx * rvx + rvy * rvy);
1029
+ this.#explosions.push(new Explosion(position, hue, this.#baseSize, "heart", this.#scale, angle, Math.max(.1, speed + MULBERRY$1.nextBetween(-.15, .15))));
1030
+ }
1031
+ }
1032
+ #createSpiralExplosion(position, hue) {
1033
+ const arms = Math.floor(MULBERRY$1.nextBetween(3, 5));
1034
+ const particlesPerArm = Math.floor(MULBERRY$1.nextBetween(15, 20));
1035
+ const twist = MULBERRY$1.nextBetween(2, 3.5);
1036
+ const baseRotation = MULBERRY$1.nextBetween(0, Math.PI * 2);
1037
+ for (let arm = 0; arm < arms; arm++) {
1038
+ const baseAngle = baseRotation + arm / arms * Math.PI * 2;
1039
+ const armHue = hue + arm * (360 / arms / 3);
1040
+ for (let i = 0; i < particlesPerArm; i++) {
1041
+ const progress = i / particlesPerArm;
1042
+ const angle = baseAngle + progress * twist;
1043
+ const speed = 2 + progress * 8;
1044
+ this.#explosions.push(new Explosion(position, armHue, this.#baseSize, "spiral", this.#scale, angle, speed + MULBERRY$1.nextBetween(-.3, .3)));
1045
+ }
1046
+ }
1047
+ }
1048
+ #createFlowerExplosion(position, hue) {
1049
+ const velocity = MULBERRY$1.nextBetween(4, 7);
1050
+ const count = Math.floor(MULBERRY$1.nextBetween(70, 90));
1051
+ const petals = Math.floor(MULBERRY$1.nextBetween(2, 4));
1052
+ const rotation = MULBERRY$1.nextBetween(0, Math.PI * 2);
1053
+ for (let i = 0; i < count; i++) {
1054
+ const t = i / count * Math.PI * 2;
1055
+ const speed = velocity * Math.abs(Math.cos(petals * t));
1056
+ if (speed < .3) continue;
1057
+ this.#explosions.push(new Explosion(position, hue + MULBERRY$1.nextBetween(-15, 15), this.#baseSize, "flower", this.#scale, t + rotation, speed + MULBERRY$1.nextBetween(-.2, .2)));
1058
+ }
1059
+ }
1060
+ #createConcentricExplosion(position, hue) {
1061
+ const outerCount = Math.floor(MULBERRY$1.nextBetween(35, 50));
1062
+ const outerSpeed = MULBERRY$1.nextBetween(7, 10);
1063
+ for (let i = 0; i < outerCount; i++) {
1064
+ const angle = i / outerCount * Math.PI * 2;
1065
+ this.#explosions.push(new Explosion(position, hue, this.#baseSize, "ring", this.#scale, angle + MULBERRY$1.nextBetween(-.05, .05), outerSpeed + MULBERRY$1.nextBetween(-.25, .25)));
1066
+ }
1067
+ const innerCount = Math.floor(MULBERRY$1.nextBetween(25, 35));
1068
+ const innerSpeed = MULBERRY$1.nextBetween(3, 5);
1069
+ for (let i = 0; i < innerCount; i++) {
1070
+ const angle = i / innerCount * Math.PI * 2;
1071
+ this.#explosions.push(new Explosion(position, hue + 120, this.#baseSize, "ring", this.#scale, angle + MULBERRY$1.nextBetween(-.05, .05), innerSpeed + MULBERRY$1.nextBetween(-.25, .25)));
1072
+ }
1073
+ }
1074
+ #createFirework(position) {
1075
+ const hue = this.#hue;
1076
+ const targetX = position?.x || this.#positionRandom.nextBetween(this.width * .1, this.width * .9);
1077
+ const targetY = position?.y || this.height * .1 + this.#positionRandom.nextBetween(0, this.height * .5);
1078
+ const firework = new Firework({
1079
+ x: this.width * .3 + this.#positionRandom.nextBetween(0, this.width * .4),
1080
+ y: this.height
1081
+ }, {
1082
+ x: targetX,
1083
+ y: targetY
1084
+ }, hue, this.#tailWidth, this.#baseSize);
1085
+ firework.addEventListener("remove", () => {
1086
+ this.#fireworks.splice(this.#fireworks.indexOf(firework), 1);
1087
+ this.#createExplosion(firework.position, hue);
1088
+ }, { once: true });
1089
+ this.#fireworks.push(firework);
1090
+ }
1091
+ #pickVariant() {
1092
+ const roll = MULBERRY$1.nextBetween(0, 100);
1093
+ if (roll < 12) return "peony";
1094
+ if (roll < 22) return "chrysanthemum";
1095
+ if (roll < 29) return "willow";
1096
+ if (roll < 34) return "ring";
1097
+ if (roll < 39) return "palm";
1098
+ if (roll < 44) return "crackle";
1099
+ if (roll < 48) return "crossette";
1100
+ if (roll < 55) return "saturn";
1101
+ if (roll < 62) return "dahlia";
1102
+ if (roll < 67) return "brocade";
1103
+ if (roll < 71) return "horsetail";
1104
+ if (roll < 75) return "strobe";
1105
+ if (roll < 82) return "heart";
1106
+ if (roll < 89) return "spiral";
1107
+ if (roll < 94) return "flower";
1108
+ return "concentric";
1109
+ }
1110
+ };
1111
+ //#endregion
1112
+ //#region src/snow/consts.ts
1113
+ const MULBERRY = mulberry32(13);
1114
+ //#endregion
1115
+ //#region src/snow/simulation.ts
1116
+ const SPRITE_SIZE = 64;
1117
+ const SPRITE_CENTER = SPRITE_SIZE / 2;
1118
+ const SPRITE_RADIUS = SPRITE_SIZE / 2;
1119
+ var SnowSimulation = class extends LimitedFrameRateCanvas {
1120
+ #scale;
1121
+ #size;
1122
+ #speed;
1123
+ #baseOpacity;
1124
+ #maxParticles;
1125
+ #time = 0;
1126
+ #ratio = 1;
1127
+ #snowflakes = [];
1128
+ #sprites = [];
1129
+ constructor(canvas, config = {}) {
1130
+ super(canvas, 60, config.canvasOptions ?? { colorSpace: "display-p3" });
1131
+ this.#scale = config.scale ?? 1;
1132
+ this.#maxParticles = config.particles ?? 200;
1133
+ this.#size = (config.size ?? 9) * this.#scale;
1134
+ this.#speed = config.speed ?? 2;
1135
+ const { r, g, b, a } = this.#parseColor(config.fillStyle ?? "rgb(255 255 255 / .75)");
1136
+ this.#baseOpacity = a;
1137
+ this.canvas.style.position = "absolute";
1138
+ this.canvas.style.top = "0";
1139
+ this.canvas.style.left = "0";
1140
+ this.canvas.style.height = "100%";
1141
+ this.canvas.style.width = "100%";
1142
+ if (this.isSmall) this.#maxParticles = Math.floor(this.#maxParticles / 2);
1143
+ this.#sprites = this.#createSprites(r, g, b);
1144
+ for (let i = 0; i < this.#maxParticles; ++i) this.#snowflakes.push(this.#createSnowflake(true));
1145
+ }
1146
+ draw() {
1147
+ this.canvas.height = this.height;
1148
+ this.canvas.width = this.width;
1149
+ const ctx = this.context;
1150
+ ctx.clearRect(0, 0, this.width, this.height);
1151
+ for (const snowflake of this.#snowflakes) {
1152
+ const px = snowflake.x * this.width;
1153
+ const py = snowflake.y * this.height;
1154
+ const displayRadius = snowflake.radius * snowflake.depth * this.#ratio;
1155
+ const displaySize = displayRadius * 2;
1156
+ if (displaySize < .5) continue;
1157
+ ctx.globalAlpha = this.#baseOpacity * (.15 + snowflake.depth * .85);
1158
+ if (snowflake.spriteIndex === 3) {
1159
+ ctx.save();
1160
+ ctx.translate(px, py);
1161
+ ctx.rotate(snowflake.rotation);
1162
+ ctx.drawImage(this.#sprites[snowflake.spriteIndex], -displayRadius, -displayRadius, displaySize, displaySize);
1163
+ ctx.restore();
1164
+ } else ctx.drawImage(this.#sprites[snowflake.spriteIndex], px - displayRadius, py - displayRadius, displaySize, displaySize);
1165
+ }
1166
+ ctx.globalAlpha = 1;
1167
+ }
1168
+ tick() {
1169
+ const speedFactor = this.height / (420 * this.#ratio) / this.#speed * this.deltaFactor;
1170
+ this.#time += .015 * speedFactor;
1171
+ const wind = Math.sin(this.#time * .7) * .5 + Math.sin(this.#time * 1.9 + 3) * .25 + Math.sin(this.#time * 4.3 + 1) * .1;
1172
+ for (let index = 0; index < this.#snowflakes.length; index++) {
1173
+ const snowflake = this.#snowflakes[index];
1174
+ const swing = Math.sin(this.#time * snowflake.swingFrequency + snowflake.swingOffset) * snowflake.swingAmplitude;
1175
+ snowflake.x += (swing + wind * snowflake.depth * 2) / (4e3 * speedFactor);
1176
+ snowflake.y += (snowflake.fallSpeed * 2 + snowflake.depth + snowflake.radius * .15) / (700 * speedFactor);
1177
+ snowflake.rotation += snowflake.rotationSpeed / speedFactor;
1178
+ if (snowflake.x > 1.15 || snowflake.x < -.15 || snowflake.y > 1.05) {
1179
+ const recycled = this.#createSnowflake(false);
1180
+ if (index % 3 > 0) {
1181
+ recycled.x = MULBERRY.next();
1182
+ recycled.y = -.05 - MULBERRY.next() * .15;
1183
+ } else if (wind > .2) {
1184
+ recycled.x = -.15;
1185
+ recycled.y = MULBERRY.next() * .8;
1186
+ } else if (wind < -.2) {
1187
+ recycled.x = 1.15;
1188
+ recycled.y = MULBERRY.next() * .8;
1189
+ } else {
1190
+ recycled.x = MULBERRY.next();
1191
+ recycled.y = -.05 - MULBERRY.next() * .15;
1192
+ }
1193
+ this.#snowflakes[index] = recycled;
1194
+ }
1195
+ }
1196
+ }
1197
+ #parseColor(fillStyle) {
1198
+ const canvas = document.createElement("canvas");
1199
+ canvas.width = 1;
1200
+ canvas.height = 1;
1201
+ const ctx = canvas.getContext("2d");
1202
+ ctx.fillStyle = fillStyle;
1203
+ ctx.fillRect(0, 0, 1, 1);
1204
+ const data = ctx.getImageData(0, 0, 1, 1).data;
1205
+ return {
1206
+ r: data[0],
1207
+ g: data[1],
1208
+ b: data[2],
1209
+ a: data[3] / 255
1210
+ };
1211
+ }
1212
+ #createSprites(r, g, b) {
1213
+ const sprites = [];
1214
+ for (const profile of [
1215
+ [
1216
+ [0, .8],
1217
+ [.3, .4],
1218
+ [.7, .1],
1219
+ [1, 0]
1220
+ ],
1221
+ [
1222
+ [0, 1],
1223
+ [.15, .7],
1224
+ [.5, .2],
1225
+ [1, 0]
1226
+ ],
1227
+ [
1228
+ [0, .9],
1229
+ [.25, .5],
1230
+ [.5, .1],
1231
+ [1, 0]
1232
+ ]
1233
+ ]) {
1234
+ const canvas = document.createElement("canvas");
1235
+ canvas.width = SPRITE_SIZE;
1236
+ canvas.height = SPRITE_SIZE;
1237
+ const ctx = canvas.getContext("2d");
1238
+ const gradient = ctx.createRadialGradient(SPRITE_CENTER, SPRITE_CENTER, 0, SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS);
1239
+ for (const [stop, alpha] of profile) gradient.addColorStop(stop, `rgba(${r}, ${g}, ${b}, ${alpha})`);
1240
+ ctx.fillStyle = gradient;
1241
+ ctx.beginPath();
1242
+ ctx.arc(SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS, 0, Math.PI * 2);
1243
+ ctx.fill();
1244
+ sprites.push(canvas);
1245
+ }
1246
+ sprites.push(this.#createCrystalSprite(r, g, b));
1247
+ return sprites;
1248
+ }
1249
+ #createCrystalSprite(r, g, b) {
1250
+ const canvas = document.createElement("canvas");
1251
+ canvas.width = SPRITE_SIZE;
1252
+ canvas.height = SPRITE_SIZE;
1253
+ const ctx = canvas.getContext("2d");
1254
+ const glow = ctx.createRadialGradient(SPRITE_CENTER, SPRITE_CENTER, 0, SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS);
1255
+ glow.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.6)`);
1256
+ glow.addColorStop(.25, `rgba(${r}, ${g}, ${b}, 0.25)`);
1257
+ glow.addColorStop(.6, `rgba(${r}, ${g}, ${b}, 0.05)`);
1258
+ glow.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
1259
+ ctx.fillStyle = glow;
1260
+ ctx.beginPath();
1261
+ ctx.arc(SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS, 0, Math.PI * 2);
1262
+ ctx.fill();
1263
+ ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 0.7)`;
1264
+ ctx.lineWidth = 1.5;
1265
+ ctx.lineCap = "round";
1266
+ const armLength = SPRITE_RADIUS * .75;
1267
+ for (let arm = 0; arm < 6; arm++) {
1268
+ const angle = arm / 6 * Math.PI * 2 - Math.PI / 2;
1269
+ const tipX = SPRITE_CENTER + Math.cos(angle) * armLength;
1270
+ const tipY = SPRITE_CENTER + Math.sin(angle) * armLength;
1271
+ ctx.beginPath();
1272
+ ctx.moveTo(SPRITE_CENTER, SPRITE_CENTER);
1273
+ ctx.lineTo(tipX, tipY);
1274
+ ctx.stroke();
1275
+ for (const position of [.4, .65]) {
1276
+ const branchX = SPRITE_CENTER + Math.cos(angle) * armLength * position;
1277
+ const branchY = SPRITE_CENTER + Math.sin(angle) * armLength * position;
1278
+ const branchLength = armLength * (.4 - position * .3);
1279
+ for (const side of [-1, 1]) {
1280
+ const branchAngle = angle + side * Math.PI / 3;
1281
+ ctx.beginPath();
1282
+ ctx.moveTo(branchX, branchY);
1283
+ ctx.lineTo(branchX + Math.cos(branchAngle) * branchLength, branchY + Math.sin(branchAngle) * branchLength);
1284
+ ctx.stroke();
1285
+ }
1286
+ }
1287
+ }
1288
+ const centerGlow = ctx.createRadialGradient(SPRITE_CENTER, SPRITE_CENTER, 0, SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS * .12);
1289
+ centerGlow.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.9)`);
1290
+ centerGlow.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
1291
+ ctx.fillStyle = centerGlow;
1292
+ ctx.beginPath();
1293
+ ctx.arc(SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS * .12, 0, Math.PI * 2);
1294
+ ctx.fill();
1295
+ return canvas;
1296
+ }
1297
+ #createSnowflake(initialSpread) {
1298
+ const depth = .3 + MULBERRY.next() * .7;
1299
+ const radius = MULBERRY.next() * this.#size + 2 * this.#scale;
1300
+ let spriteIndex;
1301
+ if (depth > .85 && radius > this.#size * .6 && MULBERRY.next() > .65) spriteIndex = 3;
1302
+ else if (depth < .45) spriteIndex = 2;
1303
+ else spriteIndex = MULBERRY.next() > .5 ? 0 : 1;
1304
+ return {
1305
+ x: MULBERRY.next(),
1306
+ y: initialSpread ? MULBERRY.next() * 2 - 1 : -.05 - MULBERRY.next() * .15,
1307
+ depth,
1308
+ radius,
1309
+ rotation: MULBERRY.next() * Math.PI * 2,
1310
+ rotationSpeed: (MULBERRY.next() - .5) * .03,
1311
+ swingAmplitude: .3 + MULBERRY.next() * .7,
1312
+ swingFrequency: .5 + MULBERRY.next() * 1.5,
1313
+ swingOffset: MULBERRY.next() * Math.PI * 2,
1314
+ fallSpeed: .5 + MULBERRY.next() * .5,
1315
+ spriteIndex
1316
+ };
1317
+ }
1318
+ };
1319
+ //#endregion
1320
+ export { ConfettiSimulation, FIREWORK_VARIANTS, FireworkSimulation, LimitedFrameRateCanvas, SnowSimulation };
1321
+
1322
+ //# sourceMappingURL=index.mjs.map