HappyNewYear2026

This sketch animates a glittering Times Square-style ball dropping down a starry sky while cycling through iconic 2025 moments. When the ball reaches the bottom, the scene explodes into colorful confetti, fireworks, and a bold "2026" celebration.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the ball drop — Increase the ball's descent speed so it reaches the bottom faster, compressing the entire countdown into a few seconds.
  2. Change the ball's landing position — Make the ball land higher or lower on the screen by editing the target y-position it descends toward.
  3. Hide the countdown number — Comment out the countdown display to see just the flashback icons appear without the large numbers.
  4. Make confetti fall slower — Reduce gravity on confetti so pieces drift down more gently instead of plummeting.
  5. Increase icon cycle speed — Make the ball's icon change more frequently so 2025 moments flash by faster.
  6. Add more confetti — Spawn more confetti pieces when the ball lands to create a denser celebration.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates an interactive New Year's Eve celebration that animates a disco ball descending through the sky. The ball is a shimmering focal point that cycles through eight iconic 2025 moments (a lunar lander, turtle, solar panels, cowboy hat, squid, Vatican keys, football-and-heart, and AI circuits) while shedding golden particles and music notes. A ticker tape scrolls at the bottom with 2025 headlines, and a countdown timer displays numbers 10 through 1 as the ball falls. When the ball hits bottom, confetti explodes upward and colorful fireworks burst across the screen. The sketch uses p5.js animation loops, particle systems, state machines, easing functions, and the Tone.js library for sound.

The code is organized into a main draw loop that manages four distinct states (PRE_DROP, DROPPING, EXPLOSION, and COUNTING/FLASHING), plus eight custom classes: Ball (the disco ball with rotating facets and icon overlay), ParticleSystem and Particle (manage emitters of bitcoins, music notes, flames, and lightning), TickerTape (scrolling headlines), Countdown (timer display with flashback icons), ConfettiSystem and Confetti (colorful paper pieces), and FireworkSystem with Firework and FireworkExplosion classes (rockets that launch and burst). By studying this sketch, you will learn how to layer multiple animation systems, switch behavior based on state, use easing functions for natural motion, generate and manage many particles efficiently, and synchronize visual and audio feedback.

⚙️ How It Works

  1. When the sketch loads, preload() imports two fonts from a CDN and defines eight ball icon drawing functions that represent 2025 highlights, plus nine countdown flashback icons. It also initializes Tone.js synthesizers for fireworks and countdown beeps. setup() creates a full-screen canvas, initializes the Ball, TickerTape, Countdown, and two particle effect systems (ConfettiSystem and FireworkSystem), and creates a UI slider and mute button.
  2. On every frame, draw() clears the background to midnight blue and renders the ticker tape. It then enters a state machine: PRE_DROP displays "Click or Tap to Start," DROPPING updates the ball's position using an eased cubic function, emits particles from the ball's path, updates the countdown based on the ball's progress, and checks if the ball has reached the bottom. When dropProgress reaches 1, the state switches to EXPLOSION.
  3. During the DROPPING state, the ball's y-position is calculated by lerping from the top to the bottom of the canvas, eased with easeInOutCubic() to create smooth acceleration and deceleration. The currentIconIndex cycles through the eight icons every 300 frames. Every 10 frames, four particle systems add particles (bitcoin symbols, music notes, flames, and lightning bolts) from the ball's position, with their speed scaled by how far the ball has fallen.
  4. When the ball hits the bottom (dropProgress >= 1), the state changes to EXPLOSION. The initExplosion() function spawns 500 confetti pieces from the center-bottom and launches 5 initial fireworks. Each firework rises for a random duration (60 to 120 frames), then explodes into 50 to 100 colorful particles that fall and fade. The display2026() function renders a large "2026" text in gold.
  5. The Countdown class maps the ball's dropProgress to numbers 10 through 1. Each time the number changes, it plays a beep sound and displays a flashback icon above the countdown number that fades in and out over 60 frames. The TickerTape continuously scrolls headlines left, wrapping them to the right edge when they exit the screen.
  6. After 10 seconds of explosion (600 frames), resetSketch() clears all particles and returns to the PRE_DROP state. Clicking on the canvas during PRE_DROP starts the ball drop; clicking during EXPLOSION launches additional fireworks at the mouse position.

🎓 Concepts You'll Learn

State machinesParticle systemsEasing functionsAnimation loopsClass-based architectureCollision detectionArray managementAudio synthesis with Tone.js

📝 Code Breakdown

preload()

preload() is special in p5.js—it runs BEFORE setup(), allowing you to load fonts, images, and sounds that setup() and draw() will need. This sketch loads fonts from the internet (Fontsource CDN) instead of keeping them locally, which makes the sketch smaller.

function preload() {
  // Load Fonts from Fontsource CDN
  // Montserrat Bold for main text and countdown
  montserratBold = loadFont('https://unpkg.com/@fontsource/montserrat@latest/files/montserrat-latin-700-normal.woff');
  // Roboto Regular for ticker tape
  robotoRegular = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff');

  ballIcons = [
    // 0: Blue Ghost lunar lander silhouette
    (size) => {
      fill(silver);
      noStroke();
      triangle(-size * 0.4, size * 0.2, size * 0.4, size * 0.2, 0, -size * 0.4);
      ellipse(0, size * 0.1, size * 0.6, size * 0.3);
      rectMode(CENTER);
      rect(0, size * 0.4, size * 0.2, size * 0.2);
    },
    // ... (7 more icon functions)
  ];

  bitcoinIcon = (size) => {
    fill(gold);
    noStroke();
    rectMode(CENTER);
    rect(0, 0, size, size);
    fill(0);
    textAlign(CENTER, CENTER);
    textFont(montserratBold);
    textSize(size * 0.8);
    text('₿', 0, 0);
  };

  musicNoteIcon = (size) => {
    fill(255);
    noStroke();
    ellipse(0, size * 0.2, size * 0.5, size * 0.3);
    rectMode(CENTER);
    rect(size * 0.15, -size * 0.05, size * 0.1, size * 0.5);
    line(size * 0.15, -size * 0.3, size * 0.4, -size * 0.3);
  };

  countdownIcons = [
    // 10 icon functions (same as ball icons)
  ];

  fireworksSynth = new Tone.PolySynth(Tone.Synth, {
    oscillator: { type: "square" },
    envelope: { attack: 0.05, decay: 0.2, sustain: 0.1, release: 0.5 },
    volume: -10
  }).toDestination();

  countdownBeep = new Tone.Synth({
    oscillator: { type: "sine" },
    envelope: { attack: 0.01, decay: 0.1, sustain: 0.0, release: 0.1 },
    volume: -10
  }).toDestination();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Font Loading from CDN montserratBold = loadFont('https://unpkg.com/@fontsource/montserrat@latest/files/montserrat-latin-700-normal.woff');

Imports external fonts from a content delivery network so text displays in a specific typeface

calculation Ball Icons Definition ballIcons = [ ... ];

Defines eight drawing functions that create iconic symbols representing 2025 moments to be displayed on the ball

calculation Audio Synthesizer Initialization fireworksSynth = new Tone.PolySynth(Tone.Synth, { ... }).toDestination();

Creates a multi-voice synthesizer that plays chords when fireworks explode

montserratBold = loadFont('https://unpkg.com/@fontsource/montserrat@latest/files/montserrat-latin-700-normal.woff');
Loads a bold font file from the internet. p5.js will use this font whenever you call textFont(montserratBold).
ballIcons = [ ... ];
Creates an array of 8 drawing functions. Each function takes a size parameter and draws one of the 2025 icons using p5.js shapes.
bitcoinIcon = (size) => { fill(gold); ... text('₿', 0, 0); };
Defines a small function that draws a gold square with a Bitcoin symbol (₿) inside—used by particles that fall from the ball.
countdownIcons = [ ... ];
Creates an array of 10 drawing functions (one for each countdown number 10 down to 1) that flash above the countdown display.
fireworksSynth = new Tone.PolySynth(Tone.Synth, { ... }).toDestination();
Initializes a Tone.js synthesizer with a square wave that plays rapid chords when fireworks explode, creating a celebratory sound.

setup()

setup() runs once when the sketch starts. Use it to initialize your canvas, set global variables, and create the main objects that your draw loop will animate every frame.

🔬 This creates four different particle emitters. What happens if you comment out the flames line (add // at the start) so they never get created?

  // Initialize particle systems
  particleSystems.push(new ParticleSystem(bitcoinIcon, color(gold), 0.5)); // Bitcoin particles
  particleSystems.push(new ParticleSystem(musicNoteIcon, color(255), 0.5)); // Music notes
  particleSystems.push(new ParticleSystem(null, color(20, 100, 100), 0.8, true)); // Flame particles
  particleSystems.push(new ParticleSystem(null, color(120, 100, 100), 0.6, false, true)); // Lightning bolts
function setup() {
  createCanvas(windowWidth, windowHeight);
  colorMode(HSB, 360, 100, 100, 100); // Using HSB for easier color manipulation
  noStroke();

  // Initialize main objects
  ball = new Ball();
  tickerTape = new TickerTape();
  countdownTimer = new Countdown();
  confettiSystem = new ConfettiSystem();
  fireworkSystem = new FireworkSystem();

  // Initialize particle systems
  particleSystems.push(new ParticleSystem(bitcoinIcon, color(gold), 0.5)); // Bitcoin particles
  particleSystems.push(new ParticleSystem(musicNoteIcon, color(255), 0.5)); // Music notes
  particleSystems.push(new ParticleSystem(null, color(20, 100, 100), 0.8, true)); // Flame particles
  particleSystems.push(new ParticleSystem(null, color(120, 100, 100), 0.6, false, true)); // Lightning bolts

  // Create UI elements
  createUI();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Canvas Setup createCanvas(windowWidth, windowHeight);

Creates a full-window canvas and resizes it to fill the entire browser

calculation HSB Color Mode colorMode(HSB, 360, 100, 100, 100);

Switches to Hue-Saturation-Brightness color space, making it easier to manipulate colors by their visual properties

calculation Game Object Initialization ball = new Ball();

Creates instances of all the major animation classes (Ball, TickerTape, Countdown, etc.) that will be used in the draw loop

for-loop Particle System Array Population particleSystems.push(new ParticleSystem(bitcoinIcon, color(gold), 0.5));

Adds four particle emitters to the array—each will emit a different visual effect (bitcoins, music notes, flames, lightning)

createCanvas(windowWidth, windowHeight);
Creates a canvas as wide and tall as the browser window—windowWidth and windowHeight are p5.js variables that update if you resize the window.
colorMode(HSB, 360, 100, 100, 100);
Tells p5.js to use HSB (Hue, Saturation, Brightness) instead of RGB. This makes color manipulation easier because you can change hue independently of brightness.
noStroke();
Sets all shapes drawn to have no outline—only filled areas will be visible, unless you explicitly set a stroke later.
ball = new Ball();
Creates a new Ball object and stores it in the global ball variable so draw() can access it every frame.
particleSystems.push(new ParticleSystem(bitcoinIcon, color(gold), 0.5));
Creates a particle emitter that will draw bitcoins in gold, and adds it to the particleSystems array. The 0.5 is a size multiplier.

draw()

draw() runs 60 times per second. This is the heartbeat of your animation. The switch statement is a state machine that controls which animations are active. In each state, you update() your objects (change their position, age, etc.) and then display() them (draw them). This separation makes the code cleaner and more maintainable.

🔬 This block animates the ball and all four particle systems. What happens if you comment out the particle loop (the for loop)? The ball still falls, but what visual effect disappears?

    case DROPPING:
      // Update and display ball
      ball.update();
      ball.display();

      // Update and display particles
      for (let ps of particleSystems) {
        ps.update();
        ps.display();
      }

🔬 This condition checks if the ball has reached the bottom. What visual effect would you see if you changed >= 1 to < 0.5, so the explosion happens immediately when you click to start?

      // Check for state transition to EXPLOSION
      if (ball.dropProgress >= 1) {
        currentState = EXPLOSION;
        initExplosion();
      }
function draw() {
  // Background: Starry NYC night sky
  background(midnightBlue);

  // Update and display ticker tape
  tickerTape.update();
  tickerTape.display();

  switch (currentState) {
    case PRE_DROP:
      // Display initial "Happy New Year!" or "Get Ready!" message
      displayPreDropMessage();
      break;

    case DROPPING:
      // Update and display ball
      ball.update();
      ball.display();

      // Update and display particles
      for (let ps of particleSystems) {
        ps.update();
        ps.display();
      }

      // Update and display countdown (now driven by ball's dropProgress)
      countdownTimer.update(ball.dropProgress);
      countdownTimer.display();

      // Check for state transition to EXPLOSION
      if (ball.dropProgress >= 1) {
        currentState = EXPLOSION;
        initExplosion();
      }
      break;

    case EXPLOSION:
      // Display "2026"
      display2026();

      // Update and display confetti
      confettiSystem.update();
      confettiSystem.display();

      // Update and display fireworks
      fireworkSystem.update();
      fireworkSystem.display();

      // Optionally, transition back to PRE_DROP after some time
      if (frameCount % 600 === 0) { // After about 10 seconds of explosion
        resetSketch();
      }
      break;
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Background Clear background(midnightBlue);

Fills the entire canvas with midnight blue, erasing the previous frame so you don't see motion trails

switch-case State Machine switch (currentState) { case PRE_DROP: ... case DROPPING: ... case EXPLOSION: ... }

Controls which animations play based on the current state—before drop, during drop, or after explosion

conditional DROPPING State Block case DROPPING: ball.update(); ball.display(); ... if (ball.dropProgress >= 1) { currentState = EXPLOSION; initExplosion(); }

Updates and renders the ball, particles, and countdown while the ball is falling; switches to EXPLOSION when it hits the bottom

for-loop Particle System Update Loop for (let ps of particleSystems) { ps.update(); ps.display(); }

Iterates through all four particle emitters, updating their positions and drawing them every frame

background(midnightBlue);
Clears the entire canvas with midnight blue color. Without this line, every frame would draw on top of the previous one, creating trails.
tickerTape.update();
Updates the ticker tape's position—scrolls the headlines left by a small amount each frame.
tickerTape.display();
Draws the ticker tape headlines at their current scroll position.
switch (currentState) {
Starts a switch statement that checks the value of currentState and runs different code for each state (PRE_DROP, DROPPING, EXPLOSION).
case DROPPING:
If currentState equals DROPPING, the code inside this block runs—this animates the ball falling.
ball.update();
Updates the ball's position based on its drop speed and easing function. The ball gets slightly lower each frame.
ball.display();
Draws the disco ball at its current position with spinning facets and the current icon.
for (let ps of particleSystems) { ps.update(); ps.display(); }
Loops through all four particle emitters (bitcoin, music notes, flames, lightning) and updates + draws each one.
countdownTimer.update(ball.dropProgress);
Updates the countdown timer based on how far the ball has fallen. The countdown goes from 10 to 1 as dropProgress goes from 0 to 1.
if (ball.dropProgress >= 1) { currentState = EXPLOSION; initExplosion(); }
When the ball reaches the bottom (dropProgress becomes 1), changes the state to EXPLOSION and calls initExplosion() to spawn confetti and fireworks.
case EXPLOSION:
When the state is EXPLOSION, this block runs instead—displaying "2026" and animating confetti and fireworks.
if (frameCount % 600 === 0) { resetSketch(); }
Every 600 frames (about 10 seconds at 60fps), resets the sketch back to PRE_DROP so the animation can be restarted.

displayPreDropMessage()

This function draws the welcome message. It uses push() and pop() to isolate text styling so it doesn't affect other drawings. Notice that textSize uses width * 0.08, which makes the text scale with the window size.

function displayPreDropMessage() {
  push();
  textAlign(CENTER, CENTER);
  textFont(montserratBold);
  textSize(width * 0.08);
  fill(gold);
  text("Click or Tap to Start", width / 2, height / 2 - height * 0.1);
  textSize(width * 0.04);
  fill(silver);
  text("2025 Rewind Ball Drop", width / 2, height / 2);
  pop();
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Text Style Setup textFont(montserratBold); textSize(width * 0.08); fill(gold);

Configures font, size, and color before drawing text

push();
Saves the current drawing state (position, rotation, fill, stroke, etc.) so changes don't affect other drawings.
textAlign(CENTER, CENTER);
Tells p5.js to center text both horizontally and vertically around the coordinates you provide.
textFont(montserratBold);
Uses the Montserrat Bold font that was loaded in preload() for all text drawn after this line.
textSize(width * 0.08);
Sets the text size to 8% of the canvas width—this scales responsively if the window is resized.
fill(gold);
Sets the text color to gold (the variable defined at the top of the file).
text("Click or Tap to Start", width / 2, height / 2 - height * 0.1);
Draws the instruction text centered horizontally and slightly above the middle of the screen.
pop();
Restores the drawing state that was saved by push(), so text settings don't affect future drawings.

display2026()

This simple function displays the celebration text during the EXPLOSION state. It's separate from displayPreDropMessage() for clarity, but follows the same pattern: push/pop to isolate styling, then draw text.

function display2026() {
  push();
  textAlign(CENTER, CENTER);
  textFont(montserratBold);
  textSize(width * 0.15);
  fill(gold);
  text("2026", width / 2, height / 2);
  pop();
}
Line-by-line explanation (3 lines)
push();
Saves the current drawing state.
textSize(width * 0.15);
Makes the "2026" text very large—15% of the canvas width—so it dominates the screen.
text("2026", width / 2, height / 2);
Draws "2026" in the exact center of the screen in large, bold, gold text.

initExplosion()

initExplosion() is called once when the state transitions from DROPPING to EXPLOSION. It spawns a burst of particles that drive the celebration. Two loops create many objects at once—confetti and fireworks. This 'bulk creation' pattern is efficient and looks visually impressive.

🔬 These two loops spawn confetti and fireworks when the ball hits bottom. What happens if you swap the two loops' counts—launch 500 fireworks and only 5 confetti?

  // Generate lots of confetti
  for (let i = 0; i < 500; i++) {
    confettiSystem.addConfetti(width / 2, height * 0.9);
  }
  // Launch initial fireworks
  for (let i = 0; i < 5; i++) {
    fireworkSystem.addFirework(random(width * 0.2, width * 0.8), height, random(-PI / 4, -3 * PI / 4));
  }
function initExplosion() {
  // Generate lots of confetti
  for (let i = 0; i < 500; i++) {
    confettiSystem.addConfetti(width / 2, height * 0.9);
  }
  // Launch initial fireworks
  for (let i = 0; i < 5; i++) {
    fireworkSystem.addFirework(random(width * 0.2, width * 0.8), height, random(-PI / 4, -3 * PI / 4));
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Confetti Spawning Loop for (let i = 0; i < 500; i++) { confettiSystem.addConfetti(width / 2, height * 0.9); }

Spawns 500 pieces of confetti at the bottom center of the screen

for-loop Firework Launch Loop for (let i = 0; i < 5; i++) { fireworkSystem.addFirework(random(width * 0.2, width * 0.8), height, random(-PI / 4, -3 * PI / 4)); }

Launches 5 fireworks from random horizontal positions along the bottom, angled upward

for (let i = 0; i < 500; i++) {
Starts a loop that repeats 500 times—once for each confetti piece to create.
confettiSystem.addConfetti(width / 2, height * 0.9);
Creates one confetti piece at the center-bottom of the screen. All 500 are created at the same position but will scatter randomly as they animate.
for (let i = 0; i < 5; i++) {
Starts a loop that repeats 5 times—once for each initial firework.
fireworkSystem.addFirework(random(width * 0.2, width * 0.8), height, random(-PI / 4, -3 * PI / 4));
Launches a firework from a random x-position (between 20% and 80% across the screen), from the bottom, at a random angle between -45° and -135° (all pointing generally upward).

resetSketch()

resetSketch() is called after 10 seconds of explosion to loop the animation back to the start. It resets all state and clears all particles, preparing for another playthrough. This allows the animation to be watched again without reloading the page.

function resetSketch() {
  currentState = PRE_DROP;
  ball.reset();
  tickerTape.reset();
  countdownTimer.reset();
  confettiSystem = new ConfettiSystem(); // Clear confetti
  fireworkSystem = new FireworkSystem(); // Clear fireworks
  // Clear all particle systems
  for (let ps of particleSystems) {
    ps.particles = [];
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation State Reset currentState = PRE_DROP;

Returns the sketch to the waiting state so the animation can be restarted

calculation Object Resets ball.reset(); tickerTape.reset(); countdownTimer.reset();

Calls the reset() method on each major object to restore them to their initial state

for-loop Particle Array Clear for (let ps of particleSystems) { ps.particles = []; }

Removes all remaining particles by clearing each emitter's particle array

currentState = PRE_DROP;
Sets the state back to the waiting state, so draw() will display the start message again.
ball.reset();
Calls the Ball's reset() method, which moves the ball back to the top and resets its progress.
tickerTape.reset();
Resets the ticker tape scroll position so headlines appear from the right again.
confettiSystem = new ConfettiSystem();
Creates a brand new, empty ConfettiSystem, effectively discarding all old confetti particles.
for (let ps of particleSystems) { ps.particles = []; }
Loops through all four particle emitters and clears their particles arrays, removing all leftover bitcoins, music notes, flames, and lightning.

mousePressed()

mousePressed() is a p5.js callback that fires whenever the user clicks or taps. It checks the current state and responds accordingly—either starting the drop or adding extra fireworks. This pattern is common for interactive sketches: one click starts the main animation, and clicks during the celebration add extra effects.

function mousePressed() {
  if (currentState === EXPLOSION) {
    fireworkSystem.addFirework(mouseX, mouseY, 0); // Launch a firework at mouse position
  } else if (currentState === PRE_DROP) {
    // Start the ball drop on click/tap
    currentState = DROPPING;
    ball.startDrop();
    // countdownTimer.startCountdown(); // Removed: Countdown now implicitly starts with ball.update()
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Click to Add Fireworks if (currentState === EXPLOSION) { fireworkSystem.addFirework(mouseX, mouseY, 0); }

During explosion, clicking launches an extra firework at the mouse position

conditional Click to Start Drop else if (currentState === PRE_DROP) { currentState = DROPPING; ball.startDrop(); }

Before the drop, clicking starts the animation by switching to DROPPING state

if (currentState === EXPLOSION) {
Checks if we are in the EXPLOSION state (the celebration after the ball lands).
fireworkSystem.addFirework(mouseX, mouseY, 0);
Launches a firework at the exact position the user clicked, letting them add extra explosions during the celebration.
} else if (currentState === PRE_DROP) {
If we're in the waiting state (PRE_DROP), the user can click to start the animation.
currentState = DROPPING;
Changes the state to DROPPING, which activates the ball drop animation in draw().
ball.startDrop();
Calls the ball's startDrop() method, which resets dropProgress to 0 so the fall animation begins from the top.

windowResized()

windowResized() is called automatically whenever the user resizes the browser window. It re-initializes the canvas and resets all objects so they work correctly at the new size. This is essential for responsive, full-screen p5.js sketches.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  ball.reset(); // Recenter ball
  tickerTape.reset();
  countdownTimer.reset();
  confettiSystem = new ConfettiSystem();
  fireworkSystem = new FireworkSystem();
  // Adjust UI element positions if necessary
  dropSpeedSlider.position(10, 10);
  muteButton.position(180, 10);
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

calculation Canvas Resize resizeCanvas(windowWidth, windowHeight);

Adjusts the canvas to match the new window dimensions

calculation UI Element Repositioning dropSpeedSlider.position(10, 10); muteButton.position(180, 10);

Moves UI buttons to ensure they remain visible after the window resize

resizeCanvas(windowWidth, windowHeight);
p5.js calls this function automatically when the browser window is resized. It updates the canvas to fill the new window size.
ball.reset();
Resets the ball position to the center of the (now larger or smaller) canvas.
dropSpeedSlider.position(10, 10);
Repositions the speed slider to (10, 10) pixels from the top-left, ensuring it stays visible in the resized window.

Ball

The Ball class is the centerpiece of the animation. Its update() method drives the drop using easeInOutCubic(), which transforms linear progress (0 to 1) into smooth, natural-looking motion. The display() method draws two things: a spinning disco ball made of 30 rotating colored squares, and a cycling icon overlay. By separating update() and display(), the code becomes easy to modify—you can change how the ball moves without changing how it looks, or vice versa.

🔬 This code cycles through the 8 icons. What happens if you change ICON_CHANGE_INTERVAL from 300 to 100? What if you change it to 1000?

      // Cycle ball icons
      this.iconChangeTimer--;
      if (this.iconChangeTimer <= 0) {
        this.currentIconIndex = (this.currentIconIndex + 1) % ballIcons.length;
        this.iconChangeTimer = ICON_CHANGE_INTERVAL;
      }

🔬 This loop places facets evenly around a circle. What happens if you change numFacets from 30 to 10? To 100? How does the ball's appearance change?

    for (let i = 0; i < numFacets; i++) {
      let angle1 = map(i, 0, numFacets, 0, TWO_PI);
      let angle2 = map(i + 1, 0, numFacets, 0, TWO_PI);

      let x1 = radius * cos(angle1);
      let y1 = radius * sin(angle1);
class Ball {
  constructor() {
    this.reset();
  }

  reset() {
    this.x = width / 2;
    this.y = height * 0.15; // Starting position at the top
    this.size = min(width, height) * 0.2;
    this.targetY = height * 0.9; // Target position at the bottom
    this.dropProgress = 0; // 0 to 1 for easing
    this.dropSpeed = ballDropSpeed;
    this.currentIconIndex = 0;
    this.iconChangeTimer = ICON_CHANGE_INTERVAL;
    this.rotation = 0;
  }

  startDrop() {
    this.dropProgress = 0;
    this.dropSpeed = ballDropSpeed;
  }

  update() {
    if (this.dropProgress < 1) {
      // Eased descent
      this.dropProgress = min(this.dropProgress + this.dropSpeed, 1);
      // Using a cubic ease-in-out function for smooth acceleration and deceleration
      let easedProgress = this.easeInOutCubic(this.dropProgress);
      this.y = lerp(height * 0.15, this.targetY, easedProgress);

      this.rotation += TWO_PI / 360; // Gentle rotation

      // Cycle ball icons
      this.iconChangeTimer--;
      if (this.iconChangeTimer <= 0) {
        this.currentIconIndex = (this.currentIconIndex + 1) % ballIcons.length;
        this.iconChangeTimer = ICON_CHANGE_INTERVAL;
      }

      // Generate particles based on ball's position
      if (frameCount % 10 === 0) { // Generate particles every 10 frames
        let numParticles = 5;
        let speedFactor = map(this.dropProgress, 0, 1, 0.5, 2); // Particles speed up as ball drops
        particleSystems[0].addParticles(this.x, this.y + this.size / 2, numParticles, speedFactor, color(gold)); // Bitcoin
        particleSystems[1].addParticles(this.x, this.y + this.size / 2, numParticles, speedFactor, color(255)); // Music notes
        particleSystems[2].addParticles(this.x, this.y + this.size / 2, numParticles / 2, speedFactor, color(20, 100, 100, 100)); // Flames
        particleSystems[3].addParticles(this.x, this.y + this.size / 2, numParticles / 2, speedFactor, color(120, 100, 100, 100)); // Lightning bolts
      }
    }
  }

  display() {
    push();
    translate(this.x, this.y);
    rotate(this.rotation);

    // Draw disco ball texture
    const numFacets = 30; // Number of facets around the ball
    const facetSize = this.size * 0.1;
    const radius = this.size / 2;

    for (let i = 0; i < numFacets; i++) {
      let angle1 = map(i, 0, numFacets, 0, TWO_PI);
      let angle2 = map(i + 1, 0, numFacets, 0, TWO_PI);

      let x1 = radius * cos(angle1);
      let y1 = radius * sin(angle1);
      let x2 = radius * cos(angle2);
      let y2 = radius * sin(angle2);

      // Create a gradient effect for sparkle
      let c1 = color(silver);
      let c2 = color(silver);
      let c3 = color(gold);

      // Randomly choose a color for each facet to simulate sparkle
      let r = random(1);
      if (r < 0.2) {
        c1 = color(magenta);
        c2 = color(magenta);
        c3 = color(silver);
      } else if (r < 0.4) {
        c1 = color(seaGreen);
        c2 = color(seaGreen);
        c3 = color(silver);
      } else if (r < 0.6) {
        c1 = color(gold);
        c2 = color(gold);
        c3 = color(silver);
      }

      // Draw a "facet" (a small square or rectangle)
      rectMode(CENTER);
      fill(c1);
      rect(x1, y1, facetSize, facetSize);

      // Draw a small highlight
      fill(255, 50);
      ellipse(x1 + random(-facetSize / 4, facetSize / 4), y1 + random(-facetSize / 4, facetSize / 4), facetSize * random(0.2, 0.5));
    }

    // Overlay current icon
    push();
    fill(midnightBlue);
    noStroke();
    ellipse(0, 0, this.size * 0.7); // Dark circle behind icon for contrast
    fill(255);
    noStroke();
    let iconSize = this.size * 0.5;
    if (ballIcons[this.currentIconIndex]) {
      if (typeof ballIcons[this.currentIconIndex] === 'function') {
        ballIcons[this.currentIconIndex](iconSize); // Call drawing function
      } else if (ballIcons[this.currentIconIndex] instanceof p5.Image) {
        imageMode(CENTER);
        image(ballIcons[this.currentIconIndex], 0, 0, iconSize, iconSize); // Draw image
      }
    }
    pop();

    pop();
  }

  // Easing function: Cubic ease-in-out
  easeInOutCubic(t) {
    t *= 2;
    if (t < 1) return 0.5 * t * t * t;
    t -= 2;
    return 0.5 * (t * t * t + 2);
  }
}
Line-by-line explanation (21 lines)

🔧 Subcomponents:

calculation Reset Method this.x = width / 2; this.y = height * 0.15; ...

Initializes all ball properties to their starting values

calculation Drop Progress Update this.dropProgress = min(this.dropProgress + this.dropSpeed, 1);

Increments the ball's progress from 0 to 1, capped at 1 so it doesn't overshoot

calculation Easing Function Application let easedProgress = this.easeInOutCubic(this.dropProgress);

Transforms linear progress into smooth, natural-looking motion with ease-in and ease-out

calculation Position Interpolation this.y = lerp(height * 0.15, this.targetY, easedProgress);

Calculates the ball's current y-position between top and bottom based on eased progress

conditional Icon Cycle Check if (this.iconChangeTimer <= 0) { this.currentIconIndex = (this.currentIconIndex + 1) % ballIcons.length; this.iconChangeTimer = ICON_CHANGE_INTERVAL; }

Advances to the next icon every 300 frames, cycling through all eight 2025 moments

conditional Particle Emission if (frameCount % 10 === 0) { ... particleSystems[0].addParticles(...); ... }

Every 10 frames, emits bitcoins, music notes, flames, and lightning from the ball's position

for-loop Disco Ball Facet Loop for (let i = 0; i < numFacets; i++) { ... rect(x1, y1, facetSize, facetSize); ... }

Draws 30 small rotating squares around the ball to create a disco ball sparkle effect

constructor() { this.reset(); }
The constructor runs when a new Ball is created. It calls reset() to initialize all properties.
this.x = width / 2;
Sets the ball's starting x-position to the center of the screen.
this.size = min(width, height) * 0.2;
Sets the ball's diameter to 20% of the smaller canvas dimension, so it scales responsively.
this.dropProgress = 0; // 0 to 1 for easing
Initializes dropProgress to 0. As the ball falls, this increases from 0 to 1, driving all the animation.
if (this.dropProgress < 1) {
Only animates if the ball hasn't reached the bottom yet (dropProgress < 1).
this.dropProgress = min(this.dropProgress + this.dropSpeed, 1);
Increases dropProgress by dropSpeed each frame, but caps it at 1 so it never goes higher.
let easedProgress = this.easeInOutCubic(this.dropProgress);
Transforms the linear dropProgress into a smooth eased value that starts slow, speeds up, and slows down again near the end.
this.y = lerp(height * 0.15, this.targetY, easedProgress);
Calculates the ball's y-position by interpolating between the top (height * 0.15) and bottom (targetY) based on easedProgress.
this.rotation += TWO_PI / 360;
Adds a tiny rotation each frame (1 degree, since TWO_PI/360 ≈ 0.0175 radians), making the ball spin gently.
this.iconChangeTimer--;
Decrements the timer each frame. When it hits 0, the icon changes and the timer resets.
if (frameCount % 10 === 0) {
This condition is true every 10th frame (6 times per second), so particles emit 6 times per second instead of 60 times, saving performance.
particleSystems[0].addParticles(this.x, this.y + this.size / 2, numParticles, speedFactor, color(gold));
Emits 5 bitcoin particles from slightly below the ball. The speedFactor increases from 0.5 to 2 as the ball falls, making later particles faster.
for (let i = 0; i < numFacets; i++) {
Loops 30 times to draw 30 facets around the ball in a circle.
let angle1 = map(i, 0, numFacets, 0, TWO_PI);
Calculates the angle for this facet by mapping the loop counter (0 to 29) to radians (0 to TWO_PI), spreading them evenly around a circle.
let x1 = radius * cos(angle1);
Converts the angle to a 2D position using cosine for the x-coordinate on a circle.
rect(x1, y1, facetSize, facetSize);
Draws a small square at the calculated position—this square will sparkle as it rotates.
let r = random(1);
Generates a random number between 0 and 1 to randomly choose a color for this facet.
if (r < 0.2) { c1 = color(magenta); ... }
20% of the time, the facet is magenta instead of silver, creating a twinkling, colorful sparkle effect.
ellipse(0, 0, this.size * 0.7);
Draws a dark background circle behind the icon so the icon is visible against the sparkly ball.
if (typeof ballIcons[this.currentIconIndex] === 'function') { ballIcons[this.currentIconIndex](iconSize); }
Checks if the current icon is a function (a drawing routine). If so, calls it to draw the icon. Otherwise, draws it as an image.
t *= 2; if (t < 1) return 0.5 * t * t * t;
This is the cubic ease-in-out formula. It accelerates from rest (slow start), then decelerates at the end (slow finish), creating natural-looking motion.

Particle

The Particle class represents a single particle in a system. Each particle has position (x, y), velocity (vx, vy), and a lifetime. The update() method moves it and applies gravity. The display() method renders it based on its type (flame, lightning, icon, or default circle) and fades it out as it ages. By storing particles in an array and calling update/display on each, you can animate thousands of particles efficiently.

🔬 Flame particles draw a yellow core inside the orange flame. What happens if you change this.size * 0.6 to this.size (making the core as big as the whole flame)? Or to this.size * 0.3 (smaller)?

      // Add a yellow core
      fill(60, 100, 100, currentAlpha * 1.2); // Yellow, slightly brighter
      ellipse(0, 0, this.size * 0.6, this.size);
class Particle {
  constructor(x, y, vx, vy, size, color, icon, isFlame, isLightning) {
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
    this.size = size;
    this.color = color;
    this.icon = icon;
    this.isFlame = isFlame || false;
    this.isLightning = isLightning || false;
    this.lifetime = 255; // For alpha fading
    this.rotation = random(TWO_PI);
    this.rotationSpeed = random(-0.1, 0.1);
  }

  update() {
    this.x += this.vx;
    this.y += this.vy;
    this.vy += 0.05; // Gravity
    this.lifetime -= 5;
    this.rotation += this.rotationSpeed;
  }

  display() {
    push();
    translate(this.x, this.y);
    rotate(this.rotation);

    let currentAlpha = map(this.lifetime, 0, 255, 0, 100);
    fill(hue(this.color), saturation(this.color), brightness(this.color), currentAlpha);
    noStroke();

    if (this.isFlame) {
      // Draw flame shape
      ellipse(0, 0, this.size, this.size * 2);
      ellipse(0, -this.size * 0.5, this.size * 0.8, this.size * 1.5);
      ellipse(0, -this.size, this.size * 0.6, this.size);
      // Add a yellow core
      fill(60, 100, 100, currentAlpha * 1.2); // Yellow, slightly brighter
      ellipse(0, 0, this.size * 0.6, this.size);
    } else if (this.isLightning) {
      // Draw lightning bolt
      stroke(hue(this.color), saturation(this.color), brightness(this.color), currentAlpha);
      strokeWeight(2);
      line(-this.size / 2, -this.size / 2, this.size / 2, this.size / 2);
      line(this.size / 2, -this.size / 2, -this.size / 2, this.size / 2);
      line(0, -this.size / 2, 0, this.size / 2);
      line(-this.size / 2, 0, this.size / 2, 0);
    } else if (this.icon) {
      // Draw image icon or custom shape function
      if (typeof this.icon === 'function') {
        this.icon(this.size); // Call drawing function
      } else if (this.icon instanceof p5.Image) {
        imageMode(CENTER);
        image(this.icon, 0, 0, this.size, this.size); // Draw image
      }
    } else {
      // Default: draw a circle
      ellipse(0, 0, this.size);
    }

    pop();
  }

  isDead() {
    return this.lifetime <= 0;
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Gravity Simulation this.vy += 0.05; // Gravity

Increases downward velocity each frame, simulating gravity pulling particles down

calculation Alpha Fade Calculation let currentAlpha = map(this.lifetime, 0, 255, 0, 100);

Maps lifetime (255 to 0) to alpha (100 to 0), making particles fade out as they age

conditional Type-based Rendering if (this.isFlame) { ... } else if (this.isLightning) { ... } else if (this.icon) { ... }

Draws different visual styles based on the particle type—flames, lightning bolts, icons, or simple circles

this.x += this.vx;
Updates the particle's x-position by adding its horizontal velocity.
this.y += this.vy;
Updates the particle's y-position by adding its vertical velocity.
this.vy += 0.05; // Gravity
Increases downward velocity slightly each frame, simulating gravity pulling the particle down. Over time, particles accelerate downward.
this.lifetime -= 5;
Decrements lifetime by 5 each frame. When lifetime reaches 0, the particle is marked as dead and removed.
let currentAlpha = map(this.lifetime, 0, 255, 0, 100);
Maps the lifetime value (which goes from 255 to 0) to an alpha value (which goes from 100 to 0). Particles fade out as they age.
fill(hue(this.color), saturation(this.color), brightness(this.color), currentAlpha);
Extracts the hue, saturation, and brightness from the particle's color and applies the fading alpha so the particle becomes more transparent.
if (this.isFlame) { ellipse(0, 0, this.size, this.size * 2); ... }
If this is a flame particle, draws three overlapping ellipses to create a flame shape that tapers upward.
} else if (this.isLightning) { line(-this.size / 2, -this.size / 2, this.size / 2, this.size / 2); ... }
If this is a lightning particle, draws crossed lines to simulate a lightning bolt effect.
} else if (this.icon) { if (typeof this.icon === 'function') { this.icon(this.size); } ... }
If this particle has an icon (bitcoin, music note, etc.), calls the icon drawing function to render it.
} else { ellipse(0, 0, this.size); }
If none of the above, draws a simple filled circle.
return this.lifetime <= 0;
Returns true if the particle is dead (lifetime reached 0), signaling that it should be removed from the system.

ParticleSystem

ParticleSystem manages a collection of particles. addParticles() spawns multiple particles at once with slightly randomized angles and speeds. update() calls update() on every particle and removes dead ones. The backward iteration (for (let i = length - 1; i >= 0; i--)) is a critical pattern—when you remove items from an array while looping, you must iterate backward to avoid skipping items. This is one of the most important programming patterns in particle effects.

🔬 These lines set each particle's velocity using angle and speed. All angles are between 0 and PI (upward). What would happen if you changed random(PI) to random(TWO_PI) so particles launch in all directions, including downward?

      let angle = random(PI); // Emit upwards
      let speed = random(1, 3) * speedFactor;
      let vx = speed * cos(angle);
      let vy = -speed * sin(angle); // Negative for upward motion
class ParticleSystem {
  constructor(icon, color, sizeFactor, isFlame, isLightning) {
    this.particles = [];
    this.icon = icon;
    this.color = color;
    this.sizeFactor = sizeFactor || 1;
    this.isFlame = isFlame || false;
    this.isLightning = isLightning || false;
  }

  addParticles(x, y, count, speedFactor, colorOverride) {
    for (let i = 0; i < count; i++) {
      let angle = random(PI); // Emit upwards
      let speed = random(1, 3) * speedFactor;
      let vx = speed * cos(angle);
      let vy = -speed * sin(angle); // Negative for upward motion
      let size = random(10, 20) * this.sizeFactor;
      let particleColor = colorOverride || this.color;
      this.particles.push(new Particle(x, y, vx, vy, size, particleColor, this.icon, this.isFlame, this.isLightning));
    }
  }

  update() {
    for (let i = this.particles.length - 1; i >= 0; i--) {
      this.particles[i].update();
      if (this.particles[i].isDead()) {
        this.particles.splice(i, 1);
      }
    }
  }

  display() {
    for (let particle of this.particles) {
      particle.display();
    }
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

for-loop Particle Creation Loop for (let i = 0; i < count; i++) { ... this.particles.push(new Particle(...)); }

Spawns multiple particles at once, each with a random upward angle and speed

for-loop Particle Update Loop for (let i = this.particles.length - 1; i >= 0; i--) { this.particles[i].update(); if (this.particles[i].isDead()) { this.particles.splice(i, 1); } }

Updates all particles and removes dead ones (iterates backward to safely splice)

constructor(icon, color, sizeFactor, isFlame, isLightning) {
Creates a particle system that will emit particles with a specific icon, color, and visual style (flame or lightning).
this.particles = [];
Initializes an empty array that will hold all the particles currently alive in this system.
let angle = random(PI);
Generates a random angle between 0 and PI (0° to 180°), pointing upward. PI is 180°, 0 is 0°, so all angles point above the horizon.
let vx = speed * cos(angle);
Calculates horizontal velocity using cosine—at angle 0 or PI, vx is ±1; at PI/2 (90°), vx is 0.
let vy = -speed * sin(angle);
Calculates vertical velocity using sine. The negative sign makes vy negative (upward, since positive y is down in p5.js).
let size = random(10, 20) * this.sizeFactor;
Randomizes particle size between 10 and 20, then multiplies by the system's sizeFactor for variety within the system's style.
this.particles.push(new Particle(x, y, vx, vy, size, particleColor, this.icon, this.isFlame, this.isLightning));
Creates a new Particle and adds it to the particles array. The particle will inherit the system's icon, color, and type flags.
for (let i = this.particles.length - 1; i >= 0; i--) {
Iterates through the particles array backward (from last to first). This is important when splicing—removing items shifts indices, so backward iteration is safe.
if (this.particles[i].isDead()) { this.particles.splice(i, 1); }
If a particle is dead, removes it from the array using splice(). Because we iterate backward, removing an item doesn't affect the indices of items we haven't processed yet.
for (let particle of this.particles) { particle.display(); }
Loops through all living particles and calls display() on each, rendering them to the screen.

TickerTape

TickerTape implements an infinite scrolling marquee. Headlines start off-screen to the right, scroll left, and when they exit the left edge, are repositioned to the right again. The wrapping logic checks if a headline has scrolled far enough left to be completely invisible, then moves it to the end of the queue. This pattern is used in many scrolling UI systems (news tickers, credit rolls, etc.).

🔬 This loop calculates each headline's starting position. What happens if you remove the random(50, 150) part so the spacing is always constant? Try: currentX += textWidth(headline + " • ") + 50;

    for (let headline of this.headlines) {
      this.xOffsets.push(currentX);
      currentX += textWidth(headline + " • ") + random(50, 150); // Add spacing
    }
class TickerTape {
  constructor() {
    this.y = height * 0.95;
    this.headlines = [
      "FIRST AMERICAN POPE",
      "SOLAR OVERTAKES COAL",
      "GREEN TURTLE RECOVERY",
      "BLUE GHOST LANDS ON MOON",
      "BEYONCÉ'S COWBOY CARTER GRAMMY WIN",
      "COLOSSAL SQUID FILMED IN WILD",
      "TAYLOR SWIFT & TRAVIS KELCE ENGAGED",
      "GPT-5 LAUNCHES",
      "$1.5T AI INVESTMENT BOOM",
      "BITCOIN HITS $126K ATH",
      "KENDRICK'S 'NOT LIKE US' WINS SONG OF YEAR",
      "LA WILDFIRES DEVASTATE REGION"
    ];
    this.scrollSpeed = 1;
    this.xOffsets = [];
    this.setupHeadlines();
  }

  setupHeadlines() {
    push();
    textFont(robotoRegular);
    textSize(width * 0.02);
    let currentX = width;
    for (let headline of this.headlines) {
      this.xOffsets.push(currentX);
      currentX += textWidth(headline + " • ") + random(50, 150); // Add spacing
    }
    pop();
  }

  reset() {
    this.xOffsets = [];
    this.setupHeadlines();
  }

  update() {
    for (let i = 0; i < this.xOffsets.length; i++) {
      this.xOffsets[i] -= this.scrollSpeed;
      // Loop headlines when they go off screen
      if (this.xOffsets[i] < -textWidth(this.headlines[i] + " • ")) {
        // Find the rightmost headline and place this one after it
        let maxRight = 0;
        for (let j = 0; j < this.xOffsets.length; j++) {
          maxRight = max(maxRight, this.xOffsets[j] + textWidth(this.headlines[j] + " • "));
        }
        this.xOffsets[i] = maxRight + random(50, 150);
      }
    }
  }

  display() {
    push();
    fill(champagne);
    textFont(robotoRegular);
    textSize(width * 0.02);
    textAlign(LEFT, CENTER);
    for (let i = 0; i < this.xOffsets.length; i++) {
      text(this.headlines[i] + " • ", this.xOffsets[i], this.y);
    }
    pop();
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

for-loop Headline Position Setup for (let headline of this.headlines) { this.xOffsets.push(currentX); currentX += textWidth(headline + " • ") + random(50, 150); }

Pre-calculates the starting x-position of each headline so they don't overlap

for-loop Scroll Update Loop for (let i = 0; i < this.xOffsets.length; i++) { this.xOffsets[i] -= this.scrollSpeed; ... }

Moves each headline left and loops it to the right when it exits the screen

conditional Headline Wrapping Logic if (this.xOffsets[i] < -textWidth(this.headlines[i] + " • ")) { let maxRight = 0; ... this.xOffsets[i] = maxRight + random(50, 150); }

When a headline scrolls off the left, repositions it to the right of all other headlines

this.y = height * 0.95;
Sets the ticker tape to display near the bottom of the screen (95% down).
this.headlines = [ ... ];
An array of 12 news headlines from 2025 that will scroll across the bottom.
let currentX = width;
Starts the first headline at the right edge of the screen (x = width).
this.xOffsets.push(currentX);
Records the starting x-position of this headline so update() knows where to scroll it.
currentX += textWidth(headline + " • ") + random(50, 150);
Advances currentX to the right of this headline plus a random gap, so the next headline starts after this one without overlapping.
this.xOffsets[i] -= this.scrollSpeed;
Moves the headline left by 1 pixel per frame (scrollSpeed = 1). Negative x moves left.
if (this.xOffsets[i] < -textWidth(this.headlines[i] + " • ")) {
Checks if the headline has scrolled so far left that it's completely off the screen (its right edge is beyond the left edge at x=0).
let maxRight = 0; for (let j = 0; j < this.xOffsets.length; j++) { maxRight = max(maxRight, this.xOffsets[j] + textWidth(this.headlines[j] + " • ")); }
Finds the rightmost position of any headline currently on screen by checking the right edge (x + width) of each one.
this.xOffsets[i] = maxRight + random(50, 150);
Repositions the off-screen headline to the right of all visible headlines plus a random gap, creating an infinite loop effect.
text(this.headlines[i] + " • ", this.xOffsets[i], this.y);
Draws each headline at its current scrolled position. The " • " is a separator between headlines.

Countdown

The Countdown class synchronizes with the Ball's dropProgress to display numbers 10 through 1 as the ball falls. The clever mapping (10 - dropProgress * 9) ensures that when dropProgress is 0, the number is 10, and when dropProgress is 1, the number is 1. When the number changes, the countdown briefly flashes a flashback icon above the number and plays a beep. This provides visual and audio feedback that keeps the player engaged during the drop.

🔬 This block executes when the countdown number changes. What happens if you change this.flashTimer = 60 to this.flashTimer = 30? What about changing it to 120?

      if (newNumber !== this.number) {
        this.lastNumber = this.number; // Store old number for icon selection
        this.number = newNumber;
        this.state = FLASHING;
        // Icon index for flashback: 10 -> 0, 9 -> 1, ..., 1 -> 9
        this.iconIndex = 10 - this.lastNumber;
        this.flashTimer = 60;
class Countdown {
  constructor() {
    this.reset();
  }

  reset() {
    this.number = 10;
    this.x = width / 2;
    this.y = height / 2;
    this.size = min(width, height) * 0.15;
    this.state = PRE_DROP; // PRE_DROP, COUNTING, FLASHING
    this.iconIndex = 0; // Index for current icon
    this.flashTimer = 0;
    this.lastNumber = 11; // To track when the number changes (set to 11 to trigger first 10)
  }

  // Modified update function to take ball's dropProgress
  update(dropProgress) {
    if (dropProgress < 1) {
      this.state = COUNTING;
      // Map dropProgress (0 to 1) to countdown numbers (10 to 1)
      // We want dropProgress 0 to be 10, dropProgress ~0.9 to be 1
      let newNumber = max(1, floor(10 - dropProgress * 9));

      if (newNumber !== this.number) {
        this.lastNumber = this.number; // Store old number for icon selection
        this.number = newNumber;
        this.state = FLASHING;
        // Icon index for flashback: 10 -> 0, 9 -> 1, ..., 1 -> 9
        this.iconIndex = 10 - this.lastNumber;
        this.flashTimer = 60; // Flash for 1 second (approx 1 second at 60fps)
        if (!isMuted) countdownBeep.triggerAttackRelease("C4", "8n");
      }
    } else {
      this.state = PRE_DROP; // Countdown finished
    }

    if (this.state === FLASHING) {
      this.flashTimer--;
      if (this.flashTimer <= 0) {
        this.state = COUNTING;
      }
    }
  }

  display() {
    if (currentState === DROPPING && this.state !== PRE_DROP) {
      push();
      translate(this.x, this.y);

      // Draw glowing effect
      drawingContext.shadowOffsetX = 0;
      drawingContext.shadowOffsetY = 0;
      drawingContext.shadowBlur = 30;
      drawingContext.shadowColor = color(gold);

      textAlign(CENTER, CENTER);
      textFont(montserratBold);
      textSize(this.size);
      fill(gold);
      text(this.number, 0, 0);

      // Draw flashback icon if flashing
      if (this.state === FLASHING && countdownIcons[this.iconIndex]) {
        let iconSize = this.size * 0.6;
        let iconAlpha = map(this.flashTimer, 0, 60, 0, 100); // Fade in/out over 1 second
        push();
        translate(0, -this.size * 0.8); // Position above number
        fill(hue(gold), saturation(gold), brightness(gold), iconAlpha);
        noStroke();
        if (typeof countdownIcons[this.iconIndex] === 'function') {
          countdownIcons[this.iconIndex](iconSize);
        } else if (countdownIcons[this.iconIndex] instanceof p5.Image) {
          imageMode(CENTER);
          image(countdownIcons[this.iconIndex], 0, 0, iconSize, iconSize);
        }
        pop();
      }

      pop();
    }
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Progress to Countdown Mapping let newNumber = max(1, floor(10 - dropProgress * 9));

Converts dropProgress (0 to 1) into countdown numbers (10 to 1)

conditional Number Change Detection if (newNumber !== this.number) { ... this.state = FLASHING; ... countdownBeep.triggerAttackRelease("C4", "8n"); }

When the countdown number changes, triggers a beep and switches to FLASHING state to display the icon

conditional Flash Timer Decrement if (this.state === FLASHING) { this.flashTimer--; if (this.flashTimer <= 0) { this.state = COUNTING; } }

Counts down the flash duration and returns to COUNTING when the flash ends

update(dropProgress) {
Takes the ball's dropProgress (0 to 1) as a parameter to synchronize the countdown with the ball's position.
let newNumber = max(1, floor(10 - dropProgress * 9));
Converts dropProgress to a countdown number: when dropProgress is 0, newNumber is 10; when dropProgress is 1, newNumber is 1. The floor() rounds down, max() ensures it never goes below 1.
if (newNumber !== this.number) {
Only executes if the number has changed—prevents redundant state changes on every frame.
this.lastNumber = this.number;
Saves the old number so you can look up which icon to display for this countdown step.
this.iconIndex = 10 - this.lastNumber;
Maps the old number (10, 9, ..., 1) to an icon index (0, 1, ..., 9) that matches the countdownIcons array.
this.flashTimer = 60;
Sets the flash to last 60 frames (about 1 second at 60fps), creating a brief highlight when the number changes.
if (!isMuted) countdownBeep.triggerAttackRelease("C4", "8n");
Plays a beep sound unless the user has muted the sketch. The "C4" is the note, "8n" is the duration (an eighth note).
drawingContext.shadowOffsetX = 0;
Uses the raw canvas context to apply a drop shadow effect to the countdown number, making it glow.
let iconAlpha = map(this.flashTimer, 0, 60, 0, 100);
As flashTimer counts down from 60 to 0, iconAlpha fades from 100 (fully opaque) to 0 (invisible), creating a fade-out effect.

Confetti

Confetti is simpler than the particle effects—each piece is just a colored rectangle that rises, rotates, and falls due to gravity. The constructor randomizes velocity, rotation speed, size, and color to create visual variety. The lifetime limits how long each piece lasts, preventing confetti from piling up on screen indefinitely.

class Confetti {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.vx = random(-5, 5);
    this.vy = random(-10, -3);
    this.rot = random(TWO_PI);
    this.rotSpeed = random(-0.1, 0.1);
    this.size = random(5, 15);
    this.color = random([color(seaGreen), color(gold), color(midnightBlue)]);
    this.lifetime = 120; // Lasts for 2 seconds
  }

  update() {
    this.x += this.vx;
    this.y += this.vy;
    this.vy += 0.2; // Gravity
    this.rot += this.rotSpeed;
    this.lifetime--;
  }

  display() {
    push();
    translate(this.x, this.y);
    rotate(this.rot);
    fill(this.color);
    noStroke();
    rectMode(CENTER);
    rect(0, 0, this.size, this.size / 2);
    pop();
  }

  isDead() {
    return this.lifetime <= 0;
  }
}
Line-by-line explanation (6 lines)
this.vx = random(-5, 5);
Randomizes horizontal velocity between -5 and +5, so confetti pieces scatter left and right.
this.vy = random(-10, -3);
Randomizes upward velocity between -10 and -3 (negative because up is negative in p5.js), so confetti launches upward.
this.color = random([color(seaGreen), color(gold), color(midnightBlue)]);
Randomly chooses one of three colors for the confetti—seaGreen, gold, or midnight blue—for visual variety.
this.lifetime = 120;
Sets lifetime to 120 frames. At 60fps, that's 2 seconds before the confetti disappears.
this.vy += 0.2;
Applies gravity by increasing downward velocity slightly each frame, pulling confetti toward the ground.
rect(0, 0, this.size, this.size / 2);
Draws a small rectangle (roughly twice as wide as it is tall) to represent a confetti piece.

ConfettiSystem

ConfettiSystem is a simple container that manages a collection of Confetti pieces. It follows the same pattern as ParticleSystem: addConfetti() spawns new pieces, update() moves and ages them while removing dead pieces, and display() renders them. The backward iteration pattern is critical for safe array removal.

class ConfettiSystem {
  constructor() {
    this.confetti = [];
  }

  addConfetti(x, y) {
    this.confetti.push(new Confetti(x, y));
  }

  update() {
    for (let i = this.confetti.length - 1; i >= 0; i--) {
      this.confetti[i].update();
      if (this.confetti[i].isDead()) {
        this.confetti.splice(i, 1);
      }
    }
  }

  display() {
    for (let c of this.confetti) {
      c.display();
    }
  }
}
Line-by-line explanation (3 lines)
this.confetti = [];
Initializes an empty array to store all confetti pieces currently alive.
addConfetti(x, y) { this.confetti.push(new Confetti(x, y)); }
Creates a new Confetti object at position (x, y) and adds it to the array.
for (let i = this.confetti.length - 1; i >= 0; i--) { ... if (this.confetti[i].isDead()) { this.confetti.splice(i, 1); } }
Iterates backward through confetti pieces, updates each one, and removes dead pieces. Backward iteration is important when splicing.

Firework

Firework implements a multi-stage animation: first it launches upward with gravity slowing it down, then when the fuse expires or it starts falling, it explodes into a FireworkExplosion. The state machine pattern (launch → explode → dead) keeps the logic clean. Each firework is independent, so multiple fireworks can be in different states at the same time.

🔬 This logic fires a firework by applying gravity to slow the upward motion until it starts falling. What happens if you change this.vy >= 0 to this.vy >= 1 (waiting until it's falling faster)? Or 0.5 (exploding sooner)?

      this.vy += 0.1; // Gravity slows launch
      this.fuseTime--;
      if (this.fuseTime <= 0 || this.vy >= 0) { // Explode when fuse runs out or rocket starts falling
        this.state = 'explode';
        this.explosions.push(new FireworkExplosion(this.x, this.y, this.color));
class Firework {
  constructor(x, y, angle) {
    this.x = x;
    this.y = y;
    this.vy = random(-10, -5); // Launch speed
    this.vx = random(-2, 2);   // Slight horizontal drift
    this.color = random([color(magenta), color(gold), color(seaGreen), color(silver)]);
    this.explosions = [];
    this.state = 'launch'; // 'launch', 'explode', 'dead'
    this.fuseTime = random(60, 120); // Time before explosion
    this.angle = angle || -PI / 2; // Default upwards
  }

  update() {
    if (this.state === 'launch') {
      this.x += this.vx;
      this.y += this.vy;
      this.vy += 0.1; // Gravity slows launch
      this.fuseTime--;
      if (this.fuseTime <= 0 || this.vy >= 0) { // Explode when fuse runs out or rocket starts falling
        this.state = 'explode';
        this.explosions.push(new FireworkExplosion(this.x, this.y, this.color));
      }
    } else if (this.state === 'explode') {
      for (let i = this.explosions.length - 1; i >= 0; i--) {
        this.explosions[i].update();
        if (this.explosions[i].isDead()) {
          this.explosions.splice(i, 1);
        }
      }
      if (this.explosions.length === 0) {
        this.state = 'dead';
      }
    }
  }

  display() {
    if (this.state === 'launch') {
      push();
      translate(this.x, this.y);
      rotate(this.angle + PI / 2); // Orient rocket upwards
      fill(this.color);
      triangle(-5, 0, 5, 0, 0, -20); // Simple rocket shape
      fill(gold);
      ellipse(0, 5, 10, 10); // Rocket base
      pop();
    } else if (this.state === 'explode') {
      for (let e of this.explosions) {
        e.display();
      }
    }
  }

  isDead() {
    return this.state === 'dead';
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Launch State Logic if (this.state === 'launch') { this.x += this.vx; this.y += this.vy; this.vy += 0.1; this.fuseTime--; if (this.fuseTime <= 0 || this.vy >= 0) { this.state = 'explode'; ... } }

Moves the firework upward, applies gravity, and triggers explosion when fuse runs out or rocket starts falling

conditional Explode State Logic else if (this.state === 'explode') { for (let i = this.explosions.length - 1; i >= 0; i--) { this.explosions[i].update(); if (this.explosions[i].isDead()) { this.explosions.splice(i, 1); } } if (this.explosions.length === 0) { this.state = 'dead'; } }

Updates and cleans up explosion particles, then transitions to 'dead' when all particles are gone

this.vy = random(-10, -5);
Sets upward velocity (negative because up is negative in p5.js) between -10 and -5, so the firework launches upward at various speeds.
this.vx = random(-2, 2);
Gives slight random horizontal drift so fireworks don't all go straight up.
this.fuseTime = random(60, 120);
Sets a random fuse duration between 1 and 2 seconds, so fireworks explode at different heights.
if (this.fuseTime <= 0 || this.vy >= 0) {
Triggers explosion if either the fuse runs out OR the rocket starts falling (vy becomes positive, meaning downward velocity). This ensures fireworks explode at a reasonable height.
this.explosions.push(new FireworkExplosion(this.x, this.y, this.color));
Creates a FireworkExplosion at the current position when the fuse triggers, spawning 50-100 colorful particles.
triangle(-5, 0, 5, 0, 0, -20);
Draws a simple triangle to represent the firework rocket—pointed at the top.
if (this.explosions.length === 0) { this.state = 'dead'; }
Once all explosion particles are gone, marks the firework as dead so it can be removed from the system.

FireworkExplosion

FireworkExplosion creates a burst of particles that radiate outward from a point in all directions. Unlike particle emitters that launch particles upward (from the ball), explosions use random(TWO_PI) to scatter particles across all angles. The isDead() method is clever—it checks both lifetime and particle count, ensuring the explosion object isn't removed until all its visual components are truly gone.

class FireworkExplosion {
  constructor(x, y, color) {
    this.x = x;
    this.y = y;
    this.color = color;
    this.particles = [];
    this.lifetime = 120; // 2 seconds
    this.initParticles();
    if (!isMuted) fireworksSynth.triggerAttackRelease(["C5", "E5", "G5"], "4n");
  }

  initParticles() {
    let numParticles = random(50, 100);
    for (let i = 0; i < numParticles; i++) {
      let angle = random(TWO_PI);
      let speed = random(1, 5);
      let vx = speed * cos(angle);
      let vy = speed * sin(angle);
      let size = random(2, 5);
      this.particles.push(new Particle(this.x, this.y, vx, vy, size, this.color));
    }
  }

  update() {
    for (let i = this.particles.length - 1; i >= 0; i--) {
      this.particles[i].update();
      if (this.particles[i].isDead()) {
        this.particles.splice(i, 1);
      }
    }
    this.lifetime--;
  }

  display() {
    for (let p of this.particles) {
      p.display();
    }
  }

  isDead() {
    return this.lifetime <= 0 && this.particles.length === 0;
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Particle Burst Initialization let numParticles = random(50, 100); for (let i = 0; i < numParticles; i++) { let angle = random(TWO_PI); ... this.particles.push(new Particle(...)); }

Spawns 50-100 particles radiating in all directions from the explosion point

this.lifetime = 120;
Sets lifetime to 120 frames (2 seconds at 60fps). Even after all particles are gone, the explosion lives this long to ensure cleanup.
let numParticles = random(50, 100);
Randomizes the number of particles so each explosion looks different—some are sparse, some are dense.
let angle = random(TWO_PI);
Gives each particle a random angle from 0 to TWO_PI (0° to 360°), so particles burst in all directions.
let vx = speed * cos(angle);
Converts angle and speed to x-velocity using cosine.
let vy = speed * sin(angle);
Converts angle and speed to y-velocity using sine. Unlike particle emission (which uses negative sin for upward), explosions radiate outward in all directions.
if (!isMuted) fireworksSynth.triggerAttackRelease(["C5", "E5", "G5"], "4n");
Plays a chord (C major: C, E, G) using the fireworks synthesizer, creating an celebratory sound effect.
return this.lifetime <= 0 && this.particles.length === 0;
Returns true only when both the lifetime has expired AND all particles are gone, ensuring full cleanup.

FireworkSystem

FireworkSystem is a straightforward container for Firework objects, following the same update/display/cleanup pattern as ParticleSystem and ConfettiSystem. It allows the main draw() function to manage dozens of independent fireworks, each in a different state, without complex bookkeeping.

class FireworkSystem {
  constructor() {
    this.fireworks = [];
  }

  addFirework(x, y, angle) {
    this.fireworks.push(new Firework(x, y, angle));
  }

  update() {
    for (let i = this.fireworks.length - 1; i >= 0; i--) {
      this.fireworks[i].update();
      if (this.fireworks[i].isDead()) {
        this.fireworks.splice(i, 1);
      }
    }
  }

  display() {
    for (let f of this.fireworks) {
      f.display();
    }
  }
}
Line-by-line explanation (2 lines)
addFirework(x, y, angle) { this.fireworks.push(new Firework(x, y, angle)); }
Creates a new Firework at position (x, y) with an optional angle and adds it to the array.
for (let i = this.fireworks.length - 1; i >= 0; i--) { ... if (this.fireworks[i].isDead()) { this.fireworks.splice(i, 1); } }
Iterates backward through fireworks, updates each one, and removes dead fireworks using the safe backward iteration pattern.

📦 Key Variables

ball object

Stores the main Ball object that animates the disco ball falling down the screen

let ball = new Ball();
particleSystems array

An array of ParticleSystem objects (bitcoins, music notes, flames, lightning) that emit particles from the falling ball

let particleSystems = [];
tickerTape object

Stores the TickerTape object that scrolls 2025 headlines at the bottom of the screen

let tickerTape = new TickerTape();
countdownTimer object

Stores the Countdown object that displays numbers 10-1 and flashback icons as the ball falls

let countdownTimer = new Countdown();
confettiSystem object

Stores the ConfettiSystem that manages colored confetti pieces during the explosion

let confettiSystem = new ConfettiSystem();
fireworkSystem object

Stores the FireworkSystem that manages rocket fireworks during the explosion

let fireworkSystem = new FireworkSystem();
ballDropSpeed number

Controls how quickly the ball descends—higher values make it fall faster

let ballDropSpeed = 0.005;
currentState number

Tracks which state the sketch is in (PRE_DROP, DROPPING, EXPLOSION, COUNTING, FLASHING) to control which animations play

let currentState = PRE_DROP;
ballIcons array

An array of 8 drawing functions that display different 2025 iconic moments on the ball as it falls

let ballIcons = [];
countdownIcons array

An array of 10 drawing functions (one for each countdown number) that flash above the countdown number

let countdownIcons = [];
isMuted boolean

Tracks whether audio is muted so sound effects don't play when true

let isMuted = false;
montserratBold object

Stores the loaded Montserrat Bold font for title and countdown text

let montserratBold;
robotoRegular object

Stores the loaded Roboto Regular font for the ticker tape headlines

let robotoRegular;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

PERFORMANCE Ball.display() - facet rendering

The disco ball draws 30 facets with random() calls every frame, regenerating random colors each time. This is computationally wasteful because the colors change every frame even though the visual effect doesn't require it.

💡 Pre-calculate a static color array for facets in Ball.reset() so colors are chosen once and remain consistent, only updating rarely. Alternatively, use a fixed color pattern instead of full randomness.

BUG ParticleSystem.update() - backward iteration

While the code is correct (iterating backward when splicing), it's computationally expensive to splice from arrays frequently. Removing many particles every frame can cause GC pauses in extended animations.

💡 Implement object pooling: instead of creating and destroying Particle objects, reuse them by marking them as inactive and resetting their properties. This trades memory for speed and eliminates garbage collection stalls.

FEATURE TickerTape class

Headlines are hardcoded in the constructor. If you want to add or change headlines, you must edit the code and reload.

💡 Pass headlines as a constructor parameter: constructor(headlines) and this.headlines = headlines. This makes the class reusable and more flexible.

STYLE Global color definitions

Colors are defined as hex strings (e.g., '#ffd700') but the sketch uses HSB color mode with color() calls throughout. The hex strings are then used inconsistently—sometimes passed to color(), sometimes used directly in fill().

💡 Define colors in HSB values that match the colorMode(HSB, 360, 100, 100, 100) declaration, e.g., const gold = color(51, 100, 100). This eliminates conversion confusion and ensures consistency.

BUG Countdown.display() - shadow effect

The code uses drawingContext.shadowColor = color(gold), but color(gold) returns an HSB color object in this sketch. Browser canvas shadows expect CSS color strings or hex values, not p5.js color objects, potentially causing unexpected rendering.

💡 Use hex color strings directly: drawingContext.shadowColor = '#ffd700' or convert the color object to a string using a utility function.

FEATURE Particle class - lifetime management

All particles fade out linearly from lifetime 255 to 0. For more natural effects, different particle types could use different fade curves (exponential, ease-out, etc.).

💡 Add an optional easing function parameter to Particle so flames fade differently than music notes, creating more visual interest and realism.

🔄 Code Flow

Code flow showing preload, setup, draw, displaypredropmessage, display2026, initexplosion, resetsketch, mousepressed, windowresized, ball, particle, particlesystem, tickertape, countdown, confetti, confettisystem, firework, fireworkexplosion, fireworksystem

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] click setup href "#fn-setup" click draw href "#fn-draw" setup --> font-loading[Font Loading from CDN] setup --> canvas-creation[Canvas Setup] setup --> hsb-colormode[HSB Color Mode] setup --> object-initialization[Game Object Initialization] setup --> particle-systems-init[Particle Systems Initialization] click font-loading href "#sub-font-loading" click canvas-creation href "#sub-canvas-creation" click hsb-colormode href "#sub-hsb-colormode" click object-initialization href "#sub-object-initialization" click particle-systems-init href "#sub-particle-systems-init" draw --> background-clear[Background Clear] draw --> state-machine[State Machine] click background-clear href "#sub-background-clear" click state-machine href "#sub-state-machine" state-machine --> dropping-state[DROPPING State Block] state-machine --> explosion-state[EXPLOSION State Block] click dropping-state href "#sub-dropping-state" click explosion-state href "#sub-explosion-state" dropping-state --> particle-loop[Particle System Update Loop] dropping-state --> text-styling[Text Style Setup] dropping-state --> update-drop-progress[Drop Progress Update] dropping-state --> easing-calculation[Easing Function Application] dropping-state --> lerp-position[Position Interpolation] dropping-state --> icon-cycling[Icon Cycle Check] dropping-state --> particle-generation[Particle Emission] click particle-loop href "#sub-particle-loop" click text-styling href "#sub-text-styling" click update-drop-progress href "#sub-update-drop-progress" click easing-calculation href "#sub-easing-calculation" click lerp-position href "#sub-lerp-position" click icon-cycling href "#sub-icon-cycling" click particle-generation href "#sub-particle-generation" explosion-state --> display2026[display2026] explosion-state --> confetti-loop[Confetti Spawning Loop] explosion-state --> firework-loop[Firework Launch Loop] explosion-state --> explosion-interaction[Click to Add Fireworks] click display2026 href "#fn-display2026" click confetti-loop href "#sub-confetti-loop" click firework-loop href "#sub-firework-loop" click explosion-interaction href "#sub-explosion-interaction" confetti-loop --> addparticles-loop[Particle Creation Loop] click addparticles-loop href "#sub-addparticles-loop" firework-loop --> launch-state[Launch State Logic] click launch-state href "#sub-launch-state" draw --> particle-systems-clear[Particle Array Clear] draw --> ui-reposition[UI Element Repositioning] draw --> canvas-resize[Canvas Resize] click particle-systems-clear href "#sub-particle-systems-clear" click ui-reposition href "#sub-ui-reposition" click canvas-resize href "#sub-canvas-resize" mousepressed[mousePressed] --> start-drop-interaction[Click to Start Drop] click mousepressed href "#fn-mousepressed" click start-drop-interaction href "#sub-start-drop-interaction" windowresized[windowResized] --> canvas-resize click windowresized href "#fn-windowresized" resetsketch[resetSketch] --> state-reset[State Reset] resetsketch --> object-reset[Object Resets] click resetsketch href "#fn-resetsketch" click state-reset href "#sub-state-reset" click object-reset href "#sub-object-reset"

❓ Frequently Asked Questions

What visual experience does the HappyNewYear2026 sketch offer?

The sketch presents a shimmering New Year’s Eve ball gliding down a starry sky, cycling through glowing icons, culminating in a vibrant explosion of confetti, fireworks, and a bold '2026' display.

Can users interact with the HappyNewYear2026 sketch, and if so, how?

Yes, users can adjust the ball's drop speed using a slider and toggle sound effects with a mute button.

What creative coding techniques are showcased in the HappyNewYear2026 sketch?

The sketch demonstrates particle systems, state management for animation, and dynamic icon cycling to enhance visual storytelling.

Preview

HappyNewYear2026 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of HappyNewYear2026 - Code flow showing preload, setup, draw, displaypredropmessage, display2026, initexplosion, resetsketch, mousepressed, windowresized, ball, particle, particlesystem, tickertape, countdown, confetti, confettisystem, firework, fireworkexplosion, fireworksystem
Code Flow Diagram