boids/boids.mjs

253 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Vector from './vector.mjs';
class Boids
{
constructor(canvas, params = {})
{
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',
}, params);
// Current width and height of the canvas
this.width = canvas.width;
this.height = canvas.height;
// Current center point of the canvas
this.center = new Vector(canvas.width / 2, canvas.height / 2);
// Last time where the canvas was repainted
this.lastTime = null;
// List of active simulated boids
this.boids = [];
// 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;
// Outline of the shape of a boid when angled at 0 rad
this.boidShape = [
new Vector(1, 0),
new Vector(-.5, .5),
new Vector(-.5, -.5),
new Vector(1, 0),
];
this.boidShapeLength = this.boidShape.length;
}
/** Change the canvas dimensions */
resize(width, height)
{
this.canvas.width = width;
this.canvas.height = height;
this.width = width;
this.height = height;
this.center = new Vector(width / 2, height / 2);
}
/** Introduce a new boid in the simulation. */
add(center)
{
this.boids.push({
pos: center.clone(),
vel: new Vector(Math.random() - 0.5, Math.random() - 0.5),
});
}
/** Start the simulation. */
start()
{
if (this.animationId !== null)
{
return;
}
this._step = this._step.bind(this);
this.animationId = requestAnimationFrame(this._step);
}
/** Pause the simulation. */
pause()
{
cancelAnimationFrame(this.animationId);
this.animationId = null;
this.lastTime = null;
}
/**
* @private
* Perform a time step of the simulation and update the canvas.
*/
_step(time)
{
if (!this.lastTime)
{
this.lastTime = time;
}
const delta = (time - this.lastTime) / 1000;
this.lastTime = time;
this._update(delta);
this._draw();
if (this.animationId !== null)
{
this.animationId = requestAnimationFrame(this._step);
}
}
/**
* @private
* Advance the simulation.
*/
_update(delta)
{
const length = this.boids.length;
for (let i = 0; i < length; ++i)
{
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;
for (let j = 0; j < length; ++j)
{
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);
}
}
}
}
// 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);
}
}
/**
* @private
* Redraw the boids.
*/
_draw()
{
const length = this.boids.length;
const transformed = this.registers[0];
this.ctx.clearRect(0, 0, this.width, this.height);
// Draw each boids head following the angle of its course
this.ctx.beginPath();
this.ctx.fillStyle = this.params.color;
for (let i = 0; i < length; ++i)
{
const boid = this.boids[i];
const angle = boid.vel.angle();
let isFirst = true;
for (let j = 0; j < this.boidShapeLength; ++j)
{
transformed.x = this.boidShape[j].x;
transformed.y = this.boidShape[j].y;
transformed
.rotate(angle)
.mul(this.params.radius)
.add(this.center)
.add(boid.pos);
if (isFirst)
{
this.ctx.moveTo(transformed.x, transformed.y);
isFirst = false;
}
else
{
this.ctx.lineTo(transformed.x, transformed.y);
}
}
}
this.ctx.fill();
}
}
export default Boids;