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