rainvibes

This sketch creates an immersive rainstorm experience by combining animated falling raindrops with procedurally generated rain noise and occasional thunder rumbles. Interactive sliders let you control the overall volume and the pitch/brightness of the rain and thunder sounds, creating a dynamic audiovisual meditation on weather.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the rain faster and denser — Increasing both numRainDrops and baseRainSpeed creates a heavier, more intense downpour that falls rapidly.
  2. Make thunder happen more frequently — Reducing the thunderInterval from 10000 ms to 3000 ms makes thunder rumble every 3 seconds instead of every 10 seconds.
  3. Increase the perspective depth effect — Higher rainZRange values make the difference between far and near raindrops much more dramatic, creating a stronger 3D effect.
  4. Change the sky color to a lighter daytime storm — The background color creates the mood—lighter values make it look like a daytime rain instead of a dark night storm.
  5. Make raindrops glow brighter — Increasing the RGB values of the raindrop color makes them brighter and more visible against the sky.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch immerses you in a rainstorm by combining two things: visual rain particles that fall with perspective depth, and layered procedural audio that evolves continuously. The rain sound is generated from white noise filtered and modulated to breathe like a living thing, while thunder rumbles emerge every ten seconds with their own filter sweep. The volume and pitch sliders let you sculpt the entire soundscape in real time, teaching you how to connect interactive UI controls to audio parameters.

The code is organized around three core ideas: a particle system for rain drops that use depth (the z property) to create perspective and speed variation, two p5.sound noise sources routed through low-pass filters for timbre control, and a modulation system that reads slider values and updates audio parameters every frame. By studying it you will learn how to build a particle system, route audio through filters for tone shaping, and connect the draw loop to live parameter changes that feel musical.

⚙️ How It Works

  1. When the sketch loads, setup() initializes a full-window canvas with a dark blue background, creates 400 raindrop objects scattered across the screen at different depths, and sets up two p5.sound noise sources (white for rain, brown for thunder) routed through low-pass filters. It also creates two HTML sliders for volume and pitch control.
  2. Every frame, draw() reads the slider positions, then modulates the rain audio by breathing its volume with perlin noise and sweeping its filter cutoff based on the pitch slider—lower pitch gives a dull, narrow band; higher pitch opens up the filter to a brighter range.
  3. The rain particles fall at speeds determined by their depth (z value), creating perspective—far raindrops move slowly and are thin, near raindrops move quickly and are thick. Each drop drifts horizontally using perlin noise to simulate wind, and when it falls off the bottom, it wraps back to the top at a new random x position.
  4. Every ten seconds, triggerThunder() fires, creating a sudden rumble by spiking the thunder filter amplitude and sweeping its cutoff downward over 3–5 seconds. The pitch of the thunder is also influenced by the pitch slider, so adjusting brightness changes everything.
  5. When the window resizes, windowResized() rebuilds the rain particle array to fit the new canvas dimensions while keeping all audio and UI state intact.

🎓 Concepts You'll Learn

Particle systems with depthPerlin noise for animationProcedural audio generationLow-pass filter audio effectsAudio parameter modulationInteractive UI slidersPerspective and layering

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It is the only place you should create audio sources and UI elements, because creating them repeatedly in draw() would cause clicks and pops. Notice how the rain and thunder sources are disconnected from the master output and reconnected through filters—this routing lets us apply effects before the sound reaches your ears.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();
  noiseSeed(random(1000)); // Varied patterns each run

  userStartAudio(); // Ensure audio can start after user gesture

  // --- RAIN AUDIO: dull, muffled rain noise --- //
  // White noise is a bright source; low-pass will dull it
  rainNoise = new p5.Noise('white');
  rainNoise.start();
  rainNoise.amp(1);  // Full level into the filter

  rainFilter = new p5.LowPass();
  rainFilter.freq(800); // Start around mid-lows
  rainFilter.res(2);    // Gentle resonance

  // Route rain: noise -> rainFilter -> speakers
  rainNoise.disconnect();      // Remove direct connection to master
  rainNoise.connect(rainFilter);
  rainFilter.amp(0.0);         // Start silent; control in draw()

  // --- THUNDER AUDIO: occasional low rumbles --- //
  // Brown noise: more low-frequency energy, good for thunder
  thunderNoise = new p5.Noise('brown');
  thunderNoise.start();
  thunderNoise.amp(1); // full level into its filter

  thunderFilter = new p5.LowPass();
  thunderFilter.freq(300);  // Low cutoff for boomy sound
  thunderFilter.res(3);     // Some resonance for character

  // Route thunder: noise -> thunderFilter -> speakers
  thunderNoise.disconnect();
  thunderNoise.connect(thunderFilter);
  thunderFilter.amp(0);     // Thunder is silent until triggered

  // --- UI: volume & pitch sliders --- //
  volumeLabel = createDiv('Volume');
  volumeLabel.style('color', 'white');
  volumeLabel.position(20, 5);

  volumeSlider = createSlider(0, 100, 70); // 70% default
  volumeSlider.position(20, 25);
  volumeSlider.style('width', '150px');

  pitchLabel = createDiv('Pitch (brightness)');
  pitchLabel.style('color', 'white');
  pitchLabel.position(20, 55);

  pitchSlider = createSlider(0, 100, 40); // 40% default (rather dull)
  pitchSlider.position(20, 75);
  pitchSlider.style('width', '150px');

  initRain();
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Rain Audio Initialization rainNoise = new p5.Noise('white');

Creates a white noise source and routes it through a low-pass filter to create a dull, muffled rain sound

calculation Thunder Audio Initialization thunderNoise = new p5.Noise('brown');

Creates a brown noise source (richer in low frequencies) and routes it through a low-pass filter for boomy thunder

calculation UI Slider Creation volumeSlider = createSlider(0, 100, 70);

Creates interactive HTML sliders for controlling volume and pitch, positioned in the top-left corner

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, so the rain spans your whole screen
noStroke();
Disables stroke outlines so only the raindrop lines themselves are drawn, not with extra borders
noiseSeed(random(1000));
Seeds the perlin noise generator with a random value so the wind drift pattern is different each time you run the sketch
userStartAudio();
Tells p5.sound that the user has clicked, so audio can actually play (browser security requirement)
rainNoise = new p5.Noise('white');
Creates a white noise source—bright and hissy, which we will dull down with a filter to sound like rain
rainNoise.disconnect();
Cuts off the direct connection to speakers so we can route the noise through our filter first
rainNoise.connect(rainFilter);
Plugs the white noise into the filter, so all the sound goes through it before reaching your ears
thunderNoise = new p5.Noise('brown');
Creates brown noise—naturally heavy in bass—which is perfect for thunder rumbles
thunderFilter.freq(300);
Sets the thunder filter's cutoff frequency to 300 Hz, keeping only the low, boomy part of the spectrum
volumeSlider = createSlider(0, 100, 70);
Creates a horizontal slider ranging from 0 to 100 with a default value of 70, for controlling overall volume
pitchSlider = createSlider(0, 100, 40);
Creates a horizontal slider ranging from 0 to 100 with a default value of 40, for controlling brightness/pitch
initRain();
Populates the rain array with 400 raindrop objects, each with a random position, depth, and speed

initRain()

initRain() is called in setup() and again in windowResized() to rebuild the raindrop array when the canvas size changes. Each raindrop is a JavaScript object with properties for position (x, y), depth (z), and speed/length which will be calculated in draw(). This separation of initialization from calculation keeps the code clean.

🔬 This loop creates raindrops with random starting positions. What happens if you change y: random(-height, 0) to y: 0 so all raindrops start at the exact top of the canvas?

  for (let i = 0; i < numRainDrops; i++) {
    rain.push({
      x: random(width),
      y: random(-height, 0),
function initRain() {
  rain = [];
  for (let i = 0; i < numRainDrops; i++) {
    rain.push({
      x: random(width),
      y: random(-height, 0),  // Start above or at the top of the canvas
      z: random(rainZRange),  // Depth for perspective effect
      speed: 0,               // Will be calculated based on z
      length: 0               // Will be calculated based on z
    });
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Raindrop Creation Loop for (let i = 0; i < numRainDrops; i++) {

Repeats numRainDrops times, creating a new raindrop object each iteration

rain = [];
Clears the rain array to empty, so we can fill it with fresh raindrops
for (let i = 0; i < numRainDrops; i++) {
Loops 400 times (the value of numRainDrops), creating one raindrop each iteration
x: random(width),
Assigns a random horizontal position anywhere across the canvas width
y: random(-height, 0),
Starts the raindrop above the canvas (negative y), so it falls into view from the top
z: random(rainZRange),
Assigns a random depth value between 0 and rainZRange—deeper drops (higher z) are faster and longer
speed: 0,
Initializes speed to 0; it will be calculated in draw() based on the z depth
length: 0
Initializes length to 0; it will be calculated in draw() based on the z depth

draw()

draw() is the heartbeat of the sketch. It runs 60 times per second, reading the sliders, modulating the audio, updating every raindrop position, and redrawing them. The combination of `background()` at the start (to erase the previous frame) and `line()` at the end (to draw the new frame) creates the illusion of smooth motion. Notice how the rain audio is also modulated here—the volume and filter cutoff change smoothly with perlin noise, so the sound evolves organically without any jarring jumps.

🔬 These four lines create the perspective effect by mapping depth (z) to size, brightness, speed, and length. What happens if you reverse the ranges so far drops are BIG and bright while near drops are small and dim? Try swapping the second and third arguments of one map() call.

    let dropSize  = map(drop.z, 0, rainZRange, 1, 3);
    let dropAlpha = map(drop.z, 0, rainZRange, 80, 200);
    drop.speed    = map(drop.z, 0, rainZRange, baseRainSpeed * 0.5, baseRainSpeed * 1.5);
    drop.length   = map(drop.z, 0, rainZRange, 5, 15);

🔬 This line creates the wind effect using 3D perlin noise. What happens if you change the -2, 2 range to -10, 10? Or change frameCount * 0.01 to frameCount * 0.05 to make the wind change faster?

    let drift = map(
      noise(drop.x * 0.01, drop.y * 0.01, frameCount * 0.01),
      0, 1, -2, 2
    );
function draw() {
  // Read UI values each frame
  if (volumeSlider) volumeLevel = volumeSlider.value() / 100; // 0–1
  if (pitchSlider)  pitchLevel  = pitchSlider.value() / 100;  // 0–1

  // Darker blue sky
  background(0, 30, 70);

  // --- RAIN AUDIO MODULATION: dull, breathing wash --- //
  let baseVolume = 0.35;           // Overall base loudness (0–1)
  let volumeVar  = 0.15;           // Variation amount
  let vol = baseVolume + volumeVar * noise(frameCount * 0.01);
  vol *= volumeLevel;              // Scale by master volume slider
  rainFilter.amp(vol);

  // Pitch/brightness control for rain:
  // pitchLevel 0 → low/dull, 1 → bright
  let minRainCut = map(pitchLevel, 0, 1, 200, 800);
  let maxRainCut = map(pitchLevel, 0, 1, 800, 3000);
  let cutoff = map(noise(frameCount * 0.003 + 100), 0, 1, minRainCut, maxRainCut);
  rainFilter.freq(cutoff);

  // --- THUNDER TIMING (every ~10 seconds) --- //
  if (millis() - lastThunderTime > thunderInterval) {
    triggerThunder();
    lastThunderTime = millis();
  }

  // --- RAIN VISUALS --- //
  for (let drop of rain) {
    // Perspective mapping by z
    let dropSize  = map(drop.z, 0, rainZRange, 1, 3);
    let dropAlpha = map(drop.z, 0, rainZRange, 80, 200);
    drop.speed    = map(drop.z, 0, rainZRange, baseRainSpeed * 0.5, baseRainSpeed * 1.5);
    drop.length   = map(drop.z, 0, rainZRange, 5, 15);

    // Horizontal drift using noise to simulate wind
    let drift = map(
      noise(drop.x * 0.01, drop.y * 0.01, frameCount * 0.01),
      0, 1, -2, 2
    );
    drop.x += drift;

    // Fall
    drop.y += drop.speed;

    // Draw the raindrop
    stroke(150, 200, 255, dropAlpha);
    strokeWeight(dropSize);
    line(drop.x, drop.y, drop.x, drop.y + drop.length);

    // Reset when it falls off-screen
    if (drop.y > height + drop.length) {
      drop.y = random(-height, 0);
      drop.x = random(width);
      drop.z = random(rainZRange);
    }
  }
}
Line-by-line explanation (24 lines)

🔧 Subcomponents:

conditional Slider Value Reading if (volumeSlider) volumeLevel = volumeSlider.value() / 100;

Reads the position of the volume slider and converts it to a 0–1 scale

calculation Rain Audio Breathing Effect let vol = baseVolume + volumeVar * noise(frameCount * 0.01);

Uses perlin noise to smoothly vary the rain volume over time, creating a breathing, organic quality

calculation Pitch/Brightness Control let cutoff = map(noise(frameCount * 0.003 + 100), 0, 1, minRainCut, maxRainCut);

Maps perlin noise to a filter cutoff frequency range determined by the pitch slider

conditional Thunder Trigger Condition if (millis() - lastThunderTime > thunderInterval) {

Checks if enough time has passed since the last thunder; if so, triggers a new rumble

for-loop Raindrop Update Loop for (let drop of rain) {

Iterates through every raindrop, updating its position and redrawing it each frame

calculation Perspective Depth Mapping let dropSize = map(drop.z, 0, rainZRange, 1, 3);

Maps the raindrop's depth (z) to visible size and alpha, so deep drops appear small and faint

calculation Wind Drift Calculation let drift = map( noise(drop.x * 0.01, drop.y * 0.01, frameCount * 0.01), 0, 1, -2, 2 );

Uses 3D perlin noise to simulate realistic, correlated wind that pushes all raindrops together

conditional Raindrop Recycling if (drop.y > height + drop.length) {

When a raindrop falls off the bottom of the screen, resets it to a random position at the top

if (volumeSlider) volumeLevel = volumeSlider.value() / 100;
Reads the slider's current value (0–100) and converts it to a 0–1 decimal for audio control
if (pitchSlider) pitchLevel = pitchSlider.value() / 100;
Reads the pitch slider's value and converts it to 0–1, where 0 is dull and 1 is bright
background(0, 30, 70);
Fills the canvas with dark blue (RGB: almost black, hint of green, strong blue), erasing the previous frame
let baseVolume = 0.35;
Sets the baseline loudness of the rain to 35% of maximum volume
let vol = baseVolume + volumeVar * noise(frameCount * 0.01);
Uses perlin noise (which smoothly varies over time) to add a breathing flutter on top of the base volume
vol *= volumeLevel;
Multiplies the modulated volume by the slider value, so moving the slider scales everything up or down
rainFilter.amp(vol);
Sets the output level of the rain filter to the calculated volume, so you hear the result
let minRainCut = map(pitchLevel, 0, 1, 200, 800);
Maps the pitch slider to a range of low cutoff frequencies: slider at 0 gives 200 Hz (dull), slider at 1 gives 800 Hz (brighter)
let cutoff = map(noise(frameCount * 0.003 + 100), 0, 1, minRainCut, maxRainCut);
Uses slow perlin noise to sweep the filter cutoff smoothly between the min and max ranges
rainFilter.freq(cutoff);
Applies the calculated cutoff frequency to the rain filter, changing its tone in real time
if (millis() - lastThunderTime > thunderInterval) {
Checks if the time elapsed since the last thunder is greater than the thunderInterval (10 seconds)
triggerThunder();
If enough time has passed, calls the triggerThunder function to create a new rumble
lastThunderTime = millis();
Updates lastThunderTime to the current time in milliseconds, resetting the countdown
for (let drop of rain) {
Loops through each raindrop object in the rain array
let dropSize = map(drop.z, 0, rainZRange, 1, 3);
Maps the raindrop's depth (z between 0 and rainZRange) to a visible size between 1 and 3 pixels—far drops are thin, near drops are thick
drop.speed = map(drop.z, 0, rainZRange, baseRainSpeed * 0.5, baseRainSpeed * 1.5);
Maps depth to falling speed: far raindrops move slowly (0.5× base), near raindrops move fast (1.5× base), creating depth illusion
let drift = map( noise(drop.x * 0.01, drop.y * 0.01, frameCount * 0.01), 0, 1, -2, 2 );
Uses 3D perlin noise to calculate a wind drift value (−2 to +2 pixels). The position (x, y) and time (frameCount) inputs make wind push all nearby raindrops together realistically
drop.x += drift;
Moves the raindrop horizontally by the drift amount, simulating wind pushing it sideways
drop.y += drop.speed;
Moves the raindrop downward by its speed value each frame, making it fall
stroke(150, 200, 255, dropAlpha);
Sets the stroke color to light blue with alpha (transparency) based on depth—far drops are dim, near drops are bright
line(drop.x, drop.y, drop.x, drop.y + drop.length);
Draws a vertical line from the raindrop's top to bottom, where the length is also determined by depth
if (drop.y > height + drop.length) {
Checks if the raindrop has fallen completely off the bottom of the canvas
drop.y = random(-height, 0);
Resets the raindrop to a random position above the canvas so it can fall again
drop.z = random(rainZRange);
Assigns a new random depth, so recycled raindrops don't always have the same speed and size

triggerThunder()

triggerThunder() is called by draw() every 10 seconds, and it is one of the most musical functions in the sketch. It demonstrates envelope design—the quick attack (amp up in 50ms) followed by the long decay (fade over several seconds) mimics a real thunder rumble. The filter sweep from startFreq down to endFreq creates the sense of the rumble rolling away into the distance. Notice that all the parameters (duration, frequencies, amplitude) are randomized, so no two thunderstorms sound exactly the same.

🔬 These two lines create the attack and decay envelope of the thunder sound. What happens if you change 0.05 to 0.5, so the thunder swells more gradually? Or change the 0.1 delay to 0, so the fade starts immediately instead of after 0.1 seconds?

  // Quick swell up
  thunderFilter.amp(peakAmp, 0.05);          // rise to peakAmp over 0.05s

  // Long decay back to silence over random duration
  thunderFilter.amp(0, duration, 0.1);       // fade to 0 over duration s, starting 0.1s from now
function triggerThunder() {
  // Random duration between 3 and 5 seconds
  let duration = random(3, 5);

  // Use current pitch level to set thunder "pitch"/brightness
  let pitch = pitchLevel;

  // Start and end frequency ranges depend on pitch slider
  let startMin = map(pitch, 0, 1, 150, 400);
  let startMax = map(pitch, 0, 1, 300, 800);
  let endMin   = map(pitch, 0, 1, 60, 150);
  let endMax   = map(pitch, 0, 1, 120, 300);

  let startFreq = random(startMin, startMax); // initial low-pass cutoff
  let endFreq   = random(endMin, endMax);     // end cutoff

  // Louder thunder (near maximum), scaled by master volume
  let peakAmp   = random(0.8, 1.0) * volumeLevel;

  // Set starting filter state
  thunderFilter.freq(startFreq);
  thunderFilter.res(random(2, 5));

  // Quick swell up
  thunderFilter.amp(peakAmp, 0.05);          // rise to peakAmp over 0.05s

  // Long decay back to silence over random duration
  thunderFilter.amp(0, duration, 0.1);       // fade to 0 over duration s, starting 0.1s from now

  // Slow downward sweep in cutoff, like rolling thunder
  thunderFilter.freq(endFreq, duration, 0.1); // sweep over same duration
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Thunder Duration Randomization let duration = random(3, 5);

Picks a random duration between 3 and 5 seconds so each thunder rumble sounds different

calculation Pitch-Based Frequency Ranges let startMin = map(pitch, 0, 1, 150, 400);

Maps the pitch slider value to the starting frequency range of the thunder, so pitch slider controls thunder brightness

calculation Thunder Amplitude Envelope thunderFilter.amp(peakAmp, 0.05);

Quickly ramps the volume up to peak over 50 milliseconds, creating an attack

let duration = random(3, 5);
Picks a random number between 3 and 5 to determine how long this thunder rumble lasts
let pitch = pitchLevel;
Copies the current pitch slider value (0–1) into a local variable for clarity
let startMin = map(pitch, 0, 1, 150, 400);
Maps pitch slider (0–1) to a minimum starting frequency: at 0 it's 150 Hz (dull), at 1 it's 400 Hz (brighter)
let startMax = map(pitch, 0, 1, 300, 800);
Maps pitch slider to a maximum starting frequency range: at 0 it's 300 Hz, at 1 it's 800 Hz
let startFreq = random(startMin, startMax);
Picks a random frequency between startMin and startMax, so each thunder starts at a slightly different pitch
let endFreq = random(endMin, endMax);
Picks a random ending frequency (lower than starting) so the thunder rumbles downward
let peakAmp = random(0.8, 1.0) * volumeLevel;
Calculates the peak loudness as a random value between 80–100% of maximum, scaled by the master volume slider
thunderFilter.freq(startFreq);
Sets the filter cutoff to the calculated starting frequency
thunderFilter.res(random(2, 5));
Sets filter resonance (emphasis at the cutoff frequency) to a random value between 2 and 5, adding character to the rumble
thunderFilter.amp(peakAmp, 0.05);
Ramps the amplitude up to peakAmp over 0.05 seconds (50 milliseconds), creating a sudden attack
thunderFilter.amp(0, duration, 0.1);
After a 0.1-second delay, fades the amplitude down to 0 over the random duration (3–5 seconds), creating a long tail
thunderFilter.freq(endFreq, duration, 0.1);
Over the same duration and starting at the same delay, sweeps the filter cutoff down to endFreq, creating the classic rolling thunder effect

windowResized()

windowResized() is a special p5.js function that runs automatically whenever the browser window is resized. Without it, the canvas would stay small and letterboxed. By calling resizeCanvas() and initRain() here, we ensure the rain continues to fill the entire screen no matter how big or small the window becomes. The audio (rain and thunder) is not recreated—only the visual raindrops, which is efficient and preserves the sound continuity.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Rebuild raindrops for new size, but keep the same audio & sliders
  initRain();
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new window dimensions when the user resizes their browser
initRain();
Calls initRain() to regenerate all 400 raindrops so they fit the new canvas size

📦 Key Variables

rain array

An array of raindrop objects, each with position (x, y), depth (z), and calculated speed and length. Updated every frame and drawn to the screen.

let rain = [];
numRainDrops number

Controls how many raindrops are created and animated. Higher values create a denser downpour but use more processing power.

let numRainDrops = 400;
baseRainSpeed number

The falling speed of the deepest raindrops, in pixels per frame. Determines overall rain intensity.

let baseRainSpeed = 10;
rainZRange number

The depth range for perspective effect: larger values create more extreme differences between near and far raindrops.

let rainZRange = 10;
rainNoise object

A p5.sound white noise source that generates the base rain sound before filtering.

let rainNoise;
rainFilter object

A p5.sound low-pass filter applied to the rain noise to create a dull, muffled tone.

let rainFilter;
thunderNoise object

A p5.sound brown noise source (bass-heavy) that generates the raw thunder sound before filtering.

let thunderNoise;
thunderFilter object

A p5.sound low-pass filter applied to thunder noise, with a sweeping frequency envelope for a rolling sound.

let thunderFilter;
lastThunderTime number

Stores the time (in milliseconds) of the last thunder event, used to determine when the next one should occur.

let lastThunderTime = 0;
thunderInterval number

The time interval (in milliseconds) between thunder events. 10000 ms = 10 seconds.

const thunderInterval = 10000;
volumeSlider object

An HTML slider UI element that controls the overall volume of rain and thunder.

let volumeSlider;
pitchSlider object

An HTML slider UI element that controls the brightness/pitch of both rain and thunder sounds.

let pitchSlider;
volumeLevel number

Stores the current volume slider value normalized to 0–1, updated every frame.

let volumeLevel = 1.0;
pitchLevel number

Stores the current pitch slider value normalized to 0–1, updated every frame.

let pitchLevel  = 0.5;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

PERFORMANCE draw()

The rain filter's frequency is recalculated every frame using noise(), even when the perlin noise values change very slowly. This is fine, but could be optimized.

💡 Consider calculating minRainCut and maxRainCut outside the loop (in setup or at the top of draw) since they depend only on pitchLevel, which changes slowly with slider movement.

FEATURE global variables

Once created, the rain and thunder audio sources play continuously—there is no way to stop or pause them without muting the browser tab.

💡 Add a pause button that calls rainNoise.stop(), rainNoise.start(), thunderNoise.stop(), etc., allowing users to silence the sketch without stopping visual animation.

BUG initRain()

Raindrops are initialized with y: random(-height, 0), but they can appear partially visible at the top of the canvas before falling fully into frame.

💡 Change to y: random(-rainZRange * 3, 0) so drops start further above the canvas, giving a more seamless entry as they fall into view.

STYLE setup()

The slider positioning uses hardcoded pixel values (position(20, 25)), which may overlap or look cramped on very small screens.

💡 Use relative positioning or calculate positions based on window size: volumeSlider.position(20, 25); could become volumeSlider.position(20, height * 0.1);

FEATURE triggerThunder()

Thunder is silent by default (amp = 0) until triggered, but there is no visual lightning flash to accompany the sound.

💡 Add a brief white screen flash when triggerThunder() is called by setting background to white for one frame, creating a convincing storm experience.

🔄 Code Flow

Code flow showing setup, initrain, draw, triggerthunder, windowresized

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

graph TD start[Start] --> setup[setup] setup --> initrain[initRain] setup --> rain-audio-setup[rain-audio-setup] setup --> thunder-audio-setup[thunder-audio-setup] setup --> ui-sliders-setup[ui-sliders-setup] setup --> draw[draw loop] click setup href "#fn-setup" click initrain href "#fn-initrain" click rain-audio-setup href "#sub-rain-audio-setup" click thunder-audio-setup href "#sub-thunder-audio-setup" click ui-sliders-setup href "#sub-ui-sliders-setup" click draw href "#fn-draw" draw --> slider-reading[slider-reading] draw --> rain-audio-modulation[rain-audio-modulation] draw --> pitch-control[pitch-control] draw --> thunder-trigger[thunder-trigger] draw --> rain-loop[rain-loop] click slider-reading href "#sub-slider-reading" click rain-audio-modulation href "#sub-rain-audio-modulation" click pitch-control href "#sub-pitch-control" click thunder-trigger href "#sub-thunder-trigger" click rain-loop href "#sub-rain-loop" rain-loop --> rain-loop-create[rain-loop] rain-loop-create --> perspective-mapping[perspective-mapping] rain-loop-create --> wind-drift[wind-drift] rain-loop-create --> raindrop-reset[raindrop-reset] click rain-loop-create href "#sub-rain-loop" click perspective-mapping href "#sub-perspective-mapping" click wind-drift href "#sub-wind-drift" click raindrop-reset href "#sub-raindrop-reset" thunder-trigger --> duration-random[duration-random] thunder-trigger --> pitch-mapping[pitch-mapping] thunder-trigger --> amplitude-envelope[amplitude-envelope] click duration-random href "#sub-duration-random" click pitch-mapping href "#sub-pitch-mapping" click amplitude-envelope href "#sub-amplitude-envelope" windowresized[windowResized] --> initrain click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual effects does the rainvibes sketch provide?

The rainvibes sketch creates a visually immersive experience that simulates falling raindrops across the entire canvas, enhancing the atmosphere with depth and perspective.

How can users interact with the rainvibes sketch?

Users can adjust the volume and pitch of the rain and thunder sounds using sliders, allowing for personalized auditory experiences.

What creative coding techniques are showcased in the rainvibes sketch?

This sketch demonstrates the use of noise synthesis for realistic soundscapes and low-pass filtering to manipulate audio frequencies, enhancing the overall ambiance.

Preview

rainvibes - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of rainvibes - Code flow showing setup, initrain, draw, triggerthunder, windowresized
Code Flow Diagram