AI Boid Flocking Simulator - Emergent Swarm Behavior Watch realistic bird/fish swarms with separati

This sketch simulates a flock of colorful triangular boids that swarm, scatter, and regroup using the classic separation/alignment/cohesion rules pioneered by Craig Reynolds. Left-clicking spawns a new cluster of panicked boids and right-clicking drops a red predator that hunts the nearest boid, all rendered over a soft gradient background with trailing motion streaks.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make boids fully opaque — Removing the transparency makes each boid a solid, crisp color instead of letting the trails blend through it.
  2. Create a super-tight swarm — Boosting the cohesion multiplier pulls every boid much more strongly toward the group's center, producing a dense, ball-like swarm.
  3. Supersize the predator — Doubling the predator's size makes it look far more menacing and easier to spot as it chases boids across the screen.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch brings a living swarm to life on screen: dozens of small colored triangles glide, regroup, and scatter exactly like a flock of birds or school of fish. It's a textbook implementation of Craig Reynolds' boids algorithm, built entirely from p5.Vector math - each boid steers itself using three simple rules (separation, alignment, cohesion) that combine into surprisingly organic group behavior. On top of that, the sketch layers predator-avoidance forces, a panic response triggered by mouse clicks, HSB color blending, and a gradient background redrawn every frame with low alpha to create smooth motion trails.

The code is split into p5.js lifecycle functions (setup, draw, windowResized), input handlers (mousePressed, keyPressed), small helper functions for drawing the gradient and spawning groups, and two ES6 classes - Boid and Predator - that each encapsulate their own position, velocity, and behavior methods. Studying it teaches you how to structure an object-oriented simulation in p5.js, how vector steering forces are calculated and combined, and how per-pixel alpha blending can fake trailing effects without a shader.

⚙️ How It Works

  1. When the page loads, setup() creates a full-window canvas, switches to HSB color mode, paints a solid gradient background once, and seeds the world with 120 Boid objects at random positions.
  2. Every frame, draw() first paints a very faint copy of the gradient over the whole canvas instead of clearing it, which is what produces the soft trailing streaks behind every moving shape.
  3. Still inside draw(), each Predator updates its position (chasing the nearest boid) and each Boid calculates steering forces from its neighbors, then updates its position and wraps around any edge it crosses.
  4. A boid's flock() method blends four separate forces - separation (avoid crowding), alignment (match neighbor heading), cohesion (move toward the group's center), and predator avoidance - each weighted differently before being applied.
  5. Left-clicking calls spawnBoidGroup() to add a burst of new boids around the cursor and also starts a 180-frame panic timer; while panicTimer is active every boid gets an extra outward force pushing it away from the click point.
  6. Right-clicking drops a new Predator that actively hunts the closest boid, and pressing C or X instantly empties the boids or predators array to reset the scene.

🎓 Concepts You'll Learn

Boids / flocking algorithmSteering behaviors (separation, alignment, cohesion)p5.Vector mathObject-oriented classes in p5.jsHSB color mode and alpha blendingMotion trails via low-alpha overlaysEmergent collective behavior

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the right place to configure the canvas, color mode, and starting state before the animation loop takes over.

🔬 This loop seeds the flock. What happens visually if you drop the count to 10? What about pushing it to 500 - does the frame rate hold up?

  for (let i = 0; i < 120; i++) {
    boids.push(new Boid(random(width), random(height)));
  }
function setup() {
  canvas = createCanvas(windowWidth, windowHeight);
  // Disable right-click menu over the canvas so we can use RIGHT mouse
  canvas.elt.oncontextmenu = () => false;

  // Use HSB so colors are easy to control
  colorMode(HSB, 360, 100, 100, 100);

  // Gradient colors (top -> bottom)
  gradientTop = color(210, 70, 25);   // deep blue
  gradientBottom = color(280, 60, 10); // violet

  // Draw a full-intensity gradient once to initialize background
  drawInitialGradient();

  // Seed some initial boids so the scene isn't empty
  for (let i = 0; i < 120; i++) {
    boids.push(new Boid(random(width), random(height)));
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Seed Initial Boids for (let i = 0; i < 120; i++) {

Creates 120 Boid objects at random positions so the flock isn't empty at launch

canvas = createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window and stores a reference to it
canvas.elt.oncontextmenu = () => false;
Disables the browser's normal right-click context menu so right-click can be used to place predators instead
colorMode(HSB, 360, 100, 100, 100);
Switches p5's color system to Hue/Saturation/Brightness, which makes it easy to pick a range of hues for boids and gradients
gradientTop = color(210, 70, 25); // deep blue
Defines the color used at the top of the background gradient
gradientBottom = color(280, 60, 10); // violet
Defines the color used at the bottom of the background gradient
drawInitialGradient();
Paints a fully opaque gradient once so the canvas isn't blank before the first frame
for (let i = 0; i < 120; i++) {
Loops 120 times to create the starting population of boids
boids.push(new Boid(random(width), random(height)));
Creates a new Boid at a random x,y position and adds it to the global boids array

draw()

draw() is the animation loop that p5.js calls roughly 60 times per second. Everything that needs to move or update over time - trails, timers, predators, boids - happens here in a fixed order.

🔬 This loop is the heartbeat of the simulation. What happens if you comment out the boid.flock() line entirely - do the boids still look alive, or just drift randomly?

  for (let boid of boids) {
    boid.flock(boids, predators);
    if (panicTimer > 0 && panicCenter) {
      boid.panic(panicCenter);
    }
    boid.update();
    boid.edges();
    boid.show();
  }
function draw() {
  // Motion trails: instead of a solid background, overlay a faint gradient
  drawGradientOverlay(8); // small alpha -> long trails

  // Panic timer countdown
  if (panicTimer > 0) {
    panicTimer--;
    if (panicTimer === 0) panicCenter = null;
  }

  // Update + draw predators
  for (let predator of predators) {
    predator.update();
    predator.edges();
    predator.show();
  }

  // Update + draw boids
  for (let boid of boids) {
    boid.flock(boids, predators);
    if (panicTimer > 0 && panicCenter) {
      boid.panic(panicCenter);
    }
    boid.update();
    boid.edges();
    boid.show();
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

conditional Panic Timer Countdown if (panicTimer > 0) {

Decrements the panic timer each frame and clears the panic center once it reaches zero

for-loop Update & Draw Predators for (let predator of predators) {

Runs the update/edges/show cycle for every predator on screen

for-loop Update & Draw Boids for (let boid of boids) {

Runs flocking, panic, movement, and rendering for every boid on screen

drawGradientOverlay(8); // small alpha -> long trails
Paints a nearly-transparent gradient over the whole frame instead of clearing it, which lets previous frames faintly show through and creates trailing streaks
if (panicTimer > 0) {
Only runs the countdown logic while a panic burst is active
panicTimer--;
Counts the panic timer down by one frame
if (panicTimer === 0) panicCenter = null;
Once the timer expires, clears the panic center so boids stop reacting to it
for (let predator of predators) {
Loops over every predator currently placed on the canvas
predator.update();
Moves the predator toward the nearest boid
predator.edges();
Wraps the predator around the screen edges if it goes off-canvas
predator.show();
Draws the predator as a red triangle
for (let boid of boids) {
Loops over every boid currently in the flock
boid.flock(boids, predators);
Calculates and applies the combined separation/alignment/cohesion/avoidance steering forces
if (panicTimer > 0 && panicCenter) {
Checks whether a panic burst is currently active
boid.panic(panicCenter);
Applies an extra outward force pushing the boid away from the click point
boid.update();
Applies the accumulated forces to move the boid
boid.edges();
Wraps the boid around the screen edges if it leaves the canvas
boid.show();
Draws the boid as an oriented, colored triangle

windowResized()

windowResized() is a p5.js callback that automatically fires whenever the browser window changes size, letting you keep the canvas responsive.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  drawInitialGradient();
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the browser window whenever it's resized
drawInitialGradient();
Repaints a fresh, fully opaque gradient so the resized canvas doesn't show a stretched or blank background

mousePressed()

mousePressed() is a p5.js input callback that fires once whenever any mouse button is clicked. Checking the mouseButton variable lets one function handle multiple distinct interactions.

🔬 This block spawns boids AND triggers panic in one click. What happens if you delete the panicTimer line so left-click only spawns boids without scaring anyone?

  if (mouseButton === LEFT) {
    spawnBoidGroup(mouseX, mouseY, 22);
    panicCenter = createVector(mouseX, mouseY);
    panicTimer = PANIC_DURATION;
  }
function mousePressed() {
  // LEFT: spawn group + trigger a panic burst centered at the click
  if (mouseButton === LEFT) {
    spawnBoidGroup(mouseX, mouseY, 22);
    panicCenter = createVector(mouseX, mouseY);
    panicTimer = PANIC_DURATION;
  }
  // RIGHT: place a predator
  else if (mouseButton === RIGHT) {
    predators.push(new Predator(mouseX, mouseY));
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Left Click Handler if (mouseButton === LEFT) {

Spawns a fresh boid group and starts a panic burst centered at the click

conditional Right Click Handler else if (mouseButton === RIGHT) {

Places a new predator at the click location

if (mouseButton === LEFT) {
Checks whether the left mouse button triggered this click
spawnBoidGroup(mouseX, mouseY, 22);
Adds 22 new boids clustered around the mouse position
panicCenter = createVector(mouseX, mouseY);
Stores the click position as a vector so boids know which point to flee from
panicTimer = PANIC_DURATION;
Resets the panic countdown to its full length, starting a new panic burst
else if (mouseButton === RIGHT) {
Checks whether the right mouse button triggered this click
predators.push(new Predator(mouseX, mouseY));
Creates a new Predator at the click position and adds it to the predators array

keyPressed()

keyPressed() fires once every time a key is pressed. Comparing against the key variable is a simple way to add keyboard shortcuts to a sketch.

function keyPressed() {
  if (key === 'C' || key === 'c') {
    boids = [];
  } else if (key === 'X' || key === 'x') {
    predators = [];
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Clear Boids if (key === 'C' || key === 'c') {

Empties the boids array when C is pressed

conditional Clear Predators } else if (key === 'X' || key === 'x') {

Empties the predators array when X is pressed

if (key === 'C' || key === 'c') {
Checks for either uppercase or lowercase C being pressed
boids = [];
Replaces the boids array with an empty one, instantly removing every boid from the scene
} else if (key === 'X' || key === 'x') {
Checks for either uppercase or lowercase X being pressed
predators = [];
Replaces the predators array with an empty one, removing every predator

spawnBoidGroup()

This helper demonstrates how to scatter multiple objects around a point using p5.Vector.random2D() combined with a random magnitude, a common pattern for particle bursts.

function spawnBoidGroup(x, y, count) {
  for (let i = 0; i < count; i++) {
    const offset = p5.Vector.random2D().mult(random(0, 30));
    boids.push(new Boid(x + offset.x, y + offset.y));
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Spawn Loop for (let i = 0; i < count; i++) {

Creates 'count' new boids scattered in a small radius around (x, y)

for (let i = 0; i < count; i++) {
Repeats the boid-creation code 'count' times
const offset = p5.Vector.random2D().mult(random(0, 30));
Creates a random direction vector and scales it by a random distance up to 30 pixels, so boids don't all spawn on exactly the same spot
boids.push(new Boid(x + offset.x, y + offset.y));
Creates a new Boid offset from the click point and adds it to the shared boids array

drawInitialGradient()

This function fakes a vertical gradient the same way you'd see in graphic design tools - by drawing many thin horizontal lines, each a slightly different interpolated color.

function drawInitialGradient() {
  strokeWeight(1);
  for (let y = 0; y < height; y++) {
    const t = y / max(height - 1, 1);
    const c = lerpColor(gradientTop, gradientBottom, t);
    c.setAlpha(100);
    stroke(c);
    line(0, y, width, y);
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Row-by-Row Gradient for (let y = 0; y < height; y++) {

Draws one horizontal line per pixel row, interpolating its color between the top and bottom gradient colors

strokeWeight(1);
Sets the line thickness to 1 pixel so each row draws a crisp single-pixel-tall stripe
for (let y = 0; y < height; y++) {
Loops once for every vertical pixel row on the canvas
const t = y / max(height - 1, 1);
Converts the row number into a 0-to-1 fraction representing how far down the canvas this row is
const c = lerpColor(gradientTop, gradientBottom, t);
Blends between the top and bottom colors based on that fraction, producing a smooth gradient
c.setAlpha(100);
Sets this color fully opaque (100 is max alpha in this HSB color mode) so it completely covers the canvas
stroke(c);
Sets the line color for the row that's about to be drawn
line(0, y, width, y);
Draws a horizontal line spanning the full width at this row's y position

drawGradientOverlay()

Instead of calling background() to fully clear the canvas each frame, this function paints an almost-transparent copy of the background on top. Because it never fully erases, previous positions of every shape linger faintly, creating a comet-like trail effect.

function drawGradientOverlay(alphaVal) {
  strokeWeight(1);
  for (let y = 0; y < height; y++) {
    const t = y / max(height - 1, 1);
    const c = lerpColor(gradientTop, gradientBottom, t);
    c.setAlpha(alphaVal); // 0–100 in HSB alpha range
    stroke(c);
    line(0, y, width, y);
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Row-by-Row Faint Overlay for (let y = 0; y < height; y++) {

Repaints a nearly-transparent gradient over the whole canvas each frame, letting previous frames fade rather than vanish

strokeWeight(1);
Sets each drawn line to 1 pixel thick
for (let y = 0; y < height; y++) {
Loops through every row of the canvas, same as the initial gradient function
const t = y / max(height - 1, 1);
Calculates this row's position as a 0-to-1 fraction down the canvas
const c = lerpColor(gradientTop, gradientBottom, t);
Blends the top/bottom gradient colors for this row
c.setAlpha(alphaVal); // 0–100 in HSB alpha range
Uses a low transparency value passed in from draw() so this overlay only slightly darkens/tints what's beneath it instead of erasing it
stroke(c);
Applies the semi-transparent color to the upcoming line
line(0, y, width, y);
Draws the horizontal line for this row, partially blending with whatever was drawn in previous frames

Boid constructor

The constructor runs once when 'new Boid(x, y)' is called, setting up all the per-instance properties (position, velocity, appearance) that make every boid slightly unique.

constructor(x, y) {
    this.position = createVector(x, y);
    this.velocity = p5.Vector.random2D();
    this.velocity.setMag(random(1, 3));
    this.acceleration = createVector(0, 0);

    this.maxSpeed = random(2.0, 3.6);
    this.maxForce = 0.05;

    this.size = random(4, 7);

    // Colorful palette: teal -> purple range
    this.hue = random(160, 320);
    this.sat = random(60, 100);
    this.bri = random(60, 100);
  }
Line-by-line explanation (10 lines)
this.position = createVector(x, y);
Stores the boid's location as a p5.Vector so it can be used in vector math
this.velocity = p5.Vector.random2D();
Gives the boid a random starting direction as a unit-length vector
this.velocity.setMag(random(1, 3));
Scales that direction to a random speed between 1 and 3 pixels per frame
this.acceleration = createVector(0, 0);
Starts with zero acceleration; forces will be added to this each frame
this.maxSpeed = random(2.0, 3.6);
Gives each boid a slightly different top speed so the flock doesn't move in perfect lockstep
this.maxForce = 0.05;
Limits how sharply the boid can steer each frame, preventing unrealistic instant direction changes
this.size = random(4, 7);
Randomizes the boid's rendered size slightly for visual variety
this.hue = random(160, 320);
Picks a random hue between teal and purple for this boid's color
this.sat = random(60, 100);
Picks a random saturation so colors range from muted to vivid
this.bri = random(60, 100);
Picks a random brightness so boids aren't all the same shade

applyForce()

This is the standard 'force accumulator' pattern used in physics-style sketches: instead of directly changing velocity, you add forces to acceleration and let update() apply them uniformly.

applyForce(force) {
    this.acceleration.add(force);
  }
Line-by-line explanation (1 lines)
this.acceleration.add(force);
Adds an incoming force vector to the boid's total acceleration for this frame; multiple forces can stack before update() applies them

Boid update()

This update() method implements the classic physics integration pattern: acceleration changes velocity, velocity changes position, then acceleration resets for the next frame's fresh set of forces.

update() {
    this.velocity.add(this.acceleration);
    this.velocity.limit(this.maxSpeed);
    this.position.add(this.velocity);
    this.acceleration.mult(0);
  }
Line-by-line explanation (4 lines)
this.velocity.add(this.acceleration);
Applies this frame's accumulated steering forces to the boid's velocity
this.velocity.limit(this.maxSpeed);
Caps the velocity's magnitude so the boid never exceeds its top speed
this.position.add(this.velocity);
Moves the boid by adding velocity to its current position
this.acceleration.mult(0);
Resets acceleration to zero so forces don't carry over and compound into the next frame

Boid edges()

This is a classic 'screen wrap' technique used instead of bouncing off walls - it makes the canvas feel like a continuous, edgeless space.

edges() {
    const margin = 10;
    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 (5 lines)

🔧 Subcomponents:

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

Wraps the boid to the opposite horizontal edge when it exits the canvas

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

Wraps the boid to the opposite vertical edge when it exits the canvas

const margin = 10;
Allows the boid to travel slightly past the edge before wrapping, so it disappears smoothly instead of popping
if (this.position.x > width + margin) this.position.x = -margin;
If the boid exits the right edge, teleport it just off the left edge
else if (this.position.x < -margin) this.position.x = width + margin;
If the boid exits the left edge, teleport it just off the right edge
if (this.position.y > height + margin) this.position.y = -margin;
If the boid exits the bottom edge, teleport it just off the top
else if (this.position.y < -margin) this.position.y = height + margin;
If the boid exits the top edge, teleport it just off the bottom

flock()

flock() is the conductor that blends together separation, alignment, cohesion, and predator avoidance into one combined steering behavior - the weights here define the flock's overall personality.

🔬 These four multipliers balance the whole flock's personality. What happens if you set alignment's multiplier much higher than separation's - do the boids start moving in tight, orderly lines like a squadron?

    const sep = this.separation(boids).mult(1.6);
    const ali = this.alignment(boids).mult(1.0);
    const coh = this.cohesion(boids).mult(0.8);
    const avoidPred = this.avoidPredators(predators).mult(3.0);
flock(boids, predators) {
    const sep = this.separation(boids).mult(1.6);
    const ali = this.alignment(boids).mult(1.0);
    const coh = this.cohesion(boids).mult(0.8);
    const avoidPred = this.avoidPredators(predators).mult(3.0);

    this.applyForce(sep);
    this.applyForce(ali);
    this.applyForce(coh);
    this.applyForce(avoidPred);
  }
Line-by-line explanation (8 lines)
const sep = this.separation(boids).mult(1.6);
Computes the separation force and boosts its influence by 1.6x, making crowd-avoidance the strongest of the three core rules
const ali = this.alignment(boids).mult(1.0);
Computes the alignment force at its normal strength
const coh = this.cohesion(boids).mult(0.8);
Computes the cohesion force but slightly weakens it so the flock doesn't clump too tightly
const avoidPred = this.avoidPredators(predators).mult(3.0);
Computes the predator-avoidance force and triples its strength so fleeing always overrides normal flocking
this.applyForce(sep);
Adds the separation force to this frame's acceleration
this.applyForce(ali);
Adds the alignment force to this frame's acceleration
this.applyForce(coh);
Adds the cohesion force to this frame's acceleration
this.applyForce(avoidPred);
Adds the predator-avoidance force to this frame's acceleration

separation()

Separation is the 'personal space' rule of flocking - without it, boids would collapse into a single overlapping clump. Notice how this is the only rule with the extra div(d), which makes the push stronger the closer a neighbor gets.

🔬 desiredSeparation defines how much personal space each boid demands. What happens to the flock's density if you raise it to 80?

    const desiredSeparation = 25;
    const steering = createVector(0, 0);
    let total = 0;
separation(boids) {
    const desiredSeparation = 25;
    const steering = createVector(0, 0);
    let total = 0;

    for (let other of boids) {
      if (other === this) continue;
      const d = p5.Vector.dist(this.position, other.position);
      if (d > 0 && d < desiredSeparation) {
        const diff = p5.Vector.sub(this.position, other.position);
        diff.normalize();
        diff.div(d); // stronger for close neighbors
        steering.add(diff);
        total++;
      }
    }

    if (total > 0) {
      steering.div(total);
      steering.setMag(this.maxSpeed);
      steering.sub(this.velocity);
      steering.limit(this.maxForce * 1.5);
    }
    return steering;
  }
Line-by-line explanation (14 lines)

🔧 Subcomponents:

for-loop Scan Nearby Neighbors for (let other of boids) {

Checks every other boid to find ones close enough to push away from

conditional Too Close Check if (d > 0 && d < desiredSeparation) {

Only reacts to neighbors within the personal-space radius

const desiredSeparation = 25;
Sets the personal-space radius in pixels - neighbors closer than this trigger avoidance
for (let other of boids) {
Loops through every boid in the flock to check its distance
if (other === this) continue;
Skips comparing the boid to itself
const d = p5.Vector.dist(this.position, other.position);
Measures the distance between this boid and the other one
if (d > 0 && d < desiredSeparation) {
Only reacts if the other boid is inside the personal-space radius (and not at the exact same spot)
const diff = p5.Vector.sub(this.position, other.position);
Calculates a vector pointing away from the other boid
diff.normalize();
Reduces that vector to length 1 so only its direction matters so far
diff.div(d); // stronger for close neighbors
Divides by distance so closer neighbors produce a stronger push
steering.add(diff);
Accumulates this push into the total steering vector
total++;
Counts how many neighbors contributed to this force
steering.div(total);
Averages the accumulated pushes across all nearby neighbors
steering.setMag(this.maxSpeed);
Scales the desired direction to the boid's top speed
steering.sub(this.velocity);
Converts the desired velocity into a steering force by subtracting the current velocity (this is the standard Reynolds steering formula)
steering.limit(this.maxForce * 1.5);
Caps how strong this force can be, giving separation a slightly higher force limit than the other rules

alignment()

Alignment makes boids match the average direction of their neighbors, which is what produces the smooth, synchronized turning you see in real bird flocks.

alignment(boids) {
    const perceptionRadius = 60;
    const steering = createVector(0, 0);
    let total = 0;

    for (let other of boids) {
      if (other === this) continue;
      const d = p5.Vector.dist(this.position, other.position);
      if (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 (11 lines)

🔧 Subcomponents:

for-loop Scan Neighbor Velocities for (let other of boids) {

Gathers the velocity of every nearby boid to average their heading

const perceptionRadius = 60;
Defines how far away a neighbor's heading still counts toward alignment
for (let other of boids) {
Checks every boid in the flock
if (other === this) continue;
Skips itself
const d = p5.Vector.dist(this.position, other.position);
Measures distance to the other boid
if (d < perceptionRadius) {
Only counts neighbors within the perception radius
steering.add(other.velocity);
Adds the neighbor's velocity to a running total
total++;
Counts how many neighbors contributed
steering.div(total);
Averages the neighbors' velocities to find the group's average heading
steering.setMag(this.maxSpeed);
Scales the desired heading to top speed
steering.sub(this.velocity);
Turns the desired velocity into a steering force
steering.limit(this.maxForce);
Caps how strongly this force can turn the boid

cohesion()

Cohesion pulls each boid gently toward the average position of its neighbors, which is what keeps the flock from drifting apart into scattered individuals.

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

    for (let other of boids) {
      if (other === this) continue;
      const d = p5.Vector.dist(this.position, other.position);
      if (d < perceptionRadius) {
        steering.add(other.position);
        total++;
      }
    }

    if (total > 0) {
      steering.div(total);
      steering.sub(this.position);
      steering.setMag(this.maxSpeed);
      steering.sub(this.velocity);
      steering.limit(this.maxForce);
    }
    return steering;
  }
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Scan Neighbor Positions for (let other of boids) {

Gathers the position of every nearby boid to find the group's center of mass

const perceptionRadius = 70;
Defines how far away a neighbor still counts toward the perceived group center
for (let other of boids) {
Checks every boid in the flock
steering.add(other.position);
Adds the neighbor's position to a running total, used to find the average location
steering.div(total);
Averages all nearby positions to find the local center of mass
steering.sub(this.position);
Turns that center-of-mass point into a direction vector pointing from this boid toward it
steering.setMag(this.maxSpeed);
Scales that direction to top speed
steering.sub(this.velocity);
Converts it into a steering force relative to current velocity
steering.limit(this.maxForce);
Caps the force strength

avoidPredators()

This method uses the same steering-force pattern as separation, alignment, and cohesion, but with a much larger perception radius and force limit so predator-avoidance can overpower normal flocking.

avoidPredators(predators) {
    const perceptionRadius = 130;
    const steering = createVector(0, 0);
    let total = 0;

    for (let predator of predators) {
      const d = p5.Vector.dist(this.position, predator.position);
      if (d < perceptionRadius) {
        const diff = p5.Vector.sub(this.position, predator.position);
        diff.normalize();
        diff.div(d * 0.5 + 1); // very strong when close
        steering.add(diff);
        total++;
      }
    }

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

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

🔧 Subcomponents:

for-loop Scan Nearby Predators for (let predator of predators) {

Checks every predator to see if it's close enough to flee from

conditional Within Danger Range if (d < perceptionRadius) {

Only reacts to predators within the danger radius

const perceptionRadius = 130;
Sets the danger radius - predators farther than this are ignored
for (let predator of predators) {
Loops through every predator currently on screen
const d = p5.Vector.dist(this.position, predator.position);
Measures the distance to this predator
if (d < perceptionRadius) {
Only reacts if the predator is close enough to be a threat
const diff = p5.Vector.sub(this.position, predator.position);
Calculates a vector pointing away from the predator
diff.normalize();
Reduces it to a pure direction
diff.div(d * 0.5 + 1); // very strong when close
Scales the push so it grows dramatically stronger as the predator gets closer
steering.add(diff);
Accumulates this fleeing force
total++;
Counts how many predators contributed
steering.setMag(this.maxSpeed * 1.4);
Lets the fleeing direction exceed the boid's normal top speed by 40%, simulating an adrenaline burst
steering.limit(this.maxForce * 3.0);
Allows this force to be three times stronger than normal steering forces, since survival takes priority

panic()

panic() is only called while panicTimer is active, layering a temporary, distance-weighted fleeing force on top of the normal flocking behavior triggered by every left-click.

🔬 The '7' here multiplies how violently boids flee. What happens if you lower it to 2 - does the panic burst look calmer or almost unnoticeable?

      const strength = map(d, 0, maxDist, 1.8, 0.1);
      dir.mult(this.maxForce * 7 * strength);
panic(center) {
    const maxDist = 220;
    const dir = p5.Vector.sub(this.position, center);
    const d = dir.mag();
    if (d < maxDist && d > 0) {
      dir.normalize();
      const strength = map(d, 0, maxDist, 1.8, 0.1);
      dir.mult(this.maxForce * 7 * strength);
      // Random jitter to make it look chaotic
      dir.add(p5.Vector.random2D().mult(this.maxForce * 2.5));
      this.applyForce(dir);
    }
  }
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Within Panic Radius if (d < maxDist && d > 0) {

Only applies the panic force to boids close enough to the click point

const maxDist = 220;
Sets how far the panic effect reaches from the click point
const dir = p5.Vector.sub(this.position, center);
Calculates a vector pointing from the click center toward this boid
const d = dir.mag();
Measures how far this boid is from the panic center
if (d < maxDist && d > 0) {
Only reacts if the boid is within range and not exactly on top of the click point
dir.normalize();
Reduces the direction vector to unit length
const strength = map(d, 0, maxDist, 1.8, 0.1);
Maps distance to a strength value - boids closer to the click flee harder (1.8), farther ones barely react (0.1)
dir.mult(this.maxForce * 7 * strength);
Scales the fleeing force to be up to 7x stronger than normal steering, modulated by distance-based strength
dir.add(p5.Vector.random2D().mult(this.maxForce * 2.5));
Adds a small random jitter so the flight looks chaotic rather than perfectly radial
this.applyForce(dir);
Applies the combined panic-and-jitter force to the boid's acceleration

Boid show()

Using velocity.heading() to rotate the shape is what makes each boid visually point in its direction of travel - a small but essential touch that sells the illusion of purposeful movement.

show() {
    push();
    translate(this.position.x, this.position.y);
    const angle = this.velocity.heading() + HALF_PI;
    rotate(angle);
    noStroke();
    fill(this.hue, this.sat, this.bri, 80); // slightly transparent
    beginShape();
    vertex(0, -this.size * 2.2);
    vertex(-this.size, this.size * 2.2);
    vertex(this.size, this.size * 2.2);
    endShape(CLOSE);
    pop();
  }
Line-by-line explanation (11 lines)
push();
Saves the current drawing style and transform state so this boid's rotation doesn't affect anything else
translate(this.position.x, this.position.y);
Moves the drawing origin to the boid's position, so all following coordinates are relative to it
const angle = this.velocity.heading() + HALF_PI;
Calculates the angle the boid is moving in, adding a quarter-turn offset so the triangle points the right way
rotate(angle);
Rotates the drawing so the triangle faces the boid's direction of travel
fill(this.hue, this.sat, this.bri, 80); // slightly transparent
Sets the boid's unique color with some transparency, which helps the motion trails blend nicely
beginShape();
Starts defining a custom polygon
vertex(0, -this.size * 2.2);
Defines the triangle's front tip
vertex(-this.size, this.size * 2.2);
Defines the back-left corner
vertex(this.size, this.size * 2.2);
Defines the back-right corner
endShape(CLOSE);
Closes the shape into a solid filled triangle
pop();
Restores the earlier drawing state, undoing the translate and rotate for the next shape drawn

Predator constructor

Predator is a separate, simpler class from Boid - it only needs to chase the nearest boid rather than compute complex flocking rules.

constructor(x, y) {
    this.position = createVector(x, y);
    this.velocity = p5.Vector.random2D().mult(1.5);
    this.maxSpeed = 2.6;
    this.maxForce = 0.08;
    this.size = 11;
  }
Line-by-line explanation (5 lines)
this.position = createVector(x, y);
Places the predator at the clicked position
this.velocity = p5.Vector.random2D().mult(1.5);
Gives the predator a random initial direction at speed 1.5
this.maxSpeed = 2.6;
Sets the predator's top speed, slightly faster than an average boid
this.maxForce = 0.08;
Sets how sharply the predator can steer, higher than a boid's default for more aggressive turning
this.size = 11;
Sets the predator's base size, larger than any boid so it reads visually as a threat

Predator update()

Unlike the Boid class, Predator has no separate applyForce/acceleration system - it computes and applies its steering force directly within update() each frame, a simpler variant of the same steering pattern.

update() {
    let steering = createVector(0, 0);

    // Lightly seek nearest boid (if any) to keep them interesting
    const target = this.findNearestBoid();
    if (target) {
      const desired = p5.Vector.sub(target.position, this.position);
      desired.setMag(this.maxSpeed);
      steering = p5.Vector.sub(desired, this.velocity);
      steering.limit(this.maxForce);
    }

    // Add a bit of wander
    steering.add(p5.Vector.random2D().mult(0.02));

    this.velocity.add(steering);
    this.velocity.limit(this.maxSpeed);
    this.position.add(this.velocity);
  }
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Seek Nearest Boid if (target) {

Steers the predator toward the closest boid, if one exists

const target = this.findNearestBoid();
Finds the closest boid to chase
if (target) {
Only steers toward a target if at least one boid exists
const desired = p5.Vector.sub(target.position, this.position);
Calculates a vector pointing from the predator toward its target
desired.setMag(this.maxSpeed);
Scales that direction to the predator's top speed
steering = p5.Vector.sub(desired, this.velocity);
Converts the desired velocity into a steering force relative to current velocity
steering.limit(this.maxForce);
Caps how sharply the predator can turn
steering.add(p5.Vector.random2D().mult(0.02));
Adds a tiny random wander so the predator's path isn't perfectly straight even without a target
this.velocity.add(steering);
Applies the steering force to velocity
this.velocity.limit(this.maxSpeed);
Caps velocity at the predator's top speed
this.position.add(this.velocity);
Moves the predator by its velocity

findNearestBoid()

This is the classic 'find the minimum' loop pattern: track the best candidate seen so far and update it whenever something better appears.

findNearestBoid() {
    if (boids.length === 0) return null;
    let nearest = null;
    let minDist = Infinity;
    for (let b of boids) {
      const d = p5.Vector.dist(this.position, b.position);
      if (d < minDist) {
        minDist = d;
        nearest = b;
      }
    }
    return nearest;
  }
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Scan All Boids for (let b of boids) {

Checks every boid's distance to find the single closest one

conditional New Closest Found if (d < minDist) {

Updates the tracked nearest boid whenever a closer one is found

if (boids.length === 0) return null;
Immediately exits if there are no boids to chase
let minDist = Infinity;
Starts with an impossibly large distance so the very first boid checked will always count as closer
for (let b of boids) {
Checks every boid currently in the flock
const d = p5.Vector.dist(this.position, b.position);
Measures the distance from the predator to this boid
if (d < minDist) {
Checks whether this boid is closer than the closest one found so far
minDist = d;
Updates the record for the smallest distance found
nearest = b;
Remembers this boid as the current closest candidate
return nearest;
Returns the single closest boid (or null if none exist) once the loop finishes

Predator edges()

This mirrors the Boid.edges() method exactly, just with a slightly larger margin since predators are drawn bigger.

edges() {
    const margin = 20;
    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 (5 lines)
const margin = 20;
Allows the predator to travel a bit further off-screen than a boid before wrapping
if (this.position.x > width + margin) this.position.x = -margin;
Wraps the predator from the right edge to the left
else if (this.position.x < -margin) this.position.x = width + margin;
Wraps the predator from the left edge to the right
if (this.position.y > height + margin) this.position.y = -margin;
Wraps the predator from the bottom edge to the top
else if (this.position.y < -margin) this.position.y = height + margin;
Wraps the predator from the top edge to the bottom

Predator show()

Reusing the same triangle-and-rotate rendering pattern as Boid.show() but with a bright red fill and sharper proportions instantly communicates 'danger' without needing any images or sprites.

show() {
    push();
    translate(this.position.x, this.position.y);
    const angle = this.velocity.heading() + HALF_PI;
    rotate(angle);
    noStroke();
    // Aggressive red predator
    fill(0, 85, 95, 95);
    beginShape();
    vertex(0, -this.size * 2.8);
    vertex(-this.size * 1.4, this.size * 2.4);
    vertex(this.size * 1.4, this.size * 2.4);
    endShape(CLOSE);
    pop();
  }
Line-by-line explanation (8 lines)
translate(this.position.x, this.position.y);
Moves the drawing origin to the predator's position
const angle = this.velocity.heading() + HALF_PI;
Calculates the predator's facing angle from its velocity
rotate(angle);
Rotates the shape so it points in the direction of travel
fill(0, 85, 95, 95); // Aggressive red predator
Uses hue 0 (red) with high saturation and brightness to make the predator stand out as a threat
vertex(0, -this.size * 2.8);
Defines the sharp front tip of the predator's triangle, longer than a boid's for a more aggressive silhouette
vertex(-this.size * 1.4, this.size * 2.4);
Defines the wider back-left corner
vertex(this.size * 1.4, this.size * 2.4);
Defines the wider back-right corner
endShape(CLOSE);
Closes the shape into a solid filled triangle

📦 Key Variables

boids array

Holds every active Boid object currently being simulated and rendered

let boids = [];
predators array

Holds every active Predator object placed by right-clicking

let predators = [];
gradientTop object

The p5.Color used at the top of the background gradient

gradientTop = color(210, 70, 25);
gradientBottom object

The p5.Color used at the bottom of the background gradient

gradientBottom = color(280, 60, 10);
panicTimer number

Frame countdown tracking how much longer the current panic burst lasts

let panicTimer = 0;
panicCenter object

Stores the p5.Vector location boids should flee from during a panic burst, or null when inactive

let panicCenter = null;
PANIC_DURATION number

Constant defining how many frames a panic burst lasts (~3 seconds at 60fps)

const PANIC_DURATION = 180;
canvas object

Reference to the p5.js canvas DOM element, used to disable the right-click context menu

let canvas;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE Boid.separation() / alignment() / cohesion()

Each boid loops over the entire boids array for every one of the three flocking rules, giving the simulation O(n²) complexity per frame. With hundreds of boids (easily reached via repeated left-clicks), this can noticeably drop the frame rate.

💡 Use a spatial partitioning structure (a grid or quadtree) to only check boids in nearby cells, or combine the three neighbor loops into a single shared pass that computes all three sums at once.

BUG spawnBoidGroup() / mousePressed()

The boids array has no upper limit. Repeatedly left-clicking keeps adding 22 boids every time with no cap, so the simulation can grow unbounded and eventually stutter or crash the tab.

💡 Add a MAX_BOIDS constant and skip spawning (or remove the oldest boids) once boids.length exceeds it.

STYLE Boid class (separation/alignment/cohesion/avoidPredators)

Perception radii (25, 60, 70, 130) and force multipliers (1.6, 1.0, 0.8, 3.0, 1.5) are hard-coded as magic numbers scattered across multiple methods, making the flocking behavior harder to tune consistently.

💡 Move these into named constants near the top of the file (e.g. SEPARATION_RADIUS, ALIGNMENT_RADIUS) so all the tunable 'personality' knobs live in one place.

FEATURE Predator class

Predators are permanent once placed - there's no way to remove a single predator, only clear all of them at once with X.

💡 Add a click-to-remove interaction (e.g. clicking near an existing predator with a modifier key removes just that one) for more precise scene control.

🔄 Code Flow

Code flow showing setup, draw, windowresized, mousepressed, keypressed, spawnboidgroup, drawinitialgradient, drawgradientoverlay, boidconstructor, applyforce, update, edges, flock, separation, alignment, cohesion, avoidpredators, panic, show, predatorconstructor, predatorupdate, findnearestboid, predatoredges, predatorshow

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

graph TD start[Start] --> setup[setup] setup --> setup-seed-loop[Seed Initial Boids] setup-seed-loop --> draw[draw loop] click setup href "#fn-setup" click setup-seed-loop href "#sub-setup-seed-loop" draw --> draw-panic-countdown[Panic Timer Countdown] draw --> draw-predator-loop[Update & Draw Predators] draw --> draw-boid-loop[Update & Draw Boids] draw --> overlay-row-loop[Row-by-Row Faint Overlay] draw --> gradient-row-loop[Row-by-Row Gradient] click draw href "#fn-draw" click draw-panic-countdown href "#sub-draw-panic-countdown" click draw-predator-loop href "#sub-draw-predator-loop" click draw-boid-loop href "#sub-draw-boid-loop" click overlay-row-loop href "#sub-overlay-row-loop" click gradient-row-loop href "#sub-gradient-row-loop" draw-panic-countdown --> panic[panic] panic --> draw click panic href "#fn-panic" draw-predator-loop --> predatorupdate[Update Predator] predatorupdate --> predatoredges[Predator Edges] predatorupdate --> predatorshow[Show Predator] predatorshow --> draw click predatorupdate href "#fn-predatorupdate" click predatoredges href "#fn-predatoredges" click predatorshow href "#fn-predatorshow" draw-boid-loop --> flock[flock] flock --> separation[Separation] flock --> alignment[Alignment] flock --> cohesion[Cohesion] flock --> avoidpredators[Avoid Predators] flock --> panic flock --> edges[Edges] click flock href "#fn-flock" click separation href "#fn-separation" click alignment href "#fn-alignment" click cohesion href "#fn-cohesion" click avoidpredators href "#fn-avoidpredators" click edges href "#fn-edges" separation --> sep-neighbor-loop[Scan Nearby Neighbors] sep-neighbor-loop --> sep-close-check[Too Close Check] sep-close-check --> separation click sep-neighbor-loop href "#sub-sep-neighbor-loop" click sep-close-check href "#sub-sep-close-check" alignment --> ali-neighbor-loop[Scan Neighbor Velocities] ali-neighbor-loop --> alignment click ali-neighbor-loop href "#sub-ali-neighbor-loop" cohesion --> coh-neighbor-loop[Scan Neighbor Positions] coh-neighbor-loop --> cohesion click coh-neighbor-loop href "#sub-coh-neighbor-loop" avoidpredators --> avoid-predator-loop[Scan Nearby Predators] avoid-predator-loop --> avoid-range-check[Within Danger Range] avoid-range-check --> avoidpredators click avoid-predator-loop href "#sub-avoid-predator-loop" click avoid-range-check href "#sub-avoid-range-check" edges --> edges-x-wrap[Horizontal Wrap] edges --> edges-y-wrap[Vertical Wrap] edges-x-wrap --> edges edges-y-wrap --> edges click edges-x-wrap href "#sub-edges-x-wrap" click edges-y-wrap href "#sub-edges-y-wrap" mousepressed --> mousepressed-left[Left Click Handler] mousepressed --> mousepressed-right[Right Click Handler] click mousepressed href "#fn-mousepressed" click mousepressed-left href "#sub-mousepressed-left" click mousepressed-right href "#sub-mousepressed-right" keypressed --> keypressed-clear-boids[Clear Boids] keypressed --> keypressed-clear-predators[Clear Predators] click keypressed href "#fn-keypressed" click keypressed-clear-boids href "#sub-keypressed-clear-boids" click keypressed-clear-predators href "#sub-keypressed-clear-predators" spawnboidgroup --> spawn-loop[Spawn Loop] spawn-loop --> spawnboidgroup click spawn-loop href "#sub-spawn-loop"

❓ Frequently Asked Questions

What visual experience does the AI Boid Flocking Simulator create?

The sketch visually simulates realistic swarms of birds or fish, showcasing emergent behaviors as they flock together, while displaying a vibrant gradient background.

How can users interact with the AI Boid Flocking Simulator?

Users can interact by clicking the left mouse button to spawn new groups of boids and trigger a panic response, causing the boids to scatter from the click location.

What creative coding concepts are demonstrated in this sketch?

This sketch demonstrates flocking behavior, emergent systems, and particle motion, illustrating how simple rules can create complex group dynamics.

Preview

AI Boid Flocking Simulator - Emergent Swarm Behavior Watch realistic bird/fish swarms with separati - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Boid Flocking Simulator - Emergent Swarm Behavior Watch realistic bird/fish swarms with separati - Code flow showing setup, draw, windowresized, mousepressed, keypressed, spawnboidgroup, drawinitialgradient, drawgradientoverlay, boidconstructor, applyforce, update, edges, flock, separation, alignment, cohesion, avoidpredators, panic, show, predatorconstructor, predatorupdate, findnearestboid, predatoredges, predatorshow
Code Flow Diagram