boids/boids.mjs

308 lines
7.7 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 './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);
}
}
}
}
class Boids
{
constructor(canvas, obstacles, 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',
debug: false,
}, params);
// Current width and height of the canvas
this.width = canvas.width;
this.height = canvas.height;
// Current center point of the canvas
this.center = null;
// Last time where the canvas was repainted
this.lastTime = null;
// List of active simulated boids
this.boids = [];
this.boidsLength = 0;
// List of obstacles
this.obstacles = obstacles;
this.obstaclesLength = this.obstacles.length;
// 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;
this._afterResize();
}
/** Change the canvas dimensions */
resize(width, height)
{
this.canvas.width = width;
this.canvas.height = height;
this.width = width;
this.height = height;
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);
}
/** Introduce a new boid in the simulation. */
add(center)
{
this.boids.push(new Boid(this.params, center));
this.boidsLength += 1;
}
/** 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 boidsLength = this.boidsLength;
for (let i = 0; i < boidsLength; ++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;
// Compute mean flock position and velocity and compute
// the repel force
for (let j = 0; j < boidsLength; ++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));
}
// Avoid obstacles
for (let j = 0; j < this.obstaclesLength; ++j)
{
const obstacle = this.obstacles[j];
if (obstacle.intersect(me.pos, this.params.radius))
{
me.vel.sub(obstacle.center).add(me.pos);
}
}
// 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 boidsLength = this.boidsLength;
const obstaclesLength = this.obstaclesLength;
this.ctx.clearRect(
-this.width / 2, -this.height / 2,
this.width, this.height
);
// Draw obstacles
if (this.params.debug)
{
this.ctx.fillStyle = '#dddddd';
this.ctx.beginPath();
for (let i = 0; i < obstaclesLength; ++i)
{
this.obstacles[i].draw(this.ctx);
}
this.ctx.fill();
}
// Draw boids
this.ctx.fillStyle = this.params.color;
this.ctx.beginPath();
for (let i = 0; i < boidsLength; ++i)
{
this.boids[i].draw(this.ctx);
}
this.ctx.fill();
}
}
export default Boids;