Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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, {}));