Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active July 24, 2022 18:42
  • Star 5 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save veltman/995d3a677418100ac43877f3ed1cc728 to your computer and use it in GitHub Desktop.
Flocking boids
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body>
<canvas width="960" height="500"></canvas>
<canvas width="960" height="500" class="offscreen" style="display: none;"></canvas>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.5/dat.gui.min.js"></script>
<script src="vec2.js"></script>
<script>
var canvas = document.querySelector("canvas"),
context = canvas.getContext("2d"),
offscreen = document.querySelector(".offscreen"),
offscreenContext = offscreen.getContext("2d"),
gui = new dat.GUI();
var width = 960,
height = 500,
numBoids = 300,
flockmateRadius = 60,
separationDistance = 30,
maxVelocity = 2,
separationForce = 0.03,
alignmentForce = 0.03,
cohesionForce = 0.03,
startingPosition = "Random",
coloring = "By Movement",
boids;
offscreenContext.globalAlpha = 0.85;
gui.add(window, "flockmateRadius", 0, 500).step(1);
gui.add(window, "separationDistance", 0, 100).step(1);
gui.add(window, "maxVelocity", 0, 5).step(0.25);
gui.add(window, "cohesionForce", 0, 0.25);
gui.add(window, "alignmentForce", 0, 0.25);
gui.add(window, "separationForce", 0, 0.25);
gui.add(window, "numBoids", 1, 600).step(1).onChange(restart);
gui.add(window, "startingPosition", ["Random", "CircleIn", "CircleRandom", "Sine", "Phyllotaxis"]).onChange(restart);
gui.add(window, "coloring", ["Rainbow", "By Movement"]);
gui.add(window, "restart");
d3.select("canvas").on("click", function(){
var xy = d3.mouse(this);
boids.push({
color: d3.interpolateRainbow((boids.length / 10) % 1),
position: new Vec2(xy[0], xy[1]),
velocity: randomVelocity(),
last: []
});
});
restart();
requestAnimationFrame(tick);
function tick() {
offscreenContext.clearRect(0, 0, width, height);
offscreenContext.drawImage(canvas, 0, 0, width, height);
context.clearRect(0, 0, width, height);
context.drawImage(offscreen, 0, 0, width, height);
boids.forEach(function(b){
var forces = {
alignment: new Vec2(),
cohesion: new Vec2(),
separation: new Vec2()
};
b.acceleration = new Vec2();
boids.forEach(function(b2){
if (b === b2) return;
var diff = b2.position.clone().subtract(b.position),
distance = diff.length();
if (distance && distance < separationDistance) {
forces.separation.add(diff.clone().scaleTo(-1 / distance)).active = true;
}
if (distance < flockmateRadius) {
forces.cohesion.add(diff).active = true;
forces.alignment.add(b2.velocity).active = true;
}
});
for (var key in forces) {
if (forces[key].active) {
forces[key].scaleTo(maxVelocity)
.subtract(b.velocity)
.truncate(window[key + "Force"]);
b.acceleration.add(forces[key]);
}
}
if (coloring === "By Movement") {
b.last.push(b.acceleration.length() / (alignmentForce + cohesionForce + separationForce));
if (b.last.length > 20) {
b.last.shift();
}
}
});
boids.forEach(updateBoid);
requestAnimationFrame(tick);
}
function updateBoid(b) {
b.position.add(b.velocity.add(b.acceleration).truncate(maxVelocity));
if (b.position.y > height) {
b.position.y -= height;
} else if (b.position.y < 0) {
b.position.y += height;
}
if (b.position.x > width) {
b.position.x -= width;
} else if (b.position.x < 0) {
b.position.x += width;
}
context.beginPath();
if (coloring === "Rainbow") {
context.fillStyle = b.color;
} else {
context.fillStyle = d3.interpolateWarm(d3.mean(b.last));
}
context.arc(b.position.x, b.position.y, 2, 0, 2 * Math.PI);
context.fill();
}
function initializeRandom() {
return d3.range(numBoids).map(function(d, i){
return {
position: new Vec2(Math.random() * width, Math.random() * height),
velocity: randomVelocity()
};
});
}
function initializePhyllotaxis() {
return d3.range(numBoids).map(function(d, i){
var θ = Math.PI * i * (Math.sqrt(5) - 1),
r = Math.sqrt(i) * 200 / Math.sqrt(numBoids);
return {
position: new Vec2(width / 2 + r * Math.cos(θ),height / 2 - r * Math.sin(θ)),
velocity: radialVelocity(i / numBoids)
};
});
}
function initializeSine() {
return d3.range(numBoids).map(function(i){
var angle = 2 * Math.PI * i / numBoids,
x = width * i / numBoids,
y = height / 2 + Math.sin(angle) * height / 4;
return {
position: new Vec2(x, y),
velocity: radialVelocity(i / numBoids)
};
});
}
function initializeCircleIn() {
return d3.range(numBoids).map(function(i){
var angle = i * 2 * Math.PI / numBoids,
x = 200 * Math.sin(angle),
y = 200 * Math.cos(angle);
return {
position: new Vec2(x + width / 2, y + height / 2),
velocity: new Vec2(-x, -y).scale(maxVelocity)
};
});
}
function initializeCircleRandom() {
return d3.range(numBoids).map(function(i){
var angle = i * 2 * Math.PI / numBoids,
x = 200 * Math.sin(angle),
y = 200 * Math.cos(angle);
return {
position: new Vec2(x + width / 2, y + height / 2),
velocity: randomVelocity().scale(maxVelocity)
};
});
}
function randomVelocity() {
return new Vec2(1 - Math.random() * 2, 1 - Math.random() * 2).scale(maxVelocity);
}
function radialVelocity(p) {
return new Vec2(Math.sin(2 * Math.PI * p), Math.cos(2 * Math.PI * p)).scale(maxVelocity);
}
function restart() {
offscreenContext.clearRect(0, 0, width, height);
context.clearRect(0, 0, width, height);
boids = window["initialize" + startingPosition]();
boids.forEach(function(b, i){
b.color = d3.interpolateRainbow(i / numBoids);
b.last = [];
});
}
</script>
function Vec2(x, y) {
this.x = x || 0;
this.y = y || 0;
this.count = 0;
return this;
}
Vec2.prototype.add = function(v) {
this.x += v.x;
this.y += v.y;
return this;
};
Vec2.prototype.subtract = function(v) {
this.x -= v.x;
this.y -= v.y;
return this;
};
Vec2.prototype.scale = function(s) {
this.x = this.x * s;
this.y = this.y * s;
return this;
};
Vec2.prototype.scaleTo = function(s) {
var length = this.length();
this.x = this.x * s / length;
this.y = this.y * s / length;
return this;
};
Vec2.prototype.normalize = function() {
var length = this.length();
this.x = this.x / length;
this.y = this.y / length;
return this;
};
Vec2.prototype.length = function() {
return Math.sqrt(this.x * this.x + this.y * this.y);
};
Vec2.prototype.truncate = function(max) {
var length = this.length();
if (length > max) {
this.x = this.x * max / length;
this.y = this.y * max / length;
}
return this;
};
Vec2.prototype.dot = function(v) {
return this.x * v.x + this.y * v.y;
};
Vec2.prototype.clone = function() {
return new Vec2(this.x, this.y);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment