All articles
24 min read

Boids: How to Create Realistic Flocking Simulations With Code

boids algorithmflocking simulationcreative codingJavaScriptcanvasanimationartificial lifegenerative art

In 1986, Craig Reynolds created one of the most influential algorithms in computer graphics: boids. Short for "bird-oid object," the boids algorithm simulates flocking behavior using just three simple steering rules — separation, alignment, and cohesion. No central controller. No global awareness. Each boid follows only local rules, yet the flock moves with the organic grace of a murmuration of starlings.

In this article, we build 8 boid simulations from scratch using JavaScript and Canvas. Every example runs live in your browser with no dependencies. We start with the classic three-rule flock, then add predators, multiple species, obstacle avoidance, 3D perspective, mouse interaction, motion trails, and finish with a generative art composition painted by boid flight paths.

The Three Rules of Boids

Every boid has a position and a velocity. Each frame, it perceives nearby boids within a radius and computes three steering forces:

  • Separation — steer away from boids that are too close, preventing collisions
  • Alignment — steer toward the average heading of nearby boids, synchronizing direction
  • Cohesion — steer toward the average position of nearby boids, keeping the flock together

These three vectors are weighted and summed to produce a steering acceleration. The result: dozens or hundreds of independent agents that fly as one unified, shifting flock.

1. Basic Boids

The foundational implementation: a flock of 120 boids following the three classic rules. Watch how cohesive, lifelike motion emerges from purely local interactions. Each boid is drawn as a small triangle pointing in its direction of travel.

const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 500;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

const NUM = 120;
const MAX_SPEED = 3;
const MAX_FORCE = 0.15;
const PERCEPTION = 50;
const SEP_DIST = 25;

function createBoid() {
  const angle = Math.random() * Math.PI * 2;
  return {
    x: Math.random() * canvas.width,
    y: Math.random() * canvas.height,
    vx: Math.cos(angle) * (1 + Math.random() * 2),
    vy: Math.sin(angle) * (1 + Math.random() * 2),
  };
}

const boids = Array.from({ length: NUM }, createBoid);

function limit(vx, vy, max) {
  const mag = Math.sqrt(vx * vx + vy * vy);
  if (mag > max) { const s = max / mag; return [vx * s, vy * s]; }
  return [vx, vy];
}

function steer(boid) {
  let sx = 0, sy = 0, sc = 0;
  let ax = 0, ay = 0, ac = 0;
  let cx = 0, cy = 0, cc = 0;

  for (const other of boids) {
    if (other === boid) continue;
    const dx = boid.x - other.x;
    const dy = boid.y - other.y;
    const d = Math.sqrt(dx * dx + dy * dy);
    if (d < PERCEPTION) {
      ax += other.vx; ay += other.vy; ac++;
      cx += other.x; cy += other.y; cc++;
      if (d < SEP_DIST && d > 0) {
        sx += dx / d; sy += dy / d; sc++;
      }
    }
  }

  let fx = 0, fy = 0;
  if (sc > 0) {
    sx /= sc; sy /= sc;
    const m = Math.sqrt(sx * sx + sy * sy);
    if (m > 0) { sx = sx / m * MAX_SPEED - boid.vx; sy = sy / m * MAX_SPEED - boid.vy; }
    [sx, sy] = limit(sx, sy, MAX_FORCE);
    fx += sx * 1.5; fy += sy * 1.5;
  }
  if (ac > 0) {
    ax /= ac; ay /= ac;
    const m = Math.sqrt(ax * ax + ay * ay);
    if (m > 0) { ax = ax / m * MAX_SPEED - boid.vx; ay = ay / m * MAX_SPEED - boid.vy; }
    [ax, ay] = limit(ax, ay, MAX_FORCE);
    fx += ax; fy += ay;
  }
  if (cc > 0) {
    cx = cx / cc - boid.x; cy = cy / cc - boid.y;
    const m = Math.sqrt(cx * cx + cy * cy);
    if (m > 0) { cx = cx / m * MAX_SPEED - boid.vx; cy = cy / m * MAX_SPEED - boid.vy; }
    [cx, cy] = limit(cx, cy, MAX_FORCE);
    fx += cx; fy += cy;
  }
  return [fx, fy];
}

function drawBoid(b) {
  const angle = Math.atan2(b.vy, b.vx);
  const speed = Math.sqrt(b.vx * b.vx + b.vy * b.vy);
  const hue = 150 + (speed / MAX_SPEED) * 40;
  ctx.save();
  ctx.translate(b.x, b.y);
  ctx.rotate(angle);
  ctx.fillStyle = `hsl(${hue}, 70%, 55%)`;
  ctx.beginPath();
  ctx.moveTo(8, 0);
  ctx.lineTo(-4, 3.5);
  ctx.lineTo(-4, -3.5);
  ctx.closePath();
  ctx.fill();
  ctx.restore();
}

function update() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  for (const b of boids) {
    const [fx, fy] = steer(b);
    b.vx += fx; b.vy += fy;
    [b.vx, b.vy] = limit(b.vx, b.vy, MAX_SPEED);
    b.x += b.vx; b.y += b.vy;
    if (b.x < 0) b.x += canvas.width;
    if (b.x > canvas.width) b.x -= canvas.width;
    if (b.y < 0) b.y += canvas.height;
    if (b.y > canvas.height) b.y -= canvas.height;
    drawBoid(b);
  }

  ctx.fillStyle = 'rgba(110,230,180,0.4)';
  ctx.font = '12px monospace';
  ctx.fillText(`boids: ${NUM}  |  separation + alignment + cohesion`, 15, canvas.height - 12);
  requestAnimationFrame(update);
}
requestAnimationFrame(update);

2. Boids with Predator

Add a red predator that chases the nearest boid. The flock splits and reforms around the threat — exactly how real fish schools evade sharks. The predator slowly pursues the closest boid, while every boid within flee range adds an urgent escape vector.

const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 500;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

const NUM = 100;
const MAX_SPEED = 3;
const MAX_FORCE = 0.15;
const PERCEPTION = 50;
const SEP_DIST = 25;
const FLEE_DIST = 100;

const predator = {
  x: canvas.width / 2, y: canvas.height / 2,
  vx: 1, vy: 0.5, speed: 2.2,
};

function createBoid() {
  const a = Math.random() * Math.PI * 2;
  return { x: Math.random() * canvas.width, y: Math.random() * canvas.height, vx: Math.cos(a) * 2, vy: Math.sin(a) * 2 };
}
const boids = Array.from({ length: NUM }, createBoid);

function limit(vx, vy, max) {
  const m = Math.sqrt(vx * vx + vy * vy);
  if (m > max) { const s = max / m; return [vx * s, vy * s]; }
  return [vx, vy];
}

function steer(boid) {
  let sx = 0, sy = 0, sc = 0;
  let ax = 0, ay = 0, ac = 0;
  let cx = 0, cy = 0, cc = 0;
  for (const o of boids) {
    if (o === boid) continue;
    const dx = boid.x - o.x, dy = boid.y - o.y;
    const d = Math.sqrt(dx * dx + dy * dy);
    if (d < PERCEPTION) {
      ax += o.vx; ay += o.vy; ac++;
      cx += o.x; cy += o.y; cc++;
      if (d < SEP_DIST && d > 0) { sx += dx / d; sy += dy / d; sc++; }
    }
  }
  let fx = 0, fy = 0;
  if (sc > 0) { sx /= sc; sy /= sc; const m = Math.sqrt(sx*sx+sy*sy); if(m>0){sx=sx/m*MAX_SPEED-boid.vx;sy=sy/m*MAX_SPEED-boid.vy;} [sx,sy]=limit(sx,sy,MAX_FORCE); fx+=sx*1.5; fy+=sy*1.5; }
  if (ac > 0) { ax /= ac; ay /= ac; const m = Math.sqrt(ax*ax+ay*ay); if(m>0){ax=ax/m*MAX_SPEED-boid.vx;ay=ay/m*MAX_SPEED-boid.vy;} [ax,ay]=limit(ax,ay,MAX_FORCE); fx+=ax; fy+=ay; }
  if (cc > 0) { cx=cx/cc-boid.x; cy=cy/cc-boid.y; const m = Math.sqrt(cx*cx+cy*cy); if(m>0){cx=cx/m*MAX_SPEED-boid.vx;cy=cy/m*MAX_SPEED-boid.vy;} [cx,cy]=limit(cx,cy,MAX_FORCE); fx+=cx; fy+=cy; }

  // flee from predator
  const pdx = boid.x - predator.x, pdy = boid.y - predator.y;
  const pd = Math.sqrt(pdx * pdx + pdy * pdy);
  if (pd < FLEE_DIST && pd > 0) {
    const urgency = (FLEE_DIST - pd) / FLEE_DIST;
    fx += (pdx / pd) * MAX_FORCE * 6 * urgency;
    fy += (pdy / pd) * MAX_FORCE * 6 * urgency;
  }
  return [fx, fy];
}

function updatePredator() {
  let closestDist = Infinity, target = null;
  for (const b of boids) {
    const d = Math.sqrt((b.x - predator.x) ** 2 + (b.y - predator.y) ** 2);
    if (d < closestDist) { closestDist = d; target = b; }
  }
  if (target) {
    const dx = target.x - predator.x, dy = target.y - predator.y;
    const m = Math.sqrt(dx * dx + dy * dy);
    if (m > 0) { predator.vx += (dx / m) * 0.08; predator.vy += (dy / m) * 0.08; }
  }
  [predator.vx, predator.vy] = limit(predator.vx, predator.vy, predator.speed);
  predator.x += predator.vx; predator.y += predator.vy;
  if (predator.x < 0) predator.x += canvas.width;
  if (predator.x > canvas.width) predator.x -= canvas.width;
  if (predator.y < 0) predator.y += canvas.height;
  if (predator.y > canvas.height) predator.y -= canvas.height;
}

function drawBoid(b, color) {
  const angle = Math.atan2(b.vy, b.vx);
  ctx.save(); ctx.translate(b.x, b.y); ctx.rotate(angle);
  ctx.fillStyle = color;
  ctx.beginPath(); ctx.moveTo(8, 0); ctx.lineTo(-4, 3.5); ctx.lineTo(-4, -3.5); ctx.closePath(); ctx.fill();
  ctx.restore();
}

function update() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  updatePredator();
  for (const b of boids) {
    const [fx, fy] = steer(b);
    b.vx += fx; b.vy += fy;
    [b.vx, b.vy] = limit(b.vx, b.vy, MAX_SPEED);
    b.x += b.vx; b.y += b.vy;
    if (b.x < 0) b.x += canvas.width; if (b.x > canvas.width) b.x -= canvas.width;
    if (b.y < 0) b.y += canvas.height; if (b.y > canvas.height) b.y -= canvas.height;
    const pd = Math.sqrt((b.x - predator.x) ** 2 + (b.y - predator.y) ** 2);
    const hue = pd < FLEE_DIST ? 40 + (pd / FLEE_DIST) * 110 : 160;
    drawBoid(b, `hsl(${hue}, 70%, 55%)`);
  }

  // draw predator
  const pa = Math.atan2(predator.vy, predator.vx);
  ctx.save(); ctx.translate(predator.x, predator.y); ctx.rotate(pa);
  ctx.fillStyle = '#e84057';
  ctx.beginPath(); ctx.moveTo(14, 0); ctx.lineTo(-7, 6); ctx.lineTo(-7, -6); ctx.closePath(); ctx.fill();
  // threat aura
  ctx.strokeStyle = 'rgba(232, 64, 87, 0.15)';
  ctx.beginPath(); ctx.arc(0, 0, FLEE_DIST, 0, Math.PI * 2); ctx.stroke();
  ctx.restore();

  ctx.fillStyle = 'rgba(110,230,180,0.4)';
  ctx.font = '12px monospace';
  ctx.fillText('predator (red) chases nearest boid  |  flock evades', 15, canvas.height - 12);
  requestAnimationFrame(update);
}
requestAnimationFrame(update);

3. Multi-Species Flocking

Two species of boids — teal and violet — that flock with their own kind but avoid the other species. Watch how distinct groups form, merge within species, and separate between species, just like mixed flocks of different bird species in nature.

const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 500;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

const MAX_SPEED = 3;
const MAX_FORCE = 0.15;
const PERCEPTION = 50;
const SEP_DIST = 25;

function createBoid(species) {
  const a = Math.random() * Math.PI * 2;
  return { x: Math.random() * canvas.width, y: Math.random() * canvas.height, vx: Math.cos(a) * 2, vy: Math.sin(a) * 2, species };
}

const boids = [
  ...Array.from({ length: 70 }, () => createBoid(0)),
  ...Array.from({ length: 70 }, () => createBoid(1)),
];

function limit(vx, vy, max) {
  const m = Math.sqrt(vx * vx + vy * vy);
  if (m > max) { const s = max / m; return [vx * s, vy * s]; }
  return [vx, vy];
}

function steer(boid) {
  let sx = 0, sy = 0, sc = 0;
  let ax = 0, ay = 0, ac = 0;
  let cx = 0, cy = 0, cc = 0;
  let ox = 0, oy = 0, oc = 0;

  for (const o of boids) {
    if (o === boid) continue;
    const dx = boid.x - o.x, dy = boid.y - o.y;
    const d = Math.sqrt(dx * dx + dy * dy);
    if (d < PERCEPTION) {
      if (o.species === boid.species) {
        ax += o.vx; ay += o.vy; ac++;
        cx += o.x; cy += o.y; cc++;
        if (d < SEP_DIST && d > 0) { sx += dx / d; sy += dy / d; sc++; }
      } else {
        if (d < SEP_DIST * 1.8 && d > 0) { ox += dx / d; oy += dy / d; oc++; }
      }
    }
  }

  let fx = 0, fy = 0;
  if (sc > 0) { sx /= sc; sy /= sc; const m = Math.sqrt(sx*sx+sy*sy); if(m>0){sx=sx/m*MAX_SPEED-boid.vx;sy=sy/m*MAX_SPEED-boid.vy;} [sx,sy]=limit(sx,sy,MAX_FORCE); fx+=sx*1.5; fy+=sy*1.5; }
  if (ac > 0) { ax /= ac; ay /= ac; const m = Math.sqrt(ax*ax+ay*ay); if(m>0){ax=ax/m*MAX_SPEED-boid.vx;ay=ay/m*MAX_SPEED-boid.vy;} [ax,ay]=limit(ax,ay,MAX_FORCE); fx+=ax; fy+=ay; }
  if (cc > 0) { cx=cx/cc-boid.x; cy=cy/cc-boid.y; const m = Math.sqrt(cx*cx+cy*cy); if(m>0){cx=cx/m*MAX_SPEED-boid.vx;cy=cy/m*MAX_SPEED-boid.vy;} [cx,cy]=limit(cx,cy,MAX_FORCE); fx+=cx; fy+=cy; }
  // avoid other species
  if (oc > 0) { ox /= oc; oy /= oc; const m = Math.sqrt(ox*ox+oy*oy); if(m>0){ox=ox/m*MAX_SPEED-boid.vx;oy=oy/m*MAX_SPEED-boid.vy;} [ox,oy]=limit(ox,oy,MAX_FORCE); fx+=ox*2; fy+=oy*2; }
  return [fx, fy];
}

const colors = [
  [160, 70, 55],
  [270, 65, 60],
];

function drawBoid(b) {
  const angle = Math.atan2(b.vy, b.vx);
  const [h, s, l] = colors[b.species];
  ctx.save(); ctx.translate(b.x, b.y); ctx.rotate(angle);
  ctx.fillStyle = `hsl(${h}, ${s}%, ${l}%)`;
  ctx.beginPath(); ctx.moveTo(8, 0); ctx.lineTo(-4, 3.5); ctx.lineTo(-4, -3.5); ctx.closePath(); ctx.fill();
  ctx.restore();
}

function update() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  for (const b of boids) {
    const [fx, fy] = steer(b);
    b.vx += fx; b.vy += fy;
    [b.vx, b.vy] = limit(b.vx, b.vy, MAX_SPEED);
    b.x += b.vx; b.y += b.vy;
    if (b.x < 0) b.x += canvas.width; if (b.x > canvas.width) b.x -= canvas.width;
    if (b.y < 0) b.y += canvas.height; if (b.y > canvas.height) b.y -= canvas.height;
    drawBoid(b);
  }
  ctx.fillStyle = 'rgba(110,230,180,0.4)';
  ctx.font = '12px monospace';
  ctx.fillText('2 species: teal flocks with teal, violet with violet', 15, canvas.height - 12);
  requestAnimationFrame(update);
}
requestAnimationFrame(update);

4. Obstacle Avoidance

Boids navigate around circular obstacles using an additional steering force. When a boid detects an obstacle ahead, it steers perpendicular to its velocity to avoid collision. Click to add new obstacles.

const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 500;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

const MAX_SPEED = 3;
const MAX_FORCE = 0.15;
const PERCEPTION = 50;
const SEP_DIST = 25;
const OBS_MARGIN = 40;

const obstacles = [
  { x: 200, y: 200, r: 50 },
  { x: 450, y: 150, r: 35 },
  { x: 350, y: 350, r: 60 },
  { x: 550, y: 300, r: 40 },
];

canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect();
  obstacles.push({ x: e.clientX - rect.left, y: e.clientY - rect.top, r: 25 + Math.random() * 35 });
});

function createBoid() {
  const a = Math.random() * Math.PI * 2;
  let x, y, valid;
  do {
    x = Math.random() * canvas.width; y = Math.random() * canvas.height;
    valid = obstacles.every(o => Math.sqrt((x-o.x)**2+(y-o.y)**2) > o.r + 20);
  } while (!valid);
  return { x, y, vx: Math.cos(a) * 2, vy: Math.sin(a) * 2 };
}
const boids = Array.from({ length: 100 }, createBoid);

function limit(vx, vy, max) {
  const m = Math.sqrt(vx*vx+vy*vy);
  if (m > max) { const s = max / m; return [vx*s, vy*s]; }
  return [vx, vy];
}

function steer(boid) {
  let sx=0,sy=0,sc=0,ax=0,ay=0,ac=0,cx=0,cy=0,cc=0;
  for (const o of boids) {
    if (o === boid) continue;
    const dx=boid.x-o.x, dy=boid.y-o.y, d=Math.sqrt(dx*dx+dy*dy);
    if (d < PERCEPTION) {
      ax+=o.vx; ay+=o.vy; ac++;
      cx+=o.x; cy+=o.y; cc++;
      if (d < SEP_DIST && d > 0) { sx+=dx/d; sy+=dy/d; sc++; }
    }
  }
  let fx=0, fy=0;
  if(sc>0){sx/=sc;sy/=sc;const m=Math.sqrt(sx*sx+sy*sy);if(m>0){sx=sx/m*MAX_SPEED-boid.vx;sy=sy/m*MAX_SPEED-boid.vy;}[sx,sy]=limit(sx,sy,MAX_FORCE);fx+=sx*1.5;fy+=sy*1.5;}
  if(ac>0){ax/=ac;ay/=ac;const m=Math.sqrt(ax*ax+ay*ay);if(m>0){ax=ax/m*MAX_SPEED-boid.vx;ay=ay/m*MAX_SPEED-boid.vy;}[ax,ay]=limit(ax,ay,MAX_FORCE);fx+=ax;fy+=ay;}
  if(cc>0){cx=cx/cc-boid.x;cy=cy/cc-boid.y;const m=Math.sqrt(cx*cx+cy*cy);if(m>0){cx=cx/m*MAX_SPEED-boid.vx;cy=cy/m*MAX_SPEED-boid.vy;}[cx,cy]=limit(cx,cy,MAX_FORCE);fx+=cx;fy+=cy;}

  // obstacle avoidance
  for (const ob of obstacles) {
    const dx = boid.x - ob.x, dy = boid.y - ob.y;
    const d = Math.sqrt(dx * dx + dy * dy);
    const buffer = ob.r + OBS_MARGIN;
    if (d < buffer && d > 0) {
      const urgency = (buffer - d) / buffer;
      fx += (dx / d) * MAX_FORCE * 8 * urgency;
      fy += (dy / d) * MAX_FORCE * 8 * urgency;
    }
  }
  return [fx, fy];
}

function drawBoid(b) {
  const angle = Math.atan2(b.vy, b.vx);
  const speed = Math.sqrt(b.vx*b.vx+b.vy*b.vy);
  const hue = 150 + (speed/MAX_SPEED)*40;
  ctx.save(); ctx.translate(b.x, b.y); ctx.rotate(angle);
  ctx.fillStyle = `hsl(${hue}, 70%, 55%)`;
  ctx.beginPath(); ctx.moveTo(7, 0); ctx.lineTo(-3.5, 3); ctx.lineTo(-3.5, -3); ctx.closePath(); ctx.fill();
  ctx.restore();
}

function update() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // draw obstacles
  for (const ob of obstacles) {
    ctx.fillStyle = 'rgba(110,230,180,0.06)';
    ctx.beginPath(); ctx.arc(ob.x, ob.y, ob.r, 0, Math.PI * 2); ctx.fill();
    ctx.strokeStyle = 'rgba(110,230,180,0.25)';
    ctx.lineWidth = 1.5;
    ctx.beginPath(); ctx.arc(ob.x, ob.y, ob.r, 0, Math.PI * 2); ctx.stroke();
  }

  for (const b of boids) {
    const [fx, fy] = steer(b);
    b.vx += fx; b.vy += fy;
    [b.vx, b.vy] = limit(b.vx, b.vy, MAX_SPEED);
    b.x += b.vx; b.y += b.vy;
    if (b.x < 0) b.x += canvas.width; if (b.x > canvas.width) b.x -= canvas.width;
    if (b.y < 0) b.y += canvas.height; if (b.y > canvas.height) b.y -= canvas.height;
    drawBoid(b);
  }
  ctx.fillStyle = 'rgba(110,230,180,0.4)';
  ctx.font = '12px monospace';
  ctx.fillText('click to add obstacles  |  boids steer around them', 15, canvas.height - 12);
  requestAnimationFrame(update);
}
requestAnimationFrame(update);

5. 3D Perspective Boids

Boids flying in a 3D space projected onto the 2D canvas with perspective. Depth is simulated with size scaling and opacity — closer boids appear larger and brighter. The flock moves through a virtual volume, wheeling and turning in three dimensions.

const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 500;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

const NUM = 150;
const MAX_SPEED = 2.5;
const MAX_FORCE = 0.1;
const PERCEPTION = 60;
const SEP_DIST = 25;
const DEPTH = 600;
const FOV = 400;

function createBoid() {
  const a = Math.random() * Math.PI * 2;
  const b = Math.random() * Math.PI * 2;
  return {
    x: (Math.random() - 0.5) * 500,
    y: (Math.random() - 0.5) * 400,
    z: Math.random() * DEPTH,
    vx: Math.cos(a) * 1.5,
    vy: Math.sin(a) * 1.5,
    vz: Math.sin(b) * 1,
  };
}
const boids = Array.from({ length: NUM }, createBoid);

function limit3(vx, vy, vz, max) {
  const m = Math.sqrt(vx*vx+vy*vy+vz*vz);
  if (m > max) { const s = max/m; return [vx*s, vy*s, vz*s]; }
  return [vx, vy, vz];
}

function steer(boid) {
  let sx=0,sy=0,sz=0,sc=0;
  let ax=0,ay=0,az=0,ac=0;
  let cx=0,cy=0,cz=0,cc=0;
  for (const o of boids) {
    if (o === boid) continue;
    const dx=boid.x-o.x,dy=boid.y-o.y,dz=boid.z-o.z;
    const d = Math.sqrt(dx*dx+dy*dy+dz*dz);
    if (d < PERCEPTION) {
      ax+=o.vx;ay+=o.vy;az+=o.vz;ac++;
      cx+=o.x;cy+=o.y;cz+=o.z;cc++;
      if (d < SEP_DIST && d > 0) { sx+=dx/d;sy+=dy/d;sz+=dz/d;sc++; }
    }
  }
  let fx=0,fy=0,fz=0;
  if(sc>0){sx/=sc;sy/=sc;sz/=sc;const m=Math.sqrt(sx*sx+sy*sy+sz*sz);if(m>0){sx=sx/m*MAX_SPEED-boid.vx;sy=sy/m*MAX_SPEED-boid.vy;sz=sz/m*MAX_SPEED-boid.vz;}[sx,sy,sz]=limit3(sx,sy,sz,MAX_FORCE);fx+=sx*1.5;fy+=sy*1.5;fz+=sz*1.5;}
  if(ac>0){ax/=ac;ay/=ac;az/=ac;const m=Math.sqrt(ax*ax+ay*ay+az*az);if(m>0){ax=ax/m*MAX_SPEED-boid.vx;ay=ay/m*MAX_SPEED-boid.vy;az=az/m*MAX_SPEED-boid.vz;}[ax,ay,az]=limit3(ax,ay,az,MAX_FORCE);fx+=ax;fy+=ay;fz+=az;}
  if(cc>0){cx=cx/cc-boid.x;cy=cy/cc-boid.y;cz=cz/cc-boid.z;const m=Math.sqrt(cx*cx+cy*cy+cz*cz);if(m>0){cx=cx/m*MAX_SPEED-boid.vx;cy=cy/m*MAX_SPEED-boid.vy;cz=cz/m*MAX_SPEED-boid.vz;}[cx,cy,cz]=limit3(cx,cy,cz,MAX_FORCE);fx+=cx;fy+=cy;fz+=cz;}
  return [fx,fy,fz];
}

function project(x, y, z) {
  const scale = FOV / (FOV + z);
  return { px: canvas.width/2 + x * scale, py: canvas.height/2 + y * scale, s: scale };
}

function update() {
  ctx.fillStyle = 'rgba(10,14,23,0.25)';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  for (const b of boids) {
    const [fx,fy,fz] = steer(b);
    b.vx+=fx;b.vy+=fy;b.vz+=fz;
    [b.vx,b.vy,b.vz] = limit3(b.vx,b.vy,b.vz, MAX_SPEED);
    b.x+=b.vx;b.y+=b.vy;b.z+=b.vz;
    // wrap in 3d volume
    if(b.x<-250) b.x+=500; if(b.x>250) b.x-=500;
    if(b.y<-200) b.y+=400; if(b.y>200) b.y-=400;
    if(b.z<0) b.z+=DEPTH; if(b.z>DEPTH) b.z-=DEPTH;
  }

  // sort by depth for painter's algorithm
  const sorted = [...boids].sort((a,b) => b.z - a.z);
  for (const b of sorted) {
    const { px, py, s } = project(b.x, b.y, b.z);
    const size = Math.max(2, 6 * s);
    const alpha = 0.2 + 0.8 * s;
    const hue = 150 + b.z / DEPTH * 50;
    const angle = Math.atan2(b.vy, b.vx);
    ctx.save(); ctx.translate(px, py); ctx.rotate(angle);
    ctx.fillStyle = `hsla(${hue}, 70%, 55%, ${alpha})`;
    ctx.beginPath(); ctx.moveTo(size * 1.3, 0); ctx.lineTo(-size * 0.7, size * 0.5); ctx.lineTo(-size * 0.7, -size * 0.5); ctx.closePath(); ctx.fill();
    ctx.restore();
  }
  ctx.fillStyle = 'rgba(110,230,180,0.4)';
  ctx.font = '12px monospace';
  ctx.fillText(`3D boids: ${NUM} agents  |  depth-sorted perspective`, 15, canvas.height - 12);
  requestAnimationFrame(update);
}
requestAnimationFrame(update);

6. Interactive Boids

Move the mouse to interact with the flock. Left-click to attract boids to the cursor; right-click to repel them. Without clicking, the mouse gently repels nearby boids. The interaction force falls off with distance for natural-looking responses.

const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 500;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

const NUM = 130;
const MAX_SPEED = 3.5;
const MAX_FORCE = 0.15;
const PERCEPTION = 50;
const SEP_DIST = 25;
const MOUSE_RADIUS = 120;

let mouse = { x: canvas.width/2, y: canvas.height/2, active: false, attract: false };

canvas.addEventListener('mousemove', (e) => {
  const r = canvas.getBoundingClientRect();
  mouse.x = e.clientX - r.left; mouse.y = e.clientY - r.top;
});
canvas.addEventListener('mousedown', (e) => {
  e.preventDefault();
  mouse.active = true;
  mouse.attract = e.button === 0;
});
canvas.addEventListener('mouseup', () => { mouse.active = false; });
canvas.addEventListener('contextmenu', (e) => e.preventDefault());

function createBoid() {
  const a = Math.random() * Math.PI * 2;
  return { x: Math.random() * canvas.width, y: Math.random() * canvas.height, vx: Math.cos(a)*2, vy: Math.sin(a)*2 };
}
const boids = Array.from({ length: NUM }, createBoid);

function limit(vx, vy, max) {
  const m = Math.sqrt(vx*vx+vy*vy);
  if (m > max) { const s = max/m; return [vx*s, vy*s]; }
  return [vx, vy];
}

function steer(boid) {
  let sx=0,sy=0,sc=0,ax=0,ay=0,ac=0,cx=0,cy=0,cc=0;
  for (const o of boids) {
    if (o === boid) continue;
    const dx=boid.x-o.x,dy=boid.y-o.y,d=Math.sqrt(dx*dx+dy*dy);
    if (d < PERCEPTION) {
      ax+=o.vx;ay+=o.vy;ac++;
      cx+=o.x;cy+=o.y;cc++;
      if(d0){sx+=dx/d;sy+=dy/d;sc++;}
    }
  }
  let fx=0,fy=0;
  if(sc>0){sx/=sc;sy/=sc;const m=Math.sqrt(sx*sx+sy*sy);if(m>0){sx=sx/m*MAX_SPEED-boid.vx;sy=sy/m*MAX_SPEED-boid.vy;}[sx,sy]=limit(sx,sy,MAX_FORCE);fx+=sx*1.5;fy+=sy*1.5;}
  if(ac>0){ax/=ac;ay/=ac;const m=Math.sqrt(ax*ax+ay*ay);if(m>0){ax=ax/m*MAX_SPEED-boid.vx;ay=ay/m*MAX_SPEED-boid.vy;}[ax,ay]=limit(ax,ay,MAX_FORCE);fx+=ax;fy+=ay;}
  if(cc>0){cx=cx/cc-boid.x;cy=cy/cc-boid.y;const m=Math.sqrt(cx*cx+cy*cy);if(m>0){cx=cx/m*MAX_SPEED-boid.vx;cy=cy/m*MAX_SPEED-boid.vy;}[cx,cy]=limit(cx,cy,MAX_FORCE);fx+=cx;fy+=cy;}

  // mouse interaction
  const mdx = boid.x - mouse.x, mdy = boid.y - mouse.y;
  const md = Math.sqrt(mdx*mdx + mdy*mdy);
  if (md < MOUSE_RADIUS && md > 0) {
    const str = (MOUSE_RADIUS - md) / MOUSE_RADIUS;
    if (mouse.active && mouse.attract) {
      fx -= (mdx/md) * MAX_FORCE * 5 * str;
      fy -= (mdy/md) * MAX_FORCE * 5 * str;
    } else {
      const power = mouse.active ? 8 : 2;
      fx += (mdx/md) * MAX_FORCE * power * str;
      fy += (mdy/md) * MAX_FORCE * power * str;
    }
  }
  return [fx, fy];
}

function drawBoid(b) {
  const angle = Math.atan2(b.vy, b.vx);
  const md = Math.sqrt((b.x-mouse.x)**2 + (b.y-mouse.y)**2);
  const hue = md < MOUSE_RADIUS ? (mouse.active && mouse.attract ? 60 : 180) : 155;
  const lightness = md < MOUSE_RADIUS ? 60 : 50;
  ctx.save(); ctx.translate(b.x, b.y); ctx.rotate(angle);
  ctx.fillStyle = `hsl(${hue}, 70%, ${lightness}%)`;
  ctx.beginPath(); ctx.moveTo(8, 0); ctx.lineTo(-4, 3.5); ctx.lineTo(-4, -3.5); ctx.closePath(); ctx.fill();
  ctx.restore();
}

function update() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // mouse cursor indicator
  if (mouse.active) {
    ctx.strokeStyle = mouse.attract ? 'rgba(230,200,60,0.3)' : 'rgba(230,80,80,0.3)';
    ctx.lineWidth = 1;
    ctx.beginPath(); ctx.arc(mouse.x, mouse.y, MOUSE_RADIUS, 0, Math.PI*2); ctx.stroke();
  }

  for (const b of boids) {
    const [fx, fy] = steer(b);
    b.vx += fx; b.vy += fy;
    [b.vx, b.vy] = limit(b.vx, b.vy, MAX_SPEED);
    b.x += b.vx; b.y += b.vy;
    if(b.x<0) b.x+=canvas.width; if(b.x>canvas.width) b.x-=canvas.width;
    if(b.y<0) b.y+=canvas.height; if(b.y>canvas.height) b.y-=canvas.height;
    drawBoid(b);
  }
  ctx.fillStyle = 'rgba(110,230,180,0.4)';
  ctx.font = '12px monospace';
  ctx.fillText('move mouse: gentle repel  |  left-click: attract  |  right-click: repel', 15, canvas.height - 12);
  requestAnimationFrame(update);
}
requestAnimationFrame(update);

7. Boids with Trails

Each boid leaves a fading trail behind it, revealing the flow patterns of the flock. The trails make the invisible steering forces visible — you can see how boids curve around each other, how the flock carves arcs through space, and how individual paths weave into the collective motion.

const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 500;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

const NUM = 80;
const MAX_SPEED = 2.8;
const MAX_FORCE = 0.12;
const PERCEPTION = 55;
const SEP_DIST = 25;
const TRAIL_LEN = 20;

function createBoid(i) {
  const a = Math.random() * Math.PI * 2;
  const x = Math.random() * canvas.width;
  const y = Math.random() * canvas.height;
  return { x, y, vx: Math.cos(a)*2, vy: Math.sin(a)*2, hue: 140 + (i/NUM)*60, trail: [] };
}
const boids = Array.from({ length: NUM }, (_, i) => createBoid(i));

function limit(vx, vy, max) {
  const m = Math.sqrt(vx*vx+vy*vy);
  if (m > max) { const s = max/m; return [vx*s, vy*s]; }
  return [vx, vy];
}

function steer(boid) {
  let sx=0,sy=0,sc=0,ax=0,ay=0,ac=0,cx=0,cy=0,cc=0;
  for (const o of boids) {
    if (o === boid) continue;
    const dx=boid.x-o.x,dy=boid.y-o.y,d=Math.sqrt(dx*dx+dy*dy);
    if (d < PERCEPTION) {
      ax+=o.vx;ay+=o.vy;ac++;
      cx+=o.x;cy+=o.y;cc++;
      if(d0){sx+=dx/d;sy+=dy/d;sc++;}
    }
  }
  let fx=0,fy=0;
  if(sc>0){sx/=sc;sy/=sc;const m=Math.sqrt(sx*sx+sy*sy);if(m>0){sx=sx/m*MAX_SPEED-boid.vx;sy=sy/m*MAX_SPEED-boid.vy;}[sx,sy]=limit(sx,sy,MAX_FORCE);fx+=sx*1.5;fy+=sy*1.5;}
  if(ac>0){ax/=ac;ay/=ac;const m=Math.sqrt(ax*ax+ay*ay);if(m>0){ax=ax/m*MAX_SPEED-boid.vx;ay=ay/m*MAX_SPEED-boid.vy;}[ax,ay]=limit(ax,ay,MAX_FORCE);fx+=ax;fy+=ay;}
  if(cc>0){cx=cx/cc-boid.x;cy=cy/cc-boid.y;const m=Math.sqrt(cx*cx+cy*cy);if(m>0){cx=cx/m*MAX_SPEED-boid.vx;cy=cy/m*MAX_SPEED-boid.vy;}[cx,cy]=limit(cx,cy,MAX_FORCE);fx+=cx;fy+=cy;}
  return [fx, fy];
}

function update() {
  ctx.fillStyle = 'rgba(10,14,23,0.12)';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  for (const b of boids) {
    const [fx, fy] = steer(b);
    b.vx += fx; b.vy += fy;
    [b.vx, b.vy] = limit(b.vx, b.vy, MAX_SPEED);
    b.x += b.vx; b.y += b.vy;
    if(b.x<0) b.x+=canvas.width; if(b.x>canvas.width) b.x-=canvas.width;
    if(b.y<0) b.y+=canvas.height; if(b.y>canvas.height) b.y-=canvas.height;

    b.trail.push({ x: b.x, y: b.y });
    if (b.trail.length > TRAIL_LEN) b.trail.shift();

    // draw trail
    if (b.trail.length > 1) {
      for (let i = 1; i < b.trail.length; i++) {
        const prev = b.trail[i-1], curr = b.trail[i];
        // skip wrapping segments
        if (Math.abs(curr.x-prev.x) > canvas.width/2 || Math.abs(curr.y-prev.y) > canvas.height/2) continue;
        const alpha = (i / b.trail.length) * 0.5;
        const width = (i / b.trail.length) * 2.5;
        ctx.strokeStyle = `hsla(${b.hue}, 70%, 55%, ${alpha})`;
        ctx.lineWidth = width;
        ctx.beginPath(); ctx.moveTo(prev.x, prev.y); ctx.lineTo(curr.x, curr.y); ctx.stroke();
      }
    }

    // draw boid head
    const angle = Math.atan2(b.vy, b.vx);
    ctx.save(); ctx.translate(b.x, b.y); ctx.rotate(angle);
    ctx.fillStyle = `hsl(${b.hue}, 80%, 65%)`;
    ctx.beginPath(); ctx.moveTo(6, 0); ctx.lineTo(-3, 2.5); ctx.lineTo(-3, -2.5); ctx.closePath(); ctx.fill();
    ctx.restore();
  }

  ctx.fillStyle = 'rgba(110,230,180,0.4)';
  ctx.font = '12px monospace';
  ctx.fillText(`${NUM} boids with motion trails  |  revealing flock flow patterns`, 15, canvas.height - 12);
  requestAnimationFrame(update);
}
requestAnimationFrame(update);

8. Generative Boid Art

Boids as painters: each boid deposits a subtle, semi-transparent mark at every step, and over time the flock's collective motion paints an organic, flowing composition. The canvas never clears — the image builds up over time, creating layered patterns that reveal the hidden structure of flocking behavior. Click to reset the canvas.

const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 500;
canvas.style.background = '#0a0e17';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

// fill initial background
ctx.fillStyle = '#0a0e17';
ctx.fillRect(0, 0, canvas.width, canvas.height);

const NUM = 60;
const MAX_SPEED = 2;
const MAX_FORCE = 0.08;
const PERCEPTION = 60;
const SEP_DIST = 25;

let time = 0;

function createBoid(i) {
  const a = Math.random() * Math.PI * 2;
  return {
    x: Math.random() * canvas.width,
    y: Math.random() * canvas.height,
    vx: Math.cos(a) * 1.5,
    vy: Math.sin(a) * 1.5,
    hue: 140 + (i / NUM) * 60,
    prevX: 0, prevY: 0,
  };
}
let boids = Array.from({ length: NUM }, (_, i) => createBoid(i));

canvas.addEventListener('click', () => {
  ctx.fillStyle = '#0a0e17';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  boids = Array.from({ length: NUM }, (_, i) => createBoid(i));
  time = 0;
});

function limit(vx, vy, max) {
  const m = Math.sqrt(vx*vx+vy*vy);
  if (m > max) { const s = max/m; return [vx*s, vy*s]; }
  return [vx, vy];
}

function steer(boid) {
  let sx=0,sy=0,sc=0,ax=0,ay=0,ac=0,cx=0,cy=0,cc=0;
  for (const o of boids) {
    if (o === boid) continue;
    const dx=boid.x-o.x,dy=boid.y-o.y,d=Math.sqrt(dx*dx+dy*dy);
    if (d < PERCEPTION) {
      ax+=o.vx;ay+=o.vy;ac++;
      cx+=o.x;cy+=o.y;cc++;
      if(d0){sx+=dx/d;sy+=dy/d;sc++;}
    }
  }
  let fx=0,fy=0;
  if(sc>0){sx/=sc;sy/=sc;const m=Math.sqrt(sx*sx+sy*sy);if(m>0){sx=sx/m*MAX_SPEED-boid.vx;sy=sy/m*MAX_SPEED-boid.vy;}[sx,sy]=limit(sx,sy,MAX_FORCE);fx+=sx*1.5;fy+=sy*1.5;}
  if(ac>0){ax/=ac;ay/=ac;const m=Math.sqrt(ax*ax+ay*ay);if(m>0){ax=ax/m*MAX_SPEED-boid.vx;ay=ay/m*MAX_SPEED-boid.vy;}[ax,ay]=limit(ax,ay,MAX_FORCE);fx+=ax;fy+=ay;}
  if(cc>0){cx=cx/cc-boid.x;cy=cy/cc-boid.y;const m=Math.sqrt(cx*cx+cy*cy);if(m>0){cx=cx/m*MAX_SPEED-boid.vx;cy=cy/m*MAX_SPEED-boid.vy;}[cx,cy]=limit(cx,cy,MAX_FORCE);fx+=cx;fy+=cy;}

  // gentle drift force that slowly rotates over time
  const drift = time * 0.0003;
  fx += Math.cos(drift + boid.y * 0.005) * 0.02;
  fy += Math.sin(drift + boid.x * 0.005) * 0.02;
  return [fx, fy];
}

function update() {
  time++;

  for (const b of boids) {
    b.prevX = b.x; b.prevY = b.y;
    const [fx, fy] = steer(b);
    b.vx += fx; b.vy += fy;
    [b.vx, b.vy] = limit(b.vx, b.vy, MAX_SPEED);
    b.x += b.vx; b.y += b.vy;

    // soft boundary steering instead of wrapping
    const margin = 40;
    if (b.x < margin) b.vx += 0.2;
    if (b.x > canvas.width - margin) b.vx -= 0.2;
    if (b.y < margin) b.vy += 0.2;
    if (b.y > canvas.height - margin) b.vy -= 0.2;

    // paint stroke
    const speed = Math.sqrt(b.vx*b.vx + b.vy*b.vy);
    const hueShift = Math.sin(time * 0.002 + b.hue * 0.1) * 20;
    const hue = b.hue + hueShift;
    const alpha = 0.015 + speed / MAX_SPEED * 0.025;
    const width = 1 + speed / MAX_SPEED * 2;

    // skip wrapping strokes
    if (Math.abs(b.x - b.prevX) < 100 && Math.abs(b.y - b.prevY) < 100) {
      ctx.strokeStyle = `hsla(${hue}, 65%, 55%, ${alpha})`;
      ctx.lineWidth = width;
      ctx.lineCap = 'round';
      ctx.beginPath();
      ctx.moveTo(b.prevX, b.prevY);
      ctx.lineTo(b.x, b.y);
      ctx.stroke();

      // subtle glow dot at current position
      ctx.fillStyle = `hsla(${hue}, 80%, 70%, ${alpha * 0.5})`;
      ctx.beginPath();
      ctx.arc(b.x, b.y, width * 0.8, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  // text overlay refreshed periodically
  if (time % 120 === 0) {
    ctx.fillStyle = 'rgba(10,14,23,0.8)';
    ctx.fillRect(0, canvas.height - 25, canvas.width, 25);
  }
  if (time % 120 < 2) {
    ctx.fillStyle = 'rgba(110,230,180,0.4)';
    ctx.font = '12px monospace';
    ctx.fillText('generative boid art  |  painting accumulates over time  |  click to reset', 15, canvas.height - 8);
  }
  requestAnimationFrame(update);
}
requestAnimationFrame(update);

Performance Tips for Boid Simulations

  • The naive approach is O(n²) — every boid checks every other boid. For 100-200 boids this is fine at 60fps. For thousands, use a spatial hash grid or quadtree to only check nearby cells
  • Tune perception radius carefully — smaller radius means fewer neighbor checks and faster code, but also less coordinated flocking. 40-60 pixels is the sweet spot for most visual demos
  • Limit maximum steering force, not just speed — this creates smooth, natural-looking turns instead of jerky direction changes
  • Wrap-around edges are the simplest boundary, but soft steering forces near edges (as in the art example) look more organic
  • For trails, use canvas alpha compositing — drawing a semi-transparent background rectangle each frame is cheaper than storing and rendering point arrays

Craig Reynolds' boids algorithm is one of the purest demonstrations of emergence in computer science. Three trivial rules, applied locally, produce collective intelligence that looks alive. The same principle drives cellular automata, particle systems, and genetic algorithms — simple rules, complex beauty. Explore more creative coding tutorials on the Lumitree blog, or visit the tree to discover generative micro-worlds grown from visitor seeds.

Related articles