2020-05-08 15:42:15 +00:00
|
|
|
|
import {Vector} from './geometry.mjs';
|
|
|
|
|
|
|
|
|
|
// Outline of the shape of a boid when angled at 0 rad
|
|
|
|
|
const boidShape = [
|
|
|
|
|
new Vector(1, 0),
|
|
|
|
|
new Vector(-.5, .5),
|
|
|
|
|
new Vector(-.5, -.5),
|
|
|
|
|
new Vector(1, 0),
|
|
|
|
|
];
|
|
|
|
|
const boidShapeLength = boidShape.length;
|
|
|
|
|
|
|
|
|
|
class Boid
|
|
|
|
|
{
|
|
|
|
|
constructor(params, center)
|
|
|
|
|
{
|
|
|
|
|
this.params = params;
|
|
|
|
|
this.pos = center.clone();
|
|
|
|
|
this.vel = new Vector(Math.random() - 0.5, Math.random() - 0.5);
|
|
|
|
|
this.register = new Vector(0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
draw(ctx)
|
|
|
|
|
{
|
|
|
|
|
const transformed = this.register;
|
|
|
|
|
const angle = this.vel.angle();
|
|
|
|
|
let isFirst = true;
|
|
|
|
|
|
|
|
|
|
for (let j = 0; j < boidShapeLength; ++j)
|
|
|
|
|
{
|
|
|
|
|
transformed.x = boidShape[j].x;
|
|
|
|
|
transformed.y = boidShape[j].y;
|
|
|
|
|
transformed.rotate(angle).mul(this.params.radius).add(this.pos);
|
|
|
|
|
|
|
|
|
|
if (isFirst)
|
|
|
|
|
{
|
|
|
|
|
ctx.moveTo(transformed.x, transformed.y);
|
|
|
|
|
isFirst = false;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
ctx.lineTo(transformed.x, transformed.y);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-04 15:17:55 +00:00
|
|
|
|
|
2020-05-08 09:59:46 +00:00
|
|
|
|
class Boids
|
2020-05-04 15:17:55 +00:00
|
|
|
|
{
|
2020-05-08 15:42:15 +00:00
|
|
|
|
constructor(canvas, obstacles, params = {})
|
2020-05-08 09:59:46 +00:00
|
|
|
|
{
|
|
|
|
|
this.canvas = canvas;
|
|
|
|
|
this.ctx = canvas.getContext('2d');
|
|
|
|
|
|
|
|
|
|
this.params = Object.assign({
|
|
|
|
|
centerAccel: 0.075,
|
|
|
|
|
repelAccel: 0.8,
|
|
|
|
|
matchAccel: 0.15,
|
|
|
|
|
boundsAccel: 0.01,
|
|
|
|
|
|
|
|
|
|
maxSpeed: 300,
|
|
|
|
|
|
|
|
|
|
closeDist: 20,
|
|
|
|
|
visibleDist: 60,
|
|
|
|
|
|
|
|
|
|
radius: 10,
|
|
|
|
|
color: 'black',
|
2020-05-08 16:00:56 +00:00
|
|
|
|
debug: false,
|
2020-05-08 09:59:46 +00:00
|
|
|
|
}, params);
|
|
|
|
|
|
|
|
|
|
// Current width and height of the canvas
|
|
|
|
|
this.width = canvas.width;
|
|
|
|
|
this.height = canvas.height;
|
|
|
|
|
|
|
|
|
|
// Current center point of the canvas
|
2020-05-08 15:42:15 +00:00
|
|
|
|
this.center = null;
|
2020-05-08 09:59:46 +00:00
|
|
|
|
|
|
|
|
|
// Last time where the canvas was repainted
|
|
|
|
|
this.lastTime = null;
|
|
|
|
|
|
|
|
|
|
// List of active simulated boids
|
|
|
|
|
this.boids = [];
|
2020-05-08 15:42:15 +00:00
|
|
|
|
|
|
|
|
|
// List of obstacles
|
|
|
|
|
this.obstacles = obstacles;
|
2020-05-04 16:17:25 +00:00
|
|
|
|
|
2020-05-08 09:59:46 +00:00
|
|
|
|
// Vector registers used for holding temporary values
|
|
|
|
|
this.registers = [
|
|
|
|
|
new Vector(0, 0),
|
|
|
|
|
new Vector(0, 0),
|
|
|
|
|
new Vector(0, 0)
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// When the simulation is running, the ID of the next rAF request,
|
|
|
|
|
// otherwise, null
|
|
|
|
|
this.animationId = null;
|
|
|
|
|
|
2020-05-08 15:42:15 +00:00
|
|
|
|
this._afterResize();
|
2020-05-08 09:59:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Change the canvas’ dimensions */
|
|
|
|
|
resize(width, height)
|
2020-05-04 15:17:55 +00:00
|
|
|
|
{
|
2020-05-08 09:59:46 +00:00
|
|
|
|
this.canvas.width = width;
|
|
|
|
|
this.canvas.height = height;
|
2020-05-04 15:17:55 +00:00
|
|
|
|
|
2020-05-08 09:59:46 +00:00
|
|
|
|
this.width = width;
|
|
|
|
|
this.height = height;
|
2020-05-08 15:42:15 +00:00
|
|
|
|
|
|
|
|
|
this._afterResize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_afterResize()
|
|
|
|
|
{
|
|
|
|
|
this.center = new Vector(this.width / 2, this.height / 2);
|
|
|
|
|
this.ctx.resetTransform();
|
|
|
|
|
this.ctx.translate(this.center.x, this.center.y);
|
2020-05-08 09:59:46 +00:00
|
|
|
|
}
|
2020-05-04 16:17:25 +00:00
|
|
|
|
|
2020-05-08 09:59:46 +00:00
|
|
|
|
/** Introduce a new boid in the simulation. */
|
|
|
|
|
add(center)
|
|
|
|
|
{
|
2020-05-08 15:42:15 +00:00
|
|
|
|
this.boids.push(new Boid(this.params, center));
|
2020-05-08 09:59:46 +00:00
|
|
|
|
}
|
2020-05-04 15:17:55 +00:00
|
|
|
|
|
2020-05-08 09:59:46 +00:00
|
|
|
|
/** Start the simulation. */
|
|
|
|
|
start()
|
|
|
|
|
{
|
|
|
|
|
if (this.animationId !== null)
|
2020-05-04 15:17:55 +00:00
|
|
|
|
{
|
2020-05-08 09:59:46 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
2020-05-04 16:17:25 +00:00
|
|
|
|
|
2020-05-08 09:59:46 +00:00
|
|
|
|
this._step = this._step.bind(this);
|
|
|
|
|
this.animationId = requestAnimationFrame(this._step);
|
|
|
|
|
}
|
2020-05-04 16:17:25 +00:00
|
|
|
|
|
2020-05-08 09:59:46 +00:00
|
|
|
|
/** Pause the simulation. */
|
|
|
|
|
pause()
|
|
|
|
|
{
|
|
|
|
|
cancelAnimationFrame(this.animationId);
|
|
|
|
|
this.animationId = null;
|
|
|
|
|
this.lastTime = null;
|
|
|
|
|
}
|
2020-05-04 15:17:55 +00:00
|
|
|
|
|
2020-05-08 09:59:46 +00:00
|
|
|
|
/**
|
|
|
|
|
* @private
|
|
|
|
|
* Perform a time step of the simulation and update the canvas.
|
|
|
|
|
*/
|
|
|
|
|
_step(time)
|
|
|
|
|
{
|
|
|
|
|
if (!this.lastTime)
|
2020-05-04 15:17:55 +00:00
|
|
|
|
{
|
2020-05-08 09:59:46 +00:00
|
|
|
|
this.lastTime = time;
|
2020-05-04 15:17:55 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-08 09:59:46 +00:00
|
|
|
|
const delta = (time - this.lastTime) / 1000;
|
|
|
|
|
this.lastTime = time;
|
|
|
|
|
|
|
|
|
|
this._update(delta);
|
|
|
|
|
this._draw();
|
|
|
|
|
|
|
|
|
|
if (this.animationId !== null)
|
2020-05-04 17:03:50 +00:00
|
|
|
|
{
|
2020-05-08 09:59:46 +00:00
|
|
|
|
this.animationId = requestAnimationFrame(this._step);
|
2020-05-04 17:03:50 +00:00
|
|
|
|
}
|
2020-05-08 09:59:46 +00:00
|
|
|
|
}
|
2020-05-04 15:17:55 +00:00
|
|
|
|
|
2020-05-08 09:59:46 +00:00
|
|
|
|
/**
|
|
|
|
|
* @private
|
|
|
|
|
* Advance the simulation.
|
|
|
|
|
*/
|
|
|
|
|
_update(delta)
|
|
|
|
|
{
|
2020-05-09 12:29:54 +00:00
|
|
|
|
const boidsLength = this.boids.length;
|
|
|
|
|
const obstaclesLength = this.obstacles.length;
|
2020-05-04 15:17:55 +00:00
|
|
|
|
|
2020-05-08 15:42:15 +00:00
|
|
|
|
for (let i = 0; i < boidsLength; ++i)
|
2020-05-04 15:17:55 +00:00
|
|
|
|
{
|
2020-05-08 09:59:46 +00:00
|
|
|
|
const me = this.boids[i];
|
|
|
|
|
|
|
|
|
|
const meanPos = this.registers[0].reset();
|
|
|
|
|
const meanVel = this.registers[1].reset();
|
|
|
|
|
const repelForce = this.registers[2].reset();
|
|
|
|
|
|
|
|
|
|
let visibles = 0;
|
|
|
|
|
|
2020-05-08 15:42:15 +00:00
|
|
|
|
// Compute mean flock position and velocity and compute
|
2020-05-09 12:29:54 +00:00
|
|
|
|
// the repel force from other boids
|
2020-05-08 15:42:15 +00:00
|
|
|
|
for (let j = 0; j < boidsLength; ++j)
|
2020-05-08 09:59:46 +00:00
|
|
|
|
{
|
|
|
|
|
if (i != j)
|
|
|
|
|
{
|
|
|
|
|
const you = this.boids[j];
|
|
|
|
|
const dist = Vector.distSquared(me.pos, you.pos);
|
|
|
|
|
|
|
|
|
|
if (dist < this.params.visibleDist
|
|
|
|
|
* this.params.visibleDist)
|
|
|
|
|
{
|
|
|
|
|
meanPos.add(you.pos);
|
|
|
|
|
meanVel.add(you.vel);
|
|
|
|
|
visibles += 1;
|
|
|
|
|
|
|
|
|
|
if (dist < this.params.closeDist
|
|
|
|
|
* this.params.closeDist)
|
|
|
|
|
{
|
|
|
|
|
repelForce.add(me.pos).sub(you.pos);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-09 12:29:54 +00:00
|
|
|
|
// Compute the repel force from obstacles
|
|
|
|
|
for (let j = 0; j < obstaclesLength; ++j)
|
|
|
|
|
{
|
|
|
|
|
const obstacle = this.obstacles[j];
|
|
|
|
|
|
|
|
|
|
if (obstacle.distance(me.pos) < this.params.closeDist)
|
|
|
|
|
{
|
|
|
|
|
me.vel.add(me.pos).sub(obstacle.center);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-08 09:59:46 +00:00
|
|
|
|
// Attract towards center of visible flock
|
|
|
|
|
if (visibles >= 1)
|
|
|
|
|
{
|
|
|
|
|
me.vel.add(meanPos.div(visibles)
|
|
|
|
|
.sub(me.pos)
|
|
|
|
|
.mul(this.params.centerAccel));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Attract toward center of screen if out of bounds
|
|
|
|
|
if (me.pos.x < this.width * -.4
|
|
|
|
|
|| me.pos.x > this.width * .4
|
|
|
|
|
|| me.pos.y < this.height * -.4
|
|
|
|
|
|| me.pos.y > this.height * .4)
|
|
|
|
|
{
|
|
|
|
|
me.vel.addMul(me.pos, -this.params.boundsAccel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Repel away from close boids
|
|
|
|
|
me.vel.add(repelForce.mul(this.params.repelAccel));
|
|
|
|
|
|
|
|
|
|
// Match other boids’ velocity
|
|
|
|
|
if (visibles >= 1)
|
|
|
|
|
{
|
|
|
|
|
me.vel.add(meanVel.div(visibles)
|
|
|
|
|
.sub(me.vel)
|
|
|
|
|
.mul(this.params.matchAccel));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Do not surpass maximum speed
|
|
|
|
|
const speed = me.vel.normSquared();
|
|
|
|
|
|
|
|
|
|
if (speed > this.params.maxSpeed * this.params.maxSpeed)
|
|
|
|
|
{
|
|
|
|
|
me.vel.div(Math.sqrt(speed)).mul(this.params.maxSpeed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Integrate speed
|
|
|
|
|
me.pos.addMul(me.vel, delta);
|
2020-05-04 15:17:55 +00:00
|
|
|
|
}
|
2020-05-08 09:59:46 +00:00
|
|
|
|
}
|
2020-05-04 15:17:55 +00:00
|
|
|
|
|
2020-05-08 09:59:46 +00:00
|
|
|
|
/**
|
|
|
|
|
* @private
|
|
|
|
|
* Redraw the boids.
|
|
|
|
|
*/
|
|
|
|
|
_draw()
|
|
|
|
|
{
|
2020-05-09 12:29:54 +00:00
|
|
|
|
const boidsLength = this.boids.length;
|
|
|
|
|
const obstaclesLength = this.obstacles.length;
|
2020-05-08 09:59:46 +00:00
|
|
|
|
|
2020-05-08 15:42:15 +00:00
|
|
|
|
this.ctx.clearRect(
|
|
|
|
|
-this.width / 2, -this.height / 2,
|
|
|
|
|
this.width, this.height
|
|
|
|
|
);
|
2020-05-04 15:17:55 +00:00
|
|
|
|
|
2020-05-08 15:42:15 +00:00
|
|
|
|
// Draw obstacles
|
2020-05-08 16:00:56 +00:00
|
|
|
|
if (this.params.debug)
|
2020-05-04 15:17:55 +00:00
|
|
|
|
{
|
2020-05-08 16:00:56 +00:00
|
|
|
|
this.ctx.fillStyle = '#dddddd';
|
|
|
|
|
this.ctx.beginPath();
|
2020-05-08 09:59:46 +00:00
|
|
|
|
|
2020-05-08 16:00:56 +00:00
|
|
|
|
for (let i = 0; i < obstaclesLength; ++i)
|
|
|
|
|
{
|
|
|
|
|
this.obstacles[i].draw(this.ctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.ctx.fill();
|
|
|
|
|
}
|
2020-05-08 09:59:46 +00:00
|
|
|
|
|
2020-05-08 15:42:15 +00:00
|
|
|
|
// Draw boids
|
|
|
|
|
this.ctx.fillStyle = this.params.color;
|
|
|
|
|
this.ctx.beginPath();
|
2020-05-08 09:59:46 +00:00
|
|
|
|
|
2020-05-08 15:42:15 +00:00
|
|
|
|
for (let i = 0; i < boidsLength; ++i)
|
|
|
|
|
{
|
|
|
|
|
this.boids[i].draw(this.ctx);
|
2020-05-04 15:17:55 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-08 09:59:46 +00:00
|
|
|
|
this.ctx.fill();
|
2020-05-04 15:17:55 +00:00
|
|
|
|
}
|
2020-05-08 09:59:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default Boids;
|