Skip to content

48 agents · fixed boundaries · separation · alignment · cohesion

Each agent responds only to nearby agents. No agent has knowledge of the overall system.

(function () { const canvas = document.getElementById(“igor-canvas”); const ctx = canvas.getContext(“2d”); const pauseButton = document.getElementById(“igor-pause”); const resetButton = document.getElementById(“igor-reset”); const NUMBER_OF_AGENTS = 48; const HEIGHT = 430; const MARGIN = 6; let agents = []; let running = true; let animationFrame; let lastFrame = 0; function resizeCanvas() { const ratio = Math.min(window.devicePixelRatio || 1, 2); const width = canvas.getBoundingClientRect().width; canvas.width = width * ratio; canvas.height = HEIGHT * ratio; ctx.setTransform(ratio, 0, 0, ratio, 0, 0); } function createAgents() { const width = canvas.getBoundingClientRect().width; agents = []; for (let i = 0; i < NUMBER_OF_AGENTS; i++) { const angle = Math.random() * Math.PI * 2; const speed = 1.35 + Math.random() * 0.65; agents.push({ x: MARGIN + Math.random() * (width – MARGIN * 2), y: MARGIN + Math.random() * (HEIGHT – MARGIN * 2), vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed }); } } function updateAgents() { const width = canvas.getBoundingClientRect().width; const next = []; for (let i = 0; i < agents.length; i++) { const agent = agents[i]; let separateX = 0; let separateY = 0; let alignmentX = 0; let alignmentY = 0; let cohesionX = 0; let cohesionY = 0; let neighbours = 0; for (let j = 0; j < agents.length; j++) { if (i === j) continue; const other = agents[j]; const dx = other.x – agent.x; const dy = other.y – agent.y; const distance = Math.hypot(dx, dy); if (distance < 62) { neighbours++; alignmentX += other.vx; alignmentY += other.vy; cohesionX += dx; cohesionY += dy; if (distance 0.01) { separateX -= dx / distance; separateY -= dy / distance; } } } let vx = agent.vx; let vy = agent.vy; if (neighbours > 0) { vx += separateX * 0.045; vy += separateY * 0.045; vx += ((alignmentX / neighbours) – vx) * 0.014; vy += ((alignmentY / neighbours) – vy) * 0.014; vx += (cohesionX / neighbours) * 0.00055; vy += (cohesionY / neighbours) * 0.00055; } /* Tiny amount of variation prevents mechanical locking */ vx += (Math.random() – 0.5) * 0.018; vy += (Math.random() – 0.5) * 0.018; let speed = Math.hypot(vx, vy); const maximumSpeed = 2.0; const minimumSpeed = 1.05; if (speed > maximumSpeed) { vx = vx / speed * maximumSpeed; vy = vy / speed * maximumSpeed; } if (speed < minimumSpeed) { vx = vx / speed * minimumSpeed; vy = vy / speed * minimumSpeed; } let x = agent.x + vx; let y = agent.y + vy; /* FIXED BOUNDARIES */ if (x = width – MARGIN) { x = width – MARGIN; vx = -Math.abs(vx); } if (y = HEIGHT – MARGIN) { y = HEIGHT – MARGIN; vy = -Math.abs(vy); } next.push({ x, y, vx, vy }); } agents = next; } function drawAgents() { const width = canvas.getBoundingClientRect().width; ctx.fillStyle = “#080a08”; ctx.fillRect(0, 0, width, HEIGHT); ctx.fillStyle = “#d7e6d4”; for (const agent of agents) { ctx.fillRect( Math.round(agent.x) – 1, Math.round(agent.y) – 1, 3, 3 ); } } function animate(time) { if (!running) return; if (time – lastFrame > 22) { updateAgents(); drawAgents(); lastFrame = time; } animationFrame = requestAnimationFrame(animate); } function start() { running = true; pauseButton.textContent = “Pause”; cancelAnimationFrame(animationFrame); animationFrame = requestAnimationFrame(animate); } pauseButton.addEventListener(“click”, function () { if (running) { running = false; cancelAnimationFrame(animationFrame); pauseButton.textContent = “Play”; } else { start(); } }); resetButton.addEventListener(“click”, function () { createAgents(); drawAgents(); if (!running) start(); }); window.addEventListener(“resize”, function () { resizeCanvas(); createAgents(); drawAgents(); }); resizeCanvas(); createAgents(); drawAgents(); start(); })();