Flocking Birds - Boids Algorithm - xelsed.ai

This sketch simulates a flock of 100 birds using Craig Reynolds' classic boids algorithm, where each bird follows three simple local rules - separation, alignment, and cohesion - that combine into complex, lifelike swarming motion. A soft blue sky gradient with a subtle trail effect makes the flock look like it's gliding through open air.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make trails longer and dreamier — Lowering TRAIL_ALPHA makes the sky redraw more transparent each frame, so old bird positions fade out much more slowly and create long streaking trails.
  2. Grow the flock — Increasing NUM_BOIDS spawns many more birds, creating a denser, busier swarm (though it will run a bit slower since every boid checks every other boid).
  3. Speed up the whole flock — Raising maxSpeed lets every bird fly faster, making the whole simulation feel more energetic and frantic.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch brings a flock of 100 triangular birds to life using the boids algorithm, a landmark technique in creative coding and artificial life. Each bird only looks at its nearby neighbors and reacts using three rules - steer away when too close, match the average direction of the group, and drift toward the local center of mass - yet the combined effect looks like an intelligent, coordinated swarm. The sketch layers this simulation on top of a hand-drawn sky gradient rendered into an offscreen graphics buffer, and uses partial transparency each frame to leave soft motion trails behind every bird.

The code is split into simple top-level functions (setup, draw, windowResized, drawSkyGradient) and a Boid class that bundles all the per-bird logic - position, velocity, steering forces, and drawing - into one reusable object. Studying it teaches you object-oriented design in p5.js, vector math with p5.Vector, distance-based neighbor searches, and how blending three weighted force vectors each frame produces convincing emergent behavior.

⚙️ How It Works

  1. On load, setup() creates a full-window canvas, renders a vertical sky-blue gradient once into an offscreen p5.Graphics buffer called skyGraphic, and spawns 100 Boid objects at random positions with random starting velocities.
  2. Every frame, draw() paints the sky gradient over the canvas using a low-alpha tint, which only partially erases the previous frame and leaves faint trails behind each bird instead of fully wiping the screen.
  3. Still inside draw(), the sketch loops through every boid and calls four methods in order: edges() wraps birds that fly off-screen back to the opposite side, flock() calculates and combines the three steering forces, update() applies those forces to move the bird, and show() draws it as a small rotated triangle.
  4. Inside flock(), each boid scans the entire flock three separate times - once each for alignment, cohesion, and separation - measuring distance to every other boid and only reacting to ones inside its own perception radius.
  5. Alignment steers a boid to match the average velocity of nearby flockmates, cohesion steers it toward their average position, and separation pushes it away from boids that get too close, with the repulsion strength growing sharply as distance shrinks.
  6. These three force vectors are weighted (separation counts 1.5x more heavily) and added into the boid's acceleration, which update() uses to adjust velocity and position before the acceleration resets to zero for the next frame - repeating 60 times per second creates continuous, fluid flocking motion.

🎓 Concepts You'll Learn

Object-Oriented Programming with ES6 classesp5.Vector math (add, sub, div, setMag, limit, heading)Flocking / boids algorithm (separation, alignment, cohesion)Offscreen graphics buffers with createGraphicsAlpha transparency for motion trailsDistance-based neighbor searches with dist()Screen-wrapping edge handlingSteering behaviors (desired velocity minus current velocity)

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the ideal place to build expensive one-time resources - like the sky gradient buffer here - and to populate arrays of objects before the animation loop begins.

function setup() {
  createCanvas(windowWidth, windowHeight);

  // Create sky gradient once
  skyGraphic = createGraphics(windowWidth, windowHeight);
  drawSkyGradient(skyGraphic);

  // Initialize flock
  for (let i = 0; i < NUM_BOIDS; i++) {
    boids.push(new Boid());
  }

  // Smoother visuals
  frameRate(60);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Initialize Flock for (let i = 0; i < NUM_BOIDS; i++) { boids.push(new Boid()); }

Creates NUM_BOIDS new Boid objects, each with its own random position and velocity, and stores them all in the boids array.

createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window so the flock has the full screen to fly around in.
skyGraphic = createGraphics(windowWidth, windowHeight);
Creates a separate, offscreen drawing buffer the same size as the canvas - this lets the sky be drawn once and reused every frame instead of redrawn from scratch.
drawSkyGradient(skyGraphic);
Calls the helper function to paint the blue gradient into that offscreen buffer a single time.
for (let i = 0; i < NUM_BOIDS; i++) {
Loops NUM_BOIDS times (100 by default) to build the initial flock.
boids.push(new Boid());
Creates a brand-new Boid object (with its own random position and velocity from the constructor) and adds it to the boids array.
frameRate(60);
Asks p5.js to try to run draw() 60 times per second for smooth, consistent animation.

draw()

draw() runs continuously, roughly 60 times per second. Here it demonstrates a common trick - using a semi-transparent image redraw instead of a solid background() call - to create smooth motion trails without extra particle-tracking code.

🔬 This loop is the heartbeat of the simulation, running four steps for every bird each frame. What happens visually if you comment out boid.flock(boids); - will the birds still move, and will they still flock together?

  for (let boid of boids) {
    boid.edges();
    boid.flock(boids);
    boid.update();
    boid.show();
  }
function draw() {
  // Draw the sky gradient with some transparency to create a trail effect
  tint(255, TRAIL_ALPHA);
  image(skyGraphic, 0, 0, width, height);
  noTint(); // Reset so boids are drawn normally

  // Update and render all boids
  for (let boid of boids) {
    boid.edges();
    boid.flock(boids);
    boid.update();
    boid.show();
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Update and Render All Boids for (let boid of boids) { boid.edges(); boid.flock(boids); boid.update(); boid.show(); }

Runs the full simulation cycle - wrap edges, calculate steering, move, then draw - for every single bird each frame.

tint(255, TRAIL_ALPHA);
Sets an alpha (transparency) value that will be applied to the next image drawn, so the sky redraw only partially covers the previous frame.
image(skyGraphic, 0, 0, width, height);
Draws the pre-rendered sky gradient over the canvas using that transparency - because it's only partially opaque, faint traces of the birds' previous positions remain visible, creating trails.
noTint();
Turns off the transparency effect so the birds themselves are drawn fully opaque and not faded out.
for (let boid of boids) {
Loops through every boid in the flock to update and draw it this frame.
boid.edges();
Checks if this boid has flown off any edge of the screen and wraps it around to the opposite side if so.
boid.flock(boids);
Calculates this boid's steering forces based on all its neighbors, and adds them to its acceleration.
boid.update();
Applies the accumulated acceleration to change velocity and position, then resets acceleration to zero.
boid.show();
Draws the boid as a small rotated triangle at its current position.

windowResized()

windowResized() is a special p5.js function that's automatically called whenever the browser window changes size, letting you keep full-window sketches responsive.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  skyGraphic = createGraphics(windowWidth, windowHeight);
  drawSkyGradient(skyGraphic);
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the browser window whenever it changes size (e.g. resizing the browser or rotating a device).
skyGraphic = createGraphics(windowWidth, windowHeight);
Rebuilds the offscreen sky buffer at the new size, since the old one would no longer fit the resized canvas.
drawSkyGradient(skyGraphic);
Repaints the gradient into the freshly-sized buffer so the sky always fills the screen correctly.

drawSkyGradient(pg)

This function shows a manual way to build a gradient using lerpColor() inside a loop, one row at a time, and stores the result in an offscreen p5.Graphics buffer so it only has to be computed once instead of every frame.

🔬 This loop draws the gradient one line at a time using lerpColor. What happens if you swap the order of topColor and bottomColor inside lerpColor() so the gradient flips upside down?

  for (let y = 0; y < pg.height; y++) {
    const t = y / (pg.height - 1 || 1);
    const c = pg.lerpColor(topColor, bottomColor, t);
    pg.stroke(c);
    pg.line(0, y, pg.width, y);
  }
function drawSkyGradient(pg) {
  pg.noFill();
  const topColor = pg.color(170, 220, 255);   // light blue
  const bottomColor = pg.color(30, 120, 200); // deeper blue

  for (let y = 0; y < pg.height; y++) {
    const t = y / (pg.height - 1 || 1);
    const c = pg.lerpColor(topColor, bottomColor, t);
    pg.stroke(c);
    pg.line(0, y, pg.width, y);
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Draw Gradient Line by Line for (let y = 0; y < pg.height; y++) { const t = y / (pg.height - 1 || 1); const c = pg.lerpColor(topColor, bottomColor, t); pg.stroke(c); pg.line(0, y, pg.width, y); }

Draws one horizontal line per vertical pixel, blending smoothly from the top color to the bottom color to create a gradient.

pg.noFill();
Turns off fill on the graphics buffer since we're only drawing lines (strokes), not filled shapes.
const topColor = pg.color(170, 220, 255); // light blue
Defines the color used at the very top of the sky (a pale light blue).
const bottomColor = pg.color(30, 120, 200); // deeper blue
Defines the color used at the very bottom of the sky (a deeper, richer blue).
for (let y = 0; y < pg.height; y++) {
Loops once for every single row of pixels from top to bottom of the buffer.
const t = y / (pg.height - 1 || 1);
Converts the current row into a fraction between 0 (top) and 1 (bottom); the `|| 1` avoids dividing by zero if the buffer is only 1 pixel tall.
const c = pg.lerpColor(topColor, bottomColor, t);
Blends between the top and bottom colors using that fraction, producing a smoothly interpolated color for this row.
pg.stroke(c);
Sets the drawing color for the next line to this row's blended color.
pg.line(0, y, pg.width, y);
Draws a single horizontal line across the full width of the buffer at this row, painting one 'stripe' of the gradient.

Boid constructor()

The constructor runs once for every new Boid object created with `new Boid()`. It's where you set up all the starting state - position, velocity, appearance - that each individual bird needs to exist independently from the others.

constructor() {
    this.position = createVector(random(width), random(height));

    // Random initial direction & speed
    const angle = random(TWO_PI);
    this.velocity = p5.Vector.fromAngle(angle);
    this.velocity.setMag(random(1, 2));

    this.acceleration = createVector();

    this.maxSpeed = 2.5;
    this.maxForce = 0.05;

    this.size = 6; // base size for the triangle

    // White/gray birds
    const brightness = random(210, 255);
    this.bodyColor = color(brightness);
    this.strokeColor = color(brightness * 0.8);
  }
Line-by-line explanation (11 lines)
this.position = createVector(random(width), random(height));
Gives this boid a random starting position somewhere on the canvas, stored as a p5.Vector with x and y components.
const angle = random(TWO_PI);
Picks a completely random angle in radians (TWO_PI is a full circle, about 6.28) to determine the initial flight direction.
this.velocity = p5.Vector.fromAngle(angle);
Creates a unit-length vector pointing in that random direction, which becomes the boid's velocity.
this.velocity.setMag(random(1, 2));
Scales that direction vector to a random speed between 1 and 2, so not all birds start moving at exactly the same pace.
this.acceleration = createVector();
Creates an empty (0,0) vector to hold the steering forces that will be calculated each frame.
this.maxSpeed = 2.5;
Sets the top speed this boid is allowed to travel, used later to clamp its velocity.
this.maxForce = 0.05;
Sets how strongly this boid can steer each frame - small values create smooth, gentle turning.
this.size = 6; // base size for the triangle
Sets the base size used when drawing this boid's triangle body.
const brightness = random(210, 255);
Picks a random light gray/white brightness value so each bird looks slightly different.
this.bodyColor = color(brightness);
Creates the fill color for the bird's body using that brightness value.
this.strokeColor = color(brightness * 0.8);
Creates a slightly darker outline color, derived from the same brightness, so the outline complements the body color.

edges()

This is a classic 'screen wrap' technique - instead of bouncing off walls, objects that leave one side of the canvas reappear on the opposite side, which keeps the flock endlessly circulating without ever needing to stop or turn around at the edges.

edges() {
    const margin = this.size * 2;

    if (this.position.x > width + margin) {
      this.position.x = -margin;
    } else if (this.position.x < -margin) {
      this.position.x = width + margin;
    }

    if (this.position.y > height + margin) {
      this.position.y = -margin;
    } else if (this.position.y < -margin) {
      this.position.y = height + margin;
    }
  }
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Horizontal Wrap if (this.position.x > width + margin) { this.position.x = -margin; } else if (this.position.x < -margin) { this.position.x = width + margin; }

Teleports the bird to the opposite horizontal edge once it flies fully off the left or right side of the screen.

conditional Vertical Wrap if (this.position.y > height + margin) { this.position.y = -margin; } else if (this.position.y < -margin) { this.position.y = height + margin; }

Teleports the bird to the opposite vertical edge once it flies fully off the top or bottom of the screen.

const margin = this.size * 2;
Defines a small buffer zone beyond the canvas edges so birds fully disappear before wrapping, avoiding a visible pop.
if (this.position.x > width + margin) {
Checks if the bird has flown off the right edge, past the margin.
this.position.x = -margin;
Moves the bird to just offscreen on the left, so it re-enters smoothly from the opposite side.
} else if (this.position.x < -margin) {
Checks if the bird has flown off the left edge instead.
this.position.x = width + margin;
Moves the bird to just offscreen on the right side.
if (this.position.y > height + margin) {
Same wrapping check, but for flying off the bottom of the screen.
this.position.y = -margin;
Wraps the bird back to just above the top edge.
} else if (this.position.y < -margin) {
Checks if the bird flew off the top edge instead.
this.position.y = height + margin;
Wraps the bird back to just below the bottom edge.

flock(boids)

flock() is the heart of the boids algorithm - it gathers all three independent steering rules and blends them together with adjustable weights. Tweaking these weights is the easiest way to dramatically change the flock's personality, from tight schooling to loose, scattered wandering.

🔬 These three multipliers decide how much each rule matters. What happens if you crank cohesion up to 3.0 and drop separation down to 0.3 - do the birds start clumping into a tight ball?

    alignment.mult(1.0);
    cohesion.mult(1.0);
    separation.mult(1.5);
flock(boids) {
    const alignment = this.align(boids);
    const cohesion = this.cohesion(boids);
    const separation = this.separation(boids);

    // Weight the three behaviors
    alignment.mult(1.0);
    cohesion.mult(1.0);
    separation.mult(1.5);

    this.acceleration.add(alignment);
    this.acceleration.add(cohesion);
    this.acceleration.add(separation);
  }
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Weight and Combine Forces alignment.mult(1.0); cohesion.mult(1.0); separation.mult(1.5);

Scales each steering force by a weight before combining them, controlling how strongly each rule influences the final behavior.

const alignment = this.align(boids);
Calculates the steering force that pulls this boid's heading toward the average heading of nearby flockmates.
const cohesion = this.cohesion(boids);
Calculates the steering force that pulls this boid toward the center of nearby flockmates.
const separation = this.separation(boids);
Calculates the steering force that pushes this boid away from flockmates that are too close.
alignment.mult(1.0);
Multiplies the alignment force by 1.0 (no change) - this is the 'dial' you'd turn to make matching direction more or less important.
cohesion.mult(1.0);
Multiplies the cohesion force by 1.0, similarly leaving grouping behavior at its default strength.
separation.mult(1.5);
Boosts the separation force by 50%, making birds noticeably more eager to avoid crowding than to align or group.
this.acceleration.add(alignment);
Adds the weighted alignment force into this frame's total acceleration.
this.acceleration.add(cohesion);
Adds the weighted cohesion force into the total acceleration.
this.acceleration.add(separation);
Adds the weighted separation force into the total acceleration, completing the combined steering influence for this frame.

align(boids)

align() implements the 'alignment' rule from Reynolds' original boids paper - each bird tries to match the average heading of its nearby flockmates, which is what makes the whole flock appear to move together in a shared direction.

🔬 This block converts the averaged neighbor velocity into a gentle steering nudge. What happens if you remove the steering.sub(this.velocity); line, so boids snap directly toward the average velocity instead of steering gradually toward it?

    if (total > 0) {
      steering.div(total);
      steering.setMag(this.maxSpeed);
      steering.sub(this.velocity);
      steering.limit(this.maxForce);
    }
align(boids) {
    const perceptionRadius = 60;
    const steering = createVector();
    let total = 0;

    for (let other of boids) {
      const d = dist(
        this.position.x,
        this.position.y,
        other.position.x,
        other.position.y
      );
      if (other !== this && d < perceptionRadius) {
        steering.add(other.velocity);
        total++;
      }
    }

    if (total > 0) {
      steering.div(total);
      steering.setMag(this.maxSpeed);
      steering.sub(this.velocity);
      steering.limit(this.maxForce);
    }

    return steering;
  }
Line-by-line explanation (14 lines)

🔧 Subcomponents:

for-loop Find Nearby Flockmates for (let other of boids) { const d = dist( this.position.x, this.position.y, other.position.x, other.position.y ); if (other !== this && d < perceptionRadius) { steering.add(other.velocity); total++; } }

Checks every other boid's distance and, if it's a different boid within the perception radius, adds its velocity to a running total.

conditional Average and Limit the Steering Force if (total > 0) { steering.div(total); steering.setMag(this.maxSpeed); steering.sub(this.velocity); steering.limit(this.maxForce); }

Turns the summed neighbor velocities into an average desired velocity, then converts that into a limited steering force.

const perceptionRadius = 60;
Defines how far (in pixels) this boid can 'see' other boids for alignment purposes.
const steering = createVector();
Starts an empty vector that will accumulate neighbor velocities.
let total = 0;
Counts how many neighbors were found within range, needed to compute an average later.
for (let other of boids) {
Loops through every boid in the flock, including this one.
const d = dist(this.position.x, this.position.y, other.position.x, other.position.y);
Calculates the straight-line distance between this boid and the other boid being checked.
if (other !== this && d < perceptionRadius) {
Only reacts to boids that aren't itself and that are within the perception radius.
steering.add(other.velocity);
Adds the neighbor's velocity vector into the running total.
total++;
Increments the neighbor count.
if (total > 0) {
Only calculates a steering force if at least one neighbor was found (avoids dividing by zero).
steering.div(total);
Divides the summed velocities by the count to get the average neighbor velocity.
steering.setMag(this.maxSpeed);
Scales that average direction to the boid's maximum speed, representing the 'desired' velocity.
steering.sub(this.velocity);
Subtracts the boid's current velocity from the desired velocity - this classic 'steering = desired - velocity' formula gives the force needed to turn toward that direction.
steering.limit(this.maxForce);
Caps the steering force so turning always feels smooth rather than instantaneous.
return steering;
Sends the final alignment force back to flock() to be combined with the other two behaviors.

cohesion(boids)

cohesion() implements Reynolds' 'cohesion' rule - each bird gently steers toward the average position of its neighbors, which is what keeps the flock from drifting apart into scattered individuals.

cohesion(boids) {
    const perceptionRadius = 65;
    const steering = createVector();
    let total = 0;

    for (let other of boids) {
      const d = dist(
        this.position.x,
        this.position.y,
        other.position.x,
        other.position.y
      );
      if (other !== this && d < perceptionRadius) {
        steering.add(other.position);
        total++;
      }
    }

    if (total > 0) {
      steering.div(total);
      steering.sub(this.position); // vector towards center
      steering.setMag(this.maxSpeed);
      steering.sub(this.velocity);
      steering.limit(this.maxForce);
    }

    return steering;
  }
Line-by-line explanation (11 lines)

🔧 Subcomponents:

for-loop Sum Nearby Positions for (let other of boids) { const d = dist( this.position.x, this.position.y, other.position.x, other.position.y ); if (other !== this && d < perceptionRadius) { steering.add(other.position); total++; } }

Adds up the positions of all nearby flockmates so their average location (the local center of mass) can be found.

conditional Steer Toward Center of Mass if (total > 0) { steering.div(total); steering.sub(this.position); // vector towards center steering.setMag(this.maxSpeed); steering.sub(this.velocity); steering.limit(this.maxForce); }

Turns the average neighbor position into a direction vector pointing from this boid toward that center, then limits it into a smooth steering force.

const perceptionRadius = 65;
Sets how far this boid looks for neighbors when calculating cohesion - slightly larger than alignment's radius.
for (let other of boids) {
Loops through every boid in the flock.
if (other !== this && d < perceptionRadius) {
Only considers other boids (not itself) that are within range.
steering.add(other.position);
Adds the neighbor's position into a running total, building toward an average position.
total++;
Counts how many neighbors contributed to that total.
steering.div(total);
Divides the summed positions by the count to get the average position of nearby flockmates - the local 'center of mass'.
steering.sub(this.position); // vector towards center
Subtracts this boid's own position from that average, producing a vector that points from the boid toward the group's center.
steering.setMag(this.maxSpeed);
Scales that direction to maximum speed, representing the desired velocity to reach the center.
steering.sub(this.velocity);
Subtracts current velocity to get the actual force needed to steer toward that desired velocity.
steering.limit(this.maxForce);
Caps the force so the turn stays smooth.
return steering;
Returns the cohesion force to be combined with alignment and separation in flock().

separation(boids)

separation() implements Reynolds' 'separation' rule, the one that keeps birds from crashing into each other. Dividing by distance-squared makes the repulsion grow very quickly as boids get close, which is what prevents visible overlapping in the final flock.

🔬 The maxForce * 1.2 multiplier gives separation extra strength over the other two rules. What happens if you raise it to * 3 - do birds start bouncing apart more violently and spread out more?

    if (total > 0) {
      steering.div(total);
      steering.setMag(this.maxSpeed);
      steering.sub(this.velocity);
      steering.limit(this.maxForce * 1.2);
    }
separation(boids) {
    const perceptionRadius = 30;
    const steering = createVector();
    let total = 0;

    for (let other of boids) {
      const d = dist(
        this.position.x,
        this.position.y,
        other.position.x,
        other.position.y
      );
      if (other !== this && d < perceptionRadius && d > 0) {
        // Vector pointing away, weighted by distance
        const diff = p5.Vector.sub(this.position, other.position);
        diff.div(d * d); // stronger repulsion when very close
        steering.add(diff);
        total++;
      }
    }

    if (total > 0) {
      steering.div(total);
      steering.setMag(this.maxSpeed);
      steering.sub(this.velocity);
      steering.limit(this.maxForce * 1.2);
    }

    return steering;
  }
Line-by-line explanation (11 lines)

🔧 Subcomponents:

for-loop Accumulate Repulsion from Close Neighbors for (let other of boids) { const d = dist( this.position.x, this.position.y, other.position.x, other.position.y ); if (other !== this && d < perceptionRadius && d > 0) { // Vector pointing away, weighted by distance const diff = p5.Vector.sub(this.position, other.position); diff.div(d * d); // stronger repulsion when very close steering.add(diff); total++; } }

For every very-close neighbor, computes a vector pointing away from it that gets stronger the closer they are, then sums up all these repulsion vectors.

conditional Average and Cap the Repulsion Force if (total > 0) { steering.div(total); steering.setMag(this.maxSpeed); steering.sub(this.velocity); steering.limit(this.maxForce * 1.2); }

Averages the repulsion vectors and converts them into a steering force, allowing a slightly stronger force cap (1.2x) than the other two behaviors.

const perceptionRadius = 30;
Sets a smaller radius than alignment or cohesion - separation should only react to boids that are genuinely close, not the whole local flock.
if (other !== this && d < perceptionRadius && d > 0) {
Only reacts to other boids that are within the tight radius and not exactly overlapping (d > 0 avoids a divide-by-zero below).
const diff = p5.Vector.sub(this.position, other.position);
Creates a vector pointing away from the other boid, from its position toward this boid's position.
diff.div(d * d); // stronger repulsion when very close
Divides that vector by the squared distance, so boids that are extremely close produce a much stronger push than ones near the edge of the radius.
steering.add(diff);
Adds this weighted repulsion vector into the running total.
total++;
Counts how many close neighbors contributed.
steering.div(total);
Averages all the repulsion vectors together.
steering.setMag(this.maxSpeed);
Scales the averaged repulsion direction to maximum speed, representing the desired escape velocity.
steering.sub(this.velocity);
Converts that desired velocity into an actual steering force by subtracting current velocity.
steering.limit(this.maxForce * 1.2);
Allows separation to use a slightly stronger maximum force (20% more) than alignment or cohesion, so birds prioritize avoiding collisions.
return steering;
Returns the separation force to be combined with the other two in flock().

update()

update() is the classic physics integration step used in almost every particle or agent simulation: acceleration changes velocity, velocity changes position, and forces are cleared out to be recalculated next frame.

update() {
    this.velocity.add(this.acceleration);
    this.velocity.limit(this.maxSpeed);
    this.position.add(this.velocity);
    // Reset acceleration each frame
    this.acceleration.mult(0);
  }
Line-by-line explanation (4 lines)
this.velocity.add(this.acceleration);
Applies the combined steering force calculated in flock() to this boid's velocity, gradually turning and speeding it up or down.
this.velocity.limit(this.maxSpeed);
Ensures velocity never exceeds the boid's top speed, no matter how much force was applied.
this.position.add(this.velocity);
Moves the boid by its current velocity - this is the actual step that makes it fly across the screen.
this.acceleration.mult(0);
Resets acceleration back to zero so next frame's steering forces start fresh instead of accumulating forever.

show()

show() demonstrates using translate() and rotate() together inside push()/pop() to draw a shape in its own local coordinate space - a technique used constantly in creative coding to draw and orient many independent objects without manually recalculating every point's screen position.

🔬 This shape only has three vertices, making a triangle. What happens if you add a fourth vertex like vertex(0, this.size) right before endShape() to pull in the tail and create a kite/diamond shape instead?

    beginShape();
    vertex(0, -this.size * 2);      // tip
    vertex(-this.size, this.size*2);
    vertex(this.size, this.size*2);
    endShape(CLOSE);
show() {
    push();
    translate(this.position.x, this.position.y);

    // Rotate so the triangle points along the velocity vector
    const angle = this.velocity.heading() + PI / 2;
    rotate(angle);

    stroke(this.strokeColor);
    strokeWeight(1);
    fill(this.bodyColor);

    // Triangle pointing "forward" (upward before rotation)
    beginShape();
    vertex(0, -this.size * 2);      // tip
    vertex(-this.size, this.size*2);
    vertex(this.size, this.size*2);
    endShape(CLOSE);

    pop();
  }
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Draw the Bird Triangle beginShape(); vertex(0, -this.size * 2); // tip vertex(-this.size, this.size*2); vertex(this.size, this.size*2); endShape(CLOSE);

Defines three points that form a triangle pointing 'up' before rotation, representing the bird's body shape.

push();
Saves the current drawing style and transformation state so changes made here don't affect other boids.
translate(this.position.x, this.position.y);
Moves the drawing origin to this boid's position, so all following shapes are drawn relative to it.
const angle = this.velocity.heading() + PI / 2;
Calculates the direction the boid is moving in radians, adding a quarter turn (PI/2) to correct for the triangle's default upward-pointing orientation.
rotate(angle);
Rotates the drawing canvas so the triangle will point in the direction of travel.
stroke(this.strokeColor);
Sets the outline color to this boid's individual stroke color.
strokeWeight(1);
Sets the outline thickness to 1 pixel.
fill(this.bodyColor);
Sets the fill color to this boid's individual body color.
beginShape();
Starts defining a custom shape out of individual vertex points.
vertex(0, -this.size * 2); // tip
Places the triangle's pointed tip above the center, representing the 'front' of the bird before rotation.
vertex(-this.size, this.size*2);
Places the bottom-left corner of the triangle, forming one 'wing'.
vertex(this.size, this.size*2);
Places the bottom-right corner of the triangle, forming the other 'wing'.
endShape(CLOSE);
Finishes the shape and connects the last vertex back to the first, closing the triangle outline.
pop();
Restores the saved drawing state, undoing the translate/rotate so the next boid is drawn correctly.

📦 Key Variables

boids array

Holds every Boid object in the simulation; the draw loop iterates over this array each frame to update and render the whole flock.

let boids = [];
NUM_BOIDS number

Constant controlling how many Boid objects are created in setup() - the total flock size.

const NUM_BOIDS = 100;
skyGraphic object

An offscreen p5.Graphics buffer holding the pre-rendered sky gradient, reused every frame instead of being redrawn from scratch.

let skyGraphic;
TRAIL_ALPHA number

Constant controlling the transparency used when redrawing the sky each frame; lower values leave longer motion trails behind the birds.

const TRAIL_ALPHA = 40;
this.position object

A p5.Vector storing each boid's current x/y location on the canvas.

this.position = createVector(random(width), random(height));
this.velocity object

A p5.Vector storing each boid's current speed and direction of travel.

this.velocity = p5.Vector.fromAngle(angle);
this.acceleration object

A p5.Vector that accumulates the combined steering forces each frame before being applied to velocity and reset to zero.

this.acceleration = createVector();
this.maxSpeed number

Caps how fast an individual boid can travel, used to clamp velocity in update().

this.maxSpeed = 2.5;
this.maxForce number

Caps how strongly an individual boid can steer each frame, controlling how sharply it can turn.

this.maxForce = 0.05;
this.size number

Base size used both for the triangle drawn in show() and for the edge-wrap margin in edges().

this.size = 6;
this.bodyColor object

A randomly-chosen light gray/white color used to fill each boid's triangle body.

this.bodyColor = color(brightness);
this.strokeColor object

A slightly darker color derived from bodyColor, used to outline each boid's triangle.

this.strokeColor = color(brightness * 0.8);

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE align(), cohesion(), separation()

Each of the three steering methods loops over the entire boids array and calls dist() independently, so every boid computes distance to every other boid three separate times per frame - roughly 3 x N^2 distance calculations (30,000+ per frame at 100 boids).

💡 Combine the three neighbor searches into a single loop inside flock() that computes distance once per pair and accumulates alignment, cohesion, and separation data together, or use a spatial grid / quadtree to only check nearby boids instead of the whole flock.

STYLE align(), cohesion(), separation()

The perceptionRadius values (60, 65, 30) are hardcoded as magic numbers scattered across three different methods, making them hard to find and tune together.

💡 Move these into named class-level constants (e.g. this.alignRadius, this.cohesionRadius, this.separationRadius) set once in the constructor, so all the tunable perception distances live in one place.

BUG separation()

The distance-squared division (diff.div(d * d)) can produce an extremely large repulsion force when two boids are almost perfectly overlapping (d close to 0), potentially causing a visible 'flick' or jump in velocity that breaks the smooth motion.

💡 Add a small minimum distance clamp, e.g. const safeD = Math.max(d, 5); diff.div(safeD * safeD);, to prevent the repulsion force from spiking to unrealistic values at very close range.

FEATURE draw() / mousePressed

The simulation currently has no user interaction - the flock behaves identically regardless of mouse or keyboard input, which limits how engaging and explorable the sketch feels.

💡 Add a mousePressed() function that adds a temporary 'predator' or attractor point at the mouse location, and have each boid add a small extra steering force toward or away from it, letting viewers actively influence the flock's movement.

🔄 Code Flow

Code flow showing setup, draw, windowresized, drawskygradient, constructor, edges, flock, align, cohesion, separation, update, show

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

graph TD start[Start] --> setup[setup] setup --> setup-init-flock-loop[Initialize Flock] setup-init-flock-loop --> draw[draw loop] click setup href "#fn-setup" click setup-init-flock-loop href "#sub-setup-init-flock-loop" draw --> draw-boid-loop[Update and Render All Boids] draw-boid-loop --> edges-x-wrap[Horizontal Wrap] draw-boid-loop --> edges-y-wrap[Vertical Wrap] draw-boid-loop --> flock[flock] draw-boid-loop --> show[show] click draw href "#fn-draw" click draw-boid-loop href "#sub-draw-boid-loop" click edges-x-wrap href "#sub-edges-x-wrap" click edges-y-wrap href "#sub-edges-y-wrap" click flock href "#fn-flock" click show href "#fn-show" flock --> flock-weighting[Weight and Combine Forces] flock-weighting --> align[align] flock-weighting --> cohesion[cohesion] flock-weighting --> separation[separation] click flock-weighting href "#sub-flock-weighting" click align href "#fn-align" click cohesion href "#fn-cohesion" click separation href "#fn-separation" align --> align-neighbor-loop[Find Nearby Flockmates] align-neighbor-loop --> align-average-steer[Average and Limit the Steering Force] click align-neighbor-loop href "#sub-align-neighbor-loop" click align-average-steer href "#sub-align-average-steer" cohesion --> cohesion-neighbor-loop[Sum Nearby Positions] cohesion-neighbor-loop --> cohesion-steer-to-center[Steer Toward Center of Mass] click cohesion-neighbor-loop href "#sub-cohesion-neighbor-loop" click cohesion-steer-to-center href "#sub-cohesion-steer-to-center" separation --> separation-repulsion-loop[Accumulate Repulsion from Close Neighbors] separation-repulsion-loop --> separation-limit-force[Average and Cap the Repulsion Force] click separation-repulsion-loop href "#sub-separation-repulsion-loop" click separation-limit-force href "#sub-separation-limit-force" show --> show-triangle-shape[Draw the Bird Triangle] click show-triangle-shape href "#sub-show-triangle-shape"

❓ Frequently Asked Questions

What visual experience does the Flocking Birds sketch create?

The sketch visually simulates a flock of 100 birds that gracefully swarm and swirl across a serene sky gradient, showcasing emergent behavior through the boids algorithm.

How can users interact with the Flocking Birds simulation?

Users can interact by resizing the window, which will dynamically adjust the canvas and sky gradient to maintain the immersive experience.

What creative coding concept is demonstrated in the Flocking Birds sketch?

This sketch demonstrates the boids algorithm, which uses simple rules of separation, alignment, and cohesion to create complex, natural flocking behavior.

Preview

Flocking Birds - Boids Algorithm - xelsed.ai - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Flocking Birds - Boids Algorithm - xelsed.ai - Code flow showing setup, draw, windowresized, drawskygradient, constructor, edges, flock, align, cohesion, separation, update, show
Code Flow Diagram