AI Clumsy Robot - Watch It Fail at Stairs

This sketch creates an animated robot character that repeatedly attempts to climb a staircase and fails spectacularly. The robot cycles through a complete animation sequence every 360 frames, showing different stages of the climb attempt with rotation and position changes. Speech bubbles display encouraging messages before each attempt and pain expressions after tumbling down.

๐ŸŽ“ Concepts You'll Learn

Animation cyclesLerp interpolationTransformation (translate/rotate)Conditional logicFrame-based timingText renderingCanvas resizingModulo operator

๐Ÿ”„ Code Flow

Code flow showing setup, draw, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> attempt-counter[Increment Attempts Counter] draw --> ground-and-stairs[Draw Ground and Staircase] draw --> animation-progress[Calculate Animation Progress] animation-progress --> approach-phase[Approach Phase] approach-phase --> robot-body[Draw Robot Body and Face] robot-body --> robot-mouth[Draw Robot Mouth Expression] approach-phase --> speech-bubble[Draw Speech Bubble with Message] draw --> climb-phase[Climb Phase] climb-phase --> robot-body robot-body --> robot-mouth climb-phase --> speech-bubble draw --> tumble-phase[Tumble Phase] tumble-phase --> robot-body robot-body --> robot-mouth tumble-phase --> speech-bubble draw --> recovery-phase[Recovery Phase] recovery-phase --> robot-body robot-body --> robot-mouth recovery-phase --> speech-bubble draw --> attempts-text[Display Attempts Counter] draw --> windowresized[windowResized] click setup href "#fn-setup" click draw href "#fn-draw" click attempt-counter href "#sub-attempt-counter" click ground-and-stairs href "#sub-ground-and-stairs" click animation-progress href "#sub-animation-progress" click approach-phase href "#sub-approach-phase" click climb-phase href "#sub-climb-phase" click tumble-phase href "#sub-tumble-phase" click recovery-phase href "#sub-recovery-phase" click robot-body href "#sub-robot-body" click robot-mouth href "#sub-robot-mouth" click attempts-text href "#sub-attempts-text" click speech-bubble href "#sub-speech-bubble" click windowresized href "#fn-windowresized"

๐Ÿ“ Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas and sets default drawing modes that will be used throughout the animation.

function setup(){createCanvas(windowWidth,windowHeight);rectMode(CENTER);textSize(20);}

Line by Line:

createCanvas(windowWidth,windowHeight)
Creates a canvas that fills the entire browser window, making the sketch responsive to different screen sizes
rectMode(CENTER)
Changes rectangle drawing mode so that coordinates represent the center point instead of the top-left corner
textSize(20)
Sets the default font size to 20 pixels for all text drawn in the sketch

draw()

draw() is the main animation loop that runs 60 times per second. It uses the frameCount variable and modulo arithmetic to create a repeating 360-frame cycle. The key technique is normalizing frameCount into a 0-1 progress value (u), then using conditional statements and lerp() to smoothly animate the robot through different phases. This approach makes it easy to control timing and create complex multi-phase animations.

function draw(){
  if(frameCount%cycle==1)attempts++;
  background(190);
  let g=height*0.75,s=min(width,height)*0.1;
  stroke(120);line(0,g,width,g);fill(220);let sx=width*0.6,sh=height*0.05,sw=width*0.3;
  for(let i=0;i<5;i++)rect(sx+sw/2,g-sh*(i+0.5),sw,sh);
  let u=(frameCount%cycle)/cycle;let startX=width*0.15,stairX=sx-s;
  let x,y,a=0;
  if(u<0.35){let p=u/0.35;x=lerp(startX,stairX,p);y=g-s/2;}
  else if(u<0.5){let p=(u-0.35)/0.15;x=lerp(stairX,stairX+s*0.3,p);y=lerp(g-s/2,g-s*1.2,p);a=lerp(0,-0.4,p);}
  else if(u<0.75){let p=(u-0.5)/0.25;x=stairX+s*0.3-s*1.5*p;y=g-s*1.2+s*1.6*p;a=lerp(-0.4,3.5,p);}
  else{let fx=stairX+s*0.3-s*1.5,fy=g-s*1.2+s*1.6;let p=(u-0.75)/0.25;x=lerp(fx,startX,p);y=lerp(fy,g-s/2,p);a=lerp(3.5,0,p);}
  push();translate(x,y);rotate(a);stroke(0);fill(245);rect(0,0,s,s,8);
  fill(0);let ex=s*0.2,ey=-s*0.1;circle(-ex,ey,s*0.15);circle(ex,ey,s*0.15);
  strokeWeight(2);noFill();if(u<0.5||u>0.9)line(-s*0.15,s*0.1,s*0.15,s*0.05);
  else if(u<0.75)line(-s*0.15,s*0.05,s*0.15,s*0.1);
  else line(-s*0.15,s*0.08,s*0.15,s*0.08);pop();
  noStroke();fill(0);textAlign(LEFT,TOP);text("Attempts: "+attempts,10,10);
  let msg="";if(u<0.3)msg="I got this!";else if(u>0.55&&u<0.8)msg="Ow...";
  if(msg!=""){let tw=textWidth(msg)+20,bx=x+s*1.4,by=y-s*1.4;fill(255);stroke(0);rectMode(CENTER);rect(bx,by,tw,30,10);
    triangle(bx-tw*0.3,by+15,bx-tw*0.45,by+25,bx-tw*0.15,by+25);fill(0);noStroke();textAlign(CENTER,CENTER);text(msg,bx,by);}}
}

๐Ÿ”ง Subcomponents:

conditional Increment Attempts Counter if(frameCount%cycle==1)attempts++;

Increases the attempts counter by 1 every time a new animation cycle begins (every 360 frames)

calculation Draw Ground and Staircase stroke(120);line(0,g,width,g);fill(220);let sx=width*0.6,sh=height*0.05,sw=width*0.3;for(let i=0;i<5;i++)rect(sx+sw/2,g-sh*(i+0.5),sw,sh);

Draws a horizontal ground line and creates 5 stair steps by looping and drawing rectangles at increasing heights

calculation Calculate Animation Progress let u=(frameCount%cycle)/cycle;

Converts the current frame count into a normalized value (0 to 1) that represents progress through the animation cycle

conditional Approach Phase (0-35% of cycle) if(u<0.35){let p=u/0.35;x=lerp(startX,stairX,p);y=g-s/2;}

Robot walks from the left toward the base of the stairs using linear interpolation

conditional Climb Phase (35-50% of cycle) else if(u<0.5){let p=(u-0.35)/0.15;x=lerp(stairX,stairX+s*0.3,p);y=lerp(g-s/2,g-s*1.2,p);a=lerp(0,-0.4,p);}

Robot attempts to climb up the stairs with slight forward tilt, moving upward and rotating slightly

conditional Tumble Phase (50-75% of cycle) else if(u<0.75){let p=(u-0.5)/0.25;x=stairX+s*0.3-s*1.5*p;y=g-s*1.2+s*1.6*p;a=lerp(-0.4,3.5,p);}

Robot loses balance and tumbles down the stairs with dramatic rotation (spinning 3.5 radians)

conditional Recovery Phase (75-100% of cycle) else{let fx=stairX+s*0.3-s*1.5,fy=g-s*1.2+s*1.6;let p=(u-0.75)/0.25;x=lerp(fx,startX,p);y=lerp(fy,g-s/2,p);a=lerp(3.5,0,p);}

Robot recovers from the tumble and walks back to the starting position, rotation returns to normal

calculation Draw Robot Body and Face push();translate(x,y);rotate(a);stroke(0);fill(245);rect(0,0,s,s,8);fill(0);let ex=s*0.2,ey=-s*0.1;circle(-ex,ey,s*0.15);circle(ex,ey,s*0.15);

Draws the robot as a rounded square body with two circular eyes, positioned and rotated based on animation phase

conditional Draw Robot Mouth Expression strokeWeight(2);noFill();if(u<0.5||u>0.9)line(-s*0.15,s*0.1,s*0.15,s*0.05);else if(u<0.75)line(-s*0.15,s*0.05,s*0.15,s*0.1);else line(-s*0.15,s*0.08,s*0.15,s*0.08);

Changes the robot's mouth expression based on animation phase: confident smile during approach/recovery, grimace during tumble, neutral during pain

calculation Display Attempts Counter noStroke();fill(0);textAlign(LEFT,TOP);text("Attempts: "+attempts,10,10);

Renders the attempts counter in the top-left corner of the screen

conditional Draw Speech Bubble with Message let msg="";if(u<0.3)msg="I got this!";else if(u>0.55&&u<0.8)msg="Ow...";if(msg!=""){let tw=textWidth(msg)+20,bx=x+s*1.4,by=y-s*1.4;fill(255);stroke(0);rectMode(CENTER);rect(bx,by,tw,30,10);triangle(bx-tw*0.3,by+15,bx-tw*0.45,by+25,bx-tw*0.15,by+25);fill(0);noStroke();textAlign(CENTER,CENTER);text(msg,bx,by);}

Creates and displays a speech bubble near the robot showing 'I got this!' before climbing and 'Ow...' after tumbling, with a triangular pointer

Line by Line:

if(frameCount%cycle==1)attempts++;
Uses modulo (%) to check if we're at the start of a new cycle. When frameCount is divisible by 360, increment attempts by 1. This happens once per complete animation cycle.
background(190);
Clears the canvas with a light gray color (190) at the start of each frame, creating a fresh background for the next animation frame
let g=height*0.75,s=min(width,height)*0.1;
Calculates two important measurements: g is the ground level (75% down the canvas), and s is the robot size (10% of the smaller screen dimension)
stroke(120);line(0,g,width,g);
Draws a horizontal line across the entire canvas at the ground level to represent the floor
fill(220);let sx=width*0.6,sh=height*0.05,sw=width*0.3;
Sets fill color to light gray and calculates stair dimensions: sx is the x position, sh is step height (5% of canvas), sw is step width (30% of canvas)
for(let i=0;i<5;i++)rect(sx+sw/2,g-sh*(i+0.5),sw,sh);
Loops 5 times to draw 5 stair steps. Each step is drawn higher than the previous one using (i+0.5) to create the staircase effect
let u=(frameCount%cycle)/cycle;
Converts the current frame into a progress value from 0 to 1. This makes it easy to control different animation phases based on where we are in the 360-frame cycle
let startX=width*0.15,stairX=sx-s;
Defines the robot's starting position (15% from left) and the x-coordinate where the stairs begin
if(u<0.35){let p=u/0.35;x=lerp(startX,stairX,p);y=g-s/2;}
During the first 35% of the cycle, the robot walks toward the stairs. lerp smoothly interpolates between start position and stair position based on progress p
else if(u<0.5){let p=(u-0.35)/0.15;x=lerp(stairX,stairX+s*0.3,p);y=lerp(g-s/2,g-s*1.2,p);a=lerp(0,-0.4,p);}
From 35-50% of cycle, robot climbs up with a slight forward lean. The rotation 'a' changes from 0 to -0.4 radians, tilting the robot forward
else if(u<0.75){let p=(u-0.5)/0.25;x=stairX+s*0.3-s*1.5*p;y=g-s*1.2+s*1.6*p;a=lerp(-0.4,3.5,p);}
From 50-75% of cycle, the robot tumbles down dramatically. Rotation increases from -0.4 to 3.5 radians (more than a full rotation), creating the slapstick effect
else{let fx=stairX+s*0.3-s*1.5,fy=g-s*1.2+s*1.6;let p=(u-0.75)/0.25;x=lerp(fx,startX,p);y=lerp(fy,g-s/2,p);a=lerp(3.5,0,p);}
From 75-100% of cycle, the robot recovers and walks back to the starting position. Rotation returns to 0 as the robot regains composure
push();translate(x,y);rotate(a);
Saves the current transformation state, then moves the origin to the robot's current position and rotates by angle 'a'. All subsequent drawing happens relative to this transformed space
stroke(0);fill(245);rect(0,0,s,s,8);
Draws the robot's body as a rounded square (8-pixel corner radius) with black outline and off-white fill
fill(0);let ex=s*0.2,ey=-s*0.1;circle(-ex,ey,s*0.15);circle(ex,ey,s*0.15);
Draws two black circular eyes positioned slightly above center and to the left and right of the robot's face
if(u<0.5||u>0.9)line(-s*0.15,s*0.1,s*0.15,s*0.05);
During approach and recovery phases, draws a confident smile (line angled upward from left to right)
else if(u<0.75)line(-s*0.15,s*0.05,s*0.15,s*0.1);
During tumble phase, draws a grimace (line angled downward from left to right, showing pain)
else line(-s*0.15,s*0.08,s*0.15,s*0.08);
During the final recovery phase, draws a neutral straight mouth
pop();
Restores the transformation state to what it was before push(), so subsequent drawing is no longer rotated or translated
text("Attempts: "+attempts,10,10);
Displays the attempts counter in the top-left corner, updating it every frame with the current attempts value
if(u<0.3)msg="I got this!";else if(u>0.55&&u<0.8)msg="Ow...";
Sets the speech bubble message based on animation phase: confident during early approach, pained during and after tumble
let tw=textWidth(msg)+20,bx=x+s*1.4,by=y-s*1.4;
Calculates the speech bubble dimensions and positions it to the right and above the robot
rect(bx,by,tw,30,10);triangle(bx-tw*0.3,by+15,bx-tw*0.45,by+25,bx-tw*0.15,by+25);
Draws the speech bubble as a rounded rectangle with a triangular pointer at the bottom, creating a classic comic-style speech bubble

windowResized()

windowResized() is a special p5.js function that gets called automatically whenever the browser window is resized. By calling resizeCanvas(), we ensure the sketch always fills the entire window, making it responsive to different screen sizes. This is important for creating sketches that work well on phones, tablets, and desktop computers.

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

Line by Line:

resizeCanvas(windowWidth,windowHeight);
Automatically resizes the canvas to match the current browser window dimensions whenever the window is resized

๐Ÿ“ฆ Key Variables

attempts number

Tracks how many times the robot has attempted to climb the stairs. Increments by 1 every 360 frames (each complete animation cycle)

let attempts=0;
cycle number

Defines the length of one complete animation cycle in frames. Set to 360, meaning the full climb-and-fall animation takes 360 frames (6 seconds at 60 fps)

let cycle=360;
u number

Normalized animation progress value from 0 to 1. Calculated as (frameCount % cycle) / cycle, making it easy to control different animation phases

let u=(frameCount%cycle)/cycle;
g number

The y-coordinate of the ground level, calculated as 75% down the canvas height. Used as the baseline for stairs and robot positioning

let g=height*0.75;
s number

The robot's size, calculated as 10% of the smaller screen dimension. Used to scale all robot features and ensure it looks proportional on different screens

let s=min(width,height)*0.1;
x number

The robot's current x-coordinate (horizontal position) on the canvas, updated each frame based on the animation phase

let x,y,a=0;
y number

The robot's current y-coordinate (vertical position) on the canvas, updated each frame based on the animation phase

let x,y,a=0;
a number

The robot's rotation angle in radians. Changes from 0 to -0.4 during climb, then -0.4 to 3.5 during tumble, creating the spinning effect

let x,y,a=0;
sx number

The x-coordinate where the staircase begins, positioned at 60% across the canvas width

let sx=width*0.6;
sh number

The height of each individual stair step, calculated as 5% of canvas height

let sh=height*0.05;
sw number

The width of each stair step, calculated as 30% of canvas width

let sw=width*0.3;
startX number

The robot's starting x-position before it begins walking toward the stairs, positioned at 15% from the left edge

let startX=width*0.15;
stairX number

The x-coordinate of the base of the stairs, calculated as the stair position minus the robot size

let stairX=sx-s;
p number

A local progress value (0 to 1) for the current animation phase, used with lerp() to smoothly interpolate between positions and rotations

let p=u/0.35;
msg string

The text to display in the speech bubble. Either 'I got this!' during approach or 'Ow...' during/after tumble

let msg="";
tw number

The calculated width of the speech bubble, based on the text width plus 20 pixels of padding

let tw=textWidth(msg)+20;
bx number

The x-coordinate of the speech bubble center, positioned to the right of the robot

let bx=x+s*1.4;
by number

The y-coordinate of the speech bubble center, positioned above the robot

let by=y-s*1.4;
ex number

The horizontal offset of the robot's eyes from the center of its face (20% of robot size)

let ex=s*0.2;
ey number

The vertical offset of the robot's eyes from the center of its face (-10% of robot size, placing them above center)

let ey=-s*0.1;

๐Ÿงช Try This!

Experiment with the code by making these changes:

  1. Change the cycle variable from 360 to 180 to make the robot climb twice as fast. Try values like 240 or 480 to see how it affects the animation speed.
  2. Modify the robot's color by changing fill(245) to fill(255, 0, 0) to make the robot red, or try fill(100, 200, 255) for blue.
  3. Adjust the stair height by changing sh=height*0.05 to sh=height*0.1 to make taller stairs, making the climb more challenging.
  4. Change the rotation amount during tumble from a=lerp(-0.4,3.5,p) to a=lerp(-0.4,6,p) to make the robot spin even more dramatically.
  5. Modify the speech bubble messages by changing msg="I got this!" to msg="Here we go!" or msg="Ow..." to msg="Ouch!" for different dialogue.
  6. Experiment with the robot's starting position by changing startX=width*0.15 to startX=width*0.05 to start further left, or width*0.3 to start closer to the stairs.
Open in Editor & Experiment โ†’

๐Ÿ”ง Potential Improvements

Here are some ways this code could be enhanced:

BUG draw() - speech bubble positioning

The speech bubble position (bx, by) is calculated relative to the robot's position, but the robot is drawn with translate() and rotate(). This means the speech bubble may not align perfectly with the rotated robot, especially during the tumble phase when the robot is spinning.

๐Ÿ’ก Calculate the speech bubble position before the push() statement, or adjust the offset calculations (s*1.4) to account for the robot's rotation angle.

PERFORMANCE draw() - textWidth() calculation

textWidth(msg) is called every frame inside the speech bubble conditional, even though the message text only changes at specific points in the animation cycle. This is a minor calculation that happens 60 times per second unnecessarily.

๐Ÿ’ก Cache the text widths as variables or only recalculate when the message changes: 'if(msg!=="" && msg!==prevMsg) { tw=textWidth(msg)+20; prevMsg=msg; }'

STYLE draw() - magic numbers

The code contains many magic numbers like 0.35, 0.5, 0.75, 0.15, 0.3, 1.4, 1.2, 1.5, 1.6, 3.5 that control animation timing and positions. These are hard to understand and modify without breaking the animation.

๐Ÿ’ก Define named constants at the top: const APPROACH_END=0.35, CLIMB_END=0.5, TUMBLE_END=0.75, ROBOT_OFFSET=1.4, etc. This makes the code more readable and easier to adjust.

FEATURE draw() - animation phases

The robot always follows the same path and timing. There's no variation or randomness, which could make it feel repetitive over time.

๐Ÿ’ก Add slight randomization to the tumble phase: change the rotation amount or tumble distance slightly each cycle using random(). For example: a=lerp(-0.4, 3.5 + random(-0.5, 0.5), p) to vary the spin amount.

STYLE setup() and draw()

The sketch uses very compact, minified code style with multiple statements on single lines and abbreviated variable names (g, s, u, p, a). While this saves space, it's harder for beginners to read and understand.

๐Ÿ’ก Expand the code with more descriptive variable names and line breaks: instead of 'let g=height*0.75,s=min(width,height)*0.1;' use 'let groundLevel=height*0.75; let robotSize=min(width,height)*0.1;' This improves readability significantly.

Preview

AI Clumsy Robot - Watch It Fail at Stairs - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Clumsy Robot - Watch It Fail at Stairs - Code flow showing setup, draw, windowresized
Code Flow Diagram