AI Rain Window - Meditative Rainfall

This sketch creates a meditative rainfall animation where raindrops fall down a dark blue night sky, leaving gentle trails behind them. Drops collide with each other to merge and grow larger, while occasional lightning flashes illuminate the scene. The animation is continuous and requires no interaction.

๐ŸŽ“ Concepts You'll Learn

Classes and ObjectsAnimation loopCollision detectionArray filteringGraphics layersColor interpolationTrigonometric motionParticle systemsAlpha transparency

๐Ÿ”„ Code Flow

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

๐Ÿ’ก Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> background-display[Background Display] draw --> trail-update-loop[Trail Update Loop] draw --> drop-update-loop[Drop Update and Collision Loop] draw --> lightning-trigger[Lightning Random Trigger] draw --> graphics-creation[Graphics Buffer Creation] click setup href "#fn-setup" click draw href "#fn-draw" click background-display href "#sub-background-display" click trail-update-loop href "#sub-trail-update-loop" click drop-update-loop href "#sub-drop-update-loop" click lightning-trigger href "#sub-lightning-trigger" click graphics-creation href "#sub-graphics-creation" setup --> canvas-creation[Canvas Setup] setup --> background-generation[Background Generation] setup --> drop-initialization[Drop Initialization Loop] click canvas-creation href "#sub-canvas-creation" click background-generation href "#sub-background-generation" click drop-initialization href "#sub-drop-initialization" drop-initialization --> drop-constructor[Drop Constructor] drop-initialization --> drop-reset[Drop Reset] click drop-constructor href "#sub-drop-constructor" click drop-reset href "#sub-drop-reset" background-display --> makebg[makebg] makebg --> color-definition[Color Definition] makebg --> gradient-loop[Gradient Drawing Loop] makebg --> blur-filter[Blur Filter] click makebg href "#fn-makebg" click color-definition href "#sub-color-definition" click gradient-loop href "#sub-gradient-loop" click blur-filter href "#sub-blur-filter" trail-update-loop --> trail-draw[Trail Draw] trail-update-loop --> trail-update[Trail Update] trail-update-loop --> trail-filter[Trail Cleanup] click trail-draw href "#sub-trail-draw" click trail-update href "#sub-trail-update" click trail-filter href "#sub-trail-filter" drop-update-loop --> drop-update[Drop Update] drop-update-loop --> drop-draw[Drop Draw] drop-update-loop --> collision-detection[Drop Collision Detection] click drop-update href "#sub-drop-update" click drop-draw href "#sub-drop-draw" click collision-detection href "#sub-collision-detection" lightning-trigger --> lightning-animation[Lightning Flash Animation] click lightning-animation href "#sub-lightning-animation" windowresized --> canvas-resize[Canvas Resize] windowresized --> background-regenerate[Background Regeneration] windowresized --> drop-reset-loop[Drop Reset Loop] click canvas-resize href "#sub-canvas-resize" click background-regenerate href "#sub-background-regenerate" click drop-reset-loop href "#sub-drop-reset-loop" drop-reset-loop --> drop-constructor drop-reset-loop --> drop-reset trail-update --> trail-alive[Trail Alive Check] click trail-alive href "#sub-trail-alive"

๐Ÿ“ Code Breakdown

setup()

setup() runs once when the sketch starts. It's the perfect place to initialize your canvas, create objects, and set up variables that won't change during the animation.

function setup(){createCanvas(windowWidth,windowHeight);makeBg();for(let i=0;i<120;i++)drops.push(new Drop());}

๐Ÿ”ง Subcomponents:

function-call Canvas Setup createCanvas(windowWidth,windowHeight)

Creates a canvas that fills the entire browser window

function-call Background Generation makeBg()

Generates the gradient night sky background once

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

Creates 120 raindrop objects and adds them to the drops array

Line by Line:

createCanvas(windowWidth,windowHeight)
Creates a canvas that matches the full window size, allowing the rain to fill the entire screen
makeBg()
Calls the function that creates the gradient night sky background and stores it in the bg variable
for(let i=0;i<120;i++)drops.push(new Drop())
Loops 120 times, creating a new Drop object each time and adding it to the drops array. This populates the scene with raindrops

draw()

draw() runs 60 times per second, creating smooth animation. This function handles all the updates and rendering: moving drops, checking collisions, removing dead trails, and creating lightning effects. The nested loops check every pair of drops for collisions efficiently.

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;}}

๐Ÿ”ง Subcomponents:

function-call Background Drawing image(bg,0,0,width,height)

Displays the pre-rendered gradient background on every frame

for-loop Trail Update Loop for(let t of trails)t.update(),t.draw()

Updates and draws all active trails left by stopped raindrops

calculation Trail Cleanup trails=trails.filter(t=>t.alive())

Removes dead trails (with alpha <= 0) from the trails array to save memory

for-loop Drop Update and Collision Loop for(let i=0;i<drops.length;i++){let d=drops[i];d.update();for(let j=i+1;j<drops.length;j++){...}d.draw();}

Updates each drop, checks for collisions with other drops, and draws them

conditional Drop Collision Detection 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;}

Detects when two drops are close together and merges them by growing one and resetting the other

conditional Lightning Random Trigger if(random()<.004&&!flash)flash=150

Randomly triggers a lightning flash with a 0.4% chance per frame

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

Draws a white rectangle that fades out to create the lightning flash effect

Line by Line:

image(bg,0,0,width,height)
Displays the gradient night sky background at the top-left corner, filling the entire canvas
for(let t of trails)t.update(),t.draw()
Loops through each trail object, updates its alpha value, and draws it on the canvas
trails=trails.filter(t=>t.alive())
Removes all dead trails from the array, keeping only trails with alpha > 0 to improve performance
for(let i=0;i<drops.length;i++){let d=drops[i];d.update();
Loops through each raindrop, stores it in variable d, and updates its position
for(let j=i+1;j<drops.length;j++){let o=drops[j];
For each drop, checks all drops that come after it in the array to avoid checking the same pair twice
if(abs(d.x-o.x)<3&&abs(d.y-o.y)<8)
Checks if two drops are close enough to collide: within 3 pixels horizontally and 8 pixels vertically
o.s=min(18,o.s+1)
Increases the size of the other drop by 1, but caps it at a maximum of 18 pixels
o.v=(o.v+d.v)*.5
Averages the velocities of the two drops, making the merged drop move at a speed between them
d.reset()
Resets the current drop to a new position at the top of the screen to start falling again
if(random()<.004&&!flash)flash=150
Has a 0.4% chance each frame to trigger a lightning flash (only if one isn't already happening)
fill(255,flash);noStroke();rect(0,0,width,height);flash-=5
Draws a white rectangle covering the entire canvas with decreasing opacity, creating a fading flash effect

makeBg()

createGraphics() creates an off-screen drawing surface. This is efficient because we only need to draw the background once and can reuse it every frame. lerpColor() smoothly blends between two colors, perfect for creating gradients. The blur filter adds a professional, soft appearance to the night sky.

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);}

๐Ÿ”ง Subcomponents:

function-call Graphics Buffer Creation bg=createGraphics(windowWidth,windowHeight)

Creates an off-screen graphics buffer to draw the background once and reuse it

calculation Color Definition let c1=color(5,10,30),c2=color(20,40,70)

Defines two dark blue colors for the gradient: a darker blue at the top and a slightly lighter blue at the bottom

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

Draws horizontal lines from top to bottom, interpolating between the two colors to create a smooth gradient

function-call Blur Filter bg.filter(BLUR,2)

Applies a slight blur to soften the gradient lines and create a smooth, atmospheric effect

Line by Line:

bg=createGraphics(windowWidth,windowHeight)
Creates an off-screen drawing surface (graphics buffer) that matches the window size. This allows us to draw the background once and reuse it every frame without redrawing
let c1=color(5,10,30),c2=color(20,40,70)
Defines two colors: c1 is very dark blue (almost black), and c2 is slightly lighter blue. These will blend to create the night sky
for(let y=0;y<bg.height;y++)
Loops from the top of the canvas (y=0) to the bottom (y=bg.height), processing each row
bg.stroke(lerpColor(c1,c2,y/bg.height))
Interpolates between the two colors based on the current y position. At y=0, it's c1; at y=bg.height, it's c2; in between, it blends smoothly
bg.line(0,y,bg.width,y)
Draws a horizontal line across the entire width at the current y position using the interpolated color
bg.filter(BLUR,2)
Applies a blur filter with radius 2 to the graphics buffer, softening the gradient lines into a smooth, atmospheric effect

windowResized()

windowResized() is a special p5.js function that automatically runs whenever the browser window is resized. This ensures your sketch remains responsive and looks good at any screen size. Always remember to call resizeCanvas() and update any graphics or objects that depend on canvas dimensions.

function windowResized(){resizeCanvas(windowWidth,windowHeight);makeBg();for(let d of drops)d.reset();}

๐Ÿ”ง Subcomponents:

function-call Canvas Resize resizeCanvas(windowWidth,windowHeight)

Resizes the canvas to match the new window dimensions

function-call Background Regeneration makeBg()

Regenerates the background gradient to fit the new canvas size

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

Resets all drops to new random positions within the new canvas bounds

Line by Line:

resizeCanvas(windowWidth,windowHeight)
Automatically called when the browser window is resized. This updates the canvas to match the new window dimensions
makeBg()
Regenerates the background gradient with the new canvas dimensions so it fills the entire resized window
for(let d of drops)d.reset()
Resets every raindrop to a new random position within the new canvas bounds, preventing drops from appearing outside the visible area

Drop (class)

Classes are blueprints for creating objects with properties and methods. The Drop class represents a single raindrop with its own position, velocity, and behavior. Each drop is independent, allowing the sketch to manage many drops efficiently. The ternary operator (condition ? true : false) is used throughout to change behavior based on whether the drop has stopped.

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);}
}

๐Ÿ”ง Subcomponents:

function-call Constructor constructor(){this.reset();}

Initializes a new Drop by calling reset() to set all its properties

calculation Reset Method 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;}

Sets all drop properties to random values, creating a new falling raindrop at the top of the screen

conditional Update Method 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();}

Updates the drop's position each frame, creates trails when it stops, and resets when it falls off screen

function-call Draw Method draw(){noStroke();let len=this.s*(this.stopped?.7:1.5);fill(180,200,255,180);ellipse(this.x,this.y,len*.3,len);}

Draws the raindrop as a blue ellipse, adjusting its shape based on whether it's falling or stopped

Line by Line:

constructor(){this.reset();}
When a new Drop is created, immediately call reset() to initialize all its properties
this.x=random(width)
Sets the drop's x position to a random location across the canvas width
this.y=random(-height,0)
Sets the drop's y position above the top of the canvas (between -height and 0), so it appears to fall into view
this.v=random(3,8)
Sets the drop's velocity (falling speed) to a random value between 3 and 8 pixels per frame
this.s=random(6,12)
Sets the drop's size to a random value between 6 and 12 pixels, creating variation in drop sizes
this.off=random(TWO_PI)
Sets a random offset value (0 to 2ฯ€) used for the sine wave motion to make each drop sway differently
this.stop=random(height*.3,height*.9)
Sets a random stopping point between 30% and 90% down the canvas where the drop will stop and create a trail
this.stopped=false
Initializes the drop as not stopped, so it will fall normally until it reaches its stopping point
if(!this.stopped&&this.y>this.stop){this.stopped=true;trails.push(new Trail(this.x,this.y,height));}
When the drop reaches its stopping point, set stopped to true and create a new Trail object to draw the streak
this.y+=this.v*(this.stopped?.25:1)
Updates the drop's y position by its velocity. If stopped, move at 1/4 speed; if falling, move at full speed
this.x+=sin(frameCount*.1+this.off)*.4
Adds a gentle horizontal sway using a sine wave based on the frame count and the drop's offset, creating realistic wind motion
if(this.y>height+20)this.reset()
When the drop falls below the canvas (past height+20 pixels), reset it to start falling again from the top
let len=this.s*(this.stopped?.7:1.5)
Calculates the visual length of the drop. If stopped, use 70% of size; if falling, use 150% to make it look stretched
fill(180,200,255,180);ellipse(this.x,this.y,len*.3,len)
Draws a light blue ellipse at the drop's position with width of len*.3 and height of len, creating a teardrop shape

Trail (class)

The Trail class represents the streak left behind by a stopped raindrop. It's a simple object that stores position and opacity, then gradually fades out. This separation of concerns (drops and trails as separate classes) makes the code cleaner and easier to manage. The alive() method is used to filter out dead trails from the array.

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;}
}

๐Ÿ”ง Subcomponents:

calculation Constructor constructor(x,y1,y2){this.x=x;this.y1=y1;this.y2=y2;this.a=80;}

Creates a new trail at a specific x position from y1 to y2 with initial alpha of 80

calculation Update Method update(){this.a-=.2;}

Decreases the trail's alpha (opacity) by 0.2 each frame, making it fade out

conditional Draw Method draw(){if(this.a<=0)return;stroke(170,200,255,this.a);line(this.x,this.y1,this.x,this.y2);}

Draws a vertical line at the trail's x position, fading with its alpha value

conditional Alive Check alive(){return this.a>0;}

Returns true if the trail is still visible (alpha > 0), false if it's completely faded

Line by Line:

constructor(x,y1,y2){this.x=x;this.y1=y1;this.y2=y2;this.a=80;}
Creates a trail with an x position, a starting y position (y1), an ending y position (y2), and initial alpha of 80
this.a-=.2
Decreases the alpha value by 0.2 each frame, making the trail fade out gradually over time
if(this.a<=0)return
If the trail is completely invisible (alpha <= 0), exit the function early without drawing anything
stroke(170,200,255,this.a);line(this.x,this.y1,this.x,this.y2)
Sets the stroke color to light blue with the current alpha value, then draws a vertical line from y1 to y2
return this.a>0
Returns true if the trail still has opacity (is alive), false if it's completely faded out

๐Ÿ“ฆ Key Variables

drops array

Stores all active Drop objects currently falling on the screen. Each element is a Drop instance with its own position and velocity

let drops=[];
trails array

Stores all active Trail objects created when drops stop falling. Trails fade out and are removed from this array when they become invisible

let trails=[];
bg object (p5.Graphics)

Stores the off-screen graphics buffer containing the gradient night sky background. Created once and reused every frame for efficiency

let bg;
flash number

Stores the current alpha value of the lightning flash effect. When > 0, a white rectangle fades out. Decreases by 5 each frame

let flash=0;
x (Drop property) number

The horizontal position of the raindrop on the canvas

this.x=random(width);
y (Drop property) number

The vertical position of the raindrop on the canvas

this.y=random(-height,0);
v (Drop property) number

The velocity (falling speed) of the raindrop in pixels per frame

this.v=random(3,8);
s (Drop property) number

The size of the raindrop in pixels. Increases when drops collide with each other

this.s=random(6,12);
off (Drop property) number

A random offset value (0 to 2ฯ€) used to create unique sine wave motion for each drop's horizontal sway

this.off=random(TWO_PI);
stop (Drop property) number

The y position where this drop will stop falling and create a trail (between 30% and 90% of canvas height)

this.stop=random(height*.3,height*.9);
stopped (Drop property) boolean

Tracks whether the drop has reached its stopping point. Affects movement speed and trail creation

this.stopped=false;
x (Trail property) number

The horizontal position where the trail is drawn

this.x=x;
y1 (Trail property) number

The starting y position of the trail line (where the drop stopped)

this.y1=y1;
y2 (Trail property) number

The ending y position of the trail line (usually the bottom of the canvas)

this.y2=y2;
a (Trail property) number

The alpha (opacity) value of the trail, ranging from 80 (fully visible) to 0 (invisible). Decreases each frame

this.a=80;

๐Ÿงช Try This!

Experiment with the code by making these changes:

  1. Change the number of drops created in setup() from 120 to 200 to create a heavier rainfall. Notice how the collisions create larger drops more frequently
  2. Modify the collision detection in draw() by changing the distance thresholds from '<3' and '<8' to '<10' and '<15' to make drops merge from farther away
  3. Adjust the lightning frequency by changing the random probability from '.004' to '.01' to make lightning flash more often, or to '.001' for rarer flashes
  4. Change the drop's sway motion by modifying the sine wave calculation from 'sin(frameCount*.1+this.off)*.4' to 'sin(frameCount*.05+this.off)*.8' to create slower, wider horizontal movement
  5. Modify the trail fade speed by changing 'this.a-=.2' to 'this.a-=.5' in the Trail update() method to make trails disappear faster
  6. Change the drop size range in reset() from 'random(6,12)' to 'random(3,20)' to create more variation in drop sizes
  7. Adjust the stopping point range from 'random(height*.3,height*.9)' to 'random(height*.1,height*.5)' to make drops stop higher up on the screen
Open in Editor & Experiment โ†’

๐Ÿ”ง Potential Improvements

Here are some ways this code could be enhanced:

PERFORMANCE draw() - collision detection loop

The nested loop checks every pair of drops (O(nยฒ) complexity). With 120 drops, this means ~7,200 comparisons per frame. This could become slow with more drops

๐Ÿ’ก Implement spatial partitioning (divide canvas into grid cells) to only check drops in nearby cells, reducing comparisons significantly

BUG Drop.update() - trail creation

When a drop stops, it creates a trail from this.y to height, but if the drop is near the bottom of the canvas, the trail might be very short or invisible

๐Ÿ’ก Consider creating trails that extend from the drop's stopping point downward by a fixed distance, or to the bottom of the canvas: 'new Trail(this.x, this.y, height)' is correct, but verify y1 and y2 are in the right order

STYLE sketch.js - variable initialization

Global variables are declared on one line with commas, making them harder to read and document individually

๐Ÿ’ก Separate each variable declaration: 'let drops = [];' on its own line, with comments explaining each variable's purpose

FEATURE Drop class

All drops fall at constant velocity (after accounting for sway). Real raindrops would accelerate due to gravity and have terminal velocity

๐Ÿ’ก Add gravity simulation: increase velocity slightly each frame up to a maximum speed, creating more realistic falling motion

BUG draw() - collision handling

When two drops collide, the first drop resets immediately. If multiple drops collide with the same drop in one frame, the reset might cause unexpected behavior

๐Ÿ’ก Mark drops for reset at the end of the frame rather than immediately, or use a more sophisticated collision response system

PERFORMANCE makeBg() - gradient generation

The gradient is drawn line-by-line, which is slow. For a 1080px tall canvas, this means 1,080 line draws

๐Ÿ’ก Use a shader or createImage with pixel manipulation for faster gradient generation, or cache the background more efficiently

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 setup, draw, makebg, windowresized, drop, trail
Code Flow Diagram