diff --git a/boids.mjs b/boids.mjs index 156e993..94f442c 100644 --- a/boids.mjs +++ b/boids.mjs @@ -1,8 +1,52 @@ -import Vector from './vector.mjs'; +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, params = {}) + constructor(canvas, obstacles, params = {}) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); @@ -27,13 +71,18 @@ class Boids this.height = canvas.height; // Current center point of the canvas - this.center = new Vector(canvas.width / 2, canvas.height / 2); + 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 = [ @@ -46,14 +95,7 @@ class Boids // 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; + this._afterResize(); } /** Change the canvas’ dimensions */ @@ -64,16 +106,22 @@ class Boids this.width = width; this.height = height; - this.center = new Vector(width / 2, height / 2); + + 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({ - pos: center.clone(), - vel: new Vector(Math.random() - 0.5, Math.random() - 0.5), - }); + this.boids.push(new Boid(this.params, center)); + this.boidsLength += 1; } /** Start the simulation. */ @@ -125,9 +173,9 @@ class Boids */ _update(delta) { - const length = this.boids.length; + const boidsLength = this.boidsLength; - for (let i = 0; i < length; ++i) + for (let i = 0; i < boidsLength; ++i) { const me = this.boids[i]; @@ -137,7 +185,9 @@ class Boids let visibles = 0; - for (let j = 0; j < length; ++j) + // Compute mean flock position and velocity and compute + // the repel force + for (let j = 0; j < boidsLength; ++j) { if (i != j) { @@ -188,6 +238,17 @@ class Boids .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(); @@ -207,42 +268,32 @@ class Boids */ _draw() { - const length = this.boids.length; - const transformed = this.registers[0]; + const boidsLength = this.boidsLength; + const obstaclesLength = this.obstaclesLength; - this.ctx.clearRect(0, 0, this.width, this.height); + this.ctx.clearRect( + -this.width / 2, -this.height / 2, + this.width, this.height + ); - // Draw each boid’s head following the angle of its course + // Draw obstacles + this.ctx.fillStyle = '#dddddd'; this.ctx.beginPath(); - this.ctx.fillStyle = this.params.color; - for (let i = 0; i < length; ++i) + for (let i = 0; i < obstaclesLength; ++i) { - const boid = this.boids[i]; - const angle = boid.vel.angle(); - let isFirst = true; + this.obstacles[i].draw(this.ctx); + } - for (let j = 0; j < this.boidShapeLength; ++j) - { - transformed.x = this.boidShape[j].x; - transformed.y = this.boidShape[j].y; + this.ctx.fill(); - transformed - .rotate(angle) - .mul(this.params.radius) - .add(this.center) - .add(boid.pos); + // Draw boids + this.ctx.fillStyle = this.params.color; + this.ctx.beginPath(); - if (isFirst) - { - this.ctx.moveTo(transformed.x, transformed.y); - isFirst = false; - } - else - { - this.ctx.lineTo(transformed.x, transformed.y); - } - } + for (let i = 0; i < boidsLength; ++i) + { + this.boids[i].draw(this.ctx); } this.ctx.fill(); diff --git a/vector.mjs b/geometry.mjs similarity index 57% rename from vector.mjs rename to geometry.mjs index d26fb8c..d1dcda5 100644 --- a/vector.mjs +++ b/geometry.mjs @@ -1,4 +1,4 @@ -class Vector +export class Vector { constructor(x = 0, y = 0) { @@ -82,4 +82,52 @@ class Vector } } -export default Vector; +export class Rectangle +{ + constructor(x, y, w, h) + { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + this.center = new Vector(x + w / 2, y + h / 2); + } + + intersect(point, radius) + { + return ( + point.x >= this.x - radius + && point.x < this.x + this.w + radius + && point.y >= this.y - radius + && point.y < this.y + this.h + radius + ); + } + + draw(ctx) + { + ctx.rect(this.x, this.y, this.w, this.h); + } +} + +export class Circle +{ + constructor(x, y, r) + { + this.r = r; + this.center = new Vector(x, y); + } + + intersect(point, radius) + { + return ( + Vector.distSquared(this.center, point) + <= (this.r + radius) * (this.r + radius) + ); + } + + draw(ctx) + { + ctx.moveTo(this.center.x + this.r, this.center.y); + ctx.arc(this.center.x, this.center.y, this.r, 0, 2 * Math.PI, false); + } +} diff --git a/index.html b/index.html index 4a36eb3..bdaa98d 100644 --- a/index.html +++ b/index.html @@ -25,17 +25,21 @@