Confetti for your website

I was working on the UI for a quiz app that we made as a team project in database class and I thought that it could use a little pizazz. Then I thought, nothing adds pizazz like confetti.

Each flake of confetti will be a <div>. The confetti <div>s will live in one fixed <div> that fills the viewport.

const particleDiv = document.createElement("div");
Object.assign(particleDiv.style, {
	position: "fixed",
	top: "0", left: "0",
	height: "100%", width: "100%",
	pointerEvents: "none",
});
document.body.appendChild(particleDiv);

animating indicates whether we're currently running the requestAnimationFrame loop. When there isn't any confetti, we stop the loop to save power.

let animating = false;
let particles = [];

function spawnParticle(x, y){
	const element = document.createElement("div");
	particleDiv.appendChild(element);
	const height = 5 + Math.random() * 5,
	      width = 5 + Math.random() * 5;
	Object.assign(element.style, {
		position: "absolute",
		height: `${height}px`, width: `${width}px`,
		backgroundColor: `hsl(${Math.random() * 360}, 100%, 50%)`,
		transformOrigin: "center",
	});
	const time = performance.now();
	const particle = {
		ix: x - width / 2, iy: y - height / 2, // Initial position
		ivx: Math.random() * 2 - 1, ivy: Math.random() * -3, // Initial velocity in px/frame
		theta: Math.random() * Math.PI * 2,
		element: element,
		spawnTime: time,
	};
	updateParticle(time)(particle);
	particles.push(particle);
	if(!animating) {
		requestAnimationFrame(updateParticles);
		animating = true;
	}
}

Originally, I stored a particle's current position and velocity, and on each frame I would run particle.{x, y} += particle.{vx, vy}; particle.vy += .15;. The problem was that when there were too many particles in flight at once the computer couldn't maintain a stable frame rate and the confetti fell in slow motion. By storing the initial conditions and using a little calculus, we can calculate the particle's position at any point in time that we want, so when FPS drops the confetti still falls at full speed. Choppy full speed motion looks better than slow motion.

let updateParticle = (time) => (particle) => {
	const age = (time - particle.spawnTime) * 60 / 1000; // Age in frames assuming 60fps
	if(age > 100) {
		particle.element.remove();
		return false;
	}
	const x = particle.ix + particle.ivx * age,
	      y = particle.iy + particle.ivy * age + .075 * age * age;
	particle.element.style.transform = `translateX(${x - scrollX}px) translateY(${y - scrollY}px) rotate(${particle.theta}rad)`;
	return true;
}

function updateParticles(time) {
	particles = particles.filter(updateParticle(time));
	if(particles.length == 0) {
		animating = false;
	} else {
		requestAnimationFrame(updateParticles);
	}
}

By adding a mousemove handler, we can leave a trail of confetti behind the mouse, demonstrating the potential of the confetti system while harkening back to the web design trends of yesteryear.

let distance = 0,
    timestamp = performance.now();

document.body.addEventListener("mousemove", (e) => {
	if(distance + (performance.now() - timestamp) / 10 >= 50) {
		spawnParticle(e.pageX + Math.random() * 10 - 5, e.pageY + Math.random() * 10 - 5);
		distance = 0;
		timestamp = performance.now();
	}
	distance += Math.abs(e.movementX) + Math.abs(e.movementY);
});

And on a more sensible note, we can shower an element with confetti. We used this on the quiz app to indicate that you answered a question correctly.

function confettiShower(element) {
	const {left: clientLeft, right: clientRight, top: clientTop} = element.getBoundingClientRect();
	const left = clientLeft + scrollX, right = clientRight + scrollX, top = clientTop + scrollY;
	const steps = ~~((right - left) / 20);
	for(let i = 0; i <= steps; i++) {
		spawnParticle(left + (right - left) * (i / steps), top);
	}
}

It's also fun to create a shower of confetti where you click.

To support this literate programming style, this document uses this script to run all of the code blocks:

for(const code of document.querySelectorAll("[data-execute]")) {
	const script = document.createElement("script");
	script.textContent = code.textContent;
	code.insertAdjacentElement("beforeend", script);
}
for(const button of document.querySelectorAll("[data-execute-button]")) {
	button.onclick = new Function(button.textContent);
}

(The preceding code block does not have the data-execute attribute set. Instead, it is followed by another copy of its code in a <script> tag. You've got to bootstrap the system somehow.)

This page released under CC0.


Published 2019-06-12

Next: Falling leaves