| 1 |
import confetti from 'canvas-confetti'; |
| 2 |
|
| 3 |
export const CANVAS_CONFETTI = 'extendify-agent:canvas-confetti'; |
| 4 |
|
| 5 |
let confettiInstance = null; |
| 6 |
|
| 7 |
const ensureConfettiInstance = () => { |
| 8 |
if (confettiInstance) return confettiInstance; |
| 9 |
|
| 10 |
// Custom canvas to account for height issues |
| 11 |
const canvas = document.createElement('canvas'); |
| 12 |
canvas.style.position = 'fixed'; |
| 13 |
canvas.style.top = '0'; |
| 14 |
canvas.style.left = '0'; |
| 15 |
canvas.style.width = '100%'; |
| 16 |
canvas.style.height = '100%'; |
| 17 |
canvas.style.pointerEvents = 'none'; |
| 18 |
canvas.style.zIndex = Number.MAX_SAFE_INTEGER; |
| 19 |
document.body.appendChild(canvas); |
| 20 |
|
| 21 |
confettiInstance = confetti.create(canvas, { |
| 22 |
disableForReducedMotion: true, |
| 23 |
resize: true, |
| 24 |
}); |
| 25 |
return confettiInstance; |
| 26 |
}; |
| 27 |
|
| 28 |
// Its own canvas is what confines a burst to the element that asked. |
| 29 |
export const confettiIn = (canvas) => |
| 30 |
confetti.create(canvas, { disableForReducedMotion: true, resize: true }); |
| 31 |
|
| 32 |
const TOP = () => ({ x: Math.random(), y: 0 }); |
| 33 |
|
| 34 |
// A zero start velocity is what makes it fall rather than be thrown. |
| 35 |
export const FALLING = { |
| 36 |
interval: 55, |
| 37 |
perDrop: 2, |
| 38 |
drift: 0.5, |
| 39 |
origin: TOP, |
| 40 |
colors: [ |
| 41 |
'#26ccff', |
| 42 |
'#a25afd', |
| 43 |
'#ff5e7e', |
| 44 |
'#88ff5a', |
| 45 |
'#fcff42', |
| 46 |
'#ffa62d', |
| 47 |
'#ff36ff', |
| 48 |
], |
| 49 |
shot: { |
| 50 |
particleCount: 1, |
| 51 |
startVelocity: 0, |
| 52 |
gravity: 0.9, |
| 53 |
ticks: 300, |
| 54 |
scalar: 0.85, |
| 55 |
}, |
| 56 |
}; |
| 57 |
|
| 58 |
// An absent interval is what makes it fire once instead of repeating. |
| 59 |
export const THROW = { |
| 60 |
perDrop: 2, |
| 61 |
origin: (i) => ({ x: i, y: 0.75 }), |
| 62 |
shot: { |
| 63 |
particleCount: 70, |
| 64 |
startVelocity: 50, |
| 65 |
spread: 75, |
| 66 |
decay: 0.91, |
| 67 |
ticks: 220, |
| 68 |
scalar: 0.95, |
| 69 |
}, |
| 70 |
}; |
| 71 |
|
| 72 |
const pick = (list) => list[Math.floor(Math.random() * list.length)]; |
| 73 |
|
| 74 |
// A multi-particle shot cannot spread at zero velocity, so it lands as one clump. |
| 75 |
// A one-particle shot takes only the first colour of the palette. |
| 76 |
export const dropConfetti = ( |
| 77 |
shoot, |
| 78 |
{ interval, shot, drift = 0, perDrop = 1, colors, origin = TOP }, |
| 79 |
) => { |
| 80 |
const drop = () => { |
| 81 |
for (let i = 0; i < perDrop; i++) { |
| 82 |
shoot({ |
| 83 |
...shot, |
| 84 |
origin: origin(i), |
| 85 |
...(drift && { drift: (Math.random() * 2 - 1) * drift }), |
| 86 |
...(colors && { colors: [pick(colors)] }), |
| 87 |
}); |
| 88 |
} |
| 89 |
}; |
| 90 |
drop(); |
| 91 |
if (!interval) return () => {}; |
| 92 |
const timer = setInterval(drop, interval); |
| 93 |
return () => clearInterval(timer); |
| 94 |
}; |
| 95 |
|
| 96 |
export const throwSideConfetti = (shoot) => { |
| 97 |
const LEFT = { |
| 98 |
count: 200, |
| 99 |
defaults: { |
| 100 |
origin: { y: 0.7, x: 0 }, |
| 101 |
}, |
| 102 |
shots: [ |
| 103 |
{ ratio: 0.25, spread: 26, startVelocity: 55 }, |
| 104 |
{ ratio: 0.2, spread: 60 }, |
| 105 |
{ ratio: 0.35, spread: 100, decay: 0.91, scalar: 0.8 }, |
| 106 |
{ ratio: 0.1, spread: 120, startVelocity: 25, decay: 0.92, scalar: 1.2 }, |
| 107 |
{ ratio: 0.1, spread: 120, startVelocity: 45 }, |
| 108 |
], |
| 109 |
}; |
| 110 |
const shooter = shoot ?? ensureConfettiInstance(); |
| 111 |
throwConfetti(LEFT, shooter); |
| 112 |
throwConfetti( |
| 113 |
{ |
| 114 |
...LEFT, |
| 115 |
defaults: { |
| 116 |
origin: { y: 0.7, x: 1 }, |
| 117 |
}, |
| 118 |
}, |
| 119 |
shooter, |
| 120 |
); |
| 121 |
}; |
| 122 |
|
| 123 |
const throwConfetti = (config, shoot) => { |
| 124 |
const { count = 1, defaults, shots } = config; |
| 125 |
|
| 126 |
shots.forEach(({ ratio = 1, ...opts }) => { |
| 127 |
shoot({ |
| 128 |
...(defaults ?? {}), |
| 129 |
...(opts ?? {}), |
| 130 |
particleCount: Math.floor(count * ratio), |
| 131 |
zIndex: Number.MAX_SAFE_INTEGER, |
| 132 |
}); |
| 133 |
}); |
| 134 |
}; |
| 135 |
|