I have been working on Code Monkey Clash for a while now. It’s a game where you compete against other players to write code as fast as possible. In one of the latest released I wanted to display a winner page with the top three players. A winner page deserves some cheerfulness and what’s more cheerful than confetti? After searching for confetti libraries I decided I would try to implement it myself.
Having not worked that much with animated graphics in the browser before I decided to search for some tutorials. I found a few, but most of them just used a library like js-confetti to do the heavy lifting. So I decided to ask ChatGPT for help. I know using ChatGPT is a bit controversial, but for exploratory coding tasks like this it can be quite helpful. Rather than ask for the whole solution up front I tried prompting it for some ideas on how to get started.
All artists need a canvas
Essentially the solution would require creating a canvas element and drawing on it. To get it out of the way lets make a reusable function for setting up the canvas and context. That way we don’t need to repeat the same code in every example.
<script>
function setupCanvas(id) {
const canvas = document.getElementById(id);
const ctx = canvas.getContext('2d');
canvas.width = 1600/2;
canvas.height = 900/2;
return { canvas, ctx };
}
</script>
I dream of rectangles
Say you wanted to draw a red square on position 0,0 with a size of 100x100.
You would set the fill style to red and then call fillRect(0, 0, 100, 100)
on the canvas context.
<canvas id="canvas-rect"></canvas>
<script>
function render() {
const { canvas, ctx } = setupCanvas('canvas-rect');
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);
}
render();
</script>
Paths and strokes for confetti
This is a good start, we have a canvas element and have drawn something on it. After some more prompting it seemed that drawing paths using strokes would be a good way to create the confetti. Mozilla Developer Network has good documentation on strokes. The idea is that you configure the stroke style (width and color), and then start drawing a path. To make something a little more confetti like we can draw a slightly diagonal line.
<canvas id="canvas-strokes"></canvas>
<script>
function render() {
const { canvas, ctx } = setupCanvas('canvas-strokes');
// First we set up the stroke style
ctx.strokeStyle = 'red';
ctx.lineWidth = 8;
ctx.moveTo(30, 30); // Move to where we want to start the line
ctx.lineTo(40, 50); // Draw a line to the new position
ctx.stroke(); // Fill the path according to the stroke style
}
render();
</script>
Now this is starting to look like confetti already!
Colors of the Tailwind rainbow
But currently we’re just drawing one line, and have hardcoded the color to red. Let us add some more colors to use in our examples! I let GitHub Copilot suggest the colors based on the Tailwind CSS color palette. Not sure if they actually match those in Tailwind, but they look good to me!
<script>
// For brevity I will only show a simplified example of the color function
// If you inspect my code I account for dark mode preference as my blog
// supports both light and dark mode
let colors = [
"#10b981",
"#7c3aed",
"#fbbf24",
"#ef4444",
"#3b82f6",
"#22c55e",
"#f97316",
"#ef4444",
]
</script>
With our new-found colors lets try to draw a few more confetti pieces.
We can use the colors
array to pick a random color for each piece.
Drawing a row of confetti in various colors should just require a loop!
<canvas id="canvas-colors"></canvas>
<script>
function render() {
const { canvas, ctx } = setupCanvas('canvas-colors');
ctx.lineWidth = 8;
for (let i = 0; i < 20; i++) {
ctx.strokeStyle = colors[Math.floor(Math.random() * colors.length)];
ctx.moveTo(30 + i * 30, 30); // For each iteration move further to the right
ctx.lineTo(40 + i * 30, 50); // Draw a line to the new position
ctx.stroke(); // Fill the path according to the stroke style
}
}
render();
</script>
But what the heck is this?! All our confetti pieces have the same color. As it turns out we’ve been drawing sub-paths on the same path for each iteration. So while the color changes for each iteration, it is set for all sub paths. Fortunately beginPath is here to save the day! Now we have a row of confetti pieces in various colors!
<canvas id="canvas-colors-fixed"></canvas>
<script>
function renderColors() {
const { canvas, ctx } = setupCanvas('canvas-colors-fixed');
ctx.lineWidth = 8;
for (let i = 0; i < 20; i++) {
ctx.beginPath(); // Start a new path
ctx.strokeStyle = colors[Math.floor(Math.random() * colors.length)];
ctx.moveTo(30 + i * 30, 30); // Move to where we want to start the line
ctx.lineTo(40 + i * 30, 50); // Draw a line to the new position
ctx.stroke(); // Fill the path according to the stroke style
}
}
renderColors();
</script>
Randomness is the spice of life
But what is confetti without some randomness? Right now all our confetti pieces are the same diagonal line. We can introduce some randomness by adding a random radius and tilt for each piece. This will simulate 3D space a bit as smaller pieces will appear further away. Tilting the pieces various directions will also make them look more natural and random.
<canvas id="canvas-random"></canvas>
<script>
function render() {
const { canvas, ctx } = setupCanvas('canvas-random');
for (let i = 0; i < 40; i++) {
// Make the radius of the path random so confetti are different sizes
const radius = Math.floor(Math.random() * 50) - 10
// Ensure the paths tilt in various directions
const tilt = Math.floor(Math.random() * 10) - 10
// Distribute the confetti randomly on the canvas
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
ctx.beginPath(); // Start a new path
ctx.lineWidth = radius / 2;
ctx.strokeStyle = colors[Math.floor(Math.random() * colors.length)];
ctx.moveTo(x + tilt + radius / 4, y);
ctx.lineTo(x + tilt, y + tilt + radius / 4);
ctx.stroke(); // Fill the path according to the stroke style
}
}
render();
</script>
Weightless confetti is no fun
Up until now the confetti has just been hanging in the air.
No party is complete without some gravity!
We can simulate some gravity by introducing some delta velocity for each piece.
Then we need to update the position of the confetti pieces each frame.
To do this we can use requestAnimationFrame
to call a function that updates the position and redraws the canvas.
We also need to clear the canvas each frame to avoid a mess of confetti pieces.
<canvas id="canvas-gravity"></canvas>
<script>
function renderFallingConfetti() {
const confetti = []
const start = performance.now() // To keep track of time
const { canvas, ctx } = setupCanvas('canvas-gravity');
for (let i = 0; i < 40; i++) {
const radius = Math.floor(Math.random() * 50) - 10
const tilt = Math.floor(Math.random() * 10) - 10
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
confetti.push({
x,
y,
radius,
tilt,
velocity: Math.random() * 2 + 1
})
}
function update() {
// Run for at most 10 seconds
if (performance.now() - start > 10000) return
ctx.clearRect(0, 0, canvas.width, canvas.height);
confetti.forEach((piece) => {
piece.y += piece.velocity;
ctx.beginPath();
ctx.lineWidth = piece.radius / 2;
ctx.strokeStyle = colors[Math.floor(Math.random() * colors.length)];
ctx.moveTo(piece.x + piece.tilt + piece.radius / 4, piece.y);
ctx.lineTo(piece.x + piece.tilt, piece.y + piece.tilt + piece.radius / 4);
ctx.stroke();
})
requestAnimationFrame(update);
}
update();
}
renderFallingConfetti();
</script>
Confetti don’t fall straight down
Right now our confetti animation is pretty boring. All the pieces just fall straight down as if there was no atmosphere. A party without some oxygen will quickly become a disaster! Usually confetti pieces will scatter a bit as they fall going every which way.
Each confetti piece should thus have some random horizontal velocity as well as the vertical velocity. As gravity is mostly always pointing down the vertical velocity should be positive. However, the horizontal velocity can be both positive and negative as air resistance scatter the pieces.
<canvas id="canvas-scattering"></canvas>
<script>
function renderScatteringConfetti() {
const confetti = []
const start = performance.now() // To keep track of time
const { canvas, ctx } = setupCanvas('canvas-scattering');
for (let i = 0; i < 40; i++) {
const radius = Math.floor(Math.random() * 50) - 10
const tilt = Math.floor(Math.random() * 10) - 10
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
confetti.push({
x,
y,
radius,
tilt,
color: colors[Math.floor(Math.random() * colors.length)],
yVelocity: Math.random() * 3,
xVelocity: Math.random() * 2 - 1
})
}
function update() {
// Run for at most 10 seconds
if (performance.now() - start > 10000) return
ctx.clearRect(0, 0, canvas.width, canvas.height);
confetti.forEach((piece) => {
piece.y += piece.yVelocity;
piece.x += piece.xVelocity;
ctx.beginPath();
ctx.lineWidth = piece.radius / 2;
ctx.strokeStyle = piece.color;
ctx.moveTo(piece.x + piece.tilt + piece.radius / 4, piece.y);
ctx.lineTo(piece.x + piece.tilt, piece.y + piece.tilt + piece.radius / 4);
ctx.stroke();
})
requestAnimationFrame(update);
}
update();
}
renderScatteringConfetti();
</script>
Flutter like a butterfly
I still thought the confetti was a bit boring. How can we make it look more lively? Well, ChatGPT has a good suggestion:
Cosine for Vertical Movement:
When Math.cos is used for vertical movement, it creates a smooth oscillation that starts at a maximum value and goes through a complete cycle (down to a minimum value and back to the maximum). This can simulate a fluttering effect where the object moves up and down smoothly.
Sine for Horizontal Movement:
When Math.sin is used for horizontal movement, it creates a smooth oscillation that starts at zero and goes through a complete cycle (left and right movement). This can simulate a swaying effect where the object moves side to side smoothly.
So essentially because both the sine and cosine functions in JavaScript oscillate between -1 and 1 we can use them to create smooth movements. For each confetti piece we make a phase offset that is unique to the piece. Then we make a time counter to gradually increase the phase offset. The quickler the time counter increases the quicker the oscillation will be. We also introduce some speed constants for the x and y direction to control the speed and magnitude of the movement.
<canvas id="canvas-fluttering"></canvas>
<script>
function renderFlutteringConfetti() {
const timeDelta = 0.05;
const xAmplitude = 0.5;
const yAmplitude = 1;
let time = 0;
const confetti = []
const start = performance.now() // To keep track of time
const { canvas, ctx } = setupCanvas('canvas-fluttering');
for (let i = 0; i < 40; i++) {
const radius = Math.floor(Math.random() * 50) - 10
const tilt = Math.floor(Math.random() * 10) - 10
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
confetti.push({
x,
y,
radius,
tilt,
color: colors[Math.floor(Math.random() * colors.length)],
phaseOffset: i, // Randomness from position in list
})
}
function update() {
// Run for at most 10 seconds
if (performance.now() - start > 10000) return
ctx.clearRect(0, 0, canvas.width, canvas.height);
confetti.forEach((piece, i) => {
piece.y += (Math.cos(piece.phaseOffset + time) + 1) * yAmplitude;
piece.x += Math.sin(piece.phaseOffset + time) * xAmplitude;
ctx.beginPath();
ctx.lineWidth = piece.radius / 2;
ctx.strokeStyle = piece.color;
ctx.moveTo(piece.x + piece.tilt + piece.radius / 4, piece.y);
ctx.lineTo(piece.x + piece.tilt, piece.y + piece.tilt + piece.radius / 4);
ctx.stroke();
})
time += timeDelta;
requestAnimationFrame(update);
}
update();
}
renderFlutteringConfetti();
</script>
Feel free to play around with the amplitude and time delta to get different effects!
Tying up the pieces
We have come a long way from a single red square to a fluttering confetti party! But we’re not quite there yet. When adding the flutter we completely removed the sideways movement. On average the confetti pieces move straight down! And as you might have noticed, annoyingly the confetti pieces fall off the canvas. How about we re-introduce some sideways momentum and make the confetti pieces wrap around the canvas?
<canvas id="canvas-wrapping"></canvas>
<script>
const { canvas, ctx } = setupCanvas('canvas-wrapping');
function renderWrappingConfetti() {
const timeDelta = 0.05;
const xAmplitude = 0.5;
const yAmplitude = 1;
const xVelocity = 2;
const yVelocity = 3;
let time = 0;
const confetti = []
for (let i = 0; i < 100; i++) {
const radius = Math.floor(Math.random() * 50) - 10
const tilt = Math.floor(Math.random() * 10) - 10
const xSpeed = Math.random() * xVelocity - xVelocity / 2
const ySpeed = Math.random() * yVelocity
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height - canvas.height;
confetti.push({
x,
y,
xSpeed,
ySpeed,
radius,
tilt,
color: colors[Math.floor(Math.random() * colors.length)],
phaseOffset: i, // Randomness from position in list
})
}
function update() {
// Run for at most 10 seconds
ctx.clearRect(0, 0, canvas.width, canvas.height);
confetti.forEach((piece, i) => {
piece.y += (Math.cos(piece.phaseOffset + time) + 1) * yAmplitude + piece.ySpeed;
piece.x += Math.sin(piece.phaseOffset + time) * xAmplitude + piece.xSpeed;
// Wrap around the canvas
if (piece.x < 0) piece.x = canvas.width;
if (piece.x > canvas.width) piece.x = 0;
if (piece.y > canvas.height) piece.y = 0;
ctx.beginPath();
ctx.lineWidth = piece.radius / 2;
ctx.strokeStyle = piece.color;
ctx.moveTo(piece.x + piece.tilt + piece.radius / 4, piece.y);
ctx.lineTo(piece.x + piece.tilt, piece.y + piece.tilt + piece.radius / 4);
ctx.stroke();
})
time += timeDelta;
requestAnimationFrame(update);
}
update();
}
</script>
Conclusion
If you made it this far I hope you have learned something new! With the help of Kagi Search, MDN documentation, and ChatGPT I was able to make quite the confetti party. The final result is quite lively and has some interesting movement! By using the sine and cosine functions we were able to create smooth oscillations for the confetti pieces.
The confetti example can be extended even further. For example the pieces don’t rotate as they fall. You could also make the confetti pieces have different shapes, and different colors on each side. There are tons of possibilities! Some libraries offer more advanced animation than what we have here. For me though half the fun was in the exploration and learning. In any case, feel free to play around with the code and use it for your own needs.
Addendum
This post makes no attempt at optimizing the code! Observant and knowledgeable readers will notice that the code could be optimized in various ways. I will leave that as an excersize for the reader! A great tip is looking at TypedArrays for performance improvements.