🔬 These three lines control how the text looks before it becomes particles. What happens if you change 255 (white) to color(255, 0, 0) to make red particles, or change / 8 to / 4 to make bigger text?
graphics.textSize(min(width, height) / 8); // Adjust text size based on canvas size
graphics.textAlign(CENTER, CENTER); // Center the text
graphics.fill(255); // White color for the text
function setup() {
createCanvas(windowWidth, windowHeight);
background(0); // Set initial background to black
// Create an off-screen graphics buffer to draw the text
let graphics = createGraphics(width, height);
graphics.textFont(myFont); // Set the font
graphics.textSize(min(width, height) / 8); // Adjust text size based on canvas size
graphics.textAlign(CENTER, CENTER); // Center the text
graphics.fill(255); // White color for the text
// ⇨ This is the key change: draw "AMERICA 250" instead of "AMERICA"
graphics.text("AMERICA 250", width / 2, height / 2);
// Load pixels from the off-screen buffer to find text points
graphics.loadPixels();
for (let x = 0; x < graphics.width; x += 2) { // Reduced sampling step for denser text
for (let y = 0; y < graphics.height; y += 2) { // Reduced sampling step for denser text
let index = (x + y * graphics.width) * 4;
let alpha = graphics.pixels[index + 3]; // Get the alpha value
if (alpha > 0) { // If the pixel is part of the text
textPoints.push(createVector(x, y));
}
}
}
// Sample textPoints if there are too many for performance
const desiredFireworkCount = 2000; // Desired number of sparks to form the text
if (textPoints.length > desiredFireworkCount) {
const sampledPoints = [];
while (sampledPoints.length < desiredFireworkCount) {
const randomIndex = floor(random(textPoints.length));
sampledPoints.push(textPoints[randomIndex]);
textPoints.splice(randomIndex, 1); // Remove to avoid duplicates
}
textPoints = sampledPoints;
}
// Create a FireworkRocket for each text point, launching from the bottom
for (let i = 0; i < textPoints.length; i++) {
fireworks.push(new FireworkRocket(random(width), height, textPoints[i].x, textPoints[i].y));
}
}
Line-by-line explanation (15 lines)
🔧 Subcomponents:
for-loop
Text Pixel Sampling
for (let x = 0; x < graphics.width; x += 2) { for (let y = 0; y < graphics.height; y += 2) {
Scans the off-screen text image and extracts all white pixel coordinates as target positions for particles
for-loop
Rocket Instantiation
for (let i = 0; i < textPoints.length; i++) { fireworks.push(new FireworkRocket(random(width), height, textPoints[i].x, textPoints[i].y)); }
Creates one FireworkRocket object for each text pixel, each launching from a random x position at the bottom
createCanvas(windowWidth, windowHeight);
- Creates a canvas that fills the entire browser window and responds to resizing
background(0); // Set initial background to black
- Fills the canvas with black, establishing the night-sky look
let graphics = createGraphics(width, height);
- Creates an invisible off-screen drawing surface (same size as the main canvas) where we'll render text without displaying it directly
graphics.textFont(myFont); // Set the font
- Tells the off-screen buffer to use the custom Roboto font we loaded in preload()
graphics.textSize(min(width, height) / 8); // Adjust text size based on canvas size
- Makes the text size responsive—on small screens it shrinks, on large screens it grows
graphics.text("AMERICA 250", width / 2, height / 2);
- Draws the text centered on the off-screen buffer at a huge size—this is invisible to the viewer but we'll use it as a geometry template
graphics.loadPixels();
- Copies pixel data from the off-screen buffer into an array so we can read individual pixel colors
let index = (x + y * graphics.width) * 4;
- Calculates the position in the pixels array for coordinate (x, y). Multiply by 4 because each pixel stores 4 values: red, green, blue, and alpha (transparency)
let alpha = graphics.pixels[index + 3]; // Get the alpha value
- Reads the transparency value at that pixel position. +3 skips past red, green, and blue to grab the alpha channel
if (alpha > 0) { // If the pixel is part of the text
- Only processes pixels that are visible (non-transparent). Invisible background pixels are skipped
textPoints.push(createVector(x, y));
- Stores this pixel's (x, y) location as a target position where a particle will eventually settle
const desiredFireworkCount = 2000; // Desired number of sparks to form the text
- Sets a cap on particle count. If we found more than 2000 text pixels, we'll thin them out to keep animation smooth
const randomIndex = floor(random(textPoints.length));
- Picks a random index from the full list of text pixels
sampledPoints.push(textPoints[randomIndex]); textPoints.splice(randomIndex, 1);
- Adds that pixel to our final list and removes it from the full list to avoid picking it twice
fireworks.push(new FireworkRocket(random(width), height, textPoints[i].x, textPoints[i].y));
- Creates a new rocket that starts at a random x position at the bottom of the canvas and targets the text pixel at textPoints[i]
🔬 This loop processes every rocket every frame. What happens if you comment out the fireworks[i].show() line so rockets update their position but never draw themselves—do you still see the text form?
// Update and show FireworkRockets
for (let i = fireworks.length - 1; i >= 0; i--) {
fireworks[i].update();
fireworks[i].show();
// If a rocket is "finished" (transformed into an explosion particle), remove it
if (fireworks[i].finished()) {
fireworks.splice(i, 1);
}
}
function draw() {
// Create a semi-transparent background to create trail effects
colorMode(RGB);
background(0, 0, 0, 25); // Dark background with some transparency
// Update and show FireworkRockets
for (let i = fireworks.length - 1; i >= 0; i--) {
fireworks[i].update();
fireworks[i].show();
// If a rocket is "finished" (transformed into an explosion particle), remove it
if (fireworks[i].finished()) {
fireworks.splice(i, 1);
}
}
// Update and show ExplosionParticles
for (let i = explosionParticles.length - 1; i >= 0; i--) {
explosionParticles[i].update();
explosionParticles[i].show();
// If an explosion particle has dissolved completely, remove it
if (explosionParticles[i].finished()) {
explosionParticles.splice(i, 1);
}
}
}
Line-by-line explanation (6 lines)
🔧 Subcomponents:
for-loop
Rocket Update Loop
for (let i = fireworks.length - 1; i >= 0; i--) { fireworks[i].update(); fireworks[i].show(); if (fireworks[i].finished()) { fireworks.splice(i, 1); } }
Loops backwards through all active rockets, updates their physics, draws them, and removes finished ones
for-loop
Explosion Particle Update Loop
for (let i = explosionParticles.length - 1; i >= 0; i--) { explosionParticles[i].update(); explosionParticles[i].show(); if (explosionParticles[i].finished()) { explosionParticles.splice(i, 1); } }
Loops backwards through all explosion particles, updates their motion and state, draws them, and removes fully dissolved ones
colorMode(RGB);
- Ensures colors are specified using RGB (red, green, blue) values rather than HSB (hue, saturation, brightness)
background(0, 0, 0, 25); // Dark background with some transparency
- Fills the canvas with near-black but with transparency (alpha = 25). This semi-transparent layer means each frame slightly fades older particles, creating motion trails instead of erasing them completely
for (let i = fireworks.length - 1; i >= 0; i--) {
- Loops backwards through the fireworks array (important when removing items). Backwards looping prevents skipped elements when splice() is called
fireworks[i].update();
- Calls the update() method on the rocket, which applies forces, updates velocity and position, and checks if it has reached its peak
fireworks[i].show();
- Calls the show() method to draw the rocket at its current position
if (fireworks[i].finished()) { fireworks.splice(i, 1); }
- If the rocket has finished its journey (transformed into an explosion particle), remove it from the fireworks array to save memory and processing
🔬 This is the rocket's physics engine. What happens if you change -0.2 to 0.2 (removing the negative sign) so the force pushes downward instead of up? Do rockets still explode at the right height?
if (!this.finishedMoving) {
this.applyForce(createVector(0, -0.2)); // Apply force upwards to simulate launch
this.vel.add(this.acc);
this.pos.add(this.vel);
this.acc.mult(0); // Reset acceleration
// Check if rocket has reached its peak or passed its target height
if (this.vel.y >= 0 || this.pos.y < this.targetPos.y) {
class FireworkRocket {
constructor(x, y, targetX, targetY) {
this.pos = createVector(x, y);
this.vel = createVector(0, random(-10, -15)); // Launch upwards
this.acc = createVector(0, 0);
this.targetPos = createVector(targetX, targetY);
this.color = this.getRandomColor(); // Get a patriotic color
this.finishedMoving = false; // Flag to indicate it's done moving and ready to transform
this.size = 2; // Size of the rocket particle
}
// Get a random patriotic color (red, white, blue, or gold)
getRandomColor() {
let colors = [
color(255, 0, 0), // Red
color(255), // White
color(0, 0, 255), // Blue
color(255, 204, 0) // Gold
];
return random(colors);
}
// Apply a force to the particle
applyForce(force) {
this.acc.add(force);
}
// Update the rocket's position
update() {
if (!this.finishedMoving) {
this.applyForce(createVector(0, -0.2)); // Apply force upwards to simulate launch
this.vel.add(this.acc);
this.pos.add(this.vel);
this.acc.mult(0); // Reset acceleration
// Check if rocket has reached its peak or passed its target height
if (this.vel.y >= 0 || this.pos.y < this.targetPos.y) {
// Transform into an ExplosionParticle at its current position
explosionParticles.push(
new ExplosionParticle(this.pos.x, this.pos.y, this.targetPos.x, this.targetPos.y, this.color)
);
this.finishedMoving = true;
}
}
}
// Show the rocket
show() {
if (!this.finishedMoving) {
strokeWeight(2); // Keep stroke for the rocket trail
stroke(this.color);
point(this.pos.x, this.pos.y);
}
}
// Check if the rocket has finished its movement and transformed
finished() {
return this.finishedMoving;
}
}
Line-by-line explanation (17 lines)
🔧 Subcomponents:
calculation
Patriotic Color Selection
return random(colors);
Picks a random patriotic color (red, white, blue, or gold) for the rocket to use during flight
calculation
Physics Integration
this.applyForce(createVector(0, -0.2)); this.vel.add(this.acc); this.pos.add(this.vel);
Applies upward force, updates velocity based on acceleration, and moves the rocket
conditional
Peak Detection and Transformation
if (this.vel.y >= 0 || this.pos.y < this.targetPos.y) { explosionParticles.push(new ExplosionParticle(...)); this.finishedMoving = true; }
Detects when the rocket has peaked (velocity stops going up) or reached its target height, then transforms it into an explosion particle
this.pos = createVector(x, y);
- Stores the rocket's starting position (a random x at the bottom of the screen) as a p5.Vector
this.vel = createVector(0, random(-10, -15)); // Launch upwards
- Creates a velocity vector with zero horizontal movement and a strong upward speed (negative y, since y increases downward in p5.js)
this.acc = createVector(0, 0);
- Initializes acceleration to zero; forces will be added to this each frame
this.targetPos = createVector(targetX, targetY);
- Stores the text pixel coordinate this rocket should aim for when it explodes
this.color = this.getRandomColor(); // Get a patriotic color
- Assigns this rocket a random patriotic color (red, white, blue, or gold) that will be used for the rocket trail and inherited by its explosion particles
this.finishedMoving = false; // Flag to indicate it's done moving and ready to transform
- Boolean flag tracking whether this rocket has already peaked and transformed into particles
let colors = [ color(255, 0, 0), color(255), color(0, 0, 255), color(255, 204, 0) ];
- Array of four p5.js color objects: red, white, blue, and gold—the patriotic color palette
return random(colors);
- Randomly picks one color from the array and returns it
this.acc.add(force);
- Adds the force vector to the accumulator; multiple forces can be applied per frame and they add up
this.applyForce(createVector(0, -0.2)); // Apply force upwards to simulate launch
- Applies an upward force of 0.2 pixels per frame squared, accelerating the rocket upward against gravity
this.vel.add(this.acc);
- Updates velocity by adding the accumulated forces. The rocket speeds up in the direction of the force
this.pos.add(this.vel);
- Updates position by adding velocity. The rocket moves to a new location each frame
this.acc.mult(0); // Reset acceleration
- Clears acceleration to zero at the end of the frame, ready for new forces to be added next frame
if (this.vel.y >= 0 || this.pos.y < this.targetPos.y) {
- Checks two conditions: if velocity has stopped going up (vel.y >= 0, meaning the rocket peaked) OR if the rocket has passed its target height. Either way, it's time to explode.
explosionParticles.push( new ExplosionParticle(this.pos.x, this.pos.y, this.targetPos.x, this.targetPos.y, this.color) );
- Creates a new ExplosionParticle at the rocket's current position, targeting the text coordinates, and using the same color
stroke(this.color); point(this.pos.x, this.pos.y);
- Draws a single colored pixel at the rocket's position, creating a thin colored trail as it moves upward
return this.finishedMoving;
- Returns true if the rocket has already transformed, signaling to draw() that it can be removed from the fireworks array
🔬 These lines control how particles fade during the explosion phase. What happens if you change -= 2 to -= 0.5 so particles fade much more slowly? Do they stay visible longer before settling?
// Reduce lifespan for initial fading
this.lifespan -= 2;
// Map lifespan to current size during explosion phase
this.currentSize = map(this.lifespan, 0, this.initialLifespan, 0.5, this.initialParticleSize);
🔬 This block controls the final fade-out. What happens if you change -= 1 to -= 5 so particles dissolve five times faster? How does this change the visual feel?
} else if (this.dissolving) {
// Particle is dissolving
this.lifespan -= 1; // Reduce lifespan to fade it out
// Map lifespan to current size during dissolving phase
this.currentSize = map(this.lifespan, 0, this.initialLifespan, 0, this.settledParticleSize);
}
class ExplosionParticle {
constructor(x, y, targetX, targetY, col) {
this.pos = createVector(x, y);
this.vel = p5.Vector.random2D(); // Initial random explosion direction
this.vel.mult(random(2, 8)); // Initial explosion speed
this.acc = createVector(0, 0);
this.targetPos = createVector(targetX, targetY);
this.color = col;
this.initialLifespan = 255; // Max lifespan
this.lifespan = this.initialLifespan; // Current lifespan
this.settled = false; // Flag to indicate it has reached its target
this.settleTime = 0; // Time when the particle settled
this.dissolving = false; // Flag to indicate it's currently dissolving
this.initialParticleSize = random(2, 6); // Size during explosion phase
this.settledParticleSize = 2; // Size when forming the text
this.currentSize = this.initialParticleSize; // Dynamic size
}
// Apply a force to the particle
applyForce(force) {
this.acc.add(force);
}
// Update the particle's position and move towards target
update() {
if (!this.settled) {
// Add a small gravity effect
this.applyForce(createVector(0, 0.05));
// Calculate force towards target position
let steer = p5.Vector.sub(this.targetPos, this.pos);
steer.setMag(0.1); // Adjust steering force
this.applyForce(steer);
this.vel.add(this.acc);
this.pos.add(this.vel);
this.acc.mult(0); // Reset acceleration
// Reduce lifespan for initial fading
this.lifespan -= 2;
// Map lifespan to current size during explosion phase
this.currentSize = map(this.lifespan, 0, this.initialLifespan, 0.5, this.initialParticleSize);
// Stop moving and settle if close enough to target
if (this.pos.dist(this.targetPos) < 1) {
this.pos.set(this.targetPos); // Snap to target
this.vel.mult(0); // Stop movement
this.acc.mult(0);
this.settled = true;
this.lifespan = this.initialLifespan; // Reset lifespan to full visibility for the text
this.currentSize = this.settledParticleSize; // Fixed size for text particles
this.settleTime = millis(); // Record the time it settled
}
} else if (this.settled && !this.dissolving) {
// Particle is settled, check if it's time to dissolve
if (millis() - this.settleTime > DISSOLVE_DELAY) {
this.dissolving = true;
}
} else if (this.dissolving) {
// Particle is dissolving
this.lifespan -= 1; // Reduce lifespan to fade it out
// Map lifespan to current size during dissolving phase
this.currentSize = map(this.lifespan, 0, this.initialLifespan, 0, this.settledParticleSize);
}
}
// Show the particle
show() {
noStroke(); // No stroke for a solid, fluffy look
fill(this.color.levels[0], this.color.levels[1], this.color.levels[2], this.lifespan);
circle(this.pos.x, this.pos.y, this.currentSize); // Draw as a circle
}
// Text particles are finished if they have dissolved completely
finished() {
return this.dissolving && this.lifespan < 0;
}
}
Line-by-line explanation (30 lines)
🔧 Subcomponents:
conditional
Explosion and Attraction Phase
if (!this.settled) { this.applyForce(createVector(0, 0.05)); let steer = p5.Vector.sub(this.targetPos, this.pos); steer.setMag(0.1); this.applyForce(steer);
Applies gravity and steering forces so particles burst outward then gradually curve toward their text target positions
conditional
Settling Detection
if (this.pos.dist(this.targetPos) < 1) { this.pos.set(this.targetPos); this.vel.mult(0); this.acc.mult(0); this.settled = true;
Detects when a particle gets within 1 pixel of its target, then snaps it to place and freezes it as part of the text
conditional
Dissolve Trigger
else if (this.settled && !this.dissolving) { if (millis() - this.settleTime > DISSOLVE_DELAY) { this.dissolving = true; } }
Waits for DISSOLVE_DELAY milliseconds after settling, then flags the particle to begin fading out
conditional
Dissolving Phase
else if (this.dissolving) { this.lifespan -= 1; this.currentSize = map(this.lifespan, 0, this.initialLifespan, 0, this.settledParticleSize); }
Reduces lifespan and shrinks the particle until it disappears completely
this.vel = p5.Vector.random2D(); // Initial random explosion direction
- Creates a velocity pointing in a random direction (north, northeast, east, etc.) with magnitude 1. This starts the particle moving outward in the explosion
this.vel.mult(random(2, 8)); // Initial explosion speed
- Multiplies the velocity by a random number between 2 and 8, making each particle burst outward at a different speed
this.initialLifespan = 255; // Max lifespan
- Stores the maximum lifespan (opacity) as 255. This is used later to map size and alpha during different phases
this.settled = false; // Flag to indicate it has reached its target
- Boolean flag tracking the particle's phase: false during explosion/attraction, true once it reaches the text position
this.settleTime = 0; // Time when the particle settled
- Will store the millisecond timestamp when the particle reaches its target, used to trigger dissolving after DISSOLVE_DELAY
this.initialParticleSize = random(2, 6); // Size during explosion phase
- Each particle gets a random starting size. Larger starting sizes create a more varied explosion look
this.currentSize = this.initialParticleSize; // Dynamic size
- Tracks the particle's current size, which changes dynamically during explosion, settling, and dissolving phases
if (!this.settled) {
- Only update physics and position if the particle hasn't reached its target yet
this.applyForce(createVector(0, 0.05));
- Applies a gentle downward gravity force, pulling particles down slightly as they move—makes the motion feel organic
let steer = p5.Vector.sub(this.targetPos, this.pos);
- Calculates a vector pointing from the particle to its target position by subtracting current position from target
steer.setMag(0.1); // Adjust steering force
- Normalizes the steering vector and scales it to magnitude 0.1, controlling how strongly particles are pulled toward their targets
this.applyForce(steer);
- Applies the steering force, causing the particle to curve toward its text position as it's also pushed by gravity
this.lifespan -= 2;
- Reduces lifespan by 2 each frame during the explosion phase, making particles fade as they move (before settling)
this.currentSize = map(this.lifespan, 0, this.initialLifespan, 0.5, this.initialParticleSize);
- Maps the declining lifespan to a size range: as lifespan goes from 255 down to 0, size scales from initialParticleSize down to 0.5
if (this.pos.dist(this.targetPos) < 1) {
- Checks if the particle is within 1 pixel of its target using the dist() method, indicating it has arrived
this.pos.set(this.targetPos); // Snap to target
- Forces the particle to the exact target position, eliminating any floating-point drift
this.vel.mult(0); // Stop movement
- Zeroes out velocity so the particle stops moving
this.settled = true;
- Marks the particle as settled so the update loop skips physics on future frames and checks the dissolve timer instead
this.lifespan = this.initialLifespan; // Reset lifespan to full visibility for the text
- Resets lifespan to 255 so the particle is fully opaque once it reaches the text, erasing the fading that happened during the explosion
this.currentSize = this.settledParticleSize; // Fixed size for text particles
- Sets the particle to a small fixed size (2 pixels), making the text sharp and uniform
this.settleTime = millis(); // Record the time it settled
- Captures the current time in milliseconds so the dissolve timer can measure how long the particle has been settled
} else if (this.settled && !this.dissolving) {
- If the particle is settled but not yet dissolving, check the dissolve timer
if (millis() - this.settleTime > DISSOLVE_DELAY) {
- Compares the elapsed time (now minus settleTime) to DISSOLVE_DELAY (60000 ms). If more than 60 seconds have passed, start dissolving
this.dissolving = true;
- Flags the particle to enter dissolving mode on the next update
} else if (this.dissolving) {
- If the particle is in dissolving mode, fade it out
this.lifespan -= 1; // Reduce lifespan to fade it out
- Reduces lifespan slowly (by 1 each frame instead of 2) to create a slow, elegant fade
this.currentSize = map(this.lifespan, 0, this.initialLifespan, 0, this.settledParticleSize);
- Maps declining lifespan to size: as lifespan goes from 255 to 0, size shrinks from settledParticleSize (2) to 0
fill(this.color.levels[0], this.color.levels[1], this.color.levels[2], this.lifespan);
- Sets the fill color to the particle's color using its RGB channels and alpha set to lifespan (255 = opaque, 0 = transparent)
circle(this.pos.x, this.pos.y, this.currentSize); // Draw as a circle
- Draws a circle at the particle's position with its current size, using the fill color with current opacity
return this.dissolving && this.lifespan < 0;
- Returns true only if the particle is dissolving AND its lifespan has dropped below 0, meaning it's completely invisible and can be removed