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';
|
||||
|
||||
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;
|
||||
|
|
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>
|
||||
<body>
|
||||
<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>
|
||||
</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