exploding smiley face 5 min timer

This sketch creates a 5-minute countdown timer with an animated yellow smiley face ball that bounces around the canvas. When the timer reaches zero, the smiley face freezes and explodes in an expanding orange burst, followed by a 'TIME'S UP!' message.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the timer 1 minute instead of 5 — Changing the TIMER_DURATION_MS multiplier lets you test the explosion faster without waiting
  2. Turn the timer text bright green — Changing the fill() color before text() updates the timer display color for better contrast
  3. Make the ball turn red at 10 seconds instead of 4 — Higher millisecond values in the condition trigger the urgency color change earlier in the countdown
  4. Double the explosion size range — Larger numbers in the map() function make the explosion expand further and more dramatically
  5. Make the smiley face bigger on the ball — Increasing the scaling factors (0.3, 0.2, etc.) makes the eyes larger and the smile wider relative to the ball
  6. Change the explosion color to blue — RGB values in fill() control the particle burst color—adjust red and green to change the hue
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch combines a functional countdown timer with lively animation and a dramatic finale. A yellow smiley face bounces around the canvas for exactly 5 minutes, its eyes and smile drawn on top of the ball shape. As the timer counts down, the ball turns red in the final 4 seconds to create urgency. When the timer reaches zero, the ball freezes and explodes in an expanding orange burst that fades away, with a bold 'TIME'S UP!' message replacing the animation.

The code is organized into three main sections: initialization in setup(), the core timer and animation logic in draw(), and responsive canvas resizing in windowResized(). By reading it, you will learn how to track elapsed time with millis(), manage multiple states (running vs. exploding), draw layered graphics (ball + face features), and create smooth animations using the map() function to scale visual properties over time.

⚙️ How It Works

  1. When setup() runs, the sketch records the current time with millis(), initializes the ball's position to the canvas center, gives it random bounce speeds in both directions, and sets up text formatting for the timer display.
  2. Every frame, draw() calculates how much time has passed by subtracting the start time from the current millis() value, then formats this into MM:SS and displays it at the top of the canvas.
  3. While the timer is running and no explosion is active, the ball's X and Y positions are updated by adding velocity values each frame, and if-statements check whether the ball has passed the canvas edges and flip the velocity to bounce it back.
  4. The ball is drawn as a yellow ellipse that turns red once 4 seconds remain, with black eyes and a curved mouth arc drawn on top to create the smiley face.
  5. When remainingTimeMs reaches zero, explosionActive becomes true, which stops the ball's movement and triggers a loop that draws 5 expanding orange circles with decreasing transparency, mapped from the explosion duration.
  6. Once the explosion animation duration (2 seconds) expires, noLoop() stops the draw loop permanently and displays the 'TIME'S UP!' message centered on the canvas.

🎓 Concepts You'll Learn

Time tracking with millis()Animation and state managementCollision detection and bouncingLayered graphics and compositingResponsive canvas resizingInterpolation with map()Transparency and alpha blending

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It prepares the canvas, records the starting time, and initializes all variables the draw loop will need. Notice how millis() is called here to create a reference point for calculating elapsed time throughout the animation.

function setup() {
  createCanvas(windowWidth, windowHeight);
  startTime = millis(); // Record the start time when the sketch begins
  remainingTimeMs = TIMER_DURATION_MS;

  // Initialize ball properties
  ballRadius = min(width, height) / 10; // Ball size relative to canvas
  ballX = width / 2;
  ballY = height / 2;
  ballSpeedX = random(3, 7) * (random() > 0.5 ? 1 : -1); // Random speed and direction
  ballSpeedY = random(3, 7) * (random() > 0.5 ? 1 : -1);

  textAlign(CENTER, CENTER); // Center align text
  textSize(32); // Set text size for the timer
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Ball initialization ballRadius = min(width, height) / 10; // Ball size relative to canvas

Sets the ball's size proportionally to the canvas dimensions so it scales responsively

calculation Random velocity assignment ballSpeedX = random(3, 7) * (random() > 0.5 ? 1 : -1); // Random speed and direction

Picks a random speed between 3 and 7 pixels/frame and randomly chooses left or right direction

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, making the timer fullscreen
startTime = millis(); // Record the start time when the sketch begins
millis() returns milliseconds since the sketch started—storing it now lets us calculate elapsed time later
remainingTimeMs = TIMER_DURATION_MS;
Initializes remaining time to the full duration (5 minutes in milliseconds)
ballRadius = min(width, height) / 10; // Ball size relative to canvas
Calculates ball size as 1/10 of the smaller canvas dimension, so it scales on different screen sizes
ballX = width / 2;
Places the ball's center at the horizontal midpoint of the canvas
ballY = height / 2;
Places the ball's center at the vertical midpoint of the canvas
ballSpeedX = random(3, 7) * (random() > 0.5 ? 1 : -1); // Random speed and direction
Creates a random horizontal velocity between 3 and 7 (or -3 to -7), so the ball moves left or right unpredictably
ballSpeedY = random(3, 7) * (random() > 0.5 ? 1 : -1);
Creates a random vertical velocity, making the ball bounce at a varied angle
textAlign(CENTER, CENTER); // Center align text
Centers all text both horizontally and vertically around the x,y coordinates you pass to text()
textSize(32); // Set text size for the timer
Sets the font size to 32 pixels for displaying the countdown

draw()

draw() is the heartbeat of p5.js—it runs 60 times per second by default. This function demonstrates three key animation patterns: (1) calculating elapsed time by subtracting a recorded start time from millis(), (2) managing state with boolean flags (remainingTimeMs > 0, !explosionActive) to trigger different behaviors, and (3) using map() to smoothly interpolate visual properties (size and transparency) over time. The collision detection pattern is also fundamental: checking if any part of an object has crossed a boundary, then reversing the velocity.

🔬 This code updates the ball's position and bounces it off the edges. What happens if you change ballSpeedX *= -1 to ballSpeedX *= -0.8? The ball will lose speed on every bounce—why might that be useful in a real game?

    // Update ball position
    ballX += ballSpeedX;
    ballY += ballSpeedY;

    // Handle bouncing off edges
    if (ballX + ballRadius > width || ballX - ballRadius < 0) {
      ballSpeedX *= -1; // Reverse horizontal speed
    }

🔬 This loop draws 5 expanding circles. What happens if you change the loop from i < 5 to i < 10? You'll see twice as many rings in the explosion—do they look better together or more chaotic?

    for (let i = 0; i < 5; i++) {
      // Map elapsed time to increasing size and decreasing alpha (transparency)
      let size = map(explosionElapsed, 0, explosionDuration, ballRadius * 2, ballRadius * 10);
      let alpha = map(explosionElapsed, 0, explosionDuration, 255, 0);
      fill(255, 100, 0, alpha); // Orange color fading out
      noStroke(); // No outline for explosion circles
      ellipse(ballX, ballY, size * (1 + i * 0.2)); // Draw multiple circles with slight size variations
    }
function draw() {
  background(220); // Light gray background

  // Calculate remaining time
  let elapsedTimeMs = millis() - startTime;
  remainingTimeMs = max(0, TIMER_DURATION_MS - elapsedTimeMs); // Ensure time doesn't go negative

  // Format and display the timer
  let minutes = floor(remainingTimeMs / (60 * 1000));
  let seconds = floor((remainingTimeMs % (60 * 1000)) / 1000);
  // nf() formats numbers with leading zeros (e.g., 5 -> "05")
  let formattedTime = nf(minutes, 2) + ':' + nf(seconds, 2);
  fill(0); // Black color for the timer text
  text(formattedTime, width / 2, 50);

  // Ball animation logic
  // The ball moves only if the timer is still running and explosion is not active
  if (remainingTimeMs > 0 && !explosionActive) {
    // Update ball position
    ballX += ballSpeedX;
    ballY += ballSpeedY;

    // Handle bouncing off edges
    if (ballX + ballRadius > width || ballX - ballRadius < 0) {
      ballSpeedX *= -1; // Reverse horizontal speed
    }
    if (ballY + ballRadius > height || ballY - ballRadius < 0) {
      ballSpeedY *= -1; // Reverse vertical speed
    }

    // Change ball color to red when 4 seconds remain
    if (remainingTimeMs <= 4000) {
      fill(255, 0, 0); // Red
    } else {
      fill(255, 255, 0); // Yellow (Changed from blue)
    }
    noStroke(); // No outline for the ball
    ellipse(ballX, ballY, ballRadius * 2); // Draw the ball

    // --- Draw Smiley Face ---
    // Draw face features only when the ball is not exploding
    if (remainingTimeMs > 0 && !explosionActive) {
      stroke(0); // Black color for face features
      strokeWeight(ballRadius * 0.05); // Adjust stroke weight relative to ball size
      fill(0); // Black color for eyes

      // Left Eye
      ellipse(ballX - ballRadius * 0.3, ballY - ballRadius * 0.2, ballRadius * 0.2);
      // Right Eye
      ellipse(ballX + ballRadius * 0.3, ballY - ballRadius * 0.2, ballRadius * 0.2);

      // Mouth
      noFill(); // No fill for the mouth
      arc(ballX, ballY + ballRadius * 0.2, ballRadius * 0.8, ballRadius * 0.6, 0, PI);
    }
    // --- End Smiley Face ---

  } else if (remainingTimeMs <= 0 && !explosionActive) {
    // Timer reached 0, initiate the explosion
    explosionActive = true;
    explosionStartTime = millis(); // Record explosion start time
    ballSpeedX = 0; // Stop the ball's movement
    ballSpeedY = 0;
  }

  // Explosion animation logic
  if (explosionActive) {
    let explosionElapsed = millis() - explosionStartTime;

    // Draw the explosion effect: multiple expanding circles
    for (let i = 0; i < 5; i++) {
      // Map elapsed time to increasing size and decreasing alpha (transparency)
      let size = map(explosionElapsed, 0, explosionDuration, ballRadius * 2, ballRadius * 10);
      let alpha = map(explosionElapsed, 0, explosionDuration, 255, 0);
      fill(255, 100, 0, alpha); // Orange color fading out
      noStroke(); // No outline for explosion circles
      ellipse(ballX, ballY, size * (1 + i * 0.2)); // Draw multiple circles with slight size variations
    }

    // Stop the explosion and animation after its duration
    if (explosionElapsed >= explosionDuration) {
      noLoop(); // Stop the draw loop to freeze the animation
      fill(0); // Black color for final message
      textSize(48);
      text("TIME'S UP!", width / 2, height / 2); // Display final message
    }
  }
}
Line-by-line explanation (34 lines)

🔧 Subcomponents:

calculation Time calculation let elapsedTimeMs = millis() - startTime;

Subtracts the start time from the current time to find how much time has passed

calculation Time formatting let minutes = floor(remainingTimeMs / (60 * 1000));

Converts milliseconds into minutes by dividing by 60,000 and rounding down

for-loop Ball position update ballX += ballSpeedX;

Moves the ball horizontally each frame by adding velocity

conditional Collision detection if (ballX + ballRadius > width || ballX - ballRadius < 0) {

Checks if the ball has hit the left or right edge and reverses direction

conditional Color state change if (remainingTimeMs <= 4000) {

Turns the ball red when 4 seconds or less remain, creating urgency

calculation Smiley face drawing ellipse(ballX - ballRadius * 0.3, ballY - ballRadius * 0.2, ballRadius * 0.2);

Draws the left eye of the smiley face at a position relative to the ball

for-loop Explosion ring animation for (let i = 0; i < 5; i++) {

Draws 5 concentric expanding circles that grow and fade to create the burst effect

calculation Size interpolation let size = map(explosionElapsed, 0, explosionDuration, ballRadius * 2, ballRadius * 10);

Scales the explosion circles from small (2x ball radius) to large (10x) as time passes

background(220); // Light gray background
Fills the entire canvas with light gray, erasing the previous frame so animation appears smooth
let elapsedTimeMs = millis() - startTime;
Calculates how many milliseconds have passed since setup() recorded startTime
remainingTimeMs = max(0, TIMER_DURATION_MS - elapsedTimeMs); // Ensure time doesn't go negative
Subtracts elapsed time from the total duration; max() prevents the timer from going below zero
let minutes = floor(remainingTimeMs / (60 * 1000));
Converts remaining milliseconds to minutes by dividing by 60,000 (ms per minute) and rounding down
let seconds = floor((remainingTimeMs % (60 * 1000)) / 1000);
The % operator gives the leftover milliseconds after removing full minutes, then divides by 1000 to get seconds
let formattedTime = nf(minutes, 2) + ':' + nf(seconds, 2);
nf() formats numbers with leading zeros (e.g., 5 becomes '05'), so the display always shows MM:SS
text(formattedTime, width / 2, 50);
Draws the timer text centered horizontally and 50 pixels from the top
if (remainingTimeMs > 0 && !explosionActive) {
This block only runs while the timer is running AND the explosion hasn't started yet
ballX += ballSpeedX;
Updates the ball's X position by adding its horizontal velocity, creating leftward/rightward motion
ballY += ballSpeedY;
Updates the ball's Y position by adding its vertical velocity, creating upward/downward motion
if (ballX + ballRadius > width || ballX - ballRadius < 0) {
Checks if the right edge of the ball has passed the right canvas edge OR the left edge has passed the left edge
ballSpeedX *= -1; // Reverse horizontal speed
Multiplying by -1 flips the sign, making a positive velocity negative (or vice versa) to bounce the ball back
if (remainingTimeMs <= 4000) {
When 4 seconds or less remain, the condition is true, triggering the red color
fill(255, 0, 0); // Red
Sets the fill color to pure red (RGB: red=255, green=0, blue=0)
} else {
If more than 4 seconds remain, this else block executes
fill(255, 255, 0); // Yellow (Changed from blue)
Sets the fill color to yellow (RGB: red=255, green=255, blue=0) for the first part of the countdown
ellipse(ballX, ballY, ballRadius * 2); // Draw the ball
Draws a circle (ellipse) centered at the ball's current position with diameter = ballRadius * 2
ellipse(ballX - ballRadius * 0.3, ballY - ballRadius * 0.2, ballRadius * 0.2);
Draws the left eye: offset left (- 0.3) and up (- 0.2) from the ball center, with small diameter (0.2)
ellipse(ballX + ballRadius * 0.3, ballY - ballRadius * 0.2, ballRadius * 0.2);
Draws the right eye: offset right (+ 0.3) and up, matching the left eye position
arc(ballX, ballY + ballRadius * 0.2, ballRadius * 0.8, ballRadius * 0.6, 0, PI);
Draws a curved arc (smile) from 0 to PI radians (a semi-circle), positioned below the eyes
} else if (remainingTimeMs <= 0 && !explosionActive) {
When the timer hits exactly zero and the explosion hasn't started yet, this triggers the explosion setup
explosionActive = true;
Sets the flag to true, which prevents the ball from moving and activates the explosion animation
explosionStartTime = millis(); // Record explosion start time
Stores the current time so we can measure how long the explosion has been running
ballSpeedX = 0; // Stop the ball's movement
Sets horizontal velocity to zero so the ball stops bouncing when the explosion begins
if (explosionActive) {
This entire block runs continuously once the explosion has been triggered
let explosionElapsed = millis() - explosionStartTime;
Calculates how many milliseconds have passed since the explosion started
for (let i = 0; i < 5; i++) {
Loops 5 times (i = 0, 1, 2, 3, 4) to draw 5 concentric circles with slightly different sizes
let size = map(explosionElapsed, 0, explosionDuration, ballRadius * 2, ballRadius * 10);
map() converts a value from one range to another: as explosionElapsed goes from 0 to 2000ms, size goes from 2x to 10x the ball radius
let alpha = map(explosionElapsed, 0, explosionDuration, 255, 0);
As time progresses, alpha (transparency) decreases from fully opaque (255) to fully transparent (0), making the burst fade away
fill(255, 100, 0, alpha); // Orange color fading out
Sets the fill to orange (red=255, green=100, blue=0) with the calculated alpha value, so it fades as it grows
ellipse(ballX, ballY, size * (1 + i * 0.2)); // Draw multiple circles with slight size variations
Draws expanding circles: the first (i=0) has size 1.0x, second is 1.2x, third is 1.4x, etc., creating a ring effect
if (explosionElapsed >= explosionDuration) {
After the explosion has been running for 2 seconds (2000ms), this condition becomes true
noLoop(); // Stop the draw loop to freeze the animation
Stops calling draw() repeatedly, freezing the animation permanently so the final message stays on screen
text("TIME'S UP!", width / 2, height / 2); // Display final message
Draws the final message centered on the screen after the explosion ends and the loop stops

windowResized()

windowResized() is a special p5.js function that fires whenever the user resizes their browser window. This sketch uses it to keep the timer responsive and centered, rescaling the ball and repositioning it if needed. This is crucial for maintaining a polished experience across different screen sizes. Notice how the function handles multiple states differently: during explosion, the ball is re-centered; during the running timer, the ball is constrained to stay in bounds to prevent it from being cut off on a smaller screen.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight); // Resize canvas to new window dimensions
  // Recalculate ball radius based on new dimensions
  ballRadius = min(width, height) / 10;
  // If explosion is active, ensure it stays centered
  if (explosionActive) {
    ballX = width / 2;
    ballY = height / 2;
  }
  // If timer is running, keep ball centered or adjust its position to stay within bounds
  else if (remainingTimeMs > 0) {
    ballX = constrain(ballX, ballRadius, width - ballRadius);
    ballY = constrain(ballY, ballRadius, height - ballRadius);
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Canvas resizing resizeCanvas(windowWidth, windowHeight); // Resize canvas to new window dimensions

Updates the canvas to match the new browser window size

calculation Radius recalculation ballRadius = min(width, height) / 10;

Recalculates ball size based on the new canvas dimensions to maintain proportions

conditional Explosion centering if (explosionActive) {

When exploding, the ball is repositioned to the exact center after resizing

calculation Ball boundary constraint ballX = constrain(ballX, ballRadius, width - ballRadius);

Ensures the ball stays within canvas bounds if the window shrinks while the timer is running

function windowResized() {
This special p5.js function is called automatically whenever the browser window is resized
resizeCanvas(windowWidth, windowHeight); // Resize canvas to new window dimensions
Updates the canvas size to match the new window width and height, maintaining the fullscreen effect
ballRadius = min(width, height) / 10;
Recalculates the ball's radius based on the new canvas size so it stays proportionally sized
if (explosionActive) {
If the explosion is currently playing, this block runs
ballX = width / 2;
Re-centers the explosion horizontally in the middle of the new canvas size
ballY = height / 2;
Re-centers the explosion vertically in the middle of the new canvas size
else if (remainingTimeMs > 0) {
If the timer is still running (not exploding), this block constrains the ball to stay on screen
ballX = constrain(ballX, ballRadius, width - ballRadius);
constrain() keeps ballX between ballRadius (minimum, to stay on the left) and width - ballRadius (maximum, to stay on the right)
ballY = constrain(ballY, ballRadius, height - ballRadius);
Keeps ballY within vertical bounds so the ball doesn't get stuck off-screen when the window shrinks

📦 Key Variables

TIMER_DURATION_MS number

Stores the total countdown duration in milliseconds (5 minutes = 300,000 ms)—change this to make the timer longer or shorter

let TIMER_DURATION_MS = 5 * 60 * 1000; // 5 minutes
startTime number

Records the milliseconds value when setup() runs, used as a reference point to calculate elapsed time

startTime = millis();
remainingTimeMs number

Stores how many milliseconds are left on the timer (updated every frame, counts down from TIMER_DURATION_MS to 0)

let remainingTimeMs;
ballX number

The horizontal position (x-coordinate) of the smiley ball's center on the canvas

ballX = width / 2;
ballY number

The vertical position (y-coordinate) of the smiley ball's center on the canvas

ballY = height / 2;
ballSpeedX number

How many pixels the ball moves horizontally per frame (positive = right, negative = left); reversed on wall collision

ballSpeedX = random(3, 7) * (random() > 0.5 ? 1 : -1);
ballSpeedY number

How many pixels the ball moves vertically per frame (positive = down, negative = up); reversed on wall collision

ballSpeedY = random(3, 7) * (random() > 0.5 ? 1 : -1);
ballRadius number

Half the diameter of the smiley ball—used for drawing the ball and sizing the facial features proportionally

ballRadius = min(width, height) / 10;
explosionActive boolean

A flag (true/false) that tracks whether the explosion animation is currently playing; when true, the ball stops moving and the burst is drawn

let explosionActive = false;
explosionStartTime number

Records the milliseconds value when the explosion begins, used to measure how long the explosion has been running

explosionStartTime = millis();
explosionDuration number

How long the explosion animation lasts in milliseconds (2000 ms = 2 seconds) before the animation freezes on 'TIME'S UP!'

let explosionDuration = 2000;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG draw() - collision detection

The ball can get stuck in walls if velocity is very high, because the position update can overshoot the boundary in a single frame

💡 Clamp the ball position back inside the canvas after bouncing: ballX = constrain(ballX, ballRadius, width - ballRadius);

PERFORMANCE draw() - explosion loop

After the explosion ends and noLoop() is called, the loop still runs one more frame but doesn't draw anything visible—a minor waste

💡 Move the final text() call outside the if (explosionActive) block so it only draws after noLoop() has fully stopped

STYLE variables - global state

Multiple global variables (explosionActive, explosionStartTime, ballX, ballY, etc.) make it hard to track state—as the sketch grows, this becomes messy

💡 Consider grouping related variables into objects: let ball = {x, y, speedX, speedY, radius}; let explosion = {active, startTime};

FEATURE draw() - smiley face animation

The smiley face is static—it doesn't blink or smile wider as time runs out, missing opportunities for personality and urgency feedback

💡 Use frameCount or remainingTimeMs to animate the eyes: make them blink every few frames, or widen the smile in the final seconds

BUG setup() - random velocity

It's theoretically possible (though unlikely) that random(3, 7) could generate 3 for both axes, making the ball move diagonally slowly

💡 Add a minimum magnitude check or use vector math to ensure the ball always moves at a consistent overall speed

🔄 Code Flow

Code flow showing setup, draw, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> time-calc[Time Calculation] draw --> time-format[Time Formatting] draw --> ball-init[Ball Initialization] draw --> random-velocity[Random Velocity Assignment] draw --> ball-movement[Ball Movement] draw --> collision-check[Collision Check] draw --> color-change[Color State Change] draw --> smiley-draw[Smiley Draw] draw --> explosion-loop[Explosion Loop] explosion-loop --> map-size[Size Interpolation] click setup href "#fn-setup" click draw href "#fn-draw" click time-calc href "#sub-time-calc" click time-format href "#sub-time-format" click ball-init href "#sub-ball-init" click random-velocity href "#sub-random-velocity" click ball-movement href "#sub-ball-movement" click collision-check href "#sub-collision-check" click color-change href "#sub-color-change" click smiley-draw href "#sub-smiley-draw" click explosion-loop href "#sub-explosion-loop" click map-size href "#sub-map-size" setup --> windowresized[windowResized] windowresized --> canvas-resize[Canvas Resizing] canvas-resize --> radius-recalc[Radius Recalculation] radius-recalc --> explosion-center[Explosion Centering] explosion-center --> ball-constrain[Ball Boundary Constraint] click windowresized href "#fn-windowresized" click canvas-resize href "#sub-canvas-resize" click radius-recalc href "#sub-radius-recalc" click explosion-center href "#sub-explosion-center" click ball-constrain href "#sub-ball-constrain"

❓ Frequently Asked Questions

What visual effects can users expect from the exploding smiley face sketch?

The sketch features a vibrant animated ball that bounces around the canvas, with a countdown timer that culminates in an explosion effect.

How can users interact with the exploding smiley face sketch?

While the sketch primarily runs automatically, users can observe the timer counting down and witness the ball's movement until it 'explodes' at the end of the timer.

What creative coding concepts does the exploding smiley face sketch illustrate?

This sketch demonstrates concepts such as animation, real-time countdown timers, and collision detection within a dynamic visual environment.

Preview

exploding smiley face 5 min timer - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of exploding smiley face 5 min timer - Code flow showing setup, draw, windowresized
Code Flow Diagram