vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Celebrate a landed change request, once
c8fa5759b293126f1b247abc746bc339791568cb · Keenan Nemetz · 13d ago · parent 53ff967b
Celebrate a landed change request, once A successful landing now redirects with ?merged=1, and app.js answers that parameter by fetching static/confetti.browser.js (canvas-confetti 1.9.3, ISC) and firing one burst. The flag is removed from the address as soon as it is read, so a reload, a bookmark or a shared link is an ordinary visit, and no page without the parameter ever requests the bundle. The library honours prefers-reduced-motion through disableForReducedMotion, and reset() before firing means a page restored from the back/forward cache cannot stack a second canvas. The asset is vendored the way htmx and Lucide are: the file is unmodified from the published npm tarball, with the license beside it, so script-src 'self' stays as it is. It is deliberately not in the page shell, because Static() serves assets uncompressed and a shell script would make every page pay for a celebration only one page uses. Refs #01M5XZ7T4R88BA9KV1KTSV7ZXP
server/web/static/confetti.browser.js +887
| @@ -0,0 +1,887 @@ | |||
| 1 | // canvas-confetti v1.9.3 built on 2024-04-30T22:19:17.794Z | ||
| 2 | !(function (window, module) { | ||
| 3 | // source content | ||
| 4 | /* globals Map */ | ||
| 5 | |||
| 6 | (function main(global, module, isWorker, workerSize) { | ||
| 7 | var canUseWorker = !!( | ||
| 8 | global.Worker && | ||
| 9 | global.Blob && | ||
| 10 | global.Promise && | ||
| 11 | global.OffscreenCanvas && | ||
| 12 | global.OffscreenCanvasRenderingContext2D && | ||
| 13 | global.HTMLCanvasElement && | ||
| 14 | global.HTMLCanvasElement.prototype.transferControlToOffscreen && | ||
| 15 | global.URL && | ||
| 16 | global.URL.createObjectURL); | ||
| 17 | |||
| 18 | var canUsePaths = typeof Path2D === 'function' && typeof DOMMatrix === 'function'; | ||
| 19 | var canDrawBitmap = (function () { | ||
| 20 | // this mostly supports ssr | ||
| 21 | if (!global.OffscreenCanvas) { | ||
| 22 | return false; | ||
| 23 | } | ||
| 24 | |||
| 25 | var canvas = new OffscreenCanvas(1, 1); | ||
| 26 | var ctx = canvas.getContext('2d'); | ||
| 27 | ctx.fillRect(0, 0, 1, 1); | ||
| 28 | var bitmap = canvas.transferToImageBitmap(); | ||
| 29 | |||
| 30 | try { | ||
| 31 | ctx.createPattern(bitmap, 'no-repeat'); | ||
| 32 | } catch (e) { | ||
| 33 | return false; | ||
| 34 | } | ||
| 35 | |||
| 36 | return true; | ||
| 37 | })(); | ||
| 38 | |||
| 39 | function noop() {} | ||
| 40 | |||
| 41 | // create a promise if it exists, otherwise, just | ||
| 42 | // call the function directly | ||
| 43 | function promise(func) { | ||
| 44 | var ModulePromise = module.exports.Promise; | ||
| 45 | var Prom = ModulePromise !== void 0 ? ModulePromise : global.Promise; | ||
| 46 | |||
| 47 | if (typeof Prom === 'function') { | ||
| 48 | return new Prom(func); | ||
| 49 | } | ||
| 50 | |||
| 51 | func(noop, noop); | ||
| 52 | |||
| 53 | return null; | ||
| 54 | } | ||
| 55 | |||
| 56 | var bitmapMapper = (function (skipTransform, map) { | ||
| 57 | // see https://github.com/catdad/canvas-confetti/issues/209 | ||
| 58 | // creating canvases is actually pretty expensive, so we should create a | ||
| 59 | // 1:1 map for bitmap:canvas, so that we can animate the confetti in | ||
| 60 | // a performant manner, but also not store them forever so that we don't | ||
| 61 | // have a memory leak | ||
| 62 | return { | ||
| 63 | transform: function(bitmap) { | ||
| 64 | if (skipTransform) { | ||
| 65 | return bitmap; | ||
| 66 | } | ||
| 67 | |||
| 68 | if (map.has(bitmap)) { | ||
| 69 | return map.get(bitmap); | ||
| 70 | } | ||
| 71 | |||
| 72 | var canvas = new OffscreenCanvas(bitmap.width, bitmap.height); | ||
| 73 | var ctx = canvas.getContext('2d'); | ||
| 74 | ctx.drawImage(bitmap, 0, 0); | ||
| 75 | |||
| 76 | map.set(bitmap, canvas); | ||
| 77 | |||
| 78 | return canvas; | ||
| 79 | }, | ||
| 80 | clear: function () { | ||
| 81 | map.clear(); | ||
| 82 | } | ||
| 83 | }; | ||
| 84 | })(canDrawBitmap, new Map()); | ||
| 85 | |||
| 86 | var raf = (function () { | ||
| 87 | var TIME = Math.floor(1000 / 60); | ||
| 88 | var frame, cancel; | ||
| 89 | var frames = {}; | ||
| 90 | var lastFrameTime = 0; | ||
| 91 | |||
| 92 | if (typeof requestAnimationFrame === 'function' && typeof cancelAnimationFrame === 'function') { | ||
| 93 | frame = function (cb) { | ||
| 94 | var id = Math.random(); | ||
| 95 | |||
| 96 | frames[id] = requestAnimationFrame(function onFrame(time) { | ||
| 97 | if (lastFrameTime === time || lastFrameTime + TIME - 1 < time) { | ||
| 98 | lastFrameTime = time; | ||
| 99 | delete frames[id]; | ||
| 100 | |||
| 101 | cb(); | ||
| 102 | } else { | ||
| 103 | frames[id] = requestAnimationFrame(onFrame); | ||
| 104 | } | ||
| 105 | }); | ||
| 106 | |||
| 107 | return id; | ||
| 108 | }; | ||
| 109 | cancel = function (id) { | ||
| 110 | if (frames[id]) { | ||
| 111 | cancelAnimationFrame(frames[id]); | ||
| 112 | } | ||
| 113 | }; | ||
| 114 | } else { | ||
| 115 | frame = function (cb) { | ||
| 116 | return setTimeout(cb, TIME); | ||
| 117 | }; | ||
| 118 | cancel = function (timer) { | ||
| 119 | return clearTimeout(timer); | ||
| 120 | }; | ||
| 121 | } | ||
| 122 | |||
| 123 | return { frame: frame, cancel: cancel }; | ||
| 124 | }()); | ||
| 125 | |||
| 126 | var getWorker = (function () { | ||
| 127 | var worker; | ||
| 128 | var prom; | ||
| 129 | var resolves = {}; | ||
| 130 | |||
| 131 | function decorate(worker) { | ||
| 132 | function execute(options, callback) { | ||
| 133 | worker.postMessage({ options: options || {}, callback: callback }); | ||
| 134 | } | ||
| 135 | worker.init = function initWorker(canvas) { | ||
| 136 | var offscreen = canvas.transferControlToOffscreen(); | ||
| 137 | worker.postMessage({ canvas: offscreen }, [offscreen]); | ||
| 138 | }; | ||
| 139 | |||
| 140 | worker.fire = function fireWorker(options, size, done) { | ||
| 141 | if (prom) { | ||
| 142 | execute(options, null); | ||
| 143 | return prom; | ||
| 144 | } | ||
| 145 | |||
| 146 | var id = Math.random().toString(36).slice(2); | ||
| 147 | |||
| 148 | prom = promise(function (resolve) { | ||
| 149 | function workerDone(msg) { | ||
| 150 | if (msg.data.callback !== id) { | ||
| 151 | return; | ||
| 152 | } | ||
| 153 | |||
| 154 | delete resolves[id]; | ||
| 155 | worker.removeEventListener('message', workerDone); | ||
| 156 | |||
| 157 | prom = null; | ||
| 158 | |||
| 159 | bitmapMapper.clear(); | ||
| 160 | |||
| 161 | done(); | ||
| 162 | resolve(); | ||
| 163 | } | ||
| 164 | |||
| 165 | worker.addEventListener('message', workerDone); | ||
| 166 | execute(options, id); | ||
| 167 | |||
| 168 | resolves[id] = workerDone.bind(null, { data: { callback: id }}); | ||
| 169 | }); | ||
| 170 | |||
| 171 | return prom; | ||
| 172 | }; | ||
| 173 | |||
| 174 | worker.reset = function resetWorker() { | ||
| 175 | worker.postMessage({ reset: true }); | ||
| 176 | |||
| 177 | for (var id in resolves) { | ||
| 178 | resolves[id](); | ||
| 179 | delete resolves[id]; | ||
| 180 | } | ||
| 181 | }; | ||
| 182 | } | ||
| 183 | |||
| 184 | return function () { | ||
| 185 | if (worker) { | ||
| 186 | return worker; | ||
| 187 | } | ||
| 188 | |||
| 189 | if (!isWorker && canUseWorker) { | ||
| 190 | var code = [ | ||
| 191 | 'var CONFETTI, SIZE = {}, module = {};', | ||
| 192 | '(' + main.toString() + ')(this, module, true, SIZE);', | ||
| 193 | 'onmessage = function(msg) {', | ||
| 194 | ' if (msg.data.options) {', | ||
| 195 | ' CONFETTI(msg.data.options).then(function () {', | ||
| 196 | ' if (msg.data.callback) {', | ||
| 197 | ' postMessage({ callback: msg.data.callback });', | ||
| 198 | ' }', | ||
| 199 | ' });', | ||
| 200 | ' } else if (msg.data.reset) {', | ||
| 201 | ' CONFETTI && CONFETTI.reset();', | ||
| 202 | ' } else if (msg.data.resize) {', | ||
| 203 | ' SIZE.width = msg.data.resize.width;', | ||
| 204 | ' SIZE.height = msg.data.resize.height;', | ||
| 205 | ' } else if (msg.data.canvas) {', | ||
| 206 | ' SIZE.width = msg.data.canvas.width;', | ||
| 207 | ' SIZE.height = msg.data.canvas.height;', | ||
| 208 | ' CONFETTI = module.exports.create(msg.data.canvas);', | ||
| 209 | ' }', | ||
| 210 | '}', | ||
| 211 | ].join('\n'); | ||
| 212 | try { | ||
| 213 | worker = new Worker(URL.createObjectURL(new Blob([code]))); | ||
| 214 | } catch (e) { | ||
| 215 | // eslint-disable-next-line no-console | ||
| 216 | typeof console !== undefined && typeof console.warn === 'function' ? console.warn('🎊 Could not load worker', e) : null; | ||
| 217 | |||
| 218 | return null; | ||
| 219 | } | ||
| 220 | |||
| 221 | decorate(worker); | ||
| 222 | } | ||
| 223 | |||
| 224 | return worker; | ||
| 225 | }; | ||
| 226 | })(); | ||
| 227 | |||
| 228 | var defaults = { | ||
| 229 | particleCount: 50, | ||
| 230 | angle: 90, | ||
| 231 | spread: 45, | ||
| 232 | startVelocity: 45, | ||
| 233 | decay: 0.9, | ||
| 234 | gravity: 1, | ||
| 235 | drift: 0, | ||
| 236 | ticks: 200, | ||
| 237 | x: 0.5, | ||
| 238 | y: 0.5, | ||
| 239 | shapes: ['square', 'circle'], | ||
| 240 | zIndex: 100, | ||
| 241 | colors: [ | ||
| 242 | '#26ccff', | ||
| 243 | '#a25afd', | ||
| 244 | '#ff5e7e', | ||
| 245 | '#88ff5a', | ||
| 246 | '#fcff42', | ||
| 247 | '#ffa62d', | ||
| 248 | '#ff36ff' | ||
| 249 | ], | ||
| 250 | // probably should be true, but back-compat | ||
| 251 | disableForReducedMotion: false, | ||
| 252 | scalar: 1 | ||
| 253 | }; | ||
| 254 | |||
| 255 | function convert(val, transform) { | ||
| 256 | return transform ? transform(val) : val; | ||
| 257 | } | ||
| 258 | |||
| 259 | function isOk(val) { | ||
| 260 | return !(val === null || val === undefined); | ||
| 261 | } | ||
| 262 | |||
| 263 | function prop(options, name, transform) { | ||
| 264 | return convert( | ||
| 265 | options && isOk(options[name]) ? options[name] : defaults[name], | ||
| 266 | transform | ||
| 267 | ); | ||
| 268 | } | ||
| 269 | |||
| 270 | function onlyPositiveInt(number){ | ||
| 271 | return number < 0 ? 0 : Math.floor(number); | ||
| 272 | } | ||
| 273 | |||
| 274 | function randomInt(min, max) { | ||
| 275 | // [min, max) | ||
| 276 | return Math.floor(Math.random() * (max - min)) + min; | ||
| 277 | } | ||
| 278 | |||
| 279 | function toDecimal(str) { | ||
| 280 | return parseInt(str, 16); | ||
| 281 | } | ||
| 282 | |||
| 283 | function colorsToRgb(colors) { | ||
| 284 | return colors.map(hexToRgb); | ||
| 285 | } | ||
| 286 | |||
| 287 | function hexToRgb(str) { | ||
| 288 | var val = String(str).replace(/[^0-9a-f]/gi, ''); | ||
| 289 | |||
| 290 | if (val.length < 6) { | ||
| 291 | val = val[0]+val[0]+val[1]+val[1]+val[2]+val[2]; | ||
| 292 | } | ||
| 293 | |||
| 294 | return { | ||
| 295 | r: toDecimal(val.substring(0,2)), | ||
| 296 | g: toDecimal(val.substring(2,4)), | ||
| 297 | b: toDecimal(val.substring(4,6)) | ||
| 298 | }; | ||
| 299 | } | ||
| 300 | |||
| 301 | function getOrigin(options) { | ||
| 302 | var origin = prop(options, 'origin', Object); | ||
| 303 | origin.x = prop(origin, 'x', Number); | ||
| 304 | origin.y = prop(origin, 'y', Number); | ||
| 305 | |||
| 306 | return origin; | ||
| 307 | } | ||
| 308 | |||
| 309 | function setCanvasWindowSize(canvas) { | ||
| 310 | canvas.width = document.documentElement.clientWidth; | ||
| 311 | canvas.height = document.documentElement.clientHeight; | ||
| 312 | } | ||
| 313 | |||
| 314 | function setCanvasRectSize(canvas) { | ||
| 315 | var rect = canvas.getBoundingClientRect(); | ||
| 316 | canvas.width = rect.width; | ||
| 317 | canvas.height = rect.height; | ||
| 318 | } | ||
| 319 | |||
| 320 | function getCanvas(zIndex) { | ||
| 321 | var canvas = document.createElement('canvas'); | ||
| 322 | |||
| 323 | canvas.style.position = 'fixed'; | ||
| 324 | canvas.style.top = '0px'; | ||
| 325 | canvas.style.left = '0px'; | ||
| 326 | canvas.style.pointerEvents = 'none'; | ||
| 327 | canvas.style.zIndex = zIndex; | ||
| 328 | |||
| 329 | return canvas; | ||
| 330 | } | ||
| 331 | |||
| 332 | function ellipse(context, x, y, radiusX, radiusY, rotation, startAngle, endAngle, antiClockwise) { | ||
| 333 | context.save(); | ||
| 334 | context.translate(x, y); | ||
| 335 | context.rotate(rotation); | ||
| 336 | context.scale(radiusX, radiusY); | ||
| 337 | context.arc(0, 0, 1, startAngle, endAngle, antiClockwise); | ||
| 338 | context.restore(); | ||
| 339 | } | ||
| 340 | |||
| 341 | function randomPhysics(opts) { | ||
| 342 | var radAngle = opts.angle * (Math.PI / 180); | ||
| 343 | var radSpread = opts.spread * (Math.PI / 180); | ||
| 344 | |||
| 345 | return { | ||
| 346 | x: opts.x, | ||
| 347 | y: opts.y, | ||
| 348 | wobble: Math.random() * 10, | ||
| 349 | wobbleSpeed: Math.min(0.11, Math.random() * 0.1 + 0.05), | ||
| 350 | velocity: (opts.startVelocity * 0.5) + (Math.random() * opts.startVelocity), | ||
| 351 | angle2D: -radAngle + ((0.5 * radSpread) - (Math.random() * radSpread)), | ||
| 352 | tiltAngle: (Math.random() * (0.75 - 0.25) + 0.25) * Math.PI, | ||
| 353 | color: opts.color, | ||
| 354 | shape: opts.shape, | ||
| 355 | tick: 0, | ||
| 356 | totalTicks: opts.ticks, | ||
| 357 | decay: opts.decay, | ||
| 358 | drift: opts.drift, | ||
| 359 | random: Math.random() + 2, | ||
| 360 | tiltSin: 0, | ||
| 361 | tiltCos: 0, | ||
| 362 | wobbleX: 0, | ||
| 363 | wobbleY: 0, | ||
| 364 | gravity: opts.gravity * 3, | ||
| 365 | ovalScalar: 0.6, | ||
| 366 | scalar: opts.scalar, | ||
| 367 | flat: opts.flat | ||
| 368 | }; | ||
| 369 | } | ||
| 370 | |||
| 371 | function updateFetti(context, fetti) { | ||
| 372 | fetti.x += Math.cos(fetti.angle2D) * fetti.velocity + fetti.drift; | ||
| 373 | fetti.y += Math.sin(fetti.angle2D) * fetti.velocity + fetti.gravity; | ||
| 374 | fetti.velocity *= fetti.decay; | ||
| 375 | |||
| 376 | if (fetti.flat) { | ||
| 377 | fetti.wobble = 0; | ||
| 378 | fetti.wobbleX = fetti.x + (10 * fetti.scalar); | ||
| 379 | fetti.wobbleY = fetti.y + (10 * fetti.scalar); | ||
| 380 | |||
| 381 | fetti.tiltSin = 0; | ||
| 382 | fetti.tiltCos = 0; | ||
| 383 | fetti.random = 1; | ||
| 384 | } else { | ||
| 385 | fetti.wobble += fetti.wobbleSpeed; | ||
| 386 | fetti.wobbleX = fetti.x + ((10 * fetti.scalar) * Math.cos(fetti.wobble)); | ||
| 387 | fetti.wobbleY = fetti.y + ((10 * fetti.scalar) * Math.sin(fetti.wobble)); | ||
| 388 | |||
| 389 | fetti.tiltAngle += 0.1; | ||
| 390 | fetti.tiltSin = Math.sin(fetti.tiltAngle); | ||
| 391 | fetti.tiltCos = Math.cos(fetti.tiltAngle); | ||
| 392 | fetti.random = Math.random() + 2; | ||
| 393 | } | ||
| 394 | |||
| 395 | var progress = (fetti.tick++) / fetti.totalTicks; | ||
| 396 | |||
| 397 | var x1 = fetti.x + (fetti.random * fetti.tiltCos); | ||
| 398 | var y1 = fetti.y + (fetti.random * fetti.tiltSin); | ||
| 399 | var x2 = fetti.wobbleX + (fetti.random * fetti.tiltCos); | ||
| 400 | var y2 = fetti.wobbleY + (fetti.random * fetti.tiltSin); | ||
| 401 | |||
| 402 | context.fillStyle = 'rgba(' + fetti.color.r + ', ' + fetti.color.g + ', ' + fetti.color.b + ', ' + (1 - progress) + ')'; | ||
| 403 | |||
| 404 | context.beginPath(); | ||
| 405 | |||
| 406 | if (canUsePaths && fetti.shape.type === 'path' && typeof fetti.shape.path === 'string' && Array.isArray(fetti.shape.matrix)) { | ||
| 407 | context.fill(transformPath2D( | ||
| 408 | fetti.shape.path, | ||
| 409 | fetti.shape.matrix, | ||
| 410 | fetti.x, | ||
| 411 | fetti.y, | ||
| 412 | Math.abs(x2 - x1) * 0.1, | ||
| 413 | Math.abs(y2 - y1) * 0.1, | ||
| 414 | Math.PI / 10 * fetti.wobble | ||
| 415 | )); | ||
| 416 | } else if (fetti.shape.type === 'bitmap') { | ||
| 417 | var rotation = Math.PI / 10 * fetti.wobble; | ||
| 418 | var scaleX = Math.abs(x2 - x1) * 0.1; | ||
| 419 | var scaleY = Math.abs(y2 - y1) * 0.1; | ||
| 420 | var width = fetti.shape.bitmap.width * fetti.scalar; | ||
| 421 | var height = fetti.shape.bitmap.height * fetti.scalar; | ||
| 422 | |||
| 423 | var matrix = new DOMMatrix([ | ||
| 424 | Math.cos(rotation) * scaleX, | ||
| 425 | Math.sin(rotation) * scaleX, | ||
| 426 | -Math.sin(rotation) * scaleY, | ||
| 427 | Math.cos(rotation) * scaleY, | ||
| 428 | fetti.x, | ||
| 429 | fetti.y | ||
| 430 | ]); | ||
| 431 | |||
| 432 | // apply the transform matrix from the confetti shape | ||
| 433 | matrix.multiplySelf(new DOMMatrix(fetti.shape.matrix)); | ||
| 434 | |||
| 435 | var pattern = context.createPattern(bitmapMapper.transform(fetti.shape.bitmap), 'no-repeat'); | ||
| 436 | pattern.setTransform(matrix); | ||
| 437 | |||
| 438 | context.globalAlpha = (1 - progress); | ||
| 439 | context.fillStyle = pattern; | ||
| 440 | context.fillRect( | ||
| 441 | fetti.x - (width / 2), | ||
| 442 | fetti.y - (height / 2), | ||
| 443 | width, | ||
| 444 | height | ||
| 445 | ); | ||
| 446 | context.globalAlpha = 1; | ||
| 447 | } else if (fetti.shape === 'circle') { | ||
| 448 | context.ellipse ? | ||
| 449 | context.ellipse(fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI) : | ||
| 450 | ellipse(context, fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI); | ||
| 451 | } else if (fetti.shape === 'star') { | ||
| 452 | var rot = Math.PI / 2 * 3; | ||
| 453 | var innerRadius = 4 * fetti.scalar; | ||
| 454 | var outerRadius = 8 * fetti.scalar; | ||
| 455 | var x = fetti.x; | ||
| 456 | var y = fetti.y; | ||
| 457 | var spikes = 5; | ||
| 458 | var step = Math.PI / spikes; | ||
| 459 | |||
| 460 | while (spikes--) { | ||
| 461 | x = fetti.x + Math.cos(rot) * outerRadius; | ||
| 462 | y = fetti.y + Math.sin(rot) * outerRadius; | ||
| 463 | context.lineTo(x, y); | ||
| 464 | rot += step; | ||
| 465 | |||
| 466 | x = fetti.x + Math.cos(rot) * innerRadius; | ||
| 467 | y = fetti.y + Math.sin(rot) * innerRadius; | ||
| 468 | context.lineTo(x, y); | ||
| 469 | rot += step; | ||
| 470 | } | ||
| 471 | } else { | ||
| 472 | context.moveTo(Math.floor(fetti.x), Math.floor(fetti.y)); | ||
| 473 | context.lineTo(Math.floor(fetti.wobbleX), Math.floor(y1)); | ||
| 474 | context.lineTo(Math.floor(x2), Math.floor(y2)); | ||
| 475 | context.lineTo(Math.floor(x1), Math.floor(fetti.wobbleY)); | ||
| 476 | } | ||
| 477 | |||
| 478 | context.closePath(); | ||
| 479 | context.fill(); | ||
| 480 | |||
| 481 | return fetti.tick < fetti.totalTicks; | ||
| 482 | } | ||
| 483 | |||
| 484 | function animate(canvas, fettis, resizer, size, done) { | ||
| 485 | var animatingFettis = fettis.slice(); | ||
| 486 | var context = canvas.getContext('2d'); | ||
| 487 | var animationFrame; | ||
| 488 | var destroy; | ||
| 489 | |||
| 490 | var prom = promise(function (resolve) { | ||
| 491 | function onDone() { | ||
| 492 | animationFrame = destroy = null; | ||
| 493 | |||
| 494 | context.clearRect(0, 0, size.width, size.height); | ||
| 495 | bitmapMapper.clear(); | ||
| 496 | |||
| 497 | done(); | ||
| 498 | resolve(); | ||
| 499 | } | ||
| 500 | |||
| 501 | function update() { | ||
| 502 | if (isWorker && !(size.width === workerSize.width && size.height === workerSize.height)) { | ||
| 503 | size.width = canvas.width = workerSize.width; | ||
| 504 | size.height = canvas.height = workerSize.height; | ||
| 505 | } | ||
| 506 | |||
| 507 | if (!size.width && !size.height) { | ||
| 508 | resizer(canvas); | ||
| 509 | size.width = canvas.width; | ||
| 510 | size.height = canvas.height; | ||
| 511 | } | ||
| 512 | |||
| 513 | context.clearRect(0, 0, size.width, size.height); | ||
| 514 | |||
| 515 | animatingFettis = animatingFettis.filter(function (fetti) { | ||
| 516 | return updateFetti(context, fetti); | ||
| 517 | }); | ||
| 518 | |||
| 519 | if (animatingFettis.length) { | ||
| 520 | animationFrame = raf.frame(update); | ||
| 521 | } else { | ||
| 522 | onDone(); | ||
| 523 | } | ||
| 524 | } | ||
| 525 | |||
| 526 | animationFrame = raf.frame(update); | ||
| 527 | destroy = onDone; | ||
| 528 | }); | ||
| 529 | |||
| 530 | return { | ||
| 531 | addFettis: function (fettis) { | ||
| 532 | animatingFettis = animatingFettis.concat(fettis); | ||
| 533 | |||
| 534 | return prom; | ||
| 535 | }, | ||
| 536 | canvas: canvas, | ||
| 537 | promise: prom, | ||
| 538 | reset: function () { | ||
| 539 | if (animationFrame) { | ||
| 540 | raf.cancel(animationFrame); | ||
| 541 | } | ||
| 542 | |||
| 543 | if (destroy) { | ||
| 544 | destroy(); | ||
| 545 | } | ||
| 546 | } | ||
| 547 | }; | ||
| 548 | } | ||
| 549 | |||
| 550 | function confettiCannon(canvas, globalOpts) { | ||
| 551 | var isLibCanvas = !canvas; | ||
| 552 | var allowResize = !!prop(globalOpts || {}, 'resize'); | ||
| 553 | var hasResizeEventRegistered = false; | ||
| 554 | var globalDisableForReducedMotion = prop(globalOpts, 'disableForReducedMotion', Boolean); | ||
| 555 | var shouldUseWorker = canUseWorker && !!prop(globalOpts || {}, 'useWorker'); | ||
| 556 | var worker = shouldUseWorker ? getWorker() : null; | ||
| 557 | var resizer = isLibCanvas ? setCanvasWindowSize : setCanvasRectSize; | ||
| 558 | var initialized = (canvas && worker) ? !!canvas.__confetti_initialized : false; | ||
| 559 | var preferLessMotion = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion)').matches; | ||
| 560 | var animationObj; | ||
| 561 | |||
| 562 | function fireLocal(options, size, done) { | ||
| 563 | var particleCount = prop(options, 'particleCount', onlyPositiveInt); | ||
| 564 | var angle = prop(options, 'angle', Number); | ||
| 565 | var spread = prop(options, 'spread', Number); | ||
| 566 | var startVelocity = prop(options, 'startVelocity', Number); | ||
| 567 | var decay = prop(options, 'decay', Number); | ||
| 568 | var gravity = prop(options, 'gravity', Number); | ||
| 569 | var drift = prop(options, 'drift', Number); | ||
| 570 | var colors = prop(options, 'colors', colorsToRgb); | ||
| 571 | var ticks = prop(options, 'ticks', Number); | ||
| 572 | var shapes = prop(options, 'shapes'); | ||
| 573 | var scalar = prop(options, 'scalar'); | ||
| 574 | var flat = !!prop(options, 'flat'); | ||
| 575 | var origin = getOrigin(options); | ||
| 576 | |||
| 577 | var temp = particleCount; | ||
| 578 | var fettis = []; | ||
| 579 | |||
| 580 | var startX = canvas.width * origin.x; | ||
| 581 | var startY = canvas.height * origin.y; | ||
| 582 | |||
| 583 | while (temp--) { | ||
| 584 | fettis.push( | ||
| 585 | randomPhysics({ | ||
| 586 | x: startX, | ||
| 587 | y: startY, | ||
| 588 | angle: angle, | ||
| 589 | spread: spread, | ||
| 590 | startVelocity: startVelocity, | ||
| 591 | color: colors[temp % colors.length], | ||
| 592 | shape: shapes[randomInt(0, shapes.length)], | ||
| 593 | ticks: ticks, | ||
| 594 | decay: decay, | ||
| 595 | gravity: gravity, | ||
| 596 | drift: drift, | ||
| 597 | scalar: scalar, | ||
| 598 | flat: flat | ||
| 599 | }) | ||
| 600 | ); | ||
| 601 | } | ||
| 602 | |||
| 603 | // if we have a previous canvas already animating, | ||
| 604 | // add to it | ||
| 605 | if (animationObj) { | ||
| 606 | return animationObj.addFettis(fettis); | ||
| 607 | } | ||
| 608 | |||
| 609 | animationObj = animate(canvas, fettis, resizer, size , done); | ||
| 610 | |||
| 611 | return animationObj.promise; | ||
| 612 | } | ||
| 613 | |||
| 614 | function fire(options) { | ||
| 615 | var disableForReducedMotion = globalDisableForReducedMotion || prop(options, 'disableForReducedMotion', Boolean); | ||
| 616 | var zIndex = prop(options, 'zIndex', Number); | ||
| 617 | |||
| 618 | if (disableForReducedMotion && preferLessMotion) { | ||
| 619 | return promise(function (resolve) { | ||
| 620 | resolve(); | ||
| 621 | }); | ||
| 622 | } | ||
| 623 | |||
| 624 | if (isLibCanvas && animationObj) { | ||
| 625 | // use existing canvas from in-progress animation | ||
| 626 | canvas = animationObj.canvas; | ||
| 627 | } else if (isLibCanvas && !canvas) { | ||
| 628 | // create and initialize a new canvas | ||
| 629 | canvas = getCanvas(zIndex); | ||
| 630 | document.body.appendChild(canvas); | ||
| 631 | } | ||
| 632 | |||
| 633 | if (allowResize && !initialized) { | ||
| 634 | // initialize the size of a user-supplied canvas | ||
| 635 | resizer(canvas); | ||
| 636 | } | ||
| 637 | |||
| 638 | var size = { | ||
| 639 | width: canvas.width, | ||
| 640 | height: canvas.height | ||
| 641 | }; | ||
| 642 | |||
| 643 | if (worker && !initialized) { | ||
| 644 | worker.init(canvas); | ||
| 645 | } | ||
| 646 | |||
| 647 | initialized = true; | ||
| 648 | |||
| 649 | if (worker) { | ||
| 650 | canvas.__confetti_initialized = true; | ||
| 651 | } | ||
| 652 | |||
| 653 | function onResize() { | ||
| 654 | if (worker) { | ||
| 655 | // TODO this really shouldn't be immediate, because it is expensive | ||
| 656 | var obj = { | ||
| 657 | getBoundingClientRect: function () { | ||
| 658 | if (!isLibCanvas) { | ||
| 659 | return canvas.getBoundingClientRect(); | ||
| 660 | } | ||
| 661 | } | ||
| 662 | }; | ||
| 663 | |||
| 664 | resizer(obj); | ||
| 665 | |||
| 666 | worker.postMessage({ | ||
| 667 | resize: { | ||
| 668 | width: obj.width, | ||
| 669 | height: obj.height | ||
| 670 | } | ||
| 671 | }); | ||
| 672 | return; | ||
| 673 | } | ||
| 674 | |||
| 675 | // don't actually query the size here, since this | ||
| 676 | // can execute frequently and rapidly | ||
| 677 | size.width = size.height = null; | ||
| 678 | } | ||
| 679 | |||
| 680 | function done() { | ||
| 681 | animationObj = null; | ||
| 682 | |||
| 683 | if (allowResize) { | ||
| 684 | hasResizeEventRegistered = false; | ||
| 685 | global.removeEventListener('resize', onResize); | ||
| 686 | } | ||
| 687 | |||
| 688 | if (isLibCanvas && canvas) { | ||
| 689 | if (document.body.contains(canvas)) { | ||
| 690 | document.body.removeChild(canvas); | ||
| 691 | } | ||
| 692 | canvas = null; | ||
| 693 | initialized = false; | ||
| 694 | } | ||
| 695 | } | ||
| 696 | |||
| 697 | if (allowResize && !hasResizeEventRegistered) { | ||
| 698 | hasResizeEventRegistered = true; | ||
| 699 | global.addEventListener('resize', onResize, false); | ||
| 700 | } | ||
| 701 | |||
| 702 | if (worker) { | ||
| 703 | return worker.fire(options, size, done); | ||
| 704 | } | ||
| 705 | |||
| 706 | return fireLocal(options, size, done); | ||
| 707 | } | ||
| 708 | |||
| 709 | fire.reset = function () { | ||
| 710 | if (worker) { | ||
| 711 | worker.reset(); | ||
| 712 | } | ||
| 713 | |||
| 714 | if (animationObj) { | ||
| 715 | animationObj.reset(); | ||
| 716 | } | ||
| 717 | }; | ||
| 718 | |||
| 719 | return fire; | ||
| 720 | } | ||
| 721 | |||
| 722 | // Make default export lazy to defer worker creation until called. | ||
| 723 | var defaultFire; | ||
| 724 | function getDefaultFire() { | ||
| 725 | if (!defaultFire) { | ||
| 726 | defaultFire = confettiCannon(null, { useWorker: true, resize: true }); | ||
| 727 | } | ||
| 728 | return defaultFire; | ||
| 729 | } | ||
| 730 | |||
| 731 | function transformPath2D(pathString, pathMatrix, x, y, scaleX, scaleY, rotation) { | ||
| 732 | var path2d = new Path2D(pathString); | ||
| 733 | |||
| 734 | var t1 = new Path2D(); | ||
| 735 | t1.addPath(path2d, new DOMMatrix(pathMatrix)); | ||
| 736 | |||
| 737 | var t2 = new Path2D(); | ||
| 738 | // see https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrix/DOMMatrix | ||
| 739 | t2.addPath(t1, new DOMMatrix([ | ||
| 740 | Math.cos(rotation) * scaleX, | ||
| 741 | Math.sin(rotation) * scaleX, | ||
| 742 | -Math.sin(rotation) * scaleY, | ||
| 743 | Math.cos(rotation) * scaleY, | ||
| 744 | x, | ||
| 745 | y | ||
| 746 | ])); | ||
| 747 | |||
| 748 | return t2; | ||
| 749 | } | ||
| 750 | |||
| 751 | function shapeFromPath(pathData) { | ||
| 752 | if (!canUsePaths) { | ||
| 753 | throw new Error('path confetti are not supported in this browser'); | ||
| 754 | } | ||
| 755 | |||
| 756 | var path, matrix; | ||
| 757 | |||
| 758 | if (typeof pathData === 'string') { | ||
| 759 | path = pathData; | ||
| 760 | } else { | ||
| 761 | path = pathData.path; | ||
| 762 | matrix = pathData.matrix; | ||
| 763 | } | ||
| 764 | |||
| 765 | var path2d = new Path2D(path); | ||
| 766 | var tempCanvas = document.createElement('canvas'); | ||
| 767 | var tempCtx = tempCanvas.getContext('2d'); | ||
| 768 | |||
| 769 | if (!matrix) { | ||
| 770 | // attempt to figure out the width of the path, up to 1000x1000 | ||
| 771 | var maxSize = 1000; | ||
| 772 | var minX = maxSize; | ||
| 773 | var minY = maxSize; | ||
| 774 | var maxX = 0; | ||
| 775 | var maxY = 0; | ||
| 776 | var width, height; | ||
| 777 | |||
| 778 | // do some line skipping... this is faster than checking | ||
| 779 | // every pixel and will be mostly still correct | ||
| 780 | for (var x = 0; x < maxSize; x += 2) { | ||
| 781 | for (var y = 0; y < maxSize; y += 2) { | ||
| 782 | if (tempCtx.isPointInPath(path2d, x, y, 'nonzero')) { | ||
| 783 | minX = Math.min(minX, x); | ||
| 784 | minY = Math.min(minY, y); | ||
| 785 | maxX = Math.max(maxX, x); | ||
| 786 | maxY = Math.max(maxY, y); | ||
| 787 | } | ||
| 788 | } | ||
| 789 | } | ||
| 790 | |||
| 791 | width = maxX - minX; | ||
| 792 | height = maxY - minY; | ||
| 793 | |||
| 794 | var maxDesiredSize = 10; | ||
| 795 | var scale = Math.min(maxDesiredSize/width, maxDesiredSize/height); | ||
| 796 | |||
| 797 | matrix = [ | ||
| 798 | scale, 0, 0, scale, | ||
| 799 | -Math.round((width/2) + minX) * scale, | ||
| 800 | -Math.round((height/2) + minY) * scale | ||
| 801 | ]; | ||
| 802 | } | ||
| 803 | |||
| 804 | return { | ||
| 805 | type: 'path', | ||
| 806 | path: path, | ||
| 807 | matrix: matrix | ||
| 808 | }; | ||
| 809 | } | ||
| 810 | |||
| 811 | function shapeFromText(textData) { | ||
| 812 | var text, | ||
| 813 | scalar = 1, | ||
| 814 | color = '#000000', | ||
| 815 | // see https://nolanlawson.com/2022/04/08/the-struggle-of-using-native-emoji-on-the-web/ | ||
| 816 | fontFamily = '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", "EmojiOne Color", "Android Emoji", "Twemoji Mozilla", "system emoji", sans-serif'; | ||
| 817 | |||
| 818 | if (typeof textData === 'string') { | ||
| 819 | text = textData; | ||
| 820 | } else { | ||
| 821 | text = textData.text; | ||
| 822 | scalar = 'scalar' in textData ? textData.scalar : scalar; | ||
| 823 | fontFamily = 'fontFamily' in textData ? textData.fontFamily : fontFamily; | ||
| 824 | color = 'color' in textData ? textData.color : color; | ||
| 825 | } | ||
| 826 | |||
| 827 | // all other confetti are 10 pixels, | ||
| 828 | // so this pixel size is the de-facto 100% scale confetti | ||
| 829 | var fontSize = 10 * scalar; | ||
| 830 | var font = '' + fontSize + 'px ' + fontFamily; | ||
| 831 | |||
| 832 | var canvas = new OffscreenCanvas(fontSize, fontSize); | ||
| 833 | var ctx = canvas.getContext('2d'); | ||
| 834 | |||
| 835 | ctx.font = font; | ||
| 836 | var size = ctx.measureText(text); | ||
| 837 | var width = Math.ceil(size.actualBoundingBoxRight + size.actualBoundingBoxLeft); | ||
| 838 | var height = Math.ceil(size.actualBoundingBoxAscent + size.actualBoundingBoxDescent); | ||
| 839 | |||
| 840 | var padding = 2; | ||
| 841 | var x = size.actualBoundingBoxLeft + padding; | ||
| 842 | var y = size.actualBoundingBoxAscent + padding; | ||
| 843 | width += padding + padding; | ||
| 844 | height += padding + padding; | ||
| 845 | |||
| 846 | canvas = new OffscreenCanvas(width, height); | ||
| 847 | ctx = canvas.getContext('2d'); | ||
| 848 | ctx.font = font; | ||
| 849 | ctx.fillStyle = color; | ||
| 850 | |||
| 851 | ctx.fillText(text, x, y); | ||
| 852 | |||
| 853 | var scale = 1 / scalar; | ||
| 854 | |||
| 855 | return { | ||
| 856 | type: 'bitmap', | ||
| 857 | // TODO these probably need to be transfered for workers | ||
| 858 | bitmap: canvas.transferToImageBitmap(), | ||
| 859 | matrix: [scale, 0, 0, scale, -width * scale / 2, -height * scale / 2] | ||
| 860 | }; | ||
| 861 | } | ||
| 862 | |||
| 863 | module.exports = function() { | ||
| 864 | return getDefaultFire().apply(this, arguments); | ||
| 865 | }; | ||
| 866 | module.exports.reset = function() { | ||
| 867 | getDefaultFire().reset(); | ||
| 868 | }; | ||
| 869 | module.exports.create = confettiCannon; | ||
| 870 | module.exports.shapeFromPath = shapeFromPath; | ||
| 871 | module.exports.shapeFromText = shapeFromText; | ||
| 872 | }((function () { | ||
| 873 | if (typeof window !== 'undefined') { | ||
| 874 | return window; | ||
| 875 | } | ||
| 876 | |||
| 877 | if (typeof self !== 'undefined') { | ||
| 878 | return self; | ||
| 879 | } | ||
| 880 | |||
| 881 | return this || {}; | ||
| 882 | })(), module, false)); | ||
| 883 | |||
| 884 | // end source content | ||
| 885 | |||
| 886 | window.confetti = module.exports; | ||
| 887 | }(window, {})); | ||