function setup() {
// Create a canvas that fills the entire window
createCanvas(windowWidth, windowHeight);
// Initialize gradient colors
gradientColor1 = color(224, 255, 255); // Azure (very light blue)
gradientColor2 = color(173, 216, 230); // Light Blue
// Generate initial bubbles
for (let i = 0; i < 15; i++) {
bubbles.push(new Bubble());
}
}
Line-by-line explanation (5 lines)
🔧 Subcomponents:
for-loop
Initial Bubble Creation
for (let i = 0; i < 15; i++) {
Fills the bubbles array with 15 Bubble objects before the animation starts
createCanvas(windowWidth, windowHeight);
- Makes the canvas fill the entire browser window instead of a fixed size.
gradientColor1 = color(224, 255, 255); // Azure (very light blue)
- Stores the top color of the background gradient in a p5.Color object so it doesn't need to be recreated every frame.
gradientColor2 = color(173, 216, 230); // Light Blue
- Stores the bottom color of the background gradient.
for (let i = 0; i < 15; i++) {
- Repeats 15 times to create the initial batch of bubbles.
bubbles.push(new Bubble());
- Creates a brand-new Bubble object and adds it to the end of the bubbles array.
🔬 This loop decides how many particles appear when a bubble pops. What happens if you change 10 to 2? To 50? Watch how the burst effect changes from a tiny sparkle to a small explosion.
for (let j = 0; j < 10; j++) {
burstParticles.push(new BurstParticle(bubble.x, bubble.y, bubble.color));
}
function draw() {
// Draw the background gradient
drawGradientBackground();
// Update and display bubbles
for (let i = bubbles.length - 1; i >= 0; i--) {
let bubble = bubbles[i];
bubble.update();
bubble.display();
// If bubble has popped, create burst particles and remove it
if (bubble.isPopped()) {
// Create burst particles
for (let j = 0; j < 10; j++) {
burstParticles.push(new BurstParticle(bubble.x, bubble.y, bubble.color));
}
bubbles.splice(i, 1); // Remove the popped bubble
}
}
// Update and display burst particles
for (let i = burstParticles.length - 1; i >= 0; i--) {
let particle = burstParticles[i];
particle.update();
particle.display();
// Remove dead particles
if (particle.isDead()) {
burstParticles.splice(i, 1);
}
}
// Periodically add new bubbles
if (frameCount % 60 === 0) { // Add a new bubble every 60 frames (approx 1 second)
bubbles.push(new Bubble());
}
}
Line-by-line explanation (11 lines)
🔧 Subcomponents:
for-loop
Bubble Update/Display Loop
for (let i = bubbles.length - 1; i >= 0; i--) {
Walks the bubbles array backward so items can be safely removed with splice() while looping
conditional
Popped Bubble Check
if (bubble.isPopped()) {
Detects when a bubble has floated off-screen and converts it into a burst of particles
for-loop
Burst Particle Spawner
for (let j = 0; j < 10; j++) {
Creates 10 new BurstParticle objects at the popped bubble's location
for-loop
Particle Update/Display Loop
for (let i = burstParticles.length - 1; i >= 0; i--) {
Updates, draws, and removes expired burst particles
conditional
Periodic Bubble Spawner
if (frameCount % 60 === 0) { // Add a new bubble every 60 frames (approx 1 second)
Adds a fresh bubble roughly once per second using the modulo operator on frameCount
drawGradientBackground();
- Repaints the vertical gradient every frame, which also erases the previous frame's drawing (acting like background()).
for (let i = bubbles.length - 1; i >= 0; i--) {
- Loops backward through the bubbles array - this is important because splice() shifts indexes, and looping backward avoids skipping elements.
bubble.update();
- Calls the Bubble's own update method to move it upward and apply wobble.
bubble.display();
- Draws the bubble and its shine highlight at its current position.
if (bubble.isPopped()) {
- Checks whether this bubble has floated off the top of the screen.
for (let j = 0; j < 10; j++) {
- Creates 10 particles for the pop effect.
burstParticles.push(new BurstParticle(bubble.x, bubble.y, bubble.color));
- Spawns each particle at the bubble's last position, using the bubble's own color so the burst matches.
bubbles.splice(i, 1); // Remove the popped bubble
- Removes exactly one element at index i from the bubbles array, deleting the popped bubble for good.
if (particle.isDead()) {
- Checks whether a particle's lifespan has run out.
burstParticles.splice(i, 1);
- Removes the expired particle from the array so it stops being drawn.
if (frameCount % 60 === 0) { // Add a new bubble every 60 frames (approx 1 second)
- frameCount increases by 1 every frame; the modulo operator makes this true only every 60th frame, spacing out new bubble spawns.
🔬 This loop draws one line per pixel row for a smooth gradient. What happens visually - and to performance - if you change 'y++' to 'y += 4' so it skips rows?
for (let y = 0; y <= height; y++) {
// Interpolate colors based on y position
let inter = map(y, 0, height, 0, 1);
let c = lerpColor(gradientColor1, gradientColor2, inter);
stroke(c);
line(0, y, width, y);
}
function drawGradientBackground() {
noFill();
for (let y = 0; y <= height; y++) {
// Interpolate colors based on y position
let inter = map(y, 0, height, 0, 1);
let c = lerpColor(gradientColor1, gradientColor2, inter);
stroke(c);
line(0, y, width, y);
}
}
Line-by-line explanation (6 lines)
🔧 Subcomponents:
for-loop
Gradient Line Drawing
for (let y = 0; y <= height; y++) {
Draws one horizontal line per pixel row, each colored slightly differently, to fake a smooth vertical gradient
noFill();
- Turns off shape fill since we're only drawing lines (which use stroke, not fill).
for (let y = 0; y <= height; y++) {
- Loops through every single pixel row of the canvas from top to bottom.
let inter = map(y, 0, height, 0, 1);
- Converts the current row number into a 0-to-1 progress value (0 at the top, 1 at the bottom).
let c = lerpColor(gradientColor1, gradientColor2, inter);
- Blends between the two gradient colors based on that progress value, producing a smooth transition color for this row.
stroke(c);
- Sets the line color to the blended color calculated above.
line(0, y, width, y);
- Draws a full-width horizontal line at the current row y, using the blended color.
display() {
noStroke();
fill(this.color.r, this.color.g, this.color.b, this.alpha);
circle(this.x, this.y, this.radius * 2); // Draw the main bubble
// Draw subtle shine highlight
// Position the shine slightly offset from the center
let shineSizeFactor = 0.6; // Shine size relative to bubble radius
let shineOffsetFactor = 0.5; // Offset distance from bubble center
// Animate shine position slightly
let shineAngle = map(noise(this.shineOffset), 0, 1, 0, TWO_PI);
let shineX = this.x + cos(shineAngle) * shineOffsetFactor * this.radius;
let shineY = this.y + sin(shineAngle) * shineOffsetFactor * this.radius;
fill(255, 255, 255, 180); // White, semi-transparent for shine
ellipse(shineX, shineY, this.radius * shineSizeFactor, this.radius * shineSizeFactor * 0.6); // Elongated ellipse for shine
}
Line-by-line explanation (7 lines)
fill(this.color.r, this.color.g, this.color.b, this.alpha);
- Sets the fill color using this bubble's assigned pastel color and its transparency level.
circle(this.x, this.y, this.radius * 2); // Draw the main bubble
- Draws the bubble body as a circle - note circle() takes diameter, so radius is doubled.
let shineAngle = map(noise(this.shineOffset), 0, 1, 0, TWO_PI);
- Uses Perlin noise to smoothly pick an angle (in radians) around the bubble for the highlight to sit at, causing it to slowly drift around the bubble's edge over time.
let shineX = this.x + cos(shineAngle) * shineOffsetFactor * this.radius;
- Uses trigonometry (cos) to convert the angle into an x offset from the bubble's center.
let shineY = this.y + sin(shineAngle) * shineOffsetFactor * this.radius;
- Uses sin to convert the same angle into a y offset, placing the highlight at a point on a circle around the bubble center.
fill(255, 255, 255, 180); // White, semi-transparent for shine
- Switches to a translucent white fill for the glossy highlight.
ellipse(shineX, shineY, this.radius * shineSizeFactor, this.radius * shineSizeFactor * 0.6); // Elongated ellipse for shine
- Draws a squashed (elongated) ellipse rather than a circle, which reads visually as a glassy highlight rather than a flat dot.
constructor(x, y, color) {
this.x = x;
this.y = y;
this.color = color;
this.radius = random(2, 8); // Initial size of particle
this.life = random(30, 60); // Lifespan in frames (approx 0.5 to 1 second)
// Random velocity to spread particles out
this.vx = random(-2, 2);
this.vy = random(-2, 2);
}
Line-by-line explanation (7 lines)
this.x = x;
- Places the new particle at the x position passed in - this is the popped bubble's location.
this.y = y;
- Places the new particle at the y position of the popped bubble.
this.color = color;
- Reuses the popped bubble's color so the burst visually matches the bubble that created it.
this.radius = random(2, 8); // Initial size of particle
- Gives each particle a small random size.
this.life = random(30, 60); // Lifespan in frames (approx 0.5 to 1 second)
- Gives each particle a random lifespan, so they don't all disappear at exactly the same moment.
this.vx = random(-2, 2);
- Random horizontal velocity so particles spread out in different directions.
this.vy = random(-2, 2);
- Random vertical velocity, completing a random 2D direction and speed for the particle.
update() {
this.x += this.vx;
this.y += this.vy;
this.life--; // Decrease lifespan
}
Line-by-line explanation (3 lines)
this.x += this.vx;
- Moves the particle horizontally based on its random velocity.
this.y += this.vy;
- Moves the particle vertically based on its random velocity.
this.life--; // Decrease lifespan
- Counts down the particle's remaining life by 1 every frame, eventually reaching zero.
🔬 The fade-out uses map() to turn remaining life into transparency. What happens if you raise the top alpha value from 200 to 255, or lower it to 80?
let particleAlpha = map(this.life, 0, 60, 0, 200);
fill(this.color.r, this.color.g, this.color.b, particleAlpha);
circle(this.x, this.y, this.radius * 2);
display() {
noStroke();
// Fade particle as its life decreases
let particleAlpha = map(this.life, 0, 60, 0, 200);
fill(this.color.r, this.color.g, this.color.b, particleAlpha);
circle(this.x, this.y, this.radius * 2);
}
Line-by-line explanation (3 lines)
let particleAlpha = map(this.life, 0, 60, 0, 200);
- Converts the remaining life (0 to 60) into an alpha transparency value (0 to 200), so the particle visibly fades out as its life runs down.
fill(this.color.r, this.color.g, this.color.b, particleAlpha);
- Applies the particle's color with the calculated fading transparency.
circle(this.x, this.y, this.radius * 2);
- Draws the particle as a small circle at its current position.
isDead() {
return this.life <= 0;
}
Line-by-line explanation (1 lines)
return this.life <= 0;
- Reports true once the particle's countdown timer has reached zero or below, signaling draw() to remove it from the array.