Playing around with Boids-style flocking. Click to add more, coloring by movement colors each boid by a moving average of its acceleration magnitude.
See also: Particle tentacles
More boids:
boids by Hugh Kennedy
boids-canvas by Mike Christensen
boids demo by Anoop Elias
flocking in Processing by Daniel Shiffman
forked from veltman's block: Flocking faces
xxxxxxxxxx
<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 = 400,
flockmateRadius = 107,
separationDistance = 50,
maxVelocity = 2,
separationForce = 0.11,
alignmentForce = 0.07,
cohesionForce = 0.08,
startingPosition = "Phyllotaxis",
coloring = "By Movement",
boids;
offscreenContext.globalAlpha = 0.65;
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");
var drawings;
var line = d3.line()
.x(function(d) { return d.x })
.y(function(d) { return d.y })
.curve(d3.curveBasis)
.context(context)
d3.json("complex-faces.json", function(err, data) {
drawings = data.map(function(d) {
d.strokes = d.drawing.map(function(s) {
var points = []
s[0].forEach(function(x,i) {
points.push({x: x, y: s[1][i] })
})
return points;
})
return d;
})
// console.log("DRAW", drawings)
restart();
requestAnimationFrame(tick);
})
function tick() {
if(!drawings) return;
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,i){
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, i) {
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;
}
if(!drawings) return;
context.save()
context.beginPath();
if (coloring === "Rainbow") {
// context.fillStyle = b.color;
context.strokeStyle = b.color;
} else {
// context.fillStyle = d3.interpolateWarm(d3.mean(b.last));
context.strokeStyle = d3.interpolateWarm(d3.mean(b.last));
}
// context.arc(b.position.x, b.position.y, 2, 0, 2 * Math.PI);
context.lineWidth = 5
context.translate(b.position.x, b.position.y)
context.scale(0.1, 0.1)
drawings[i].strokes.forEach(function(s) {
line(s)
})
context.stroke();
context.restore()
}
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>
https://d3js.org/d3.v4.min.js
https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.5/dat.gui.min.js