AI Raindrop Symphony - Musical Rain Visualization Watch raindrops fall and create music! Each drop

This sketch simulates gentle rainfall on a dark blue gradient background, where each raindrop that hits the bottom creates an expanding ripple, a burst of splash particles, and a soft musical note. Clicking anywhere increases the intensity of the rain and unlocks audio playback.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the storm — Increasing the spawn multiplier makes raindrops appear much more densely for the same intensity level.
  2. Change splash particle color — The hue value in fill() controls the color of every splash particle - try shifting it from aqua to pink.
  3. Make raindrops accelerate faster — Boosting the fake-gravity increment makes each raindrop speed up much more dramatically as it falls.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch turns falling raindrops into a tiny audiovisual instrument: drops accelerate downward under fake gravity, and the instant one reaches the bottom it spawns a ripple, several splash particles, and a soft pentatonic note played through p5.Oscillator and p5.Envelope. It leans on classic p5.js techniques - HSL colorMode for easy hue tweaking, an off-screen createGraphics() buffer for a pre-rendered gradient, and three small classes (Raindrop, Ripple, SplashParticle) that each manage their own position, update logic, and drawing.

The code is organized around three particle-array loops inside draw() - one for raindrops, one for ripples, one for splash particles - each looped backwards so finished elements can be safely removed with splice(). By studying it you'll learn how to build simple particle systems, how to trigger generative sound from a visual event, and how to structure a sketch so setup() does expensive one-time work while draw() just updates and renders lightweight objects every frame.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, switches to HSL color mode, and pre-renders a vertical blue gradient into an off-screen graphics buffer called bgLayer so it never has to be recalculated during animation
  2. Every frame, draw() stamps that gradient image onto the canvas, then uses a slowly-filling spawnAccumulator to decide when to create new Raindrop objects based on the current rainIntensity
  3. Each raindrop falls with its own accelerating speed (simple fake gravity), and once its y position passes the bottom edge, spawnSplashEffects() creates one Ripple and a handful of SplashParticle objects at that x position, and playRaindropNote() fires a soft randomized note
  4. Three separate backwards for-loops update and draw the raindrops, ripples, and splash particles each frame, removing any that have finished (hit the ground, faded out, or grown past their max ripple radius)
  5. Clicking or tapping the canvas calls mousePressed(), which starts the Web Audio context via userStartAudio() on the first click and permanently increases rainIntensity, making rain fall faster and more densely with every click up to a cap
  6. windowResized() keeps the canvas and the pre-rendered gradient buffer in sync whenever the browser window changes size

🎓 Concepts You'll Learn

Particle systemsOff-screen graphics buffers (createGraphics)HSL color modep5.Oscillator and p5.Envelope (generative audio)Backwards array iteration with splice()ES6 classesuserStartAudio and browser audio unlocking

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the right place for expensive one-time work like pre-rendering a gradient, so the draw() loop stays fast and smooth every frame.

function setup() {
  createCanvas(windowWidth, windowHeight);
  colorMode(HSL, 360, 100, 100, 1);

  // Pre-render gradient background to a graphics buffer
  bgLayer = createGraphics(width, height);
  bgLayer.colorMode(HSL, 360, 100, 100, 1);
  drawBackgroundGradient();
}
Line-by-line explanation (5 lines)
createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window.
colorMode(HSL, 360, 100, 100, 1);
Switches from the default RGB color system to Hue-Saturation-Lightness, with hue 0-360, saturation/lightness 0-100, and alpha 0-1. This makes it easy to pick related blue tones by just changing the hue number.
bgLayer = createGraphics(width, height);
Creates a separate, off-screen canvas the same size as the main one. Drawing here doesn't show up until you image() it onto the main canvas.
bgLayer.colorMode(HSL, 360, 100, 100, 1);
The off-screen buffer has its own independent color settings, so HSL mode must be set on it separately.
drawBackgroundGradient();
Fills bgLayer with a vertical gradient once - since gradients are slow to draw pixel-by-pixel, doing it only once in setup() instead of every frame in draw() saves a lot of performance.

drawBackgroundGradient()

This function shows a common pattern: pre-computing an expensive visual (a smooth gradient made of hundreds of individual lines) once into a buffer, rather than recalculating it 60 times per second.

🔬 This loop blends linearly from top to bottom using t. What happens visually if you replace `t` with `t * t` (making the gradient curve instead of blend evenly)?

  for (let y = 0; y < height; y++) {
    const t = y / height;
    const c = lerpColor(topColor, bottomColor, t);
    bgLayer.stroke(c);
    bgLayer.line(0, y, width, y);
  }
function drawBackgroundGradient() {
  const topColor = color(215, 80, 20);   // desaturated blue, lighter
  const bottomColor = color(215, 80, 7); // deep navy blue

  bgLayer.noFill();
  for (let y = 0; y < height; y++) {
    const t = y / height;
    const c = lerpColor(topColor, bottomColor, t);
    bgLayer.stroke(c);
    bgLayer.line(0, y, width, y);
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Row-by-row Gradient Loop for (let y = 0; y < height; y++) {

Draws one horizontal line per pixel row, each blended slightly darker than the last, to fake a smooth vertical gradient.

const topColor = color(215, 80, 20); // desaturated blue, lighter
Defines the color at the very top of the canvas using HSL values (hue 215 = blue, 80% saturation, 20% lightness).
const bottomColor = color(215, 80, 7); // deep navy blue
Defines the color at the very bottom - same hue but much darker (7% lightness) for a deep navy look.
const t = y / height;
Converts the current row number into a 0-to-1 fraction representing how far down the canvas we are.
const c = lerpColor(topColor, bottomColor, t);
lerpColor() blends smoothly between the two colors based on t - near the top it's mostly topColor, near the bottom mostly bottomColor.
bgLayer.stroke(c);
Sets the line color for this row on the off-screen buffer.
bgLayer.line(0, y, width, y);
Draws a full-width horizontal line at this row, one pixel tall, building the gradient one row at a time.

draw()

draw() runs continuously at roughly 60 frames per second. This one demonstrates three independent particle-array patterns (raindrops, ripples, splashes) all following the same safe backwards-loop-and-splice() structure for adding and removing objects during animation.

🔬 This accumulator pattern turns a fractional rate into whole raindrops. What happens if you change 0.08 to 0.5? What about 0.01?

  spawnAccumulator += rainIntensity * 0.08;
  while (spawnAccumulator >= 1) {
    createRaindrop();
    spawnAccumulator -= 1;
  }
function draw() {
  // Draw background
  image(bgLayer, 0, 0, width, height);

  drawWaterline();

  // Spawn new raindrops based on intensity
  // ~ rainIntensity * 0.08 drops per frame on average
  spawnAccumulator += rainIntensity * 0.08;
  while (spawnAccumulator >= 1) {
    createRaindrop();
    spawnAccumulator -= 1;
  }

  // Update and draw raindrops
  for (let i = raindrops.length - 1; i >= 0; i--) {
    const d = raindrops[i];
    d.update();
    d.draw();

    if (d.y + d.length >= height) {
      // Hit the bottom: ripple + splashes + note
      spawnSplashEffects(d.x);
      playRaindropNote();
      raindrops.splice(i, 1);
    } else if (d.y - d.length > height + 50) {
      // Safety clean-up (should rarely happen)
      raindrops.splice(i, 1);
    }
  }

  // Update and draw ripples
  for (let i = ripples.length - 1; i >= 0; i--) {
    const r = ripples[i];
    r.update();
    r.draw();
    if (r.isDone()) {
      ripples.splice(i, 1);
    }
  }

  // Update and draw splash particles
  for (let i = splashes.length - 1; i >= 0; i--) {
    const s = splashes[i];
    s.update();
    s.draw();
    if (s.isDone()) {
      splashes.splice(i, 1);
    }
  }

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

🔧 Subcomponents:

while-loop Raindrop Spawner while (spawnAccumulator >= 1) {

Converts a fractional spawn rate into whole raindrops by accumulating a counter and creating a drop each time it crosses 1.

for-loop Raindrop Update/Draw Loop for (let i = raindrops.length - 1; i >= 0; i--) {

Updates and draws every raindrop, checking if it has hit the ground or gone off-screen so it can be removed.

for-loop Ripple Update/Draw Loop for (let i = ripples.length - 1; i >= 0; i--) {

Updates and draws every ripple, removing it once it has grown past its max radius.

for-loop Splash Particle Update/Draw Loop for (let i = splashes.length - 1; i >= 0; i--) {

Updates and draws every splash particle, removing it once it has faded out or fallen off-screen.

conditional Raindrop Landing Check if (d.y + d.length >= height) {

Detects when a raindrop's tail reaches the bottom of the canvas and triggers the splash/ripple/sound response.

image(bgLayer, 0, 0, width, height);
Draws the pre-rendered gradient buffer onto the visible canvas every frame - this both shows the background and clears away last frame's drawings.
drawWaterline();
Draws a subtle horizontal line near the bottom to represent the water surface.
spawnAccumulator += rainIntensity * 0.08;
Adds a small fractional amount to the accumulator each frame - higher rainIntensity adds more, making drops spawn more often.
while (spawnAccumulator >= 1) { createRaindrop(); spawnAccumulator -= 1; }
Whenever the accumulator has built up to at least 1, it creates a raindrop and subtracts 1 - this lets you spawn drops at fractional, smooth average rates like 1.5 per frame.
d.update(); d.draw();
Each raindrop moves itself (falls, accelerates) then draws its current position as a short line.
if (d.y + d.length >= height) {
Checks whether the bottom tip of the raindrop's line has reached the canvas floor.
spawnSplashEffects(d.x); playRaindropNote(); raindrops.splice(i, 1);
On landing: creates the ripple/splash particles at that x position, plays a musical note, and removes the raindrop from the array since it's done.
raindrops.splice(i, 1);
splice() removes exactly one element at index i from the array - looping backwards (i--) makes this safe, since removing an element doesn't shift the indices of items still left to process.
drawHUD();
Draws the title text and instructions on top of everything else, last, so it's never hidden behind drops or splashes.

drawWaterline()

A small, purely decorative helper function - a good example of how breaking drawing code into named functions keeps draw() readable.

function drawWaterline() {
  const y = height - 8;
  stroke(200, 80, 60, 0.5);
  strokeWeight(2);
  line(0, y, width, y);
}
Line-by-line explanation (4 lines)
const y = height - 8;
Places the waterline 8 pixels above the very bottom of the canvas.
stroke(200, 80, 60, 0.5);
Sets a semi-transparent light blue color (HSL hue 200, alpha 0.5) for the line.
strokeWeight(2);
Makes the line 2 pixels thick.
line(0, y, width, y);
Draws a horizontal line spanning the full width of the canvas at that y position.

drawHUD()

Demonstrates building dynamic UI text by concatenating strings and variables, and using a ternary operator to conditionally include a hint message.

function drawHUD() {
  noStroke();
  fill(210, 30, 90, 0.85); // soft white/blue
  textSize(14);
  textAlign(LEFT, TOP);

  const title = "Raindrop Symphony";
  const hintIntensity = "Click to increase rain (" + rainIntensity + "/" + maxRainIntensity + ")";
  const hintAudio = audioStarted ? "" : "Click once to start the sound.";

  let textToShow = title + "\n" + hintIntensity;
  if (hintAudio) {
    textToShow += "\n" + hintAudio;
  }

  text(textToShow, 16, 16);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Audio Hint Check if (hintAudio) {

Only appends the 'click to start sound' line if audio hasn't been started yet.

fill(210, 30, 90, 0.85); // soft white/blue
Sets the text color to a nearly-white pale blue with slight transparency.
textAlign(LEFT, TOP);
Makes the text() call position text so its top-left corner sits at the given coordinates, rather than centering it.
const hintIntensity = "Click to increase rain (" + rainIntensity + "/" + maxRainIntensity + ")";
Builds a string showing the current rain intensity out of the maximum, updating live as rainIntensity changes.
const hintAudio = audioStarted ? "" : "Click once to start the sound.";
A ternary expression: if audio has already started, this is an empty string; otherwise it's an instruction to click.
if (hintAudio) { textToShow += "\n" + hintAudio; }
Empty strings are 'falsy' in JavaScript, so this line only runs (and adds the hint) when hintAudio actually contains text.
text(textToShow, 16, 16);
Draws all the combined text lines starting 16 pixels from the top-left corner.

createRaindrop()

This factory function centralizes raindrop creation with randomized properties - a common pattern for keeping particle systems visually varied instead of repetitive.

function createRaindrop() {
  const x = random(width);
  const y = random(-40, -10);
  const speed = random(4, 9);
  const length = random(10, 20);
  const thickness = random(1, 2);

  raindrops.push(new Raindrop(x, y, speed, length, thickness));
}
Line-by-line explanation (5 lines)
const x = random(width);
Picks a random horizontal position anywhere across the canvas width.
const y = random(-40, -10);
Starts the drop above the visible canvas (negative y), so it falls into view rather than popping in abruptly.
const speed = random(4, 9);
Gives each drop a random initial falling speed, so they don't all move in lockstep.
const length = random(10, 20);
Randomizes how long each drop's streak looks.
raindrops.push(new Raindrop(x, y, speed, length, thickness));
Creates a new Raindrop object with these random properties and adds it to the raindrops array so draw() will update and render it.

spawnSplashEffects()

Shows how one event (a raindrop landing) can fan out into multiple related objects - a ripple plus several particles - all created together in one helper function.

function spawnSplashEffects(x) {
  const surfaceY = height - 8;
  ripples.push(new Ripple(x, surfaceY));

  const count = int(random(4, 8));
  for (let i = 0; i < count; i++) {
    splashes.push(new SplashParticle(x, surfaceY));
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Splash Particle Spawner for (let i = 0; i < count; i++) {

Creates a random number (4-7) of individual splash particles at the impact point.

const surfaceY = height - 8;
Matches the same y position used for the waterline, so effects appear right at the water's surface.
ripples.push(new Ripple(x, surfaceY));
Adds exactly one new expanding ripple at the impact point.
const count = int(random(4, 8));
Picks a random whole number of splash particles between 4 and 7 (int() truncates the decimal from random()).
splashes.push(new SplashParticle(x, surfaceY));
Adds one new splash particle to the array for each loop iteration, each with its own random velocity (set inside the class constructor).

playRaindropNote()

This function is the heart of the 'symphony' - it turns a visual event (a raindrop landing) into a musical note using p5.Oscillator for tone generation and p5.Envelope to shape its volume over time, avoiding harsh clicks.

🔬 The four ADSR numbers are attack, decay, sustain, and release in seconds. What happens if you make the release much longer, like 1.5 seconds, so notes linger?

  env.setADSR(0.01, 0.18, 0.0, 0.25);
  env.setRange(0.15, 0); // quiet, gentle
function playRaindropNote() {
  if (!audioStarted) return;

  const baseFreq = 220;        // A3
  const scale = [0, 2, 4, 7, 9]; // Major pentatonic steps in semitones
  const degree = random(scale);
  const octave = random([0, 12]); // 0 or +1 octave in semitones

  const semitoneOffset = degree + octave;
  const freq = baseFreq * pow(2, semitoneOffset / 12);

  const oscType = random(['sine', 'triangle']); // soft waveforms
  const osc = new p5.Oscillator(oscType);
  osc.freq(freq);
  osc.start();
  osc.amp(0); // start silent

  const env = new p5.Envelope();
  // attack, decay, sustainRatio, release (seconds)
  env.setADSR(0.01, 0.18, 0.0, 0.25);
  env.setRange(0.15, 0); // quiet, gentle

  // Connect envelope to oscillator amplitude and play it
  osc.amp(env);
  env.play();

  // Stop oscillator after envelope finishes (~0.46s)
  const totalDuration = 0.01 + 0.18 + 0.25 + 0.02;
  setTimeout(() => {
    osc.stop();
  }, totalDuration * 1000);
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

conditional Audio Not-Started Guard if (!audioStarted) return;

Prevents trying to play sound before the browser's audio context has been unlocked by a user click.

if (!audioStarted) return;
Browsers block audio until a user interacts with the page, so this exits early if that hasn't happened yet, avoiding errors.
const scale = [0, 2, 4, 7, 9]; // Major pentatonic steps in semitones
A pentatonic scale (5 notes) always sounds pleasant together no matter which notes are combined, which is why random notes from this array never clash.
const degree = random(scale);
Picks one random semitone offset from the pentatonic scale array.
const octave = random([0, 12]);
Randomly picks either the same octave (0 semitones) or one octave higher (12 semitones), adding pitch variety.
const freq = baseFreq * pow(2, semitoneOffset / 12);
Converts a semitone offset into an actual frequency using the standard musical formula: each 12 semitones doubles the frequency (one octave).
const osc = new p5.Oscillator(oscType);
Creates a new sound-generating oscillator using either a sine or triangle waveform for a soft tone.
osc.amp(0); // start silent
The oscillator starts at zero volume so it doesn't click or pop before the envelope takes over.
env.setADSR(0.01, 0.18, 0.0, 0.25);
Configures the volume envelope's Attack, Decay, Sustain level, and Release times in seconds - here it fades in almost instantly, decays, and has no sustain before releasing.
env.setRange(0.15, 0);
Sets the loudest point of the envelope to 0.15 (quiet) and its resting point to 0 (silent).
osc.amp(env);
Connects the envelope to control the oscillator's volume over time, instead of a fixed volume.
env.play();
Triggers the envelope, which ramps the oscillator's volume up and back down automatically.
setTimeout(() => { osc.stop(); }, totalDuration * 1000);
Schedules the oscillator to fully stop (freeing up audio resources) shortly after the envelope finishes, since setTimeout() works in milliseconds.

mousePressed()

mousePressed() is a built-in p5.js function that automatically runs whenever the mouse is clicked - no need to check mouseIsPressed yourself. Combined with userStartAudio(), it's the standard pattern for unlocking Web Audio.

function mousePressed() {
  if (!audioStarted) {
    userStartAudio()
      .then(() => {
        audioStarted = true;
      })
      .catch(err => {
        console.error('Audio context not started:', err);
      });
  }

  rainIntensity = constrain(rainIntensity + 1, 1, maxRainIntensity);
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional First-Click Audio Unlock if (!audioStarted) {

Only attempts to start the audio context once, on the very first click.

if (!audioStarted) {
Checks whether audio has already been unlocked, so this only runs on the first click.
userStartAudio()
A p5.sound function that resumes the browser's audio context, which browsers keep suspended until a user gesture like a click.
.then(() => { audioStarted = true; })
userStartAudio() returns a Promise; once it resolves successfully, this callback flips the audioStarted flag so notes can play from now on.
rainIntensity = constrain(rainIntensity + 1, 1, maxRainIntensity);
Increases rainIntensity by 1 every click, but constrain() clamps the result so it never goes below 1 or above maxRainIntensity.

touchStarted()

touchStarted() is p5.js's built-in touch equivalent of mousePressed(), letting the same sketch work naturally on phones and tablets.

function touchStarted() {
  // Mirror mousePressed for touch devices
  mousePressed();
  return false; // prevent default scrolling on mobile
}
Line-by-line explanation (2 lines)
mousePressed();
Reuses all the same logic (starting audio, increasing rain intensity) for touch taps instead of duplicating code.
return false; // prevent default scrolling on mobile
Returning false from touchStarted() stops the browser's default touch behavior, like scrolling or zooming, so taps only interact with the sketch.

windowResized()

windowResized() is a built-in p5.js callback that fires automatically whenever the browser window is resized, making it easy to keep full-screen sketches responsive.

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

  bgLayer = createGraphics(windowWidth, windowHeight);
  bgLayer.colorMode(HSL, 360, 100, 100, 1);
  drawBackgroundGradient();
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
A built-in p5.js function that resizes the main canvas whenever the browser window changes size.
bgLayer = createGraphics(windowWidth, windowHeight);
The old gradient buffer was the wrong size, so a brand new one is created matching the new dimensions.
drawBackgroundGradient();
Redraws the gradient into the newly sized buffer so it fills the whole window again.

class Raindrop

Each Raindrop object manages its own position and speed independently, which is the essence of object-oriented particle systems - instead of one giant array of numbers, each particle is a self-contained object with its own update() and draw() methods.

🔬 What happens to the way drops fall if you remove gravity entirely by changing 0.12 to 0?

  update() {
    this.y += this.speed;
    this.speed += 0.12; // gravity
  }
class Raindrop {
  constructor(x, y, speed, length, thickness) {
    this.x = x;
    this.y = y;
    this.speed = speed;
    this.length = length;
    this.thickness = thickness;
  }

  update() {
    this.y += this.speed;
    this.speed += 0.12; // gravity
  }

  draw() {
    stroke(195, 90, 70, 0.9); // bright aqua
    strokeWeight(this.thickness);
    line(this.x, this.y, this.x, this.y + this.length);
  }
}
Line-by-line explanation (5 lines)
constructor(x, y, speed, length, thickness) {
Runs once when `new Raindrop(...)` is called, storing all the passed-in values as properties on this specific drop.
this.y += this.speed;
Moves the drop downward each frame by its current speed.
this.speed += 0.12; // gravity
Increases the drop's speed slightly every frame, simulating acceleration due to gravity so drops fall faster the longer they fall.
stroke(195, 90, 70, 0.9); // bright aqua
Sets a bright aqua-blue color for the raindrop's line, with a hue distinct from the background.
line(this.x, this.y, this.x, this.y + this.length);
Draws a short vertical line from the drop's current position down to its tail, giving it a streaking rain-like appearance.

class Ripple

Ripple demonstrates using map() to link one changing value (radius) to another (transparency), a very common technique for creating fade-in/fade-out effects tied to an object's lifetime or state.

🔬 map() here fades alpha from 0.7 down to 0 as the ripple grows. What happens if you flip the last two arguments to fade from 0 up to 0.7 instead, so ripples get MORE visible as they expand?

  draw() {
    const alpha = map(this.radius, 0, this.maxRadius, 0.7, 0);
    noFill();
    stroke(200, 80, 80, alpha);
    strokeWeight(1.5);
class Ripple {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.radius = 2;
    this.maxRadius = random(20, 60);
  }

  update() {
    this.radius += 1.5;
  }

  draw() {
    const alpha = map(this.radius, 0, this.maxRadius, 0.7, 0);
    noFill();
    stroke(200, 80, 80, alpha);
    strokeWeight(1.5);

    // Slightly flattened ellipse to hint at perspective
    ellipse(this.x, this.y, this.radius * 2, this.radius * 0.7);
  }

  isDone() {
    return this.radius > this.maxRadius;
  }
}
Line-by-line explanation (5 lines)
this.maxRadius = random(20, 60);
Each ripple gets its own random maximum size, so they don't all look identical.
this.radius += 1.5;
Grows the ripple's radius by a fixed amount every frame, making it expand steadily outward.
const alpha = map(this.radius, 0, this.maxRadius, 0.7, 0);
map() converts the current radius (somewhere between 0 and maxRadius) into a transparency value between 0.7 and 0 - so the ripple starts visible and fades out as it grows, just like a real water ripple.
ellipse(this.x, this.y, this.radius * 2, this.radius * 0.7);
Draws the ripple as an ellipse that's wider than it is tall (radius * 0.7 for height), giving a subtle illusion of looking at the water surface from an angle.
return this.radius > this.maxRadius;
isDone() reports true once the ripple has grown past its maximum size, signaling draw() that it's safe to remove from the ripples array.

class SplashParticle

SplashParticle combines velocity, gravity, and fading alpha in one small class - the same three ingredients used in almost every particle-based effect, from fireworks to smoke to confetti.

🔬 This uses || so the particle is removed if EITHER condition is true. What would happen if you changed || to && here - would particles disappear sooner or later?

  isDone() {
    return this.alpha <= 0 || this.y > height + 10;
  }
class SplashParticle {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.vx = random(-1.5, 1.5);
    this.vy = random(-3.5, -1.5);
    this.alpha = 0.9;
    this.radius = random(1.2, 3);
  }

  update() {
    this.x += this.vx;
    this.y += this.vy;
    this.vy += 0.18;  // gravity
    this.alpha -= 0.04;
  }

  draw() {
    noStroke();
    fill(195, 90, 80, this.alpha);
    circle(this.x, this.y, this.radius * 2);
  }

  isDone() {
    return this.alpha <= 0 || this.y > height + 10;
  }
}
Line-by-line explanation (7 lines)
this.vx = random(-1.5, 1.5);
Gives each particle a random small horizontal velocity, so splashes scatter left and right instead of moving identically.
this.vy = random(-3.5, -1.5);
Gives each particle an initial upward velocity (negative y is up in p5.js), simulating a splash bouncing off the water surface.
this.x += this.vx; this.y += this.vy;
Moves the particle each frame according to its current velocity.
this.vy += 0.18; // gravity
Continuously pulls the particle's vertical velocity downward, so it arcs upward then falls back down like a real splash droplet.
this.alpha -= 0.04;
Gradually fades the particle out over time by reducing its transparency value each frame.
fill(195, 90, 80, this.alpha);
Colors the particle using its own current alpha, so it visibly fades as it flies through the air.
return this.alpha <= 0 || this.y > height + 10;
isDone() returns true if the particle has fully faded out OR fallen below the bottom of the canvas, whichever happens first - this double-check prevents particles lingering invisibly forever.

📦 Key Variables

raindrops array

Holds every active Raindrop object currently falling on screen; grows as new drops spawn and shrinks as they land or clean up.

let raindrops = [];
ripples array

Holds every active Ripple object expanding outward from a splash point.

let ripples = [];
splashes array

Holds every active SplashParticle object flying up from an impact point.

let splashes = [];
rainIntensity number

Controls how many raindrops spawn per frame; increases each time the user clicks, up to maxRainIntensity.

let rainIntensity = 4;
maxRainIntensity number

The upper cap on rainIntensity, preventing the storm from spawning an overwhelming number of drops.

let maxRainIntensity = 12;
spawnAccumulator number

A running fractional counter used to convert rainIntensity into a smooth, whole-number spawn rate across frames.

let spawnAccumulator = 0;
bgLayer object

An off-screen p5.Graphics buffer holding the pre-rendered gradient background, drawn once and reused every frame for performance.

let bgLayer;
audioStarted boolean

Tracks whether the browser's audio context has been unlocked by a user click, so notes only try to play after that happens.

let audioStarted = false;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG playRaindropNote()

A new p5.Oscillator and p5.Envelope are created for every single raindrop that lands, and never explicitly disconnected/freed beyond osc.stop() - during heavy rain this can create dozens of oscillator objects per second, risking audio glitches or memory buildup on longer sessions.

💡 Use a small pool of pre-created oscillators that get reused, or limit how often playRaindropNote() can actually trigger a new sound (e.g. a cooldown timer) during high rainIntensity.

PERFORMANCE drawBackgroundGradient()

The gradient is redrawn with a full loop of height individual line() calls every time windowResized() fires, which can be slow on rapid window resizing.

💡 Debounce windowResized() (wait until resizing stops for ~200ms before redrawing) or generate the gradient using a single vertical linear gradient via the Canvas 2D context (drawingContext.createLinearGradient) instead of per-pixel lines.

STYLE draw()

The three particle loops (raindrops, ripples, splashes) are nearly identical in structure (update, draw, check isDone/condition, splice), leading to repeated code.

💡 Create a small reusable helper like `updateAndPrune(array)` that takes an array of objects with update()/draw()/isDone() methods and handles the loop generically, reducing duplication.

FEATURE mousePressed()

There's currently no way to decrease rainIntensity once it's been raised - the storm can only intensify, which may make the sketch feel less interactive over time.

💡 Add a key press handler (e.g. keyPressed() checking for the down arrow or 'r' to reset) that lowers rainIntensity, giving users full control over the storm's mood.

🔄 Code Flow

Code flow showing setup, drawbackgroundgradient, draw, drawwaterline, drawhud, createraindrop, spawnsplasheffects, playraindropnote, mousepressed, touchstarted, windowresized, raindrop, ripple, splashparticle

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> gradientloop[gradient-loop] draw --> spawnwhileloop[spawn-while-loop] draw --> raindroploop[raindrop-loop] draw --> rippleloop[ripple-loop] draw --> splashloop[splash-loop] gradientloop --> drawbackgroundgradient[drawbackgroundgradient] click setup href "#fn-setup" click draw href "#fn-draw" click gradientloop href "#sub-gradient-loop" spawnwhileloop --> createraindrop[createraindrop] click spawnwhileloop href "#sub-spawn-while-loop" click createraindrop href "#fn-createraindrop" raindroploop --> hitbottomcheck[hit-bottom-check] raindroploop --> playraindropnote[playraindropnote] click raindroploop href "#sub-raindrop-loop" click hitbottomcheck href "#sub-hit-bottom-check" click playraindropnote href "#fn-playraindropnote" rippleloop --> ripple[ripple] click rippleloop href "#sub-ripple-loop" splashloop --> splashcountloop[splash-count-loop] splashloop --> splashparticle[splashparticle] click splashloop href "#sub-splash-loop" click splashcountloop href "#sub-splash-count-loop" click splashparticle href "#fn-splashparticle" draw --> drawwaterline[drawwaterline] draw --> drawhud[drawhud] click drawwaterline href "#fn-drawwaterline" click drawhud href "#fn-drawhud" drawhud --> audiohintconditional[audio-hint-conditional] click audiohintconditional href "#sub-audio-hint-conditional" raindroploop --> audioguard[audio-guard] raindroploop --> audiostartcheck[audio-start-check] click audioguard href "#sub-audio-guard" click audiostartcheck href "#sub-audio-start-check" mousepressed --> audioguard touchstarted --> audioguard windowresized --> draw click mousepressed href "#fn-mousepressed" click touchstarted href "#fn-touchstarted" click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual effects can users expect from the AI Raindrop Symphony sketch?

The sketch features falling raindrops that create ripples and splashes upon hitting the ground, all set against a calming blue gradient background.

How can users interact with the AI Raindrop Symphony to experience its musical elements?

Users can interact by clicking or tapping on the sketch, which starts the audio and allows raindrops to generate musical notes upon impact.

What creative coding techniques are showcased in the AI Raindrop Symphony?

This sketch demonstrates the use of particle systems for raindrops, audio synthesis with p5.Oscillator for musical notes, and off-screen graphics for creating a smooth background.

Preview

AI Raindrop Symphony - Musical Rain Visualization Watch raindrops fall and create music! Each drop - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Raindrop Symphony - Musical Rain Visualization Watch raindrops fall and create music! Each drop - Code flow showing setup, drawbackgroundgradient, draw, drawwaterline, drawhud, createraindrop, spawnsplasheffects, playraindropnote, mousepressed, touchstarted, windowresized, raindrop, ripple, splashparticle
Code Flow Diagram