From 11ffc8184801c528b9861c572c6ed41f1cdc0bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Delabre?= Date: Mon, 4 May 2020 17:17:55 +0200 Subject: [PATCH] Initial commit --- boids.mjs | 79 +++++++++++++++++++++++++++++++++++++++++ draw.mjs | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 28 +++++++++++++++ index.mjs | 66 ++++++++++++++++++++++++++++++++++ vector.mjs | 77 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 351 insertions(+) create mode 100644 boids.mjs create mode 100644 draw.mjs create mode 100644 index.html create mode 100644 index.mjs create mode 100644 vector.mjs diff --git a/boids.mjs b/boids.mjs new file mode 100644 index 0000000..9a5ea5d --- /dev/null +++ b/boids.mjs @@ -0,0 +1,79 @@ +import Vector from './vector.mjs'; + +const findNeighbors = (boids, boid, radius) => +{ + const neighbors = []; + + for (let otherBoid of boids) + { + if (boid != otherBoid) + { + const dist = boid.pos.sub(otherBoid.pos).normSquared(); + + if (dist < radius * radius) + { + neighbors.push(otherBoid); + } + } + } + + return neighbors; +}; + +export const update = (boids, params, time) => +{ + for (let boid of boids) + { + const visible = findNeighbors(boids, boid, params.visibleDist); + const close = findNeighbors(boids, boid, params.closeDist); + + // Attract towards center of visible flock + const center = new Vector(0, 0); + + for (let otherBoid of visible) + { + center.addMut(otherBoid.pos); + } + + if (visible.length >= 1) + { + center.divMut(visible.length); + boid.vel.addMut(center.sub(boid.pos).mul(params.centerAccel)); + } + + // Attract toward center of screen + boid.vel.addMut(boid.pos.mul(-0.01)); + + // Repel away from close boids + for (let otherBoid of close) + { + boid.vel.addMut(boid.pos + .sub(otherBoid.pos) + .mul(params.repelAccel)); + } + + // Match other boids’ velocity + const velocity = new Vector(0, 0); + + for (let otherBoid of visible) + { + velocity.addMut(otherBoid.vel); + } + + if (visible.length >= 1) + { + velocity.divMut(visible.length); + boid.vel.addMut(velocity.sub(boid.vel).mul(params.matchAccel)); + } + + // Do not surpass maximum speed + const speed = boid.vel.normSquared(); + + if (speed > params.maxSpeed * params.maxSpeed) + { + boid.vel.divMut(Math.sqrt(speed)).mulMut(params.maxSpeed); + } + + boid.pos.addMut(boid.vel.mul(time)); + } +}; diff --git a/draw.mjs b/draw.mjs new file mode 100644 index 0000000..d0d256f --- /dev/null +++ b/draw.mjs @@ -0,0 +1,101 @@ +import Vector from './vector.mjs'; + +const boidShape = [ + new Vector(2, 0), + new Vector(-1, 1), + new Vector(-1, -1), + new Vector(2, 0), +]; + +export const boids = (activeBoids, params, ctx, width, height, center) => +{ + ctx.clearRect(0, 0, width, height); + + // // Draw each boid’s visibility and proximity zones + // ctx.beginPath(); + // ctx.fillStyle = '#BBEE94'; + + // for (let boid of activeBoids) + // { + // const trans = boid.pos.add(center); + + // ctx.moveTo( + // trans.x + params.visibleDist, + // trans.y + // ); + + // ctx.arc( + // trans.x, trans.y, + // params.visibleDist, + // 0, 2 * Math.PI + // ); + // } + + // ctx.fill(); + // ctx.stroke(); + + // ctx.beginPath(); + // ctx.fillStyle = '#EE9495'; + + // for (let boid of activeBoids) + // { + // const trans = boid.pos.add(center); + + // ctx.moveTo( + // trans.x + params.closeDist, + // trans.y + // ); + + // ctx.arc( + // trans.x, trans.y, + // params.closeDist, + // 0, 2 * Math.PI + // ); + // } + + // ctx.fill(); + // ctx.stroke(); + + // // Draw each boid’s velocity vector + // ctx.beginPath(); + // ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)'; + + // for (let boid of activeBoids) + // { + // const start = boid.pos.add(center); + // const end = start.add(boid.vel.mul(0.25)); + + // ctx.moveTo(start.x, start.y); + // ctx.lineTo(end.x, end.y); + // } + + // ctx.stroke(); + + // Draw each boid’s head following the angle of its course + ctx.beginPath(); + ctx.fillStyle = 'black'; + + for (let boid of activeBoids) + { + const angle = boid.vel.angle(); + let isFirst = true; + + for (let point of boidShape) + { + const trans = boid.pos.add(center) + .add(point.rotate(angle).mul(params.radius)) + + if (isFirst) + { + ctx.moveTo(trans.x, trans.y); + isFirst = false; + } + else + { + ctx.lineTo(trans.x, trans.y); + } + } + } + + ctx.fill(); +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..1ae2493 --- /dev/null +++ b/index.html @@ -0,0 +1,28 @@ + + + + Boids + + + + + + + + + diff --git a/index.mjs b/index.mjs new file mode 100644 index 0000000..5082e39 --- /dev/null +++ b/index.mjs @@ -0,0 +1,66 @@ +import Vector from './vector.mjs'; +import * as boids from './boids.mjs'; +import * as draw from './draw.mjs'; + +const params = { + centerAccel: 0.01, + repelAccel: 1, + matchAccel: 0.1, + + maxSpeed: 300, + + closeDist: 20, + visibleDist: 80, + + radius: 5, +}; + +const boidsCanvas = document.querySelector('#boids-canvas'); +const boidsCtx = boidsCanvas.getContext('2d'); + +let width = null; +let height = null; +let center = null; + +const updateSize = () => +{ + width = window.innerWidth; + height = window.innerHeight; + center = new Vector(width / 2, height / 2); + + boidsCanvas.width = width; + boidsCanvas.height = height; +}; + +updateSize(); +window.onresize = updateSize; + +const activeBoids = []; + +for (let i = 0; i < 300; ++i) +{ + activeBoids.push({ + pos: new Vector(200 + (i % 2) * (-400), 0), + vel: new Vector(Math.random() * 100, Math.random() * 100), + }); +} + +let lastTime = null; + +const loop = time => +{ + if (!lastTime) + { + lastTime = time; + } + + const delta = (time - lastTime) / 1000; + lastTime = time; + + boids.update(activeBoids, params, delta); + draw.boids(activeBoids, params, boidsCtx, width, height, center); + + requestAnimationFrame(loop); +}; + +requestAnimationFrame(loop); diff --git a/vector.mjs b/vector.mjs new file mode 100644 index 0000000..5198aba --- /dev/null +++ b/vector.mjs @@ -0,0 +1,77 @@ +class Vector +{ + constructor(x = 0, y = 0) + { + this.x = x; + this.y = y; + } + + add(vector) + { + return new Vector(this.x + vector.x, this.y + vector.y); + } + + addMut(vector) + { + this.x += vector.x; + this.y += vector.y; + return this; + } + + sub(vector) + { + return new Vector(this.x - vector.x, this.y - vector.y); + } + + subMut(vector) + { + this.x -= vector.x; + this.y -= vector.y; + return this; + } + + mul(scalar) + { + return new Vector(this.x * scalar, this.y * scalar); + } + + mulMut(scalar) + { + this.x *= scalar; + this.y *= scalar; + return this; + } + + div(scalar) + { + return this.mul(1 / scalar); + } + + divMut(scalar) + { + return this.mulMut(1 / scalar); + } + + rotate(angle) + { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + + return new Vector( + cos * this.x - sin * this.y, + cos * this.y + sin * this.x, + ); + } + + angle() + { + return Math.PI / 2 - Math.atan2(this.x, this.y); + } + + normSquared() + { + return this.x * this.x + this.y * this.y; + } +} + +export default Vector;