AI Rain Window - Meditative Rainfall

This sketch fills the screen with 120 individual raindrops that fall down a softly blurred, gradient night-sky background. Drops sway gently side to side, some slow down and 'stick' partway down leaving a glowing vertical trail behind them, and occasional soft lightning flashes brighten the whole scene.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the rain fall faster — Raising the velocity range makes every raindrop fall much more quickly, giving the scene a heavier, more urgent downpour feel.
  2. Let trails glow longer — Slowing down the alpha fade rate keeps each glowing trail visible for much longer after a drop sticks to the glass.
  3. Add more frequent lightning — Increasing the random chance each frame makes the soft white flash trigger far more often, turning the calm scene into more of a storm.
  4. Grow fatter raindrops — Increasing the size range makes every drop noticeably chunkier and more visible against the dark background.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch renders a calm nighttime rain scene: dozens of raindrops slide down a blurred blue gradient sky, some slowing to a stop and leaving a soft glowing trail behind them, while the whole window occasionally brightens with a gentle lightning flash. It's built almost entirely from two custom classes, Drop and Trail, plus an offscreen graphics buffer created once with createGraphics() so the expensive gradient-and-blur background never has to be redrawn from scratch. Techniques on display include object-oriented animation with ES6 classes, sine-wave motion for the sideways sway, alpha transparency for fading trails and flashes, and simple distance-based collision checks to merge nearby drops.

The code is organized around setup(), which builds the background once and spawns 120 Drop objects, and draw(), which runs 60 times a second to update and render everything. Inside draw() you'll find a nested loop that checks every pair of drops for near-collisions (merging them into a single bigger, slower drop), an array-filter pattern that cleans up trails once they've faded out, and a tiny state machine (the flash variable) that fires and fades a full-screen lightning flash. Studying this sketch teaches you how to combine classes, arrays of objects, and frame-based animation into a self-sustaining ambient scene with zero user interaction required.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, calls makeBg() to pre-render a blurred vertical gradient sky onto an offscreen buffer, and spawns 120 Drop objects at random positions above the screen.
  2. Every frame, draw() first stamps that pre-rendered sky image onto the canvas, instantly refreshing the background without recalculating the gradient.
  3. Each Drop updates its position (falling faster before it 'sticks', slower after), sways left-right using a sine wave, and resets to the top once it falls off the bottom of the screen.
  4. When a drop's y position passes its randomly chosen 'stop' point, it flags itself as stopped and spawns a Trail object, which draws a fading vertical line from that point down to the bottom of the window.
  5. A nested loop compares every pair of drops; if two get close enough in x and y, they're treated as merging - the surviving drop grows slightly and averages its speed with the other, which resets back to the top.
  6. A random chance each frame triggers a brief full-window white flash that fades out over subsequent frames, simulating distant lightning, and windowResized() rebuilds the background and repositions all drops whenever the browser window changes size.

🎓 Concepts You'll Learn

ES6 Classes and ObjectsOffscreen Graphics Buffers (createGraphics)Alpha Transparency and FadingSine Wave MotionArray Filtering to Manage Object LifetimesDistance-based Collision DetectionResponsive Canvas with windowResized

📝 Code Breakdown

class Drop

Drop is a self-contained ES6 class representing one raindrop: it knows how to initialize itself (reset), move itself (update), and render itself (draw). Wrapping behavior into a class like this is a core pattern for particle systems in p5.js - instead of managing dozens of loose variables, you manage an array of objects that each know how to take care of themselves.

🔬 The .25 in this.stopped?.25:1 slows a drop down to a quarter speed once it 'sticks'. What happens if you change .25 to 1 so stopped drops keep falling at full speed?

update(){if(!this.stopped&&this.y>this.stop){this.stopped=true;trails.push(new Trail(this.x,this.y,height));}this.y+=this.v*(this.stopped?.25:1);this.x+=sin(frameCount*.1+this.off)*.4;if(this.y>height+20)this.reset();}

🔬 The ellipse width is len*.3 while its height is the full len, giving drops a stretched teardrop look. What happens if you make the width equal to len as well, so drops become round?

draw(){noStroke();let len=this.s*(this.stopped?.7:1.5);fill(180,200,255,180);ellipse(this.x,this.y,len*.3,len);}
class Drop{
  constructor(){this.reset();}
  reset(){this.x=random(width);this.y=random(-height,0);this.v=random(3,8);this.s=random(6,12);this.off=random(TWO_PI);this.stop=random(height*.3,height*.9);this.stopped=false;}
  update(){if(!this.stopped&&this.y>this.stop){this.stopped=true;trails.push(new Trail(this.x,this.y,height));}this.y+=this.v*(this.stopped?.25:1);this.x+=sin(frameCount*.1+this.off)*.4;if(this.y>height+20)this.reset();}
  draw(){noStroke();let len=this.s*(this.stopped?.7:1.5);fill(180,200,255,180);ellipse(this.x,this.y,len*.3,len);}
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

conditional Stop and Spawn Trail if(!this.stopped&&this.y>this.stop){this.stopped=true;trails.push(new Trail(this.x,this.y,height));}

Marks the drop as stopped once it passes its random stop point, and creates a Trail object at that spot

conditional Recycle Off-Screen Drop if(this.y>height+20)this.reset();

Sends the drop back to a fresh random position above the canvas once it falls off the bottom

constructor(){this.reset();}
When a new Drop is created, it immediately calls reset() to set all of its starting properties in one place.
this.x=random(width);
Places the drop at a random horizontal position across the canvas width.
this.y=random(-height,0);
Starts the drop above the visible canvas (a negative y value), so it appears to fall in from off-screen.
this.v=random(3,8);
Gives the drop a random falling speed between 3 and 8 pixels per frame.
this.s=random(6,12);
Sets a random base size for the drop, used later to calculate its drawn length.
this.off=random(TWO_PI);
Picks a random starting phase for the sine wave so drops don't all sway in perfect unison.
this.stop=random(height*.3,height*.9);
Chooses a random vertical point (somewhere between 30% and 90% down the screen) where this drop will 'stick' and slow down.
this.stopped=false;
Resets the stopped flag so a recycled drop starts falling freely again.
if(!this.stopped&&this.y>this.stop){this.stopped=true;trails.push(new Trail(this.x,this.y,height));}
Checks if the drop has just reached its stop point for the first time; if so, it flags itself as stopped and adds a new Trail streak below it.
this.y+=this.v*(this.stopped?.25:1);
Moves the drop downward - at full speed before it stops, but at just 25% speed afterward, simulating it clinging to the glass.
this.x+=sin(frameCount*.1+this.off)*.4;
Nudges the drop left and right using a sine wave based on frameCount, creating a gentle swaying motion as it falls.
if(this.y>height+20)this.reset();
Once the drop is safely below the bottom edge, it resets to a new random position above the screen so the rain never runs out.
let len=this.s*(this.stopped?.7:1.5);
Calculates the drawn length of the drop - falling drops are stretched (1.5x) to look like motion streaks, stopped drops are shorter (0.7x).
fill(180,200,255,180);
Sets a semi-transparent pale blue color for the drop, matching the moonlit night sky theme.
ellipse(this.x,this.y,len*.3,len);
Draws the drop as a narrow, tall ellipse - thin width but long height - so it reads as a raindrop shape rather than a circle.

class Trail

Trail is a lightweight class whose only job is to fade out over time and report whether it's still 'alive'. This alive()/filter() pairing is a common p5.js pattern for managing temporary visual effects (particles, sparks, streaks) without them accumulating forever and slowing down the sketch.

🔬 This draws one thin vertical line per trail. What happens if you add strokeWeight(3) right before the line() call to make trails much thicker and more visible?

draw(){if(this.a<=0)return;stroke(170,200,255,this.a);line(this.x,this.y1,this.x,this.y2);}
class Trail{
  constructor(x,y1,y2){this.x=x;this.y1=y1;this.y2=y2;this.a=80;}
  update(){this.a-=.2;}
  draw(){if(this.a<=0)return;stroke(170,200,255,this.a);line(this.x,this.y1,this.x,this.y2);}
  alive(){return this.a>0;}
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Skip Drawing Faded Trail if(this.a<=0)return;

Stops the draw method early once the trail's transparency has hit zero, avoiding wasted drawing calls

constructor(x,y1,y2){this.x=x;this.y1=y1;this.y2=y2;this.a=80;}
Stores the trail's fixed horizontal position and its start/end y-coordinates, and gives it a starting opacity (alpha) of 80.
update(){this.a-=.2;}
Every frame, the trail's opacity decreases slightly, causing it to gradually fade out.
if(this.a<=0)return;
If the trail has fully faded, this exits the draw method immediately so nothing invisible gets drawn.
stroke(170,200,255,this.a);
Sets a pale blue stroke color whose transparency matches the trail's current fade level.
line(this.x,this.y1,this.x,this.y2);
Draws a straight vertical line from where the drop stopped down to the bottom of the canvas, representing the streak left behind.
alive(){return this.a>0;}
A simple helper the draw() loop uses to decide whether this trail should be kept in the array or removed.

makeBg()

createGraphics() lets you render something expensive (like a per-pixel gradient with a blur filter) exactly once into an offscreen buffer, then stamp that finished image onto the main canvas every frame with image() - a common performance technique for backgrounds that don't need to change constantly.

🔬 This loop blends linearly from top to bottom using y/bg.height. What happens if you swap that ratio for sin(y/bg.height*PI), making the middle of the sky lighter than the top and bottom?

for(let y=0;y<bg.height;y++){bg.stroke(lerpColor(c1,c2,y/bg.height));bg.line(0,y,bg.width,y);}
function makeBg(){bg=createGraphics(windowWidth,windowHeight);let c1=color(5,10,30),c2=color(20,40,70);for(let y=0;y<bg.height;y++){bg.stroke(lerpColor(c1,c2,y/bg.height));bg.line(0,y,bg.width,y);}bg.filter(BLUR,2);}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Paint Gradient Row by Row for(let y=0;y<bg.height;y++){bg.stroke(lerpColor(c1,c2,y/bg.height));bg.line(0,y,bg.width,y);}

Draws one horizontal line per pixel row, blending smoothly from color c1 at the top to c2 at the bottom to create a vertical gradient sky

bg=createGraphics(windowWidth,windowHeight);
Creates a separate offscreen drawing surface (graphics buffer) the size of the browser window, so the gradient only has to be computed once instead of every frame.
let c1=color(5,10,30),c2=color(20,40,70);
Defines two dark navy-blue colors that will be blended together to form the night sky gradient.
for(let y=0;y<bg.height;y++){bg.stroke(lerpColor(c1,c2,y/bg.height));bg.line(0,y,bg.width,y);}
Loops through every row of pixels, uses lerpColor() to blend between c1 and c2 based on how far down the row is, and draws a horizontal line of that color across the whole width.
bg.filter(BLUR,2);
Applies a blur filter to the finished gradient, softening the visible horizontal stripes into a smooth transition.

setup()

setup() runs exactly once when the sketch starts. It's the right place to prepare anything expensive (like the background buffer) and to populate arrays of objects before the animation loop begins.

🔬 This loop decides how many drops exist. What happens visually if you drop the count down to just 10? What about pushing it up to 500?

for(let i=0;i<120;i++)drops.push(new Drop());
function setup(){createCanvas(windowWidth,windowHeight);makeBg();for(let i=0;i<120;i++)drops.push(new Drop());}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Spawn 120 Raindrops for(let i=0;i<120;i++)drops.push(new Drop());

Creates 120 new Drop objects and adds each one to the drops array so the rain can begin animating

createCanvas(windowWidth,windowHeight);
Creates a canvas that fills the entire browser window.
makeBg();
Builds the blurred gradient night-sky background once, before any animation starts.
for(let i=0;i<120;i++)drops.push(new Drop());
Runs 120 times, each time creating a brand new Drop object (which randomizes its own position and speed) and adding it to the drops array.

draw()

draw() is the animation heartbeat of any p5.js sketch, running roughly 60 times per second. Here it demonstrates several important patterns at once: refreshing a cached background image, updating/drawing/filtering a dynamic array of temporary objects (trails), an O(n^2) pairwise comparison between objects (the drop-merging loop), and a simple countdown-timer pattern (the flash variable) for a one-off visual effect.

🔬 This is the distance check that decides whether two drops 'merge'. What happens if you widen the thresholds (3 and 8) to something like 10 and 20, so drops merge much more easily and often?

for(let j=i+1;j<drops.length;j++){let o=drops[j];if(abs(d.x-o.x)<3&&abs(d.y-o.y)<8){o.s=min(18,o.s+1);o.v=(o.v+d.v)*.5;d.reset();break;}}

🔬 The flash fades by 5 each frame. What happens if you change that to 1, making each lightning flash last much longer and fade more gradually?

if(flash>0){fill(255,flash);noStroke();rect(0,0,width,height);flash-=5;}
function draw(){image(bg,0,0,width,height);for(let t of trails)t.update(),t.draw();trails=trails.filter(t=>t.alive());for(let i=0;i<drops.length;i++){let d=drops[i];d.update();for(let j=i+1;j<drops.length;j++){let o=drops[j];if(abs(d.x-o.x)<3&&abs(d.y-o.y)<8){o.s=min(18,o.s+1);o.v=(o.v+d.v)*.5;d.reset();break;}}d.draw();}if(random()<.004&&!flash)flash=150;if(flash>0){fill(255,flash);noStroke();rect(0,0,width,height);flash-=5;}}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop Update and Draw Trails for(let t of trails)t.update(),t.draw();

Fades and renders every active trail streak each frame

calculation Remove Dead Trails trails=trails.filter(t=>t.alive());

Keeps only trails whose opacity is still above zero, preventing the array from growing forever

for-loop Update Drops and Merge Nearby Ones for(let j=i+1;j<drops.length;j++){let o=drops[j];if(abs(d.x-o.x)<3&&abs(d.y-o.y)<8){o.s=min(18,o.s+1);o.v=(o.v+d.v)*.5;d.reset();break;}}

Checks every later drop against the current one; if they are close enough, combines them into a single bigger, averaged-speed drop

conditional Randomly Trigger Lightning Flash if(random()<.004&&!flash)flash=150;

Has a small random chance each frame of starting a lightning flash, but only if one isn't already happening

conditional Render and Fade Lightning Flash if(flash>0){fill(255,flash);noStroke();rect(0,0,width,height);flash-=5;}

While a flash is active, draws a semi-transparent white rectangle over everything and gradually reduces its opacity

image(bg,0,0,width,height);
Stamps the pre-rendered gradient sky onto the canvas each frame, which also clears away last frame's drawing.
for(let t of trails)t.update(),t.draw();
Loops through every active trail, fading it slightly (update) and rendering it (draw) each frame.
trails=trails.filter(t=>t.alive());
Replaces the trails array with only the trails that are still visible, discarding fully-faded ones to keep memory and drawing costs low.
let d=drops[i];
Grabs the current drop being processed in this iteration of the outer loop.
d.update();
Moves this drop according to its own update() logic (falling, swaying, or resetting).
if(abs(d.x-o.x)<3&&abs(d.y-o.y)<8){o.s=min(18,o.s+1);o.v=(o.v+d.v)*.5;d.reset();break;}
If drop d is very close in both x and y to a later drop o, they're treated as colliding: o grows slightly (capped at size 18), o's speed becomes the average of both drops' speeds, and d resets to a fresh raindrop at the top - simulating two drops merging into one.
d.draw();
Renders the drop (whether it merged/reset or not) at its current position.
if(random()<.004&&!flash)flash=150;
Each frame there's a small (0.4%) random chance of starting a lightning flash, but only if no flash is currently active.
if(flash>0){fill(255,flash);noStroke();rect(0,0,width,height);flash-=5;}
While flash is greater than zero, draws a white rectangle over the whole canvas at that opacity, then decreases flash so the brightness fades out over the next several frames.

windowResized()

windowResized() is a special p5.js function that's automatically called whenever the browser window changes size, making it the natural place to rebuild anything (like a canvas-sized background buffer) that depends on the current window dimensions.

function windowResized(){resizeCanvas(windowWidth,windowHeight);makeBg();for(let d of drops)d.reset();}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Reset Every Drop for(let d of drops)d.reset();

Repositions every existing drop randomly so none are stuck outside the newly-sized canvas

resizeCanvas(windowWidth,windowHeight);
Resizes the canvas to match the browser window whenever it changes size.
makeBg();
Rebuilds the gradient background at the new size, since the old one would no longer fit or align correctly.
for(let d of drops)d.reset();
Sends every existing drop back to a fresh random position so the rain still looks correct after the resize instead of being clustered in the old window dimensions.

📦 Key Variables

drops array

Holds every Drop object currently animating on screen - the core list the sketch loops over each frame to update and draw the rain.

let drops=[];
trails array

Holds every active Trail object representing a fading streak left behind by a drop that has stopped partway down the window.

let trails=[];
bg object

Stores the offscreen p5.Graphics buffer containing the pre-rendered, blurred gradient night sky, avoiding the need to recompute the gradient every frame.

let bg;
flash number

Acts as a countdown timer for the lightning flash effect - zero means no flash, and any positive value controls the current flash brightness before it fades out.

let flash=0;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG draw() merge logic

When two drops merge, d.reset() immediately relocates drop d to a brand-new random position, but d.draw() is still called right afterward - drawing the freshly reset drop at its new spot in the same frame, which can cause a subtle visual pop instead of a clean disappearance.

💡 Skip drawing on the frame a merge happens, e.g. add a 'continue' after d.reset() (inside the outer loop) so the reset drop is only drawn starting next frame.

PERFORMANCE draw() nested collision loop

The drop-merging check is an O(n^2) nested loop comparing every drop against every other drop (about 7,000 comparisons per frame with 120 drops), which won't scale well if the drop count is increased significantly.

💡 For larger drop counts, bucket drops into a coarse spatial grid based on x/y position so each drop only needs to check nearby cells instead of every other drop.

STYLE entire sketch.js

The code is heavily minified with no whitespace or line breaks within statements, chained comma operators (t.update(),t.draw()), and single-letter variable names (d, o, t, v, s), which makes it hard for beginners to read and debug.

💡 Expand the code with consistent indentation, one statement per line, and more descriptive variable names (e.g. drop, other, trail, velocity, size) to make the logic easier to follow while learning.

FEATURE draw()

The sketch is purely ambient with no interactivity - mouseX/mouseY and keyboard input are never used, even though the description mentions it's meant for relaxation, which could still benefit from optional gentle controls.

💡 Add a subtle interactive touch, such as using mouseX to bias the wind/sway direction, or a keyPressed() toggle to speed up or slow down the rain for different moods.

🔄 Code Flow

Code flow showing drop, trail, makebg, setup, draw, windowresized

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

graph TD start[Start] --> setup[setup] setup --> setup-spawn-drops[setup-spawn-drops] setup-spawn-drops --> draw[draw loop] click setup href "#fn-setup" click setup-spawn-drops href "#sub-setup-spawn-drops" draw --> draw-trail-loop[draw-trail-loop] draw --> draw-drop-merge-loop[draw-drop-merge-loop] draw --> draw-flash-trigger[draw-flash-trigger] draw --> draw-flash-render[draw-flash-render] draw-trail-loop --> trail-alive-check[trail-alive-check] trail-alive-check -->|if alive| draw-trail-filter[draw-trail-filter] draw-trail-filter --> draw-trail-loop draw-drop-merge-loop -->|for each drop| drop-stopped-check[drop-stopped-check] drop-stopped-check -->|if stopped| trail[trail] drop-stopped-check -->|if not stopped| drop-recycle[drop-recycle] drop-recycle --> draw-drop-merge-loop draw-flash-trigger -->|if flash not active| draw-flash-render[draw-flash-render] draw-flash-render --> draw windowresized --> windowresized-reset-loop[windowresized-reset-loop] windowresized-reset-loop -->|reset every drop| windowresized click draw href "#fn-draw" click draw-trail-loop href "#sub-draw-trail-loop" click draw-drop-merge-loop href "#sub-draw-drop-merge-loop" click draw-flash-trigger href "#sub-draw-flash-trigger" click draw-flash-render href "#sub-draw-flash-render" click windowresized href "#fn-windowresized" click windowresized-reset-loop href "#sub-windowresized-reset-loop"

❓ Frequently Asked Questions

What visual experience does the AI Rain Window sketch provide?

The sketch creates a serene visual of raindrops sliding down a window, with gentle trails against a dark blue night sky, offering a calming and meditative atmosphere.

Is there any interaction needed from users in the AI Rain Window sketch?

No interaction is required; users can simply relax and enjoy the peaceful rainfall effect.

What creative coding concepts are showcased in the AI Rain Window sketch?

The sketch demonstrates object-oriented programming through the use of classes for raindrops and trails, as well as techniques for animation and background generation.

Preview

AI Rain Window - Meditative Rainfall - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Rain Window - Meditative Rainfall - Code flow showing drop, trail, makebg, setup, draw, windowresized
Code Flow Diagram