diff --git a/boids.mjs b/boids.mjs index 9833820..156e993 100644 --- a/boids.mjs +++ b/boids.mjs @@ -1,79 +1,252 @@ import Vector from './vector.mjs'; -const meanPos = new Vector(0, 0); -const meanVel = new Vector(0, 0); -const repelForce = new Vector(0, 0); - -export const update = (boids, params, width, height, time) => +class Boids { - const boidsSize = boids.length; - - for (let i = 0; i < boidsSize; ++i) + constructor(canvas, params = {}) { - const me = boids[i]; + this.canvas = canvas; + this.ctx = canvas.getContext('2d'); - meanPos.reset(); - meanVel.reset(); - repelForce.reset(); + this.params = Object.assign({ + centerAccel: 0.075, + repelAccel: 0.8, + matchAccel: 0.15, + boundsAccel: 0.01, - let visibleSize = 0; + maxSpeed: 300, - for (let j = 0; j < boidsSize; ++j) + 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) { - if (i != j) + 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) { - const you = boids[j]; - const dist = Vector.distSquared(me.pos, you.pos); - - if (dist < params.visibleDist * params.visibleDist) + if (i != j) { - meanPos.add(you.pos); - meanVel.add(you.vel); - visibleSize += 1; + const you = this.boids[j]; + const dist = Vector.distSquared(me.pos, you.pos); - if (dist < params.closeDist * params.closeDist) + if (dist < this.params.visibleDist + * this.params.visibleDist) { - repelForce.add(me.pos).sub(you.pos); + 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 boid’s 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); + } + } } - // Attract towards center of visible flock - if (visibleSize >= 1) - { - me.vel.add(meanPos.div(visibleSize) - .sub(me.pos) - .mul(params.centerAccel)); - } - - // Attract toward center of screen if out of bounds - if (me.pos.x < width * -.4 || me.pos.x > width * .4 || - me.pos.y < height * -.4 || me.pos.y > height * .4) - { - me.vel.addMul(me.pos, -params.boundsAccel); - } - - // Repel away from close boids - me.vel.add(repelForce.mul(params.repelAccel)); - - // Match other boids’ velocity - if (visibleSize >= 1) - { - me.vel.add(meanVel.div(visibleSize) - .sub(me.vel) - .mul(params.matchAccel)); - } - - // Do not surpass maximum speed - const speed = me.vel.normSquared(); - - if (speed > params.maxSpeed * params.maxSpeed) - { - me.vel.div(Math.sqrt(speed)).mul(params.maxSpeed); - } - - // Integrate speed - me.pos.addMul(me.vel, time); + this.ctx.fill(); } -}; +} + +export default Boids; diff --git a/draw.mjs b/draw.mjs deleted file mode 100644 index e3d514d..0000000 --- a/draw.mjs +++ /dev/null @@ -1,53 +0,0 @@ -import Vector from './vector.mjs'; - -const boidShape = [ - new Vector(1, 0), - new Vector(-.5, .5), - new Vector(-.5, -.5), - new Vector(1, 0), -]; - -const boidShapeSize = boidShape.length; -const transformed = new Vector(0, 0); - -export const fill = (activeBoids, params, width, height, ctx, center) => -{ - const boidsSize = activeBoids.length; - - ctx.clearRect(0, 0, width, height); - - // Draw each boid’s head following the angle of its course - ctx.beginPath(); - ctx.fillStyle = params.color; - - for (let i = 0; i < boidsSize; ++i) - { - const boid = activeBoids[i]; - const angle = boid.vel.angle(); - let isFirst = true; - - for (let j = 0; j < boidShapeSize; ++j) - { - transformed.x = boidShape[j].x; - transformed.y = boidShape[j].y; - - transformed - .rotate(angle) - .mul(params.radius) - .add(center) - .add(boid.pos); - - if (isFirst) - { - ctx.moveTo(transformed.x, transformed.y); - isFirst = false; - } - else - { - ctx.lineTo(transformed.x, transformed.y); - } - } - } - - ctx.fill(); -}; diff --git a/index.html b/index.html index 1ae2493..4a36eb3 100644 --- a/index.html +++ b/index.html @@ -23,6 +23,43 @@
- +