AI Ant Journey - Watch the Little Worker

This sketch animates a lone ant walking across a dirt-colored canvas, steering around rocks, sticks, and puddles as it carries a leaf crumb back to its nest. Every time the ant reaches the nest, a delivery counter increments and a brand new set of obstacles and a starting position are generated, so the little journey repeats forever with a different layout each time.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the ant — Increasing the ant's speed value makes it dart to the nest much faster, completing deliveries in a fraction of the time.
  2. Switch to a night scene — Changing the background color instantly transforms the daytime dirt path into a moody nighttime setting.
  3. Fill the field with obstacles — Raising the obstacle count from 6 to 25 crowds the scene, making the ant's steering behavior much more visible as it weaves through the clutter.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch tells a tiny, looping story: an ant starts on the left edge of the screen carrying a leaf, walks toward a nest on the right while dodging rocks, sticks, and puddles, and triggers a new journey the instant it arrives. It's a great study in steering behavior because the ant doesn't follow a fixed path - every frame it recalculates the angle toward its goal using atan2(), then nudges that angle away from anything too close using dist(). The little body itself is drawn with just a handful of ellipses and lines rotated into place with rotate() and push()/pop(), showing how simple primitives can read as a recognizable creature once they're combined and oriented correctly.

The code is organized around a small piece of global state - an ant object, a nest vector, and an obstacles array - that gets rebuilt by a reset() function every time a delivery is completed. draw() clears the frame, draws the nest and obstacles, then calls moveAnt() to update position and drawAnt() to render the ant at its new spot and rotation. Studying this sketch will teach you how to steer an object toward a target, fake local obstacle avoidance without a physics engine, and structure a sketch so an entire 'level' can be regenerated with one function call.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, sets up a monospace font for the delivery counter, positions the nest near the right side of the screen, and calls reset() to build the first ant and the first batch of obstacles.
  2. reset() clears the obstacles array and fills it with 6 randomly placed and randomly sized rocks, sticks, or puddles, then places the ant at the left edge of the screen at a random height.
  3. Every frame, draw() paints the dirt-colored background, draws the nest as two overlapping ellipses, calls drawObstacles() to render the current obstacle layout, then calls moveAnt() to update the ant's position.
  4. Inside moveAnt(), the ant calculates the angle straight toward the nest using atan2(), then checks every obstacle - if the ant is too close to one, it bends that angle up or down depending on which side of the obstacle it's on, creating a simple steer-around effect.
  5. The ant's x and y position are nudged forward along that (possibly bent) angle using cos() and sin(), and its rotation is stored so drawAnt() can orient the little body correctly using push(), translate(), and rotate().
  6. Once the ant gets within 25 pixels of the nest, the delivery counter increments and reset() is called again, instantly generating a new obstacle layout and a fresh starting position so the journey begins anew.

🎓 Concepts You'll Learn

Steering toward a target with atan2()Local obstacle avoidance using dist()Object literals for lightweight game statepush()/pop() and rotate() for oriented drawingReset/regenerate pattern for procedural levelsResponsive full-window canvas with windowResized()

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the right place to size the canvas, configure text styles, and set up any starting state before the animation loop takes over.

function setup(){
  createCanvas(windowWidth,windowHeight);
  textFont('monospace');textSize(16);
  nest=createVector(width*0.8,height*0.5);
  reset();
}
Line-by-line explanation (4 lines)
createCanvas(windowWidth,windowHeight);
Makes the canvas fill the entire browser window so the scene scales to any screen size.
textFont('monospace');textSize(16);
Sets the font used for the delivery counter text and how big it should be drawn.
nest=createVector(width*0.8,height*0.5);
Places the nest 80% of the way across the screen and vertically centered - this is the ant's permanent goal.
reset();
Calls the reset function to build the first ant and the first set of obstacles before the draw loop starts.

reset()

reset() is the sketch's 'level generator'. Calling it rebuilds all the state needed for a fresh journey, which is a handy pattern any time you want to procedurally regenerate a scene.

🔬 This loop scatters 6 obstacles every time the ant reaches the nest. What happens if you change the 6 to 20 - does the ant still manage to find a path through, or does it get stuck?

  obs=[];
  for(let i=0;i<6;i++)obs.push({x:random(80,width-80),y:random(80,height-80),r:random(20,40),t:int(random(3))});
function reset(){
  obs=[];
  for(let i=0;i<6;i++)obs.push({x:random(80,width-80),y:random(80,height-80),r:random(20,40),t:int(random(3))});
  ant={x:40,y:random(80,height-80),a:0,s:1.2};
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Generate Obstacles for(let i=0;i<6;i++)obs.push({x:random(80,width-80),y:random(80,height-80),r:random(20,40),t:int(random(3))});

Fills the obstacles array with 6 randomly placed and randomly typed rocks, sticks, or puddles each time the level resets.

obs=[];
Empties the obstacles array so old obstacles from the previous journey don't stick around.
for(let i=0;i<6;i++)obs.push({x:random(80,width-80),y:random(80,height-80),r:random(20,40),t:int(random(3))});
Runs 6 times, each time pushing a new obstacle object with a random position, random radius between 20-40, and a random type 0, 1, or 2 (rock, stick, or puddle).
ant={x:40,y:random(80,height-80),a:0,s:1.2};
Rebuilds the ant object: x=40 always starts it near the left edge, y is randomized, a is its facing angle (starts at 0), and s is its movement speed.

draw()

draw() runs continuously, roughly 60 times per second. Its job here is simple: clear the frame, draw the static scenery (nest, obstacles), then delegate the moving parts (the ant) to dedicated helper functions - a clean way to keep each frame's logic organized.

function draw(){
  background(190,160,120);
  fill(60,40,30);noStroke();
  ellipse(nest.x+40,nest.y,60,80);
  fill(0);ellipse(nest.x,nest.y,40,60);
  drawObstacles();
  moveAnt();
  drawAnt();
  fill(0);text("Deliveries: "+deliveries,20,30);
}
Line-by-line explanation (8 lines)
background(190,160,120);
Repaints the whole canvas with a dirt-brown color every frame, which erases the previous frame's drawing to create smooth animation instead of trails.
fill(60,40,30);noStroke();
Sets the fill color to a dark brown and turns off outlines for the nest mound shape drawn next.
ellipse(nest.x+40,nest.y,60,80);
Draws a dark oval slightly to the right of the nest position, forming a dirt mound behind the entrance hole.
fill(0);ellipse(nest.x,nest.y,40,60);
Draws a black oval on top of the mound to represent the dark entrance hole of the nest.
drawObstacles();
Calls the helper function that loops through the obstacles array and renders each rock, stick, or puddle.
moveAnt();
Calls the helper function that updates the ant's position, steering angle, and checks whether it has arrived at the nest.
drawAnt();
Calls the helper function that draws the ant's body at its current position and rotation.
fill(0);text("Deliveries: "+deliveries,20,30);
Draws the running delivery count in the top-left corner using string concatenation to combine text with the current number.

moveAnt()

moveAnt() is the steering brain of the whole sketch. It shows a classic 'seek-then-avoid' pattern: first calculate the ideal angle toward a goal, then adjust that angle based on nearby threats - a technique used in countless games and simulations, from flocking birds to enemy AI.

🔬 This loop is the ant's entire obstacle-avoidance brain. What happens if you increase the turn amount from 0.7 to 2.5 - does the ant overcorrect and wobble, or dodge more confidently?

  for(let o of obs){
    let d=dist(ant.x,ant.y,o.x,o.y);
    if(d<o.r+20)a+=(ant.y<o.y?-0.7:0.7);
  }
function moveAnt(){
  let a=atan2(nest.y-ant.y,nest.x-ant.x);
  for(let o of obs){
    let d=dist(ant.x,ant.y,o.x,o.y);
    if(d<o.r+20)a+=(ant.y<o.y?-0.7:0.7);
  }
  ant.x+=cos(a)*ant.s;
  ant.y+=sin(a)*ant.s;
  ant.a=a;
  if(dist(ant.x,ant.y,nest.x,nest.y)<25){deliveries++;reset();}
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Obstacle Avoidance Loop for(let o of obs){ let d=dist(ant.x,ant.y,o.x,o.y); if(d<o.r+20)a+=(ant.y<o.y?-0.7:0.7); }

Checks every obstacle's distance from the ant and bends the travel angle away from any obstacle that's too close.

conditional Delivery Check if(dist(ant.x,ant.y,nest.x,nest.y)<25){deliveries++;reset();}

Detects when the ant has reached the nest, increments the counter, and regenerates the scene for the next journey.

let a=atan2(nest.y-ant.y,nest.x-ant.x);
Calculates the angle from the ant straight to the nest using atan2, which returns the angle of the vector pointing from the ant toward the goal.
for(let o of obs){
Loops through every obstacle currently in the scene to check whether the ant needs to steer around it.
let d=dist(ant.x,ant.y,o.x,o.y);
Measures the straight-line distance between the ant and this particular obstacle.
if(d<o.r+20)a+=(ant.y<o.y?-0.7:0.7);
If the ant is closer than the obstacle's radius plus a 20-pixel buffer, bend the travel angle up or down depending on whether the ant is above or below the obstacle - this is what makes the ant swerve around it.
ant.x+=cos(a)*ant.s;
Moves the ant horizontally by an amount based on the (possibly bent) angle and the ant's speed.
ant.y+=sin(a)*ant.s;
Moves the ant vertically the same way, using sin() instead of cos() to get the vertical component of the direction.
ant.a=a;
Stores the final angle so drawAnt() can rotate the ant's body to face the direction it's actually moving.
if(dist(ant.x,ant.y,nest.x,nest.y)<25){deliveries++;reset();}
Checks if the ant is within 25 pixels of the nest - if so, counts a delivery and immediately regenerates the whole scene for the next trip.

drawAnt()

drawAnt() demonstrates why push(), translate(), and rotate() are so powerful: by moving and rotating the coordinate system once, every shape drawn afterward can use simple local coordinates (like -6,0 or 7,0) instead of complicated trigonometry, and the whole ant automatically faces the correct direction.

function drawAnt(){
  push();translate(ant.x,ant.y);rotate(ant.a);
  noStroke();fill(0);
  ellipse(-6,0,6,6);ellipse(0,0,7,7);ellipse(7,0,5,5);
  stroke(0);line(8,-3,12,-6);line(8,3,12,6);
  fill(30,130,40);noStroke();ellipse(-2,-8,10,6);
  pop();
}
Line-by-line explanation (6 lines)
push();translate(ant.x,ant.y);rotate(ant.a);
Saves the current drawing state, moves the origin to the ant's position, then rotates the coordinate system so everything drawn next is automatically oriented in the ant's direction of travel.
noStroke();fill(0);
Turns off outlines and sets the fill to black for the ant's body segments.
ellipse(-6,0,6,6);ellipse(0,0,7,7);ellipse(7,0,5,5);
Draws three small black ellipses in a row (abdomen, thorax, head) - because the coordinate system was rotated, these always line up along the ant's direction of movement.
stroke(0);line(8,-3,12,-6);line(8,3,12,6);
Draws two thin black lines angled outward from the head to represent antennae.
fill(30,130,40);noStroke();ellipse(-2,-8,10,6);
Draws a small green ellipse above the ant's body to represent the leaf crumb it's carrying.
pop();
Restores the drawing state to what it was before push(), so the rotation and translation don't affect anything drawn afterward.

drawObstacles()

drawObstacles() shows how a single numeric property (o.t) can drive completely different rendering logic for each object in an array - a lightweight alternative to using separate classes for each obstacle type.

🔬 Each obstacle type gets its own shape and color based on o.t. What happens if you swap the fill(90) rock color for fill(255,0,255) - can you spot rocks instantly among the sticks and puddles?

    if(o.t===0){fill(90);ellipse(o.x,o.y,o.r*1.4,o.r);}
    else if(o.t===1){stroke(80,60,40);strokeWeight(4);line(o.x-o.r,o.y+5,o.x+o.r,o.y-5);noStroke();}
    else{fill(80,150,220);ellipse(o.x,o.y,o.r,o.r*1.2);}
function drawObstacles(){
  noStroke();
  for(let o of obs){
    if(o.t===0){fill(90);ellipse(o.x,o.y,o.r*1.4,o.r);}
    else if(o.t===1){stroke(80,60,40);strokeWeight(4);line(o.x-o.r,o.y+5,o.x+o.r,o.y-5);noStroke();}
    else{fill(80,150,220);ellipse(o.x,o.y,o.r,o.r*1.2);}
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Draw Each Obstacle for(let o of obs){

Iterates over every obstacle in the array so each one gets rendered based on its type.

conditional Obstacle Type Branch if(o.t===0){...} else if(o.t===1){...} else{...}

Chooses which shape and color to draw depending on whether the obstacle is a rock (0), a stick (1), or a puddle (anything else).

noStroke();
Default to no outline before the loop, since most obstacle types don't need a stroke.
for(let o of obs){
Loops through every obstacle object stored in the global obs array.
if(o.t===0){fill(90);ellipse(o.x,o.y,o.r*1.4,o.r);}
If the obstacle's type is 0, draw it as a gray flattened ellipse - a rock.
else if(o.t===1){stroke(80,60,40);strokeWeight(4);line(o.x-o.r,o.y+5,o.x+o.r,o.y-5);noStroke();}
If the type is 1, draw a thick brown diagonal line instead - a fallen stick - then turn stroke back off for the next obstacle.
else{fill(80,150,220);ellipse(o.x,o.y,o.r,o.r*1.2);}
Any other type (2) is drawn as a blue vertical ellipse - a puddle.

windowResized()

windowResized() is a special p5.js function that p5 calls automatically whenever the browser window changes size. Handling it here keeps the nest and obstacles properly placed instead of getting cut off or stranded outside the visible area.

function windowResized(){
  resizeCanvas(windowWidth,windowHeight);
  nest.set(width*0.8,height*0.5);
  reset();
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth,windowHeight);
A built-in p5.js callback that fires automatically whenever the browser window is resized, so this line makes the canvas match the new window size.
nest.set(width*0.8,height*0.5);
Repositions the existing nest vector to the new 80%-across, centered-vertically location using p5.Vector's .set() method instead of creating a brand new vector.
reset();
Regenerates the obstacles and ant starting position so everything fits sensibly within the new canvas dimensions.

📦 Key Variables

ant object

Stores the ant's current x/y position, facing angle (a), and movement speed (s), rebuilt fresh by reset() after every delivery.

ant={x:40,y:random(80,height-80),a:0,s:1.2};
nest object (p5.Vector)

Stores the fixed x/y position of the nest that the ant is always trying to reach.

nest=createVector(width*0.8,height*0.5);
obs array

Holds all the current obstacle objects (rocks, sticks, puddles), each with a position, radius, and type used for both drawing and avoidance.

let obs=[];
deliveries number

Counts how many times the ant has successfully reached the nest, displayed as on-screen text.

let deliveries=0;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG reset()

Obstacles are placed with pure random(80,width-80)/random(80,height-80) with no check against the ant's starting position or the nest location, so an obstacle can occasionally spawn directly on top of the ant's start point or the nest, causing jittery or stuck-looking movement right after a reset.

💡 After generating each obstacle, check its distance from both ant.x/ant.y and nest.x/nest.y and re-roll the position if it's too close, e.g. using a small while loop with dist().

BUG moveAnt()

The avoidance loop can add +0.7 or -0.7 from multiple obstacles in the same frame without any clamping, so if the ant is squeezed between two obstacles on opposite sides it can end up with a wildly overcorrected angle that makes it oscillate or vibrate in place instead of smoothly gliding past.

💡 Clamp the total avoidance adjustment (e.g. constrain the accumulated turn to a maximum of +/-1.5) or blend the new angle into the old one gradually with lerp so direction changes smoothly instead of snapping.

STYLE entire sketch.js

The code uses very dense, minified-looking formatting (no spaces around operators, multiple statements per line) which makes it much harder for a beginner to read and debug.

💡 Add consistent spacing (e.g. 'if (d < o.r + 20)' instead of 'if(d<o.r+20)') and put one statement per line - it doesn't affect performance but makes the logic far easier to follow while learning.

FEATURE draw() / drawAnt()

The ant currently teleports its starting angle to 0 on reset and there's no visual trail or path history, so it's hard to see the route the ant actually took on a given journey.

💡 Store a short trail of recent ant positions in an array and draw a fading line or dots behind the ant with noFill()/vertex(), giving a satisfying 'breadcrumb trail' effect for each journey.

🔄 Code Flow

Code flow showing setup, reset, draw, moveant, drawant, drawobstacles, windowresized

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

graph TD start[Start] --> setup[setup] setup --> reset[reset] reset --> draw[draw loop] draw --> moveant[moveAnt] draw --> drawant[drawAnt] draw --> drawobstacles[drawObstacles] draw --> windowresized[windowResized] moveant --> avoidance-loop[Obstacle Avoidance Loop] avoidance-loop --> delivery-check[Delivery Check] delivery-check --> reset click setup href "#fn-setup" click reset href "#fn-reset" click draw href "#fn-draw" click moveant href "#fn-moveant" click drawant href "#fn-drawant" click drawobstacles href "#fn-drawobstacles" click windowresized href "#fn-windowresized" reset --> obstacle-loop[Generate Obstacles] obstacle-loop --> drawobstacles drawobstacles --> obstacle-draw-loop[Draw Each Obstacle] obstacle-draw-loop --> obstacle-type-branch[Obstacle Type Branch] click obstacle-loop href "#sub-obstacle-loop" click avoidance-loop href "#sub-avoidance-loop" click delivery-check href "#sub-delivery-check" click obstacle-draw-loop href "#sub-obstacle-draw-loop" click obstacle-type-branch href "#sub-obstacle-type-branch"

❓ Frequently Asked Questions

What visual experience does the AI Ant Journey sketch offer?

The sketch presents a charming animation of a tiny ant navigating through obstacles while carrying a leaf crumb, set against a visually appealing backdrop.

How can users engage with the AI Ant Journey sketch?

While the sketch runs automatically, users can resize the window to adjust the canvas and watch the ant's journey unfold.

What creative coding concepts are showcased in the AI Ant Journey sketch?

The sketch demonstrates pathfinding algorithms, object collision detection, and simple animation techniques to create a narrative-driven simulation.

Preview

AI Ant Journey - Watch the Little Worker - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Ant Journey - Watch the Little Worker - Code flow showing setup, reset, draw, moveant, drawant, drawobstacles, windowresized
Code Flow Diagram