Separate example from core

This commit is contained in:
Mattéo Delabre 2020-05-08 11:59:46 +02:00
parent fd32a3b1d0
commit 32411933cd
Signed by: matteo
GPG Key ID: AE3FBD02DC583ABB
4 changed files with 272 additions and 221 deletions

295
boids.mjs
View File

@ -1,79 +1,252 @@
import Vector from './vector.mjs'; import Vector from './vector.mjs';
const meanPos = new Vector(0, 0); class Boids
const meanVel = new Vector(0, 0);
const repelForce = new Vector(0, 0);
export const update = (boids, params, width, height, time) =>
{ {
const boidsSize = boids.length; constructor(canvas, params = {})
for (let i = 0; i < boidsSize; ++i)
{ {
const me = boids[i]; this.canvas = canvas;
this.ctx = canvas.getContext('2d');
meanPos.reset(); this.params = Object.assign({
meanVel.reset(); centerAccel: 0.075,
repelForce.reset(); 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]; if (i != j)
const dist = Vector.distSquared(me.pos, you.pos);
if (dist < params.visibleDist * params.visibleDist)
{ {
meanPos.add(you.pos); const you = this.boids[j];
meanVel.add(you.vel); const dist = Vector.distSquared(me.pos, you.pos);
visibleSize += 1;
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 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);
}
}
} }
// Attract towards center of visible flock this.ctx.fill();
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);
} }
}; }
export default Boids;

View File

@ -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 boids 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();
};

View File

@ -23,6 +23,43 @@
</head> </head>
<body> <body>
<canvas id="boids-canvas"></canvas> <canvas id="boids-canvas"></canvas>
<script src="index.mjs" type="module"></script> <script type="module">
import Boids from './boids.mjs';
import Vector from './vector.mjs';
const canvas = document.querySelector('#boids-canvas');
const boids = new Boids(canvas);
canvas.onclick = ev =>
{
const pos = new Vector(ev.offsetX, ev.offsetY);
pos.sub(boids.center);
for (let i = 0; i < 100; ++i)
{
boids.add(pos);
}
};
window.onresize = () =>
{
boids.resize(window.innerWidth, window.innerHeight);
};
document.onvisibilitychange = () =>
{
if (document.visibilityState === 'visible')
{
boids.start();
}
else
{
boids.pause();
}
};
boids.resize(window.innerWidth, window.innerHeight);
boids.start();
</script>
</body> </body>
</html> </html>

106
index.mjs
View File

@ -1,106 +0,0 @@
import Vector from './vector.mjs';
import * as boids from './boids.mjs';
import * as draw from './draw.mjs';
const params = {
centerAccel: 0.075,
repelAccel: 0.8,
matchAccel: 0.15,
boundsAccel: 0.01,
maxSpeed: 300,
closeDist: 20,
visibleDist: 60,
radius: 10,
color: '#675148',
};
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 = [];
const addBoids = (pos, count) =>
{
for (let i = 0; i < count; ++i)
{
activeBoids.push({
pos: pos.clone(),
vel: new Vector(Math.random(), Math.random()),
});
}
};
boidsCanvas.onclick = ev =>
{
const pos = new Vector(ev.offsetX, ev.offsetY);
pos.sub(center);
addBoids(pos, 100);
};
let paused = false;
let lastTime = null;
const loop = time =>
{
if (!lastTime)
{
lastTime = time;
}
const delta = (time - lastTime) / 1000;
lastTime = time;
boids.update(activeBoids, params, width, height, delta);
draw.fill(activeBoids, params, width, height, boidsCtx, center);
if (!paused)
{
requestAnimationFrame(loop);
}
};
const start = () =>
{
lastTime = null;
paused = false;
requestAnimationFrame(loop);
};
const pause = () =>
{
paused = true;
};
document.onvisibilitychange = () =>
{
if (document.visibilityState === 'visible')
{
start();
}
else
{
pause();
}
};
start();