low-key timer

This sketch combines a interactive countdown timer interface with an animated bouncing circle that dances across the full-screen canvas. Users can set a custom duration, start or reset the timer, and watch the lively motion continue as seconds tick away in MM:SS format.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the timer display huge — The font size controls how large the countdown numbers appear on screen—larger text makes the timer more visible
  2. Change the circle to red — The fill() function sets the color of the shape—RGB values (255,0,0) create red, (0,255,0) create green, and so on
  3. Make the circle bounce faster
  4. Darken the background — The background() function's number sets the canvas brightness—0 is black, 255 is white, numbers in between are shades of gray
  5. Double the circle's size — bounceRadius controls how big the circle is—increasing it makes the circle larger and changes how soon it hits canvas edges
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a soft, glowing timer interface layered over a dynamic background where a colorful circle bounces endlessly around the canvas. It combines DOM elements (input fields and buttons) with p5.js drawing, teaching you how to blend HTML controls with animations. The bouncing circle uses collision detection to reverse direction at canvas edges, while the timer uses millis() to track elapsed time and automatically stops the animation when time expires.

The code is organized into a setup() function that creates both the canvas and interactive UI elements, a draw() function that updates the timer display and bounces the circle every frame, and three event handler functions (startTimer, resetTimer, formatTime) that respond to user input. By studying it, you will learn how to create DOM elements with p5.js, layer them with z-index, respond to button clicks, and synchronize multiple animations in a single sketch.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-screen canvas positioned behind the UI and builds a centered interface panel with an input field, two buttons, and a large timer display showing the default 60 seconds in MM:SS format.
  2. Every frame, draw() clears the canvas and checks if the timer is running—if it is, it subtracts the elapsed time (measured with millis()) from the total duration and updates the display.
  3. The bouncing circle updates its position by adding velocity to its x and y coordinates on every frame, creating smooth continuous motion.
  4. When the circle reaches a canvas edge, two if-statements detect the boundary and flip the velocity to make it bounce back into view.
  5. If a user clicks 'Start Timer', the startTimer() function records the current time, sets the timer running, and gives the bouncing circle a fresh random velocity.
  6. When the countdown reaches zero, the timer stops, the display shows 'Time's Up!', and the bouncing circle freezes in place. Clicking 'Reset Timer' stops the clock and restores everything to its initial state.

🎓 Concepts You'll Learn

DOM elements and p5.jsEvent handlers (mousePressed)Collision detection and bouncingTime tracking with millis()Layering with z-index and absolute positioningString formattingCanvas resizing and responsive design

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It is the perfect place to create your canvas, initialize variables, and build any interactive DOM elements (buttons, inputs, text fields) that p5.js provides. Notice how createDiv(), createButton(), and createInput() let you build an entire HTML interface directly in JavaScript without touching index.html.

🔬 These three lines use z-index to layer the canvas behind the UI. What happens if you change '-1' to '1' so the canvas appears on top of the buttons instead?

  canvasContainer.style('z-index', '-1'); // Send canvas to background
  let canvas = createCanvas(windowWidth, windowHeight);
  canvas.parent(canvasContainer);
function setup() {
  // Create canvas and parent it to a div. This div will be placed behind the UI.
  // Using position: absolute and z-index: -1 ensures the canvas fills the background
  // and doesn't interfere with the UI elements.
  let canvasContainer = createDiv();
  canvasContainer.style('position', 'absolute');
  canvasContainer.style('top', '0');
  canvasContainer.style('left', '0');
  canvasContainer.style('z-index', '-1'); // Send canvas to background
  let canvas = createCanvas(windowWidth, windowHeight);
  canvas.parent(canvasContainer);

  // --- UI Elements ---
  // Create a container div for all UI elements.
  // We'll style this div using absolute positioning to center it on the screen.
  let uiContainer = createDiv();
  uiContainer.style('padding', '20px');
  uiContainer.style('background-color', 'rgba(255, 255, 255, 0.8)'); // Semi-transparent background
  uiContainer.style('border-radius', '10px');
  uiContainer.style('position', 'absolute');
  uiContainer.style('top', '50%');
  uiContainer.style('left', '50%');
  uiContainer.style('transform', 'translate(-50%, -50%)'); // Center the div
  uiContainer.style('text-align', 'center');
  uiContainer.style('box-shadow', '0 4px 8px rgba(0,0,0,0.2)'); // Add a subtle shadow

  // Label for the duration input
  let durationLabel = createElement('label', 'Set Timer (seconds): ');
  durationLabel.style('display', 'block'); // Make it take full width
  durationLabel.style('margin-bottom', '10px');
  durationLabel.parent(uiContainer);

  // Input field for setting the timer duration
  durationInput = createInput(timerDuration.toString(), 'number');
  durationInput.attribute('min', '1'); // Minimum value is 1 second
  durationInput.attribute('step', '1'); // Allow only whole numbers
  durationInput.style('width', '100px');
  durationInput.style('padding', '8px');
  durationInput.style('margin-bottom', '15px');
  durationInput.style('border', '1px solid #ccc');
  durationInput.style('border-radius', '4px');
  durationInput.parent(uiContainer);

  // Start Button
  startButton = createButton('Start Timer');
  startButton.style('padding', '10px 20px');
  startButton.style('margin', '5px');
  startButton.style('background-color', '#4CAF50'); // Green color
  startButton.style('color', 'white');
  startButton.style('border', 'none');
  startButton.style('border-radius', '5px');
  startButton.style('cursor', 'pointer');
  startButton.style('font-size', '1em');
  startButton.parent(uiContainer);
  startButton.mousePressed(startTimer); // Attach the startTimer function to click event

  // Reset Button
  resetButton = createButton('Reset Timer');
  resetButton.style('padding', '10px 20px');
  resetButton.style('margin', '5px');
  resetButton.style('background-color', '#f44336'); // Red color
  resetButton.style('color', 'white');
  resetButton.style('border', 'none');
  resetButton.style('border-radius', '5px');
  resetButton.style('cursor', 'pointer');
  resetButton.style('font-size', '1em');
  resetButton.parent(uiContainer);
  resetButton.mousePressed(resetTimer); // Attach the resetTimer function to click event

  // Timer Display
  timerDisplay = createDiv(formatTime(timerDuration)); // Initialize with default duration
  timerDisplay.style('font-size', '3em');
  timerDisplay.style('font-weight', 'bold');
  timerDisplay.style('margin-top', '20px');
  timerDisplay.style('color', '#333');
  timerDisplay.parent(uiContainer);

  // --- Initialize Bouncing Shape ---
  // Start the bouncing shape at the center of the canvas
  bounceX = width / 2;
  bounceY = height / 2;
  // Give it a random initial velocity
  bounceVX = random(3, 7) * (random() > 0.5 ? 1 : -1); // Random speed between 3 and 7, random direction
  bounceVY = random(3, 7) * (random() > 0.5 ? 1 : -1);
}
Line-by-line explanation (19 lines)

🔧 Subcomponents:

calculation Canvas and z-index layering canvasContainer.style('z-index', '-1');

Places the canvas behind the UI by assigning it a negative z-index, so buttons and text appear on top

calculation Center UI panel on screen uiContainer.style('transform', 'translate(-50%, -50%)');

Mathematically centers the panel horizontally and vertically by offsetting its position by half its own width and height

calculation Connect button clicks to functions startButton.mousePressed(startTimer);

Attaches the startTimer function so it runs when the user clicks the Start button

calculation Random initial velocity bounceVX = random(3, 7) * (random() > 0.5 ? 1 : -1);

Generates a random speed between 3 and 7, then randomly chooses a direction (positive or negative) so the circle bounces in an unpredictable direction

let canvasContainer = createDiv();
Creates an empty HTML div that will hold the p5.js canvas, allowing us to position it independently of the UI
canvasContainer.style('z-index', '-1');
Sets the z-index (layering depth) to -1, so the canvas appears behind everything else on the page
let canvas = createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas that fills the entire browser window using windowWidth and windowHeight
canvas.parent(canvasContainer);
Attaches the canvas to the canvasContainer div, making it a child element so the z-index layering works correctly
let uiContainer = createDiv();
Creates a new div to hold all the UI controls (input, buttons, timer display) in one organized container
uiContainer.style('position', 'absolute');
Makes the UI container position itself relative to the window rather than the normal document flow, allowing free placement
uiContainer.style('top', '50%');
Positions the top of the container 50% down the page (not quite centered because we haven't offset for its height yet)
uiContainer.style('left', '50%');
Positions the left edge of the container 50% across the page width
uiContainer.style('transform', 'translate(-50%, -50%)');
Shifts the container left by 50% of its own width and up by 50% of its own height, centering it perfectly on screen
let durationLabel = createElement('label', 'Set Timer (seconds): ');
Creates an HTML label element with the text 'Set Timer (seconds): ' to describe the input field below it
durationInput = createInput(timerDuration.toString(), 'number');
Creates a text input field of type 'number' and pre-fills it with the current timerDuration value converted to a string
durationInput.attribute('min', '1');
Sets a minimum value of 1 second—the browser won't allow the user to enter lower values
durationInput.attribute('step', '1');
Restricts input to whole numbers only, preventing decimal values like 30.5 seconds
startButton.mousePressed(startTimer);
Registers the startTimer function to run whenever the user clicks the Start Timer button
resetButton.mousePressed(resetTimer);
Registers the resetTimer function to run whenever the user clicks the Reset Timer button
timerDisplay = createDiv(formatTime(timerDuration));
Creates a large text display and initializes it by calling formatTime() to show the timer duration in MM:SS format
bounceX = width / 2;
Places the bouncing circle's starting x-position at the horizontal center of the canvas
bounceY = height / 2;
Places the bouncing circle's starting y-position at the vertical center of the canvas
bounceVX = random(3, 7) * (random() > 0.5 ? 1 : -1);
Generates a random horizontal velocity between 3 and 7 pixels per frame, then randomly flips its sign so the circle moves left or right with equal probability

draw()

draw() runs 60 times per second (by default), making it the engine of your animation. Every line inside draw() executes from top to bottom each frame, updating positions, checking conditions, and redrawing everything. This is where the timer countdown ticks and the circle bounces.

🔬 These four lines ARE the bounce—when the circle hits an edge, the velocity flips sign. What happens if you change the *= -1 to *= 0.8 so the circle loses energy and bounces smaller with each hit?

  if (bounceX > width - bounceRadius || bounceX < bounceRadius) {
    bounceVX *= -1; // Reverse X velocity
  }
  if (bounceY > height - bounceRadius || bounceY < bounceRadius) {
    bounceVY *= -1; // Reverse Y velocity
  }
function draw() {
  background(220); // Clear canvas each frame

  // --- Timer Logic ---
  let remainingTime = timerDuration * 1000; // Total duration in milliseconds

  if (timerStarted) {
    let elapsedTime = millis() - startTime; // Time passed since timer started
    remainingTime = timerDuration * 1000 - elapsedTime; // Remaining time

    // If timer has run out
    if (remainingTime <= 0) {
      timerStarted = false; // Stop the timer
      remainingTime = 0;    // Ensure remaining time doesn't go negative
      timerDisplay.html("Time's Up!"); // Display "Time's Up!"
      // Optionally, stop or change the bouncing shape's behavior
      bounceVX = 0;
      bounceVY = 0;
      return; // Exit draw loop early to prevent further calculations if timer is done
    }
  }
  // Update the timer display with the formatted time
  timerDisplay.html(formatTime(remainingTime / 1000));

  // --- Bouncing Shape Logic ---
  bounceX += bounceVX; // Update position based on velocity
  bounceY += bounceVY;

  // Check for canvas boundaries and reverse velocity if an edge is hit
  if (bounceX > width - bounceRadius || bounceX < bounceRadius) {
    bounceVX *= -1; // Reverse X velocity
  }
  if (bounceY > height - bounceRadius || bounceY < bounceRadius) {
    bounceVY *= -1; // Reverse Y velocity
  }

  // Draw the bouncing circle
  fill(0, 102, 153); // Blue color
  noStroke();
  ellipse(bounceX, bounceY, bounceRadius * 2, bounceRadius * 2); // Diameter is 2 * radius
}
Line-by-line explanation (21 lines)

🔧 Subcomponents:

calculation Calculate elapsed time let elapsedTime = millis() - startTime;

Computes how many milliseconds have passed since the timer started by subtracting the recorded start time from the current system time

calculation Calculate remaining time remainingTime = timerDuration * 1000 - elapsedTime;

Subtracts elapsed time from the total duration to find how many milliseconds are left before the countdown reaches zero

conditional Check if timer expired if (remainingTime <= 0) {

Detects when the countdown has finished so the sketch can stop the timer and freeze the bouncing circle

calculation Update circle position bounceX += bounceVX;

Moves the circle horizontally each frame by adding its velocity to its current x-position

conditional Detect horizontal edge collision if (bounceX > width - bounceRadius || bounceX < bounceRadius) {

Checks if the circle has reached the left or right edge and needs to bounce back

conditional Detect vertical edge collision if (bounceY > height - bounceRadius || bounceY < bounceRadius) {

Checks if the circle has reached the top or bottom edge and needs to bounce back

background(220);
Clears the canvas with a light gray color (220 on a 0-255 scale) each frame, erasing the previous frame's drawing and preventing motion trails
let remainingTime = timerDuration * 1000;
Initializes remainingTime to the full timer duration converted to milliseconds (multiply by 1000 because millis() returns milliseconds, not seconds)
if (timerStarted) {
Checks if the timer is currently running—the code inside only executes if the user has clicked 'Start Timer' and time hasn't run out yet
let elapsedTime = millis() - startTime;
Calculates how many milliseconds have elapsed by subtracting the time the timer started from the current system time
remainingTime = timerDuration * 1000 - elapsedTime;
Updates remainingTime to reflect how many milliseconds are left—the full duration minus the time that has already passed
if (remainingTime <= 0) {
Checks if the countdown has reached zero or gone negative, meaning time has expired
timerStarted = false;
Sets the timer flag to false so the timer stops updating and the elapsed time calculation no longer matters
timerDisplay.html("Time's Up!");
Changes the timer display text from MM:SS to 'Time's Up!' to announce that the countdown is complete
bounceVX = 0;
Stops the horizontal motion of the bouncing circle by setting its velocity to zero
bounceVY = 0;
Stops the vertical motion of the bouncing circle by setting its velocity to zero, freezing it in place
return;
Exits the draw() function early, skipping the remaining code (animation updates) so the circle stays frozen
timerDisplay.html(formatTime(remainingTime / 1000));
Updates the on-screen timer display by converting remainingTime from milliseconds back to seconds and formatting it as MM:SS
bounceX += bounceVX;
Updates the circle's horizontal position by adding its velocity—a positive velocity moves it right, negative moves it left
bounceY += bounceVY;
Updates the circle's vertical position by adding its velocity—a positive velocity moves it down, negative moves it up
if (bounceX > width - bounceRadius || bounceX < bounceRadius) {
Checks if the circle's edge (center plus radius) has crossed the left or right boundary of the canvas, accounting for the circle's size
bounceVX *= -1;
Reverses the horizontal velocity by multiplying it by -1, so the circle bounces off the left or right edge
if (bounceY > height - bounceRadius || bounceY < bounceRadius) {
Checks if the circle's edge (center plus radius) has crossed the top or bottom boundary of the canvas
bounceVY *= -1;
Reverses the vertical velocity by multiplying it by -1, so the circle bounces off the top or bottom edge
fill(0, 102, 153);
Sets the fill color to a medium blue (RGB values: 0 red, 102 green, 153 blue) for all shapes drawn after this line
noStroke();
Disables the outline stroke so the circle is filled with solid blue with no border
ellipse(bounceX, bounceY, bounceRadius * 2, bounceRadius * 2);
Draws a circle centered at (bounceX, bounceY) with width and height both equal to bounceRadius * 2 (the diameter), creating a perfectly round shape

startTimer()

startTimer() is an event handler—it runs whenever the user clicks the 'Start Timer' button. It reads user input, validates it, and then updates the global variables that control the timer and animation. Always validate user input before using it, because users will try to break your sketch!

🔬 This validation block prevents invalid input. What happens if you remove the 'return' statement so execution continues even when the input is invalid? What error do you get?

  if (isNaN(newDuration) || newDuration <= 0) {
    alert("Please enter a positive number for the timer duration.");
    return;
  }
function startTimer() {
  // Parse the value from the input field
  let newDuration = parseInt(durationInput.value());

  // Validate the input
  if (isNaN(newDuration) || newDuration <= 0) {
    alert("Please enter a positive number for the timer duration.");
    return;
  }

  timerDuration = newDuration; // Update the timer duration
  startTime = millis();        // Record the current time
  timerStarted = true;         // Set timer state to started

  // Reset bouncing shape speed to start it moving again if it stopped
  bounceVX = random(3, 7) * (random() > 0.5 ? 1 : -1);
  bounceVY = random(3, 7) * (random() > 0.5 ? 1 : -1);
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Parse user input let newDuration = parseInt(durationInput.value());

Reads the text the user typed into the input field and converts it to a whole number

conditional Validate timer duration if (isNaN(newDuration) || newDuration <= 0) {

Checks that the user entered a valid positive number—if not, shows an error message and stops execution

calculation Record start time startTime = millis();

Captures the current system time in milliseconds so we can measure how much time has passed later

calculation Randomize bouncing circle velocity bounceVX = random(3, 7) * (random() > 0.5 ? 1 : -1);

Gives the bouncing circle a fresh random speed and direction each time the timer starts, so it never bounces the same way twice

let newDuration = parseInt(durationInput.value());
Reads whatever the user typed into the duration input field and converts it from text to a whole number using parseInt()
if (isNaN(newDuration) || newDuration <= 0) {
Checks two conditions: isNaN() tests if the conversion to a number failed (user typed letters instead), and newDuration <= 0 tests if the number is zero or negative
alert("Please enter a positive number for the timer duration.");
If the input is invalid, displays a browser alert dialog box with an error message
return;
Exits the startTimer() function immediately without running the rest, preventing invalid data from being used
timerDuration = newDuration;
Stores the user's input as the new countdown duration, replacing the previous value
startTime = millis();
Records the current system time (in milliseconds since the sketch started) so we can calculate how much time has passed later
timerStarted = true;
Sets the timer flag to true, signaling to draw() that the countdown is running and elapsed time should be calculated
bounceVX = random(3, 7) * (random() > 0.5 ? 1 : -1);
Generates a new random horizontal velocity between 3 and 7, then randomly chooses its direction (positive or negative)
bounceVY = random(3, 7) * (random() > 0.5 ? 1 : -1);
Generates a new random vertical velocity so the circle starts moving in a fresh direction when the timer begins

resetTimer()

resetTimer() is another event handler that runs when the user clicks 'Reset Timer'. It clears the countdown, restores the starting values, and resets the bouncing circle to create a fresh start. Notice how it validates input and provides a fallback value—defensive programming like this makes your sketch robust.

function resetTimer() {
  timerStarted = false; // Stop the timer

  // Reset timerDuration to the current value in the input field
  timerDuration = parseInt(durationInput.value());
  if (isNaN(timerDuration) || timerDuration <= 0) {
    timerDuration = 60; // Fallback to 60 seconds if input is invalid
    durationInput.value(timerDuration.toString()); // Update input field
  }

  timerDisplay.html(formatTime(timerDuration)); // Update display to show initial time

  // Reset bouncing shape position and speed
  bounceX = width / 2;
  bounceY = height / 2;
  bounceVX = random(3, 7) * (random() > 0.5 ? 1 : -1);
  bounceVY = random(3, 7) * (random() > 0.5 ? 1 : -1);
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Stop the countdown timerStarted = false;

Sets the timer flag to false, pausing the countdown without losing the current duration value

calculation Read and validate duration timerDuration = parseInt(durationInput.value());

Reads whatever duration is currently in the input field and uses it as the reset value

conditional Validate and fallback if (isNaN(timerDuration) || timerDuration <= 0) {

Checks if the input is invalid and provides a sensible default (60 seconds) if it is

calculation Reset circle position bounceX = width / 2;

Places the bouncing circle back at the center of the canvas, undoing any movement

timerStarted = false;
Stops the countdown by setting the timer flag to false, causing draw() to stop updating the elapsed time
timerDuration = parseInt(durationInput.value());
Reads the current value in the duration input field and converts it to a number
if (isNaN(timerDuration) || timerDuration <= 0) {
Checks if the input is invalid (not a number or zero/negative)
timerDuration = 60;
If the input is invalid, sets the duration to a safe default of 60 seconds instead of crashing
durationInput.value(timerDuration.toString());
Updates the input field to show 60, so the user can see what value was used
timerDisplay.html(formatTime(timerDuration));
Updates the on-screen timer display to show the full duration in MM:SS format (e.g., 01:00 for 60 seconds)
bounceX = width / 2;
Moves the bouncing circle's x-position back to the horizontal center of the canvas
bounceY = height / 2;
Moves the bouncing circle's y-position back to the vertical center of the canvas
bounceVX = random(3, 7) * (random() > 0.5 ? 1 : -1);
Generates a new random horizontal velocity so the circle will bounce in a fresh direction when the sketch resumes
bounceVY = random(3, 7) * (random() > 0.5 ? 1 : -1);
Generates a new random vertical velocity to complete the reset

formatTime()

formatTime() is a utility function—it doesn't draw anything or respond to input, but instead transforms data into a human-readable format. p5.js's nf() function pads numbers with leading zeros, which is why the timer displays '01:05' instead of '1:5'. This is the kind of small helper function that makes code cleaner and reusable.

🔬 These two lines split the total seconds into minutes and remaining seconds. Try removing the floor() function—what happens to the display when there are fractional seconds? Why is floor() important here?

  let minutes = floor(seconds / 60);
  let remainingSeconds = floor(seconds % 60);
function formatTime(seconds) {
  let minutes = floor(seconds / 60);
  let remainingSeconds = floor(seconds % 60);
  // nf() formats a number to a string with a specified number of digits
  return nf(minutes, 2) + ':' + nf(remainingSeconds, 2);
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Extract minutes let minutes = floor(seconds / 60);

Divides the total seconds by 60 and rounds down to find how many complete minutes have passed

calculation Extract remaining seconds let remainingSeconds = floor(seconds % 60);

Uses the modulo operator (%) to find the leftover seconds after accounting for minutes (e.g., 125 seconds becomes 2:05)

calculation Format and concatenate return nf(minutes, 2) + ':' + nf(remainingSeconds, 2);

Pads both numbers with leading zeros (so 5 becomes '05') and joins them with a colon to create MM:SS format

function formatTime(seconds) {
Declares a helper function that accepts the time in seconds and returns it formatted as a string in MM:SS format
let minutes = floor(seconds / 60);
Calculates how many complete minutes fit into the total seconds by dividing by 60 and using floor() to round down
let remainingSeconds = floor(seconds % 60);
Uses the modulo operator (%) to find the leftover seconds after removing all complete minutes—for example, 125 % 60 = 5
return nf(minutes, 2) + ':' + nf(remainingSeconds, 2);
Concatenates the formatted numbers with a colon: nf() pads each number to 2 digits with leading zeros (so 5 becomes '05'), then joins them as 'MM:SS'

windowResized()

windowResized() is a special p5.js callback function that runs automatically whenever the user resizes their browser window. Without it, the canvas would stay its original size and not adapt to the viewport. This is essential for responsive sketches that look good on phones, tablets, and desktop screens.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Re-initialize bouncing shape position to keep it in view
  bounceX = width / 2;
  bounceY = height / 2;
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Resize canvas resizeCanvas(windowWidth, windowHeight);

Updates the p5.js canvas to match the new browser window dimensions when the user resizes their window

calculation Recenter circle bounceX = width / 2;

Moves the bouncing circle back to the center so it doesn't get stranded off-screen after the canvas resizes

function windowResized() {
p5.js automatically calls this function whenever the browser window or viewport is resized
resizeCanvas(windowWidth, windowHeight);
Updates the p5.js canvas size to match the new browser window dimensions (windowWidth and windowHeight are updated by p5.js automatically)
bounceX = width / 2;
Resets the bouncing circle's x-position to the center of the resized canvas
bounceY = height / 2;
Resets the bouncing circle's y-position to the center of the resized canvas

📦 Key Variables

timerDuration number

Stores the countdown duration in seconds—updated when the user clicks 'Start Timer' with a new input value

let timerDuration = 60;
timerStarted boolean

A flag that tracks whether the timer is currently running (true) or stopped (false)—used in draw() to decide whether to update elapsed time

let timerStarted = false;
startTime number

Stores the exact millisecond when the timer was clicked to start, allowing draw() to calculate how much time has passed

let startTime;
timerDisplay object (p5.Element)

A reference to the HTML div element that shows the countdown in MM:SS format—updated every frame by calling .html()

let timerDisplay;
durationInput object (p5.Element)

A reference to the number input field where the user types how many seconds the timer should count down

let durationInput;
startButton object (p5.Element)

A reference to the 'Start Timer' button element—clicking it triggers the startTimer() function

let startButton;
resetButton object (p5.Element)

A reference to the 'Reset Timer' button element—clicking it triggers the resetTimer() function

let resetButton;
bounceX number

The horizontal position (x-coordinate) of the bouncing circle on the canvas, updated every frame by adding bounceVX

let bounceX;
bounceY number

The vertical position (y-coordinate) of the bouncing circle on the canvas, updated every frame by adding bounceVY

let bounceY;
bounceVX number

The horizontal velocity of the bouncing circle in pixels per frame—positive moves right, negative moves left

let bounceVX;
bounceVY number

The vertical velocity of the bouncing circle in pixels per frame—positive moves down, negative moves up

let bounceVY;
bounceRadius number

The radius of the bouncing circle in pixels (diameter is bounceRadius * 2)—used for drawing and collision detection

let bounceRadius = 25;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG startTimer() and resetTimer() validation

If a user enters a very large number (like 999999), the timer will display correctly but the sketch may stutter if it tries to compute enormous elapsed time differences

💡 Add a maximum limit check: if (isNaN(newDuration) || newDuration <= 0 || newDuration > 3600) to cap the timer at one hour

PERFORMANCE draw() timer display update

timerDisplay.html() is called every frame (60 times per second), but the display only needs to update when the seconds actually change

💡 Store the previous secondsValue and only call timerDisplay.html() when it changes: let newSeconds = floor(remainingTime / 1000); if (newSeconds !== lastDisplayedSeconds) { timerDisplay.html(...); lastDisplayedSeconds = newSeconds; }

STYLE bounceVX and bounceVY initialization in setup() and startTimer()

The velocity randomization code is repeated three times (setup, startTimer, resetTimer), violating the DRY principle and making future changes harder

💡 Create a helper function: function randomizeVelocity() { bounceVX = random(3, 7) * (random() > 0.5 ? 1 : -1); bounceVY = random(3, 7) * (random() > 0.5 ? 1 : -1); } and call it in all three places

FEATURE Timer expiration

When the timer expires, only the circle stops moving—there's no audio or visual feedback (flash, color change) to grab the user's attention that time is up

💡 Add visual feedback when remainingTime <= 0: change the background color, flash the timer display, or change the UI container's background color to red to make the timeout more obvious

BUG draw() early return

When the timer expires, return; exits draw() early, preventing the bouncing circle from being drawn—the circle disappears instead of staying frozen at its last position

💡 Remove the return statement so draw() continues to call ellipse(bounceX, bounceY, bounceRadius * 2, bounceRadius * 2) even when the timer has ended, keeping the circle visible and frozen

🔄 Code Flow

Code flow showing setup, draw, starttimer, resettimer, formattime, windowresized

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

graph TD start[Start] --> setup[setup] setup --> canvas-setup[Canvas Setup] setup --> ui-container-centering[UI Container Centering] setup --> button-event-binding[Button Event Binding] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-setup href "#sub-canvas-setup" click ui-container-centering href "#sub-ui-container-centering" click button-event-binding href "#sub-button-event-binding" draw --> random-velocity-init[Random Velocity Init] draw --> position-update[Position Update] draw --> timer-elapsed-calc[Timer Elapsed Calc] draw --> timer-remaining-calc[Timer Remaining Calc] draw --> timer-expired-check[Timer Expired Check] draw --> horizontal-bounce-detect[Horizontal Bounce Detect] draw --> vertical-bounce-detect[Vertical Bounce Detect] click draw href "#fn-draw" click random-velocity-init href "#sub-random-velocity-init" click position-update href "#sub-position-update" click timer-elapsed-calc href "#sub-timer-elapsed-calc" click timer-remaining-calc href "#sub-timer-remaining-calc" click timer-expired-check href "#sub-timer-expired-check" click horizontal-bounce-detect href "#sub-horizontal-bounce-detect" click vertical-bounce-detect href "#sub-vertical-bounce-detect" starttimer[startTimer] --> input-parsing[Input Parsing] starttimer --> input-validation[Input Validation] starttimer --> time-recording[Time Recording] starttimer --> velocity-randomize[Velocity Randomize] click starttimer href "#fn-starttimer" click input-parsing href "#sub-input-parsing" click input-validation href "#sub-input-validation" click time-recording href "#sub-time-recording" click velocity-randomize href "#sub-velocity-randomize" resettimer[resetTimer] --> duration-reset[Duration Reset] resettimer --> fallback-validation[Fallback Validation] resettimer --> circle-reset[Circle Reset] click resettimer href "#fn-resettimer" click duration-reset href "#sub-duration-reset" click fallback-validation href "#sub-fallback-validation" click circle-reset href "#sub-circle-reset" formattime[formatTime] --> minutes-calc[Minutes Calc] formattime --> remaining-seconds-calc[Remaining Seconds Calc] formattime --> format-and-join[Format and Join] click formattime href "#fn-formattime" click minutes-calc href "#sub-minutes-calc" click remaining-seconds-calc href "#sub-remaining-seconds-calc" click format-and-join href "#sub-format-and-join" windowresized[windowResized] --> canvas-resize[Canvas Resize] windowresized --> circle-recenter[Circle Recenter] click windowresized href "#fn-windowresized" click canvas-resize href "#sub-canvas-resize" click circle-recenter href "#sub-circle-recenter"

❓ Frequently Asked Questions

What visual elements are featured in the low-key timer sketch?

The sketch showcases a soft, glowing timer interface that floats over a full-screen background, complemented by a colorful circle that bounces around the canvas.

How can users interact with the low-key timer sketch?

Users can set their desired countdown duration, start or reset the timer, and watch the animated circle continue to move as time progresses.

What creative coding concept does the low-key timer sketch illustrate?

This sketch demonstrates the use of real-time animation and user interface integration in p5.js, showcasing how dynamic visuals can enhance user engagement.

Preview

low-key timer - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of low-key timer - Code flow showing setup, draw, starttimer, resettimer, formattime, windowresized
Code Flow Diagram