| @@ -1,6 +1,8 @@ | ||
| 1 | 1 | import confetti from 'canvas-confetti'; |
| 2 | 2 | |
| 3 | +export const CANVAS_CONFETTI = 'extendify-agent:canvas-confetti'; | |
| 4 | + | |
| 3 | 5 | let confettiInstance = null; |
| 4 | 6 | |
| 5 | 7 | const ensureConfettiInstance = () => { |
| 6 | 8 | if (confettiInstance) return confettiInstance; |
| @@ -22,9 +24,77 @@ | ||
| 22 | 24 | }); |
| 23 | 25 | return confettiInstance; |
| 24 | 26 | }; |
| 25 | 27 | |
| 26 | -export const throwSideConfetti = () => { | |
| 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) => { | |
| 27 | 97 | const LEFT = { |
| 28 | 98 | count: 200, |
| 29 | 99 | defaults: { |
| 30 | 100 | origin: { y: 0.7, x: 0 }, |
| @@ -36,20 +106,23 @@ | ||
| 36 | 106 | { ratio: 0.1, spread: 120, startVelocity: 25, decay: 0.92, scalar: 1.2 }, |
| 37 | 107 | { ratio: 0.1, spread: 120, startVelocity: 45 }, |
| 38 | 108 | ], |
| 39 | 109 | }; |
| 40 | - throwConfetti(LEFT); | |
| 41 | - throwConfetti({ | |
| 42 | - ...LEFT, | |
| 43 | - defaults: { | |
| 44 | - origin: { y: 0.7, x: 1 }, | |
| 110 | + const shooter = shoot ?? ensureConfettiInstance(); | |
| 111 | + throwConfetti(LEFT, shooter); | |
| 112 | + throwConfetti( | |
| 113 | + { | |
| 114 | + ...LEFT, | |
| 115 | + defaults: { | |
| 116 | + origin: { y: 0.7, x: 1 }, | |
| 117 | + }, | |
| 45 | 118 | }, |
| 46 | - }); | |
| 119 | + shooter, | |
| 120 | + ); | |
| 47 | 121 | }; |
| 48 | 122 | |
| 49 | -const throwConfetti = (config) => { | |
| 123 | +const throwConfetti = (config, shoot) => { | |
| 50 | 124 | const { count = 1, defaults, shots } = config; |
| 51 | - const shoot = ensureConfettiInstance(); | |
| 52 | 125 | |
| 53 | 126 | shots.forEach(({ ratio = 1, ...opts }) => { |
| 54 | 127 | shoot({ |
| 55 | 128 | ...(defaults ?? {}), |