Separate example from core
This commit is contained in:
parent
fd32a3b1d0
commit
32411933cd
295
boids.mjs
295
boids.mjs
|
@ -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 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
|
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;
|
||||||
|
|
53
draw.mjs
53
draw.mjs
|
@ -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();
|
|
||||||
};
|
|
39
index.html
39
index.html
|
@ -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
106
index.mjs
|
@ -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();
|
|
Loading…
Reference in New Issue