48 agents · fixed boundaries · separation · alignment · cohesion
Pause
New random start
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();
})();