@konfetti-js/core 0.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.
package/dist/index.js ADDED
@@ -0,0 +1,789 @@
1
+ // src/shapes.ts
2
+ function drawCircle(ctx, size) {
3
+ ctx.beginPath();
4
+ ctx.arc(0, 0, size / 2, 0, Math.PI * 2);
5
+ ctx.fill();
6
+ }
7
+ function drawSquare(ctx, size) {
8
+ const halfSize = size / 2;
9
+ ctx.fillRect(-halfSize, -halfSize, size, size);
10
+ }
11
+ var SHAPES = ["circle", "square"];
12
+ var RENDERERS = [drawCircle, drawSquare];
13
+ function isShape(value) {
14
+ return SHAPES.includes(value);
15
+ }
16
+ function shapeToIndex(shape) {
17
+ return Math.max(0, SHAPES.indexOf(shape));
18
+ }
19
+ function getShapeRenderer(index) {
20
+ return RENDERERS[index] ?? drawCircle;
21
+ }
22
+
23
+ // src/utils.ts
24
+ var DEFAULT_COLORS = [
25
+ "#26ccff",
26
+ // cyan
27
+ "#a25afd",
28
+ // purple
29
+ "#ff5e7e",
30
+ // pink
31
+ "#88ff5a",
32
+ // green
33
+ "#fcff42",
34
+ // yellow
35
+ "#ffa62d",
36
+ // orange
37
+ "#ff36ff"
38
+ // magenta
39
+ ];
40
+ var DEFAULT_OPTIONS = {
41
+ particleCount: 50,
42
+ angle: 90,
43
+ spread: 45,
44
+ startVelocity: 45,
45
+ decay: 0.9,
46
+ gravity: 1,
47
+ drift: 0,
48
+ ticks: 200,
49
+ origin: { x: 0.5, y: 0.5 },
50
+ colors: DEFAULT_COLORS,
51
+ shapes: ["circle", "square"],
52
+ scalar: 1,
53
+ zIndex: 100,
54
+ disableForReducedMotion: true
55
+ };
56
+ function finite(value, fallback) {
57
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
58
+ }
59
+ function resolveOptions(options = {}) {
60
+ const d = DEFAULT_OPTIONS;
61
+ const colors = Array.isArray(options.colors) ? options.colors.filter((c) => typeof c === "string" && c.length > 0) : [];
62
+ const shapes = Array.isArray(options.shapes) ? options.shapes.filter(isShape) : [];
63
+ return {
64
+ particleCount: Math.max(0, Math.round(finite(options.particleCount, d.particleCount))),
65
+ angle: finite(options.angle, d.angle),
66
+ spread: finite(options.spread, d.spread),
67
+ startVelocity: finite(options.startVelocity, d.startVelocity),
68
+ decay: clamp(finite(options.decay, d.decay), 0, 1),
69
+ gravity: finite(options.gravity, d.gravity),
70
+ drift: finite(options.drift, d.drift),
71
+ ticks: Math.max(1, finite(options.ticks, d.ticks)),
72
+ origin: {
73
+ x: finite(options.origin?.x, d.origin.x),
74
+ y: finite(options.origin?.y, d.origin.y)
75
+ },
76
+ colors: colors.length > 0 ? colors : [...d.colors],
77
+ shapes: shapes.length > 0 ? shapes : [...d.shapes],
78
+ scalar: Math.max(0, finite(options.scalar, d.scalar)),
79
+ zIndex: finite(options.zIndex, d.zIndex),
80
+ disableForReducedMotion: options.disableForReducedMotion ?? d.disableForReducedMotion
81
+ };
82
+ }
83
+ function mergeOptions(base, override) {
84
+ const merged = { ...base };
85
+ if (override) {
86
+ for (const [key, value] of Object.entries(override)) {
87
+ if (value !== void 0) merged[key] = value;
88
+ }
89
+ if (override.origin) merged.origin = mergeOrigin(base.origin, override.origin);
90
+ }
91
+ return merged;
92
+ }
93
+ function mergeOrigin(base, override) {
94
+ return {
95
+ ...base,
96
+ ...override?.x !== void 0 && { x: override.x },
97
+ ...override?.y !== void 0 && { y: override.y }
98
+ };
99
+ }
100
+ function degToRad(deg) {
101
+ return deg * (Math.PI / 180);
102
+ }
103
+ function randomRange(min, max) {
104
+ return Math.random() * (max - min) + min;
105
+ }
106
+ function randomItem(arr) {
107
+ return arr[Math.floor(Math.random() * arr.length)];
108
+ }
109
+ function isBrowser() {
110
+ return typeof window !== "undefined" && typeof document !== "undefined";
111
+ }
112
+ function prefersReducedMotion() {
113
+ if (typeof window === "undefined") return false;
114
+ const mediaQuery = window.matchMedia?.("(prefers-reduced-motion: reduce)");
115
+ return mediaQuery?.matches ?? false;
116
+ }
117
+ function clamp(value, min, max) {
118
+ return Math.min(Math.max(value, min), max);
119
+ }
120
+
121
+ // src/particle.ts
122
+ var MAX_PARTICLES = 1e4;
123
+ var MAX_PALETTE = 256;
124
+ var ParticlePool = class {
125
+ constructor(initialCapacity = 512) {
126
+ /** Number of active particles */
127
+ this.activeCount = 0;
128
+ /**
129
+ * Colors in use; particles store an index into this list, so any CSS color
130
+ * works without parsing it
131
+ */
132
+ this.palette = [];
133
+ this.colorIds = /* @__PURE__ */ new Map();
134
+ this.data = new Float32Array(initialCapacity * 16 /* SIZE */);
135
+ }
136
+ /** How many particles fit before the buffer has to grow */
137
+ get capacity() {
138
+ return this.data.length / 16 /* SIZE */;
139
+ }
140
+ /**
141
+ * Remove every particle
142
+ */
143
+ reset() {
144
+ this.activeCount = 0;
145
+ this.clearPalette();
146
+ }
147
+ /**
148
+ * Spawn particles with the given options
149
+ */
150
+ spawn(options, canvasWidth, canvasHeight) {
151
+ const count = Math.min(options.particleCount, MAX_PARTICLES - this.activeCount);
152
+ if (count <= 0) return;
153
+ this.ensureCapacity(this.activeCount + count);
154
+ const data = this.data;
155
+ const originX = options.origin.x * canvasWidth;
156
+ const originY = options.origin.y * canvasHeight;
157
+ const angleRad = degToRad(options.angle);
158
+ const spreadRad = degToRad(options.spread);
159
+ for (let i = 0; i < count; i++) {
160
+ const idx = (this.activeCount + i) * 16 /* SIZE */;
161
+ const particleAngle = angleRad + randomRange(-spreadRad / 2, spreadRad / 2);
162
+ const velocity = options.startVelocity * randomRange(0.5, 1);
163
+ data[idx + 0 /* X */] = originX;
164
+ data[idx + 1 /* Y */] = originY;
165
+ data[idx + 2 /* VelocityX */] = Math.cos(particleAngle) * velocity;
166
+ data[idx + 3 /* VelocityY */] = -Math.sin(particleAngle) * velocity;
167
+ data[idx + 4 /* Rotation */] = randomRange(0, Math.PI * 2);
168
+ data[idx + 5 /* RotationSpeed */] = randomRange(-0.1, 0.1);
169
+ data[idx + 6 /* Tilt */] = randomRange(0, Math.PI * 2);
170
+ data[idx + 7 /* TiltSpeed */] = randomRange(0.05, 0.15);
171
+ data[idx + 8 /* Color */] = this.colorId(randomItem(options.colors));
172
+ data[idx + 9 /* Shape */] = shapeToIndex(randomItem(options.shapes));
173
+ data[idx + 10 /* Scalar */] = options.scalar * randomRange(0.8, 1.2);
174
+ data[idx + 11 /* Life */] = options.ticks;
175
+ data[idx + 12 /* MaxLife */] = options.ticks;
176
+ data[idx + 13 /* Drift */] = options.drift + randomRange(-0.5, 0.5);
177
+ data[idx + 14 /* Gravity */] = options.gravity;
178
+ data[idx + 15 /* Decay */] = options.decay;
179
+ }
180
+ this.activeCount += count;
181
+ }
182
+ /**
183
+ * Remove dead particles, keeping live ones packed at the front
184
+ */
185
+ compact() {
186
+ const data = this.data;
187
+ let writeIdx = 0;
188
+ for (let readIdx = 0; readIdx < this.activeCount; readIdx++) {
189
+ const srcIdx = readIdx * 16 /* SIZE */;
190
+ if (data[srcIdx + 11 /* Life */] <= 0) continue;
191
+ if (writeIdx !== readIdx) {
192
+ const dstIdx = writeIdx * 16 /* SIZE */;
193
+ data.copyWithin(dstIdx, srcIdx, srcIdx + 16 /* SIZE */);
194
+ }
195
+ writeIdx++;
196
+ }
197
+ this.activeCount = writeIdx;
198
+ if (writeIdx === 0) {
199
+ this.clearPalette();
200
+ } else if (this.palette.length > MAX_PALETTE) {
201
+ this.prunePalette();
202
+ }
203
+ }
204
+ /**
205
+ * Keep only colors that live particles still use, renumbering them, so a long
206
+ * stream with random colors does not grow the palette forever
207
+ */
208
+ prunePalette() {
209
+ const data = this.data;
210
+ const oldPalette = [...this.palette];
211
+ const remap = /* @__PURE__ */ new Map();
212
+ this.clearPalette();
213
+ for (let i = 0; i < this.activeCount; i++) {
214
+ const idx = i * 16 /* SIZE */ + 8 /* Color */;
215
+ const oldId = data[idx];
216
+ let newId = remap.get(oldId);
217
+ if (newId === void 0) {
218
+ newId = this.colorId(oldPalette[oldId]);
219
+ remap.set(oldId, newId);
220
+ }
221
+ data[idx] = newId;
222
+ }
223
+ }
224
+ colorId(color) {
225
+ let id = this.colorIds.get(color);
226
+ if (id === void 0) {
227
+ id = this.palette.length;
228
+ this.palette.push(color);
229
+ this.colorIds.set(color, id);
230
+ }
231
+ return id;
232
+ }
233
+ clearPalette() {
234
+ this.palette.length = 0;
235
+ this.colorIds.clear();
236
+ }
237
+ ensureCapacity(needed) {
238
+ if (needed <= this.capacity) return;
239
+ let capacity = this.capacity;
240
+ while (capacity < needed) capacity *= 2;
241
+ const grown = new Float32Array(Math.min(capacity, MAX_PARTICLES) * 16 /* SIZE */);
242
+ grown.set(this.data.subarray(0, this.activeCount * 16 /* SIZE */));
243
+ this.data = grown;
244
+ }
245
+ };
246
+
247
+ // src/physics.ts
248
+ function updateParticles(pool, dt) {
249
+ const data = pool.data;
250
+ for (let i = 0; i < pool.activeCount; i++) {
251
+ const idx = i * 16 /* SIZE */;
252
+ let vx = data[idx + 2 /* VelocityX */];
253
+ let vy = data[idx + 3 /* VelocityY */];
254
+ const tilt = data[idx + 6 /* Tilt */];
255
+ vy += data[idx + 14 /* Gravity */] * dt;
256
+ vx += data[idx + 13 /* Drift */] * Math.sin(tilt) * dt;
257
+ const damping = data[idx + 15 /* Decay */] ** dt;
258
+ vx *= damping;
259
+ vy *= damping;
260
+ data[idx + 2 /* VelocityX */] = vx;
261
+ data[idx + 3 /* VelocityY */] = vy;
262
+ data[idx + 0 /* X */] += vx * dt;
263
+ data[idx + 1 /* Y */] += vy * dt;
264
+ data[idx + 4 /* Rotation */] += data[idx + 5 /* RotationSpeed */] * dt;
265
+ data[idx + 6 /* Tilt */] = tilt + data[idx + 7 /* TiltSpeed */] * dt;
266
+ data[idx + 11 /* Life */] -= dt;
267
+ }
268
+ pool.compact();
269
+ return pool.activeCount > 0;
270
+ }
271
+
272
+ // src/presets.ts
273
+ var FIREWORK_COLORS = ["#ff0000", "#ffff00", "#00ff00", "#00ffff", "#ff00ff"];
274
+ var FIREWORK = {
275
+ particleCount: 80,
276
+ angle: 90,
277
+ spread: 360,
278
+ startVelocity: 35,
279
+ decay: 0.91,
280
+ gravity: 1,
281
+ colors: FIREWORK_COLORS
282
+ };
283
+ var PRIDE_COLORS = [
284
+ "#e40303",
285
+ // red
286
+ "#ff8c00",
287
+ // orange
288
+ "#ffed00",
289
+ // yellow
290
+ "#008026",
291
+ // green
292
+ "#004dff",
293
+ // blue
294
+ "#750787"
295
+ // purple
296
+ ];
297
+ var PRESETS = {
298
+ // Burst of confetti from bottom center shooting up
299
+ cannon: {
300
+ followsOrigin: true,
301
+ bursts: [
302
+ {
303
+ options: {
304
+ particleCount: 100,
305
+ angle: 90,
306
+ spread: 70,
307
+ startVelocity: 50,
308
+ decay: 0.91,
309
+ gravity: 1,
310
+ origin: { x: 0.5, y: 1 }
311
+ }
312
+ }
313
+ ]
314
+ },
315
+ // 360 degree burst from center
316
+ explosion: {
317
+ followsOrigin: true,
318
+ bursts: [
319
+ {
320
+ options: {
321
+ particleCount: 150,
322
+ angle: 90,
323
+ spread: 360,
324
+ startVelocity: 40,
325
+ decay: 0.92,
326
+ gravity: 0.8,
327
+ origin: { x: 0.5, y: 0.5 }
328
+ }
329
+ }
330
+ ]
331
+ },
332
+ // Three bursts across the top half
333
+ fireworks: {
334
+ followsOrigin: false,
335
+ bursts: [
336
+ { delay: 0, options: { ...FIREWORK, origin: { x: 0.3, y: 0.4 } } },
337
+ { delay: 150, options: { ...FIREWORK, origin: { x: 0.5, y: 0.3 } } },
338
+ { delay: 300, options: { ...FIREWORK, origin: { x: 0.7, y: 0.4 } } }
339
+ ]
340
+ },
341
+ // Particles falling from the top
342
+ rain: {
343
+ followsOrigin: false,
344
+ bursts: [
345
+ {
346
+ options: {
347
+ particleCount: 50,
348
+ angle: 270,
349
+ spread: 60,
350
+ startVelocity: 10,
351
+ decay: 0.95,
352
+ gravity: 0.5,
353
+ drift: 0,
354
+ origin: { x: 0.5, y: 0 },
355
+ ticks: 300
356
+ }
357
+ }
358
+ ]
359
+ },
360
+ // Gentle falling white flakes
361
+ snow: {
362
+ followsOrigin: false,
363
+ bursts: [
364
+ {
365
+ options: {
366
+ particleCount: 30,
367
+ angle: 270,
368
+ spread: 120,
369
+ startVelocity: 5,
370
+ decay: 0.98,
371
+ gravity: 0.3,
372
+ drift: 1,
373
+ origin: { x: 0.5, y: -0.1 },
374
+ ticks: 400,
375
+ colors: ["#ffffff", "#e0e0e0", "#c0c0c0"],
376
+ shapes: ["circle"]
377
+ }
378
+ }
379
+ ]
380
+ },
381
+ // Bursts from both sides
382
+ sideCannons: {
383
+ followsOrigin: false,
384
+ bursts: [
385
+ {
386
+ options: {
387
+ particleCount: 50,
388
+ angle: 60,
389
+ spread: 55,
390
+ startVelocity: 50,
391
+ origin: { x: 0, y: 0.7 }
392
+ }
393
+ },
394
+ {
395
+ options: {
396
+ particleCount: 50,
397
+ angle: 120,
398
+ spread: 55,
399
+ startVelocity: 50,
400
+ origin: { x: 1, y: 0.7 }
401
+ }
402
+ }
403
+ ]
404
+ },
405
+ // Rainbow colors
406
+ pride: {
407
+ followsOrigin: true,
408
+ bursts: [
409
+ {
410
+ options: {
411
+ particleCount: 100,
412
+ spread: 70,
413
+ origin: { x: 0.5, y: 0.6 },
414
+ colors: PRIDE_COLORS
415
+ }
416
+ }
417
+ ]
418
+ }
419
+ };
420
+ var CONTINUOUS_DEFAULTS = {
421
+ particleCount: 10,
422
+ angle: 90,
423
+ spread: 55,
424
+ startVelocity: 45,
425
+ origin: { x: 0.5, y: 1 }
426
+ };
427
+ var PRESET_NAMES = Object.keys(PRESETS);
428
+ function isPresetName(value) {
429
+ return typeof value === "string" && PRESET_NAMES.includes(value);
430
+ }
431
+
432
+ // src/renderer.ts
433
+ var BASE_SIZE = 10;
434
+ function createOverlayCanvas() {
435
+ const canvas = document.createElement("canvas");
436
+ canvas.className = "konfetti-canvas";
437
+ canvas.setAttribute("aria-hidden", "true");
438
+ Object.assign(canvas.style, {
439
+ position: "fixed",
440
+ top: "0",
441
+ left: "0",
442
+ width: "100%",
443
+ height: "100%",
444
+ pointerEvents: "none",
445
+ zIndex: "100"
446
+ });
447
+ document.body.appendChild(canvas);
448
+ return canvas;
449
+ }
450
+ var Renderer = class {
451
+ constructor(canvas) {
452
+ /** Size in CSS pixels; the backing store is this times the pixel ratio */
453
+ this.width = 0;
454
+ this.height = 0;
455
+ this.pixelRatio = 1;
456
+ /** Particle indices grouped by palette color, reused between frames */
457
+ this.buckets = [];
458
+ this.ownsCanvas = !canvas;
459
+ this.canvas = canvas ?? createOverlayCanvas();
460
+ this.ctx = this.canvas.getContext("2d");
461
+ this.resize();
462
+ }
463
+ /**
464
+ * Match the canvas to its display size. The full-screen canvas also renders at
465
+ * the screen's pixel density; a canvas you pass in keeps one pixel per CSS
466
+ * pixel, because without a CSS size its display size follows its pixel size
467
+ * and would grow on every resize.
468
+ */
469
+ resize() {
470
+ if (this.ownsCanvas) {
471
+ this.width = window.innerWidth;
472
+ this.height = window.innerHeight;
473
+ this.pixelRatio = window.devicePixelRatio || 1;
474
+ } else {
475
+ const rect = this.canvas.getBoundingClientRect();
476
+ this.width = rect.width;
477
+ this.height = rect.height;
478
+ this.pixelRatio = 1;
479
+ }
480
+ this.canvas.width = Math.round(this.width * this.pixelRatio);
481
+ this.canvas.height = Math.round(this.height * this.pixelRatio);
482
+ }
483
+ /**
484
+ * Canvas size in CSS pixels
485
+ */
486
+ getDimensions() {
487
+ return { width: this.width, height: this.height };
488
+ }
489
+ /**
490
+ * Set the stacking order; only applies to the full-screen canvas we own
491
+ */
492
+ setZIndex(zIndex) {
493
+ if (this.ownsCanvas) this.canvas.style.zIndex = String(zIndex);
494
+ }
495
+ /**
496
+ * Clear the canvas
497
+ */
498
+ clear() {
499
+ if (!this.ctx) return;
500
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0);
501
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
502
+ }
503
+ /**
504
+ * Draw all particles, batched by color to limit fillStyle changes
505
+ */
506
+ render(pool) {
507
+ this.clear();
508
+ const ctx = this.ctx;
509
+ if (!ctx) return;
510
+ const data = pool.data;
511
+ const ratio = this.pixelRatio;
512
+ const buckets = this.buckets;
513
+ for (const bucket of buckets) bucket.length = 0;
514
+ for (let i = 0; i < pool.activeCount; i++) {
515
+ const colorId = data[i * 16 /* SIZE */ + 8 /* Color */];
516
+ if (!buckets[colorId]) buckets[colorId] = [];
517
+ buckets[colorId].push(i);
518
+ }
519
+ for (let colorId = 0; colorId < buckets.length; colorId++) {
520
+ const bucket = buckets[colorId];
521
+ if (!bucket || bucket.length === 0) continue;
522
+ ctx.fillStyle = pool.palette[colorId];
523
+ for (const particle of bucket) {
524
+ const idx = particle * 16 /* SIZE */;
525
+ const tilt = data[idx + 6 /* Tilt */];
526
+ const rotation = data[idx + 4 /* Rotation */];
527
+ const opacity = Math.min(
528
+ 1,
529
+ data[idx + 11 /* Life */] / data[idx + 12 /* MaxLife */] * 2
530
+ );
531
+ const size = BASE_SIZE * data[idx + 10 /* Scalar */] * (0.8 + 0.2 * Math.cos(tilt));
532
+ if (size < 0.5 || opacity < 0.01) continue;
533
+ const cos = Math.cos(rotation);
534
+ const sin = Math.sin(rotation);
535
+ const squash = 0.6 + 0.4 * Math.abs(Math.cos(tilt));
536
+ ctx.globalAlpha = opacity;
537
+ ctx.setTransform(
538
+ ratio * cos,
539
+ ratio * sin,
540
+ -ratio * sin * squash,
541
+ ratio * cos * squash,
542
+ ratio * data[idx + 0 /* X */],
543
+ ratio * data[idx + 1 /* Y */]
544
+ );
545
+ getShapeRenderer(data[idx + 9 /* Shape */])(ctx, size);
546
+ }
547
+ }
548
+ ctx.globalAlpha = 1;
549
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
550
+ }
551
+ /**
552
+ * Destroy renderer and remove the canvas if we created it
553
+ */
554
+ destroy() {
555
+ this.clear();
556
+ if (this.ownsCanvas) this.canvas.remove();
557
+ }
558
+ };
559
+
560
+ // src/konfetti.ts
561
+ var FRAME_MS = 1e3 / 60;
562
+ var MAX_STEP = 4;
563
+ var Konfetti = class {
564
+ constructor(canvas, createOptions = {}) {
565
+ this.pool = new ParticlePool();
566
+ this.renderer = null;
567
+ this.frameId = null;
568
+ /** Timestamp of the previous frame, or when the loop was scheduled */
569
+ this.lastTime = 0;
570
+ this.paused = false;
571
+ this.destroyed = false;
572
+ this.resizeWithWindow = false;
573
+ this.timeouts = /* @__PURE__ */ new Set();
574
+ this.intervals = /* @__PURE__ */ new Set();
575
+ this.handleResize = () => {
576
+ this.renderer?.resize();
577
+ };
578
+ this.handleVisibilityChange = () => {
579
+ if (document.hidden) {
580
+ this.cancelFrame();
581
+ } else {
582
+ this.scheduleFrame();
583
+ }
584
+ };
585
+ this.tick = (time) => {
586
+ this.frameId = null;
587
+ if (!this.renderer) return;
588
+ const dt = clamp((time - this.lastTime) / FRAME_MS, 0, MAX_STEP);
589
+ this.lastTime = time;
590
+ const hasParticles = updateParticles(this.pool, dt);
591
+ this.renderer.render(this.pool);
592
+ if (hasParticles) {
593
+ this.frameId = requestAnimationFrame(this.tick);
594
+ }
595
+ };
596
+ if (!isBrowser()) {
597
+ this.destroyed = true;
598
+ return;
599
+ }
600
+ this.renderer = new Renderer(canvas);
601
+ this.resizeWithWindow = createOptions.resize !== false;
602
+ if (this.resizeWithWindow) window.addEventListener("resize", this.handleResize);
603
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
604
+ }
605
+ /**
606
+ * Fire one burst. Skipped while the page is hidden, after destroy(), and for
607
+ * visitors who prefer reduced motion (unless disableForReducedMotion is false).
608
+ */
609
+ fire(options) {
610
+ if (this.destroyed || !this.renderer || document.hidden) return;
611
+ const resolved = resolveOptions(options);
612
+ if (resolved.disableForReducedMotion && prefersReducedMotion()) return;
613
+ this.renderer.setZIndex(resolved.zIndex);
614
+ const { width, height } = this.renderer.getDimensions();
615
+ this.pool.spawn(resolved, width, height);
616
+ this.scheduleFrame();
617
+ }
618
+ /**
619
+ * Fire a built-in preset; options override the preset's own values
620
+ */
621
+ preset(name, options) {
622
+ if (!isPresetName(name) || this.destroyed) return;
623
+ for (const burst of PRESETS[name].bursts) {
624
+ const merged = mergeOptions(burst.options, options);
625
+ if (burst.delay) {
626
+ const id = setTimeout(() => {
627
+ this.timeouts.delete(id);
628
+ this.fire(merged);
629
+ }, burst.delay);
630
+ this.timeouts.add(id);
631
+ } else {
632
+ this.fire(merged);
633
+ }
634
+ }
635
+ }
636
+ /**
637
+ * Fire a small burst every `interval` ms until the returned function is
638
+ * called, or until reset() or destroy()
639
+ */
640
+ continuous(options, interval = 250) {
641
+ if (this.destroyed) return () => {
642
+ };
643
+ const merged = mergeOptions(CONTINUOUS_DEFAULTS, options);
644
+ const delay = Number.isFinite(interval) && interval >= 16 ? interval : 250;
645
+ const id = setInterval(() => this.fire(merged), delay);
646
+ this.intervals.add(id);
647
+ return () => {
648
+ clearInterval(id);
649
+ this.intervals.delete(id);
650
+ };
651
+ }
652
+ /**
653
+ * Pause the animation; particles stay where they are
654
+ */
655
+ pause() {
656
+ this.paused = true;
657
+ this.cancelFrame();
658
+ }
659
+ /**
660
+ * Resume after pause()
661
+ */
662
+ resume() {
663
+ this.paused = false;
664
+ this.scheduleFrame();
665
+ }
666
+ /**
667
+ * Clear all particles, cancel scheduled bursts and streams, stop animating and
668
+ * undo pause()
669
+ */
670
+ reset() {
671
+ this.paused = false;
672
+ this.cancelFrame();
673
+ for (const id of this.timeouts) clearTimeout(id);
674
+ for (const id of this.intervals) clearInterval(id);
675
+ this.timeouts.clear();
676
+ this.intervals.clear();
677
+ this.pool.reset();
678
+ this.renderer?.clear();
679
+ }
680
+ /**
681
+ * Reset, remove listeners and remove the canvas if this instance created it.
682
+ * The instance ignores every call afterwards.
683
+ */
684
+ destroy() {
685
+ if (this.destroyed) return;
686
+ this.reset();
687
+ this.destroyed = true;
688
+ if (this.resizeWithWindow) window.removeEventListener("resize", this.handleResize);
689
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
690
+ this.renderer?.destroy();
691
+ }
692
+ /**
693
+ * Start the loop if there is something to animate and nothing prevents it
694
+ */
695
+ scheduleFrame() {
696
+ if (this.frameId !== null || this.paused || this.destroyed || document.hidden || this.pool.activeCount === 0) {
697
+ return;
698
+ }
699
+ this.lastTime = performance.now();
700
+ this.frameId = requestAnimationFrame(this.tick);
701
+ }
702
+ cancelFrame() {
703
+ if (this.frameId !== null) {
704
+ cancelAnimationFrame(this.frameId);
705
+ this.frameId = null;
706
+ }
707
+ }
708
+ };
709
+
710
+ // src/global.ts
711
+ var globalInstance = null;
712
+ function getGlobalInstance() {
713
+ if (!isBrowser()) return null;
714
+ globalInstance ?? (globalInstance = new Konfetti());
715
+ return globalInstance;
716
+ }
717
+ function fire(options) {
718
+ getGlobalInstance()?.fire(options);
719
+ }
720
+ function firePreset(name, options) {
721
+ getGlobalInstance()?.preset(name, options);
722
+ }
723
+ function originFromElement(element) {
724
+ const rect = element.getBoundingClientRect();
725
+ return {
726
+ x: (rect.left + rect.width / 2) / window.innerWidth,
727
+ y: (rect.top + rect.height / 2) / window.innerHeight
728
+ };
729
+ }
730
+ function fireFromElement(element, options = {}) {
731
+ if (!isBrowser()) return;
732
+ const { preset: name, ...rest } = options;
733
+ const preset = isPresetName(name) ? name : void 0;
734
+ const followsOrigin = preset ? PRESETS[preset].followsOrigin : true;
735
+ const merged = followsOrigin ? { ...rest, origin: mergeOrigin(originFromElement(element), rest.origin) } : rest;
736
+ if (preset) {
737
+ firePreset(preset, merged);
738
+ } else {
739
+ fire(merged);
740
+ }
741
+ }
742
+ function reset() {
743
+ globalInstance?.reset();
744
+ }
745
+ function destroy() {
746
+ globalInstance?.destroy();
747
+ globalInstance = null;
748
+ }
749
+ function create(canvas, createOptions) {
750
+ const instance = new Konfetti(canvas, createOptions);
751
+ const fireFn = ((options) => instance.fire(options));
752
+ fireFn.preset = (name, options) => instance.preset(name, options);
753
+ fireFn.continuous = (options, interval) => instance.continuous(options, interval);
754
+ fireFn.reset = () => instance.reset();
755
+ fireFn.destroy = () => instance.destroy();
756
+ return fireFn;
757
+ }
758
+ function continuous(options, interval) {
759
+ return getGlobalInstance()?.continuous(options, interval) ?? (() => {
760
+ });
761
+ }
762
+ var cannon = (options) => firePreset("cannon", options);
763
+ var explosion = (options) => firePreset("explosion", options);
764
+ var fireworks = (options) => firePreset("fireworks", options);
765
+ var rain = (options) => firePreset("rain", options);
766
+ var snow = (options) => firePreset("snow", options);
767
+ var sideCannons = (options) => firePreset("sideCannons", options);
768
+ var pride = (options) => firePreset("pride", options);
769
+ export {
770
+ Konfetti,
771
+ PRESET_NAMES,
772
+ cannon,
773
+ continuous,
774
+ create,
775
+ destroy,
776
+ explosion,
777
+ fire,
778
+ fireFromElement,
779
+ firePreset,
780
+ fireworks,
781
+ isPresetName,
782
+ originFromElement,
783
+ pride,
784
+ rain,
785
+ reset,
786
+ sideCannons,
787
+ snow
788
+ };
789
+ //# sourceMappingURL=index.js.map