.celebrate {
position: relative;
z-index: 1;
border: 0;
cursor: pointer;
padding: 14px 26px;
border-radius: 12px;
font: 600 16px system-ui, sans-serif;
color: #fff;
background: var(--brand);
}
.celebrate:active { transform: scale(0.97); }
/* The canvas covers the page but never blocks clicks. */
.confetti-canvas { position: fixed; inset: 0; width: 100%; height: 100%; pointer-events: none; }
// Confetti burst: each piece is a small rectangle with a position, a velocity,
// a spin and a color. Every frame we apply gravity and drag, then redraw.
const canvas = document.querySelector('.confetti-canvas');
const context = canvas.getContext('2d');
const colors = ['#3684ff', '#12a150', '#e08a1e', '#c93a3f', '#a855f7', '#ffd23f'];
const prefersReducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
const pieceCount = prefersReducedMotion ? 40 : 120; // a smaller burst with reduced motion
const gravity = 0.35; // pulls pieces down each frame
const drag = 0.985; // slows pieces a little each frame (air resistance)
let pieces = [];
let frame = 0;
function resizeCanvas() {
canvas.width = innerWidth * devicePixelRatio;
canvas.height = innerHeight * devicePixelRatio;
context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); // draw in CSS pixels
}
function burstFrom(x, y) {
for (let i = 0; i < pieceCount; i += 1) {
const angle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI * 0.9; // mostly upward
const speed = 6 + Math.random() * 8;
pieces.push({
x, y,
velocityX: Math.cos(angle) * speed,
velocityY: Math.sin(angle) * speed,
rotation: Math.random() * Math.PI,
spin: prefersReducedMotion ? 0 : (Math.random() - 0.5) * 0.3,
width: 6 + Math.random() * 6,
height: 4 + Math.random() * 4,
color: colors[i % colors.length],
});
}
if (!frame) frame = requestAnimationFrame(draw);
}
function draw() {
context.clearRect(0, 0, innerWidth, innerHeight);
for (const piece of pieces) {
piece.velocityX *= drag;
piece.velocityY = piece.velocityY * drag + gravity;
piece.x += piece.velocityX;
piece.y += piece.velocityY;
piece.rotation += piece.spin;
context.save();
context.translate(piece.x, piece.y);
context.rotate(piece.rotation);
context.fillStyle = piece.color;
context.fillRect(-piece.width / 2, -piece.height / 2, piece.width, piece.height);
context.restore();
}
// Forget pieces that have fallen off the bottom; stop when none are left.
pieces = pieces.filter((piece) => piece.y < innerHeight + 20);
frame = pieces.length ? requestAnimationFrame(draw) : 0;
}
resizeCanvas();
addEventListener('resize', resizeCanvas);
document.querySelector('.celebrate').addEventListener('click', (event) => {
const box = event.currentTarget.getBoundingClientRect();
burstFrom(box.left + box.width / 2, box.top + box.height / 2);
});