Iridescent Soap Bubbles - xelsed.ai

This sketch creates a dreamy soap bubble machine that continuously spawns iridescent bubbles at the bottom of the canvas. Each bubble floats upward with gentle wobbling motion, displays animated rainbow gradients, and gradually fades out before popping. The soft blue gradient background creates a calming, meditative atmosphere.

πŸŽ“ Concepts You'll Learn

Object-oriented programming with classesCanvas gradients (radial and linear)Animation with trigonometric functionsParticle system with lifecycle managementFrame-rate independent spawningAlpha transparency and fade effectsdrawingContext for advanced 2D renderingArray manipulation and iteration

πŸ”„ Code Flow

Code flow showing setup, draw, spawnbubbles, updateanddrawbubbles, bubble, drawbackgroundgradient, windowresized

πŸ’‘ Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> spawnbubbles[spawnbubbles] draw --> updateanddrawbubbles[updateanddrawbubbles] draw --> drawbackgroundgradient[drawbackgroundgradient] click setup href "#fn-setup" click draw href "#fn-draw" click spawnbubbles href "#fn-spawnbubbles" click updateanddrawbubbles href "#fn-updateanddrawbubbles" click drawbackgroundgradient href "#fn-drawbackgroundgradient" subgraph spawnbubbles_flow[spawnbubbles Flow] accumulator-update[accumulator-update] --> spawn-loop[spawn-loop] spawn-loop -->|if enough time| newbubble[Create New Bubble] spawn-loop -->|if max count reached| endspawn[End Spawn] click accumulator-update href "#sub-accumulator-update" click spawn-loop href "#sub-spawn-loop" end subgraph updateanddrawbubbles_flow[updateanddrawbubbles Flow] reverse-loop[reverse-loop] --> dead-check[dead-check] dead-check -->|remove dead bubbles| endcheck[End Check] click reverse-loop href "#sub-reverse-loop" click dead-check href "#sub-dead-check" end subgraph bubble_flow[Bubble Class Flow] radius-calculation[radius-calculation] --> spawn-position[spawn-position] spawn-position --> velocity-setup[velocity-setup] velocity-setup --> wobble-setup[wobble-setup] wobble-setup --> fade-calculation[fade-calculation] fade-calculation --> wobble-calculation[wobble-calculation] wobble-calculation --> sheen-calculation[sheen-calculation] sheen-calculation --> gradient-creation[gradient-creation] gradient-creation --> color-stops[color-stops] click radius-calculation href "#sub-radius-calculation" click spawn-position href "#sub-spawn-position" click velocity-setup href "#sub-velocity-setup" click wobble-setup href "#sub-wobble-setup" click fade-calculation href "#sub-fade-calculation" click wobble-calculation href "#sub-wobble-calculation" click sheen-calculation href "#sub-sheen-calculation" click gradient-creation href "#sub-gradient-creation" click color-stops href "#sub-color-stops" end subgraph drawbackgroundgradient_flow[drawbackgroundgradient Flow] gradient-creation[gradient-creation] --> color-stops[color-stops] click gradient-creation href "#sub-gradient-creation" click color-stops href "#sub-color-stops" end windowresized[windowresized] --> resize[resizeCanvas] click windowresized href "#fn-windowresized"

πŸ“ Code Breakdown

setup()

setup() runs once when the sketch starts. Here we initialize the canvas size to match the window and set default drawing properties. Using windowWidth/windowHeight makes the sketch responsive to window resizing.

function setup() {
  createCanvas(windowWidth, windowHeight); // 2D renderer
  noStroke();
}

Line by Line:

createCanvas(windowWidth, windowHeight)
Creates a canvas that fills the entire browser window. windowWidth and windowHeight are p5.js variables that automatically detect the screen size.
noStroke()
Disables stroke (outline) by default for all shapes. Individual shapes can override this later if needed.

draw()

draw() runs 60 times per second (by default). Each frame, we redraw the background, spawn new bubbles, and update/display all existing bubbles. This creates the continuous animation effect.

function draw() {
  drawBackgroundGradient();
  spawnBubbles();
  updateAndDrawBubbles();
}

Line by Line:

drawBackgroundGradient()
Calls the function that fills the canvas with a soft blue gradient background, clearing the previous frame.
spawnBubbles()
Calls the function that creates new bubbles at a controlled rate (3.5 per second on average).
updateAndDrawBubbles()
Calls the function that moves all existing bubbles and draws them on screen, plus removes bubbles that have popped.

spawnBubbles()

This function uses an accumulator pattern to spawn bubbles at a consistent rate regardless of frame rate. Instead of spawning a fixed number per frame, we track elapsed time and spawn when enough time has passed. This is more realistic and predictable than frame-dependent spawning.

function spawnBubbles() {
  // Use deltaTime so rate is frame-rate independent
  spawnAccumulator += BUBBLES_PER_SECOND * (deltaTime / 1000);

  while (spawnAccumulator >= 1 && bubbles.length < MAX_BUBBLES) {
    bubbles.push(new Bubble());
    spawnAccumulator--;
  }
}

πŸ”§ Subcomponents:

calculation Frame-Rate Independent Accumulation spawnAccumulator += BUBBLES_PER_SECOND * (deltaTime / 1000)

Accumulates spawn time based on actual elapsed time (deltaTime), not frame count. This ensures consistent spawn rate regardless of frame rate.

while-loop Bubble Creation Loop while (spawnAccumulator >= 1 && bubbles.length < MAX_BUBBLES)

Creates new bubbles when enough time has accumulated AND we haven't reached the maximum bubble count.

Line by Line:

spawnAccumulator += BUBBLES_PER_SECOND * (deltaTime / 1000)
Adds to the accumulator based on how much real time passed since the last frame (deltaTime is in milliseconds, so we divide by 1000 to convert to seconds). This makes spawning frame-rate independent.
while (spawnAccumulator >= 1 && bubbles.length < MAX_BUBBLES)
Loops as long as: (1) the accumulator has reached 1 or more (meaning 1 second's worth of spawn time has passed), AND (2) we haven't created 80 bubbles yet (MAX_BUBBLES limit).
bubbles.push(new Bubble())
Creates a new Bubble object and adds it to the bubbles array. The Bubble constructor initializes all properties for that bubble.
spawnAccumulator--
Decrements the accumulator by 1, representing that we've 'used up' one bubble's worth of spawn time.

updateAndDrawBubbles()

This function demonstrates proper array iteration when removing items. Always iterate backwards when splicing from an array, or use filter() instead. The pattern of update-then-display is common in game/animation loops.

function updateAndDrawBubbles() {
  for (let i = bubbles.length - 1; i >= 0; i--) {
    const b = bubbles[i];
    b.update();
    b.display();
    if (b.isDead()) {
      bubbles.splice(i, 1); // "pop" – remove from array
    }
  }
}

πŸ”§ Subcomponents:

for-loop Reverse Iteration Loop for (let i = bubbles.length - 1; i >= 0; i--)

Loops through the bubbles array backwards. This is crucial because we're removing items from the array during iteration.

conditional Bubble Removal Check if (b.isDead()) { bubbles.splice(i, 1) }

Removes dead bubbles from the array to prevent memory buildup.

Line by Line:

for (let i = bubbles.length - 1; i >= 0; i--)
Loops backwards through the bubbles array (from last to first). We go backwards because removing items from an array shifts indices, which breaks forward loops.
const b = bubbles[i]
Creates a shorthand reference to the current bubble object for cleaner code.
b.update()
Calls the bubble's update method, which moves the bubble based on its velocity.
b.display()
Calls the bubble's display method, which draws the bubble with its gradient and effects.
if (b.isDead())
Checks if the bubble has exceeded its lifespan or moved off-screen.
bubbles.splice(i, 1)
Removes the dead bubble from the array. splice(i, 1) removes 1 element starting at index i.

class Bubble

The Bubble class demonstrates object-oriented design in p5.js. Each bubble is an independent object with its own properties (position, velocity, color, etc.) and methods (update, display, isDead). The display() method is particularly sophisticatedβ€”it uses canvas gradients, trigonometry for the orbiting sheen, and sine waves for wobble to create a realistic, animated bubble effect.

class Bubble {
  constructor() {
    // Radius: more small than big for variation
    const minR = 16;
    const maxR = 70;
    const t = pow(random(), 1.4); // bias toward smaller sizes
    this.radius = lerp(minR, maxR, t);

    // Spawn near bottom center with some horizontal spread
    const spread = width * 0.35;
    this.x = width / 2 + random(-spread, spread);
    this.y = height + random(10, 80); // start just below bottom

    // Vertical speed (rise)
    this.vy = -lerp(0.7, 1.6, 1 - t); // smaller bubbles a bit slower

    // Slow horizontal drift
    this.vxBase = random(-0.15, 0.15);

    // Wobble: horizontal sinusoidal offset
    this.wobbleAmp = random(5, 18);
    this.wobbleFreq = random(0.3, 0.8); // cycles per second
    this.phase = random(TWO_PI);

    // Lifetime for popping
    this.birth = millis();
    this.lifespan = random(8000, 18000); // 8–18 seconds

    // Iridescent color + transparency
    this.baseHue = random(0, 360);
    this.baseAlpha = random(0.30, 0.55);

    // Sheen highlight drift around the bubble
    this.sheenSpeed = random(-0.6, 0.6); // radians per second
  }

  update() {
    this.y += this.vy;
    this.x += this.vxBase * 0.7; // gentle sideways drift
  }

  getFadeFactor() {
    const age = millis() - this.birth;
    const t = constrain(age / this.lifespan, 0, 1);

    // Fade-in over first 12% of life
    const fadeIn = constrain((t - 0.0) / 0.12, 0, 1);
    // Fade-out over last 35% of life
    const fadeOut = 1 - constrain((t - 0.65) / 0.35, 0, 1);

    return fadeIn * fadeOut;
  }

  isDead() {
    const age = millis() - this.birth;
    // Pop when above the top or past lifespan
    return age > this.lifespan || this.y + this.radius < -40;
  }

  display() {
    const ctx = drawingContext; // 2D CanvasRenderingContext2D
    const r = this.radius;
    const time = millis() / 1000;

    const fade = this.getFadeFactor();
    if (fade <= 0) return;

    const alphaScale = this.baseAlpha * fade;

    // Wobble: time-based horizontal oscillation
    const wobbleX = sin(TWO_PI * this.wobbleFreq * time + this.phase) * this.wobbleAmp;
    const x = this.x + wobbleX;
    const y = this.y;

    // Sheen highlight: small bright spot that orbits around the bubble
    const angle = this.sheenSpeed * time + this.phase;
    const sheenOffset = r * 0.4;
    const gx = x + cos(angle) * sheenOffset;
    const gy = y + sin(angle) * sheenOffset;

    // Iridescent radial gradient
    const grad = ctx.createRadialGradient(gx, gy, r * 0.15, x, y, r);

    // Animate the rainbow hue slightly over time
    const hue = (this.baseHue + time * 40) % 360;

    grad.addColorStop(0.0, `rgba(255,255,255,${0.9 * alphaScale})`);
    grad.addColorStop(0.25, `hsla(${hue}, 95%, 85%, ${0.75 * alphaScale})`);
    grad.addColorStop(0.5, `hsla(${(hue + 60) % 360}, 90%, 70%, ${0.45 * alphaScale})`);
    grad.addColorStop(0.8, `hsla(${(hue + 140) % 360}, 90%, 65%, ${0.20 * alphaScale})`);
    grad.addColorStop(1.0, `rgba(255,255,255,0)`);

    ctx.fillStyle = grad;

    noStroke();
    ellipse(x, y, r * 2, r * 2);

    // Subtle outer rim to make the bubble edge visible
    stroke(255, 255, 255, 160 * alphaScale);
    strokeWeight(r * 0.06);
    noFill();
    ellipse(x, y, r * 2.02, r * 2.02);

    // Small inner highlight arc for extra realism
    push();
    translate(x, y);
    rotate(-PI / 3);
    stroke(255, 255, 255, 200 * alphaScale);
    strokeWeight(r * 0.07);
    noFill();
    arc(0, 0, r * 1.1, r * 1.1, -PI * 0.15, PI * 0.35);
    pop();
  }
}

πŸ”§ Subcomponents:

calculation Biased Radius Generation const t = pow(random(), 1.4); this.radius = lerp(minR, maxR, t)

Creates more small bubbles than large ones by using pow() to bias the random value toward 0.

calculation Spawn Position Setup this.x = width / 2 + random(-spread, spread); this.y = height + random(10, 80)

Positions bubbles at the bottom center with horizontal spread, starting just below the visible canvas.

calculation Velocity Initialization this.vy = -lerp(0.7, 1.6, 1 - t); this.vxBase = random(-0.15, 0.15)

Sets upward velocity (negative y) and slow horizontal drift. Smaller bubbles rise slower.

calculation Wobble Parameters this.wobbleAmp = random(5, 18); this.wobbleFreq = random(0.3, 0.8); this.phase = random(TWO_PI)

Initializes parameters for the sinusoidal side-to-side motion that makes bubbles look natural.

calculation Fade Factor Computation const fadeIn = constrain((t - 0.0) / 0.12, 0, 1); const fadeOut = 1 - constrain((t - 0.65) / 0.35, 0, 1)

Creates smooth fade-in at start (first 12% of life) and fade-out at end (last 35% of life).

calculation Wobble Offset Calculation const wobbleX = sin(TWO_PI * this.wobbleFreq * time + this.phase) * this.wobbleAmp

Uses sine wave to create smooth horizontal oscillation that varies with time.

calculation Sheen Highlight Position const angle = this.sheenSpeed * time + this.phase; const gx = x + cos(angle) * sheenOffset; const gy = y + sin(angle) * sheenOffset

Positions a bright spot that orbits around the bubble using trigonometry, creating the illusion of light reflecting off a curved surface.

calculation Radial Gradient Setup const grad = ctx.createRadialGradient(gx, gy, r * 0.15, x, y, r)

Creates a radial gradient from a small bright center (at the sheen position) to the bubble edge, with the gradient center offset to create 3D effect.

calculation Gradient Color Stops grad.addColorStop(...)

Defines 5 color transitions across the gradient: white core, iridescent highlight, warm tint, cool rim, and transparent edge.

Line by Line:

const t = pow(random(), 1.4)
Creates a biased random number between 0 and 1. pow(random(), 1.4) pushes values toward 0, making more bubbles small than large.
this.radius = lerp(minR, maxR, t)
Interpolates between minimum radius (16) and maximum radius (70) using the biased value t. Smaller t means smaller bubbles.
const spread = width * 0.35
Calculates how far left/right bubbles can spawn from center (35% of canvas width).
this.x = width / 2 + random(-spread, spread)
Spawns bubble horizontally at canvas center plus a random offset within the spread range.
this.y = height + random(10, 80)
Spawns bubble just below the bottom of the canvas (between 10-80 pixels below), so it enters from below.
this.vy = -lerp(0.7, 1.6, 1 - t)
Sets upward velocity (negative because y increases downward in p5.js). Smaller bubbles (high t) get slower speed (0.7), larger bubbles (low t) get faster speed (1.6).
this.vxBase = random(-0.15, 0.15)
Sets a slow horizontal drift velocity between -0.15 and 0.15 pixels per frame.
this.wobbleAmp = random(5, 18)
Sets the amplitude (width) of the side-to-side wobble motion in pixels.
this.wobbleFreq = random(0.3, 0.8)
Sets how many complete wobble cycles happen per second (0.3 to 0.8 Hz).
this.phase = random(TWO_PI)
Sets a random starting phase (0 to 2Ο€) so bubbles don't all wobble in sync.
this.birth = millis()
Records the current time in milliseconds when the bubble is created, used to calculate age later.
this.lifespan = random(8000, 18000)
Sets how long the bubble lives in milliseconds (8 to 18 seconds) before it pops.
this.baseHue = random(0, 360)
Sets a random starting hue (0-360 degrees) for the iridescent color effect.
this.baseAlpha = random(0.30, 0.55)
Sets the bubble's base transparency (0.30 to 0.55, where 1.0 is fully opaque).
this.sheenSpeed = random(-0.6, 0.6)
Sets how fast the sheen highlight orbits around the bubble in radians per second.
this.y += this.vy
In update(), moves the bubble upward by adding its vertical velocity.
this.x += this.vxBase * 0.7
In update(), applies gentle horizontal drift (multiplied by 0.7 to make it slower than the base velocity).
const age = millis() - this.birth
In getFadeFactor(), calculates how many milliseconds the bubble has existed.
const t = constrain(age / this.lifespan, 0, 1)
Converts age to a normalized value from 0 (birth) to 1 (death), clamped to prevent going over 1.
const fadeIn = constrain((t - 0.0) / 0.12, 0, 1)
Calculates fade-in factor: goes from 0 to 1 during the first 12% of the bubble's life.
const fadeOut = 1 - constrain((t - 0.65) / 0.35, 0, 1)
Calculates fade-out factor: stays at 1 until 65% of life, then fades from 1 to 0 over the last 35%.
return fadeIn * fadeOut
Multiplies the two factors together. The result is 0 at start, rises to 1 in the middle, then falls to 0 at the end.
return age > this.lifespan || this.y + this.radius < -40
In isDead(), returns true if the bubble exceeded its lifespan OR if it moved more than 40 pixels above the top of the canvas.
const ctx = drawingContext
In display(), gets the 2D canvas context for direct gradient manipulation (p5.js feature).
const fade = this.getFadeFactor(); if (fade <= 0) return
Gets the fade factor and exits early if it's 0 or less (invisible bubble), saving computation.
const wobbleX = sin(TWO_PI * this.wobbleFreq * time + this.phase) * this.wobbleAmp
Calculates horizontal wobble offset using sine wave. The sine oscillates between -1 and 1, multiplied by wobbleAmp.
const angle = this.sheenSpeed * time + this.phase
Calculates the current angle for the orbiting sheen highlight, increasing over time.
const gx = x + cos(angle) * sheenOffset; const gy = y + sin(angle) * sheenOffset
Uses cos/sin to position the sheen highlight in a circle around the bubble center.
const grad = ctx.createRadialGradient(gx, gy, r * 0.15, x, y, r)
Creates a radial gradient from a small circle (radius 0.15r) at the sheen position to a larger circle (radius r) at the bubble center.
const hue = (this.baseHue + time * 40) % 360
Animates the hue by adding time*40 to the base hue, then using modulo to keep it in the 0-360 range.
grad.addColorStop(0.0, `rgba(255,255,255,${0.9 * alphaScale})`)
Adds a white color stop at the gradient center (0.0) with high opacity for the bright core.
grad.addColorStop(0.25, `hsla(${hue}, 95%, 85%, ${0.75 * alphaScale})`)
Adds an iridescent highlight color at 25% of the gradient radius, using the animated hue.
grad.addColorStop(0.5, `hsla(${(hue + 60) % 360}, 90%, 70%, ${0.45 * alphaScale})`)
Adds a warm tint (hue shifted +60Β°) at the middle of the gradient.
grad.addColorStop(0.8, `hsla(${(hue + 140) % 360}, 90%, 65%, ${0.20 * alphaScale})`)
Adds a cool rim color (hue shifted +140Β°) near the bubble edge with lower opacity.
grad.addColorStop(1.0, `rgba(255,255,255,0)`)
Adds a fully transparent color stop at the gradient edge (1.0) so the bubble fades out smoothly.
ctx.fillStyle = grad; ellipse(x, y, r * 2, r * 2)
Sets the gradient as the fill style and draws the main bubble circle.
stroke(255, 255, 255, 160 * alphaScale); strokeWeight(r * 0.06); ellipse(x, y, r * 2.02, r * 2.02)
Draws a subtle white outline slightly larger than the bubble to define its edge clearly.
push(); translate(x, y); rotate(-PI / 3)
Saves the current transformation state, moves origin to bubble center, and rotates -60Β° for the highlight arc.
arc(0, 0, r * 1.1, r * 1.1, -PI * 0.15, PI * 0.35)
Draws a white arc (curved line) that looks like a light reflection on the bubble surface.
pop()
Restores the previous transformation state so other bubbles aren't affected by this rotation.

drawBackgroundGradient()

This function uses the native Canvas API (via drawingContext) to create a gradient background. The gradient is vertical with three color stops, creating a smooth transition from light at the top to deep blue at the bottom. This is called every frame to clear the canvas and redraw the background.

function drawBackgroundGradient() {
  const ctx = drawingContext;
  const gradient = ctx.createLinearGradient(0, 0, 0, height);

  // Soft blue vertical gradient
  gradient.addColorStop(0.0, '#cbeaff');  // very light at top
  gradient.addColorStop(0.4, '#a3d6ff');  // soft sky blue
  gradient.addColorStop(1.0, '#3b6fb3');  // deeper blue at bottom

  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, width, height);
}

πŸ”§ Subcomponents:

calculation Linear Gradient Setup const gradient = ctx.createLinearGradient(0, 0, 0, height)

Creates a vertical gradient from top (0) to bottom (height).

calculation Gradient Color Stops gradient.addColorStop(...)

Defines three color transitions: light blue at top, medium blue in middle, deep blue at bottom.

Line by Line:

const ctx = drawingContext
Gets the 2D canvas context for direct canvas manipulation (more powerful than p5.js functions).
const gradient = ctx.createLinearGradient(0, 0, 0, height)
Creates a linear gradient that goes vertically from y=0 (top) to y=height (bottom). The x-coordinates are both 0 because we want vertical, not horizontal.
gradient.addColorStop(0.0, '#cbeaff')
Adds a light cyan color at position 0.0 (start of gradient, top of canvas).
gradient.addColorStop(0.4, '#a3d6ff')
Adds a soft sky blue at position 0.4 (40% down the gradient).
gradient.addColorStop(1.0, '#3b6fb3')
Adds a deeper blue at position 1.0 (end of gradient, bottom of canvas).
ctx.fillStyle = gradient
Sets the gradient as the current fill color for the canvas context.
ctx.fillRect(0, 0, width, height)
Fills the entire canvas with the gradient, clearing the previous frame and creating the background.

windowResized()

windowResized() is a special p5.js function that's called automatically whenever the browser window is resized. By calling resizeCanvas() here, we ensure the sketch always fills the entire window, making it responsive to different screen sizes.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}

Line by Line:

resizeCanvas(windowWidth, windowHeight)
Automatically resizes the canvas to match the new window dimensions whenever the browser window is resized.

πŸ“¦ Key Variables

bubbles array

Stores all currently active Bubble objects. New bubbles are added via push(), and dead bubbles are removed via splice().

let bubbles = [];
spawnAccumulator number

Accumulates time to track when to spawn the next bubble. Incremented by deltaTime each frame and decremented when a bubble is created.

let spawnAccumulator = 0;
BUBBLES_PER_SECOND number

Constant that defines the target spawn rate (3.5 bubbles per second on average). Controls how frequently new bubbles are created.

const BUBBLES_PER_SECOND = 3.5;
MAX_BUBBLES number

Constant that limits the maximum number of bubbles on screen at once (80). Prevents memory issues and maintains performance.

const MAX_BUBBLES = 80;
radius number

Property of each Bubble object. Stores the bubble's size in pixels (16-70). Affects visual size and physics.

this.radius = lerp(minR, maxR, t);
x number

Property of each Bubble object. Stores the bubble's horizontal position on the canvas, updated by wobble and drift.

this.x = width / 2 + random(-spread, spread);
y number

Property of each Bubble object. Stores the bubble's vertical position on the canvas, updated by upward velocity each frame.

this.y = height + random(10, 80);
vy number

Property of each Bubble object. Stores the bubble's vertical velocity (upward speed). Negative because y increases downward.

this.vy = -lerp(0.7, 1.6, 1 - t);
vxBase number

Property of each Bubble object. Stores the bubble's base horizontal drift velocity, creating slow side-to-side movement.

this.vxBase = random(-0.15, 0.15);
wobbleAmp number

Property of each Bubble object. Stores the amplitude (width in pixels) of the sinusoidal wobble motion.

this.wobbleAmp = random(5, 18);
wobbleFreq number

Property of each Bubble object. Stores the frequency of wobble in cycles per second (0.3-0.8 Hz).

this.wobbleFreq = random(0.3, 0.8);
phase number

Property of each Bubble object. Stores a random starting phase (0 to 2Ο€) for wobble and sheen, so bubbles don't move in sync.

this.phase = random(TWO_PI);
birth number

Property of each Bubble object. Stores the millisecond timestamp when the bubble was created, used to calculate age.

this.birth = millis();
lifespan number

Property of each Bubble object. Stores how long the bubble lives in milliseconds (8000-18000 ms) before popping.

this.lifespan = random(8000, 18000);
baseHue number

Property of each Bubble object. Stores the starting hue (0-360Β°) for the iridescent color effect, animated over time.

this.baseHue = random(0, 360);
baseAlpha number

Property of each Bubble object. Stores the base transparency (0.30-0.55) before fade effects are applied.

this.baseAlpha = random(0.30, 0.55);
sheenSpeed number

Property of each Bubble object. Stores how fast the sheen highlight orbits around the bubble in radians per second.

this.sheenSpeed = random(-0.6, 0.6);

πŸ§ͺ Try This!

Experiment with the code by making these changes:

  1. Increase BUBBLES_PER_SECOND from 3.5 to 8 to create a denser bubble stream. Watch how more bubbles fill the screen.
  2. Change the lifespan range in the Bubble constructor from random(8000, 18000) to random(3000, 8000) to make bubbles pop faster and create a more chaotic effect.
  3. Modify the wobbleAmp range from random(5, 18) to random(20, 40) to make bubbles wobble much more dramatically side-to-side.
  4. Change the gradient colors in drawBackgroundGradient() from blues to purples: '#e0d5ff', '#c4b5ff', '#7b68ee' to create a different mood.
  5. Increase the sheenSpeed range from random(-0.6, 0.6) to random(-2, 2) to make the sheen highlight orbit faster around each bubble.
  6. Modify the fade-in duration from 0.12 (12%) to 0.30 (30%) in getFadeFactor() to make bubbles appear more gradually.
  7. Change the vy calculation to this.vy = -lerp(0.3, 0.8, 1 - t) to make all bubbles rise more slowly and float longer.
  8. Adjust the gradient color stops to add more colors: add gradient.addColorStop(0.6, '#ffb3ba') to introduce pink tones into the iridescent effect.
Open in Editor & Experiment β†’

πŸ”§ Potential Improvements

Here are some ways this code could be enhanced:

PERFORMANCE display() method, gradient creation

A new gradient is created every frame for every bubble, which is computationally expensive. The gradient position changes due to the moving sheen, but this could be optimized.

πŸ’‘ Consider caching gradient creation or using a pre-computed texture. For now, the current approach is acceptable for 80 bubbles, but at higher bubble counts, this would become a bottleneck.

BUG spawnBubbles() function

If deltaTime is very large (e.g., browser tab loses focus), spawnAccumulator could accumulate a huge value, causing many bubbles to spawn at once when the tab regains focus.

πŸ’‘ Add a cap to spawnAccumulator: spawnAccumulator = min(spawnAccumulator, 5) to prevent burst spawning after lag.

STYLE Bubble constructor

The radius calculation uses pow(random(), 1.4) which is not immediately intuitive. The magic number 1.4 lacks explanation.

πŸ’‘ Add a comment explaining the bias: // pow() with exponent > 1 biases toward smaller values, creating more small bubbles than large ones

FEATURE Bubble class

Bubbles always spawn from the bottom center. There's no variation in spawn location or behavior.

πŸ’‘ Add optional parameters to the Bubble constructor to allow spawning from different locations, or create a factory function that can spawn bubbles in different patterns (e.g., from sides, random positions).

PERFORMANCE getFadeFactor() method

millis() is called multiple times per bubble per frame (in getFadeFactor and display), which is redundant.

πŸ’‘ Pass time as a parameter to getFadeFactor() or calculate it once in display() and reuse it.

BUG display() method, arc drawing

The highlight arc is always drawn at -PI/3 rotation regardless of bubble position or time. This makes the highlight look static and unnatural.

πŸ’‘ Rotate the arc based on the sheen angle: rotate(angle) instead of rotate(-PI / 3) to make the highlight follow the orbiting sheen position.

Preview

Iridescent Soap Bubbles - xelsed.ai - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Iridescent Soap Bubbles - xelsed.ai - Code flow showing setup, draw, spawnbubbles, updateanddrawbubbles, bubble, drawbackgroundgradient, windowresized
Code Flow Diagram