AI Gravity Text - Type Letters That Fall and Stack

This sketch turns a simple text input into a live physics playground: every character you type falls from the top of the screen, bounces off the walls and floor, and stacks on top of other letters like Tetris blocks. Pressing SPACE launches the entire pile upward in an explosion before gravity pulls everything back down.

🧪 Try This!

Experiment with the code by making these changes:

  1. Turn up the gravity — Increasing g makes letters accelerate downward much faster, so they slam into the floor and each other harder.
  2. Make the explosion more powerful — Increasing the upward velocity range makes the spacebar explosion launch letters much higher into the air.
  3. Give it a colorful backdrop — The background is currently pure black; swapping in an RGB value gives the whole scene a different mood.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch links an HTML text input to a tiny physics engine: whatever you type becomes a falling, bouncing, stacking letter block on the canvas. It's a great sketch to study because it combines several core p5.js and programming ideas at once - gravity and velocity, wall and floor collisions, pairwise object-to-object collision resolution, DOM input events, and alpha fading over time.

The code keeps a single array of plain JavaScript objects called letters, each one storing a character, position, and velocity. setup() wires up the text input, physics() moves every letter and resolves collisions each frame, and draw() renders everything and removes old letters. By studying it you'll learn how a handful of small rules - gravity, bounce damping, and 'push apart on overlap' - combine into a surprisingly convincing physical simulation.

⚙️ How It Works

  1. setup() creates a full-window canvas and a small text input box in the corner, and configures rectMode, textAlign, and textSize for later drawing.
  2. Each time you type, an input event handler compares the new text's length to the previous length (lastLen) and pushes a new letter object into the letters array for every freshly typed, non-space character, starting at the top of the screen with a small random sideways drift.
  3. Every frame, draw() clears the screen, draws the instructions, and calls physics(), which adds gravity (g) to each letter's vertical velocity and moves every letter by its velocity.
  4. physics() also checks each letter against the canvas walls and floor, bouncing it back with reduced velocity, then compares every pair of letters so their rectangular boxes never overlap - pushing them apart sideways or letting them rest on top of each other depending on which direction has the smaller overlap.
  5. Back in draw(), letters older than 30 seconds are filtered out of the array, and each remaining letter is drawn as a colored rounded rectangle (pink-ish for vowels, blue-ish for consonants) with its character on top, gradually fading out as it nears the end of its 30-second life.
  6. Pressing the spacebar triggers keyPressed(), which gives every current letter a random burst of sideways and strongly upward velocity, making the whole pile explode into the air before gravity and collisions settle it again.

🎓 Concepts You'll Learn

Physics simulation (gravity and velocity)Axis-aligned bounding box collision detectionDOM input integration with createInputArray filtering for object lifecycle managementRGBA alpha for fade-out effectsEvent handling (input events and keyPressed)

📝 Code Breakdown

setup()

setup() runs once at the start. Here it also wires up a p5.js DOM element (createInput) and attaches a custom callback with inp.input(), a common pattern for connecting HTML controls to a sketch's behavior.

🔬 This loop skips spaces so they never become falling blocks. What happens if you delete the `if(ch===' ')continue;` line - will spaces fall as invisible blank blocks that still take up collision space?

    for(let i=lastLen;i<v.length;i++){
      let ch=v[i];if(ch===' ')continue;
      letters.push({x:random(w,width-w),y:0,vx:random(-1,1),vy:0,ch,t:millis()});
    }
function setup(){
  createCanvas(windowWidth,windowHeight);
  rectMode(CENTER);textAlign(CENTER,CENTER);textSize(28);
  inp=createInput('');inp.position(10,10);inp.size(260);
  inp.input(()=>{
    let v=inp.value();
    for(let i=lastLen;i<v.length;i++){
      let ch=v[i];if(ch===' ')continue;
      letters.push({x:random(w,width-w),y:0,vx:random(-1,1),vy:0,ch,t:millis()});
    }
    lastLen=v.length;
  });
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop New Character Loop for(let i=lastLen;i<v.length;i++){

Only processes characters typed since the last update, using lastLen as a bookmark, so old characters aren't re-added

conditional Skip Space Check let ch=v[i];if(ch===' ')continue;

Prevents spaces from becoming falling letter blocks

createCanvas(windowWidth,windowHeight);
Makes the canvas fill the entire browser window.
rectMode(CENTER);textAlign(CENTER,CENTER);textSize(28);
Configures drawing so rectangles are positioned by their center point, text is centered on its x/y coordinate, and the default font size is 28.
inp=createInput('');inp.position(10,10);inp.size(260);
Creates an empty HTML text input, places it near the top-left corner, and makes it 260 pixels wide.
inp.input(()=>{
Registers a callback function that runs every time the text inside the input box changes.
let v=inp.value();
Reads the current full text of the input box into the variable v.
let ch=v[i];if(ch===' ')continue;
Grabs the newly typed character; if it's a space, skip it so it never becomes a letter block.
letters.push({x:random(w,width-w),y:0,vx:random(-1,1),vy:0,ch,t:millis()});
Adds a new letter object to the array: a random horizontal starting position, y=0 (top of screen), a small random sideways velocity, zero vertical velocity, the character itself, and a timestamp for aging/fading.
lastLen=v.length;
Updates the bookmark so the next input event only looks at characters typed after this point.

physics()

physics() shows a compact but complete 2D physics loop: apply gravity, integrate velocity into position, resolve boundary collisions, then resolve object-to-object collisions using axis-of-least-penetration - a common technique in simple rigid body simulations and games.

🔬 This line makes letters bounce softly off the floor and eventually settle. What happens if you change -0.3 to -0.9 for a much bouncier floor, or remove the `if(abs(l.vy)<0.3)l.vy=0;` check entirely so letters never fully stop bouncing?

    if(l.y>height-h/2){l.y=height-h/2;l.vy*=-0.3;if(abs(l.vy)<0.3)l.vy=0;}

🔬 This decides whether two overlapping letters push apart sideways or stack vertically, based on whichever overlap is smaller. What happens if you swap the two branches so letters always try to push apart vertically first - will they still be able to form neat stacks?

      if(abs(dx)<abs(dy)){a.x-=dx;b.x+=dx;a.vx*=0.5;b.vx*=0.5;}
      else{a.y-=dy;b.y+=dy;a.vy=0;b.vy=0;}
function physics(){
  for(let l of letters){
    l.vy+=g;l.x+=l.vx;l.y+=l.vy;
    if(l.x<w/2||l.x>width-w/2){l.vx*=-0.5;l.x=constrain(l.x,w/2,width-w/2);}
    if(l.y>height-h/2){l.y=height-h/2;l.vy*=-0.3;if(abs(l.vy)<0.3)l.vy=0;}
  }
  for(let i=0;i<letters.length;i++)for(let j=i+1;j<letters.length;j++){
    let a=letters[i],b=letters[j];
    if(abs(a.x-b.x)<w&&abs(a.y-b.y)<h){
      let dx=(w-abs(a.x-b.x))/2*(a.x<b.x?-1:1);
      let dy=(h-abs(a.y-b.y))/2*(a.y<b.y?-1:1);
      if(abs(dx)<abs(dy)){a.x-=dx;b.x+=dx;a.vx*=0.5;b.vx*=0.5;}
      else{a.y-=dy;b.y+=dy;a.vy=0;b.vy=0;}
    }
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop Gravity and Movement Loop for(let l of letters){

Applies gravity to every letter's vertical velocity and moves each letter by its velocity

conditional Wall Bounce Check if(l.x<w/2||l.x>width-w/2){l.vx*=-0.5;l.x=constrain(l.x,w/2,width-w/2);}

Reverses and dampens horizontal velocity when a letter hits the left or right edge, and clamps it inside bounds

conditional Floor Bounce Check if(l.y>height-h/2){l.y=height-h/2;l.vy*=-0.3;if(abs(l.vy)<0.3)l.vy=0;}

Stops letters from falling past the floor, gives a small bounce, and settles tiny bounces to zero

for-loop Pairwise Collision Loop for(let i=0;i<letters.length;i++)for(let j=i+1;j<letters.length;j++){

Compares every unique pair of letters to detect and resolve overlapping rectangles

conditional Rectangle Overlap Test if(abs(a.x-b.x)<w&&abs(a.y-b.y)<h){

Detects whether two letters' boxes are overlapping in both x and y

conditional Resolve Along Smaller Axis if(abs(dx)<abs(dy)){a.x-=dx;b.x+=dx;a.vx*=0.5;b.vx*=0.5;}

Pushes the two overlapping letters apart along whichever axis has the smaller overlap - sideways separation or vertical stacking

l.vy+=g;l.x+=l.vx;l.y+=l.vy;
Gravity (g) is added to the letter's vertical velocity every frame, then position is updated by adding velocity - the classic 'accelerate then move' physics pattern.
if(l.x<w/2||l.x>width-w/2){l.vx*=-0.5;l.x=constrain(l.x,w/2,width-w/2);}
If a letter's center goes past the left or right edge (accounting for half its width), reverse its horizontal velocity and cut it in half, then clamp its position so it can't leave the screen.
if(l.y>height-h/2){l.y=height-h/2;l.vy*=-0.3;if(abs(l.vy)<0.3)l.vy=0;}
If a letter has fallen past the floor, snap it back to floor level, give it a small bounce (30% of its speed, reversed), and stop the bounce entirely once it's too small to notice.
for(let i=0;i<letters.length;i++)for(let j=i+1;j<letters.length;j++){
A nested loop that compares every letter to every other letter exactly once (j always starts after i), checking all unique pairs for collisions.
if(abs(a.x-b.x)<w&&abs(a.y-b.y)<h){
Two letters are considered overlapping if the horizontal distance between their centers is less than the box width AND the vertical distance is less than the box height.
let dx=(w-abs(a.x-b.x))/2*(a.x<b.x?-1:1);
Calculates how much horizontal overlap exists and which direction to push letter a to separate them.
let dy=(h-abs(a.y-b.y))/2*(a.y<b.y?-1:1);
Calculates how much vertical overlap exists and which direction to push letter a to separate them.
if(abs(dx)<abs(dy)){a.x-=dx;b.x+=dx;a.vx*=0.5;b.vx*=0.5;}
If the horizontal overlap is smaller, resolve the collision by pushing the letters apart sideways and slowing their horizontal speed.
else{a.y-=dy;b.y+=dy;a.vy=0;b.vy=0;}
Otherwise, the vertical overlap is smaller, so push the letters apart vertically and zero out their vertical velocity - this is what makes letters rest and stack on top of each other.

draw()

draw() is the animation loop that runs continuously. This sketch demonstrates a common pattern: update state (physics), clean up expired data (the filter), then render everything in a single pass, using alpha values calculated from elapsed time to create smooth fades.

🔬 Vowels get a reddish-pink fill and consonants get blue. What happens if you flip the ternary conditions so vowels turn blue and consonants turn pink?

    let v='aeiouAEIOU'.includes(l.ch);
    fill(v?255:100,100,v?100:255,a);rect(l.x,l.y,w,h,6);
function draw(){
  background(0);
  fill(255);text('Type to drop letters! Press SPACE to explode them',width/2,30);
  physics();
  let now=millis();
  letters=letters.filter(l=>now-l.t<30000);
  noStroke();
  for(let l of letters){
    let age=(now-l.t)/30000,a=255*(1-age);
    let v='aeiouAEIOU'.includes(l.ch);
    fill(v?255:100,100,v?100:255,a);rect(l.x,l.y,w,h,6);
    fill(255,a);text(l.ch.toUpperCase(),l.x,l.y+2);
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Letter Expiration Filter letters=letters.filter(l=>now-l.t<30000);

Removes any letter that has existed for 30 seconds or more from the array

for-loop Letter Render Loop for(let l of letters){

Draws every remaining letter as a colored rounded rectangle with its character on top

conditional Vowel/Consonant Color Check let v='aeiouAEIOU'.includes(l.ch);

Determines whether to color the letter block pink (vowel) or blue (consonant)

background(0);
Fills the whole canvas with black at the start of every frame, erasing the previous frame's drawing.
fill(255);text('Type to drop letters! Press SPACE to explode them',width/2,30);
Draws the white instruction text centered at the top of the screen.
physics();
Calls the physics function to update every letter's position and resolve collisions for this frame.
let now=millis();
Records the current time in milliseconds since the sketch started, used to calculate each letter's age.
letters=letters.filter(l=>now-l.t<30000);
Creates a new array containing only letters younger than 30000 milliseconds (30 seconds), effectively deleting expired ones.
let age=(now-l.t)/30000,a=255*(1-age);
Calculates the letter's age as a fraction from 0 (just created) to 1 (about to expire), then converts that into an alpha value that fades from 255 (fully visible) to 0 (invisible).
let v='aeiouAEIOU'.includes(l.ch);
Checks whether the letter's character is a vowel (upper or lowercase) by searching for it in a string of vowels.
fill(v?255:100,100,v?100:255,a);rect(l.x,l.y,w,h,6);
Sets a reddish-pink color for vowels or a bluish color for consonants (using the alpha for fading), then draws a rounded rectangle (corner radius 6) at the letter's position as its 'block'.
fill(255,a);text(l.ch.toUpperCase(),l.x,l.y+2);
Draws the letter's character in white (fading with the same alpha) on top of its colored block, uppercase and nudged down 2 pixels to look centered.

keyPressed()

keyPressed() is a built-in p5.js function that fires once whenever any key is pressed. Checking keyCode lets you respond to specific keys, and returning false stops the browser's default action for that key.

🔬 This explodes every letter upward when you press SPACE. What happens if you change random(-12,-6) to random(6,12) (positive instead of negative) - would the letters shoot downward through the floor instead of up?

  if(keyCode===32){
    for(let l of letters){l.vx=random(-5,5);l.vy=random(-12,-6);}
    return false;
  }
function keyPressed(){
  if(keyCode===32){
    for(let l of letters){l.vx=random(-5,5);l.vy=random(-12,-6);}
    return false;
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Spacebar Detection if(keyCode===32){

Checks if the key that was just pressed is the spacebar (keyCode 32)

for-loop Explosion Velocity Loop for(let l of letters){l.vx=random(-5,5);l.vy=random(-12,-6);}

Gives every existing letter a random burst of sideways and upward velocity

if(keyCode===32){
keyCode 32 is the spacebar - this checks specifically for that key so other keys don't trigger the explosion.
for(let l of letters){l.vx=random(-5,5);l.vy=random(-12,-6);}
Loops through every letter currently on screen and assigns it a new random horizontal velocity and a random strongly negative (upward) vertical velocity, launching the whole pile into the air.
return false;
Prevents the browser's default behavior for the spacebar (like scrolling the page), which p5.js checks for when a keyPressed function returns false.

windowResized()

windowResized() is a built-in p5.js function that automatically runs whenever the browser window changes size, letting you keep the canvas responsive to the viewport.

function windowResized(){resizeCanvas(windowWidth,windowHeight);}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth,windowHeight);
Resizes the canvas to match the browser window's new width and height whenever the user resizes their window.

📦 Key Variables

letters array

Stores every currently active falling letter as an object with position (x, y), velocity (vx, vy), the character (ch), and a creation timestamp (t).

let letters=[];
inp object

Holds a reference to the p5.js DOM text input element the user types into.

let inp;
lastLen number

Remembers the length of the input text from the previous update so only newly typed characters spawn new letters.

let lastLen=0;
g number

The gravity constant added to every letter's vertical velocity each frame, controlling fall speed.

let g=0.4;
w number

The width of each letter's rectangle, used for drawing and collision detection.

let w=40;
h number

The height of each letter's rectangle, used for drawing and collision detection.

let h=50;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE physics() pairwise collision loop

The nested for loop checking every pair of letters runs in O(n^2) time, meaning if a user types a lot of text (or lets many letters accumulate before the 30-second expiry), the frame rate will slow down significantly.

💡 Use a spatial grid or sort letters by x-position to limit collision checks to nearby letters only, or cap the maximum number of active letters.

BUG keyPressed()

keyPressed() fires globally regardless of whether the text input has focus, so pressing space while typing in the input box both adds a space character to the text field (which is then skipped) and triggers the explosion effect simultaneously - this may or may not be intended but could surprise users.

💡 Check document.activeElement or use inp.elt to detect if the input has focus before triggering the explosion, if a cleaner separation between typing and exploding is desired.

STYLE throughout the sketch

Variable names are extremely terse (l, a, b, v, dx, dy) and many statements are packed onto single lines, which makes the code hard to read and modify for beginners.

💡 Expand to more descriptive names (e.g. letter, letterA, letterB, isVowel) and spread statements across multiple lines with consistent indentation for readability.

FEATURE draw() and physics()

There's no visual or audio feedback when letters actually collide with each other or land on the floor, which could make the physics feel more satisfying.

💡 Add a brief flash, particle burst, or short sound effect (using p5.sound) whenever a collision is resolved or a letter first touches the floor.

🔄 Code Flow

Code flow showing setup, physics, draw, keypressed, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> newcharloop[new-char-loop] newcharloop --> skipspace[skip-space] skipspace --> gravityupdateloop[gravity-update-loop] gravityupdateloop --> wallbounce[wall-bounce] wallbounce --> floorbounce[floor-bounce] floorbounce --> collisionpairsloop[collision-pairs-loop] collisionpairsloop --> overlapcheck[overlap-check] overlapcheck --> resolveaxis[resolve-axis] resolveaxis --> expirefilter[expire-filter] expirefilter --> renderloop[render-loop] renderloop --> vowelcheck[vowel-check] renderloop --> draw draw --> keypressed[keypressed] keypressed --> spacebarcheck[spacebar-check] spacebarcheck --> explodeloop[explode-loop] explodeloop --> draw draw --> windowresized[windowresized] click setup href "#fn-setup" click draw href "#fn-draw" click newcharloop href "#sub-new-char-loop" click skipspace href "#sub-skip-space" click gravityupdateloop href "#sub-gravity-update-loop" click wallbounce href "#sub-wall-bounce" click floorbounce href "#sub-floor-bounce" click collisionpairsloop href "#sub-collision-pairs-loop" click overlapcheck href "#sub-overlap-check" click resolveaxis href "#sub-resolve-axis" click expirefilter href "#sub-expire-filter" click renderloop href "#sub-render-loop" click vowelcheck href "#sub-vowel-check" click keypressed href "#fn-keypressed" click spacebarcheck href "#sub-spacebar-check" click explodeloop href "#sub-explode-loop" click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual effect does the AI Gravity Text sketch produce?

The sketch visually creates falling letters that tumble down the screen and stack like Tetris blocks, simulating realistic physics with bouncing and collisions.

How can users interact with the AI Gravity Text sketch?

Users can type any letters to make them drop from the top of the screen, and pressing the SPACE key causes the letters to explode in different directions.

What creative coding concept is showcased in this sketch?

This sketch demonstrates particle physics by applying gravity and collision detection to letters, allowing them to behave as physical objects in a playful manner.

Preview

AI Gravity Text - Type Letters That Fall and Stack - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Gravity Text - Type Letters That Fall and Stack - Code flow showing setup, physics, draw, keypressed, windowresized
Code Flow Diagram