|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<style> |
|
body { |
|
margin: 0; |
|
} |
|
|
|
#controls { |
|
font-family: sans-serif; |
|
padding: 5px; |
|
position: absolute; |
|
text-align: center; |
|
width: 100%; |
|
bottom: 0px; |
|
} |
|
|
|
#controls .control { |
|
background: rgba(255, 255, 255, .95); |
|
display: inline-block; |
|
padding: 10px; |
|
text-align: left; |
|
width: 200px; |
|
} |
|
|
|
#controls .control .range input, #controls .control .range .value { |
|
display: inline-block; |
|
} |
|
|
|
#controls .control .range .value { |
|
font-size: 12px; |
|
margin-top: -5px; |
|
vertical-align: middle; |
|
} |
|
|
|
#controls .control .description { |
|
font-size: 12px; |
|
} |
|
</style> |
|
</head> |
|
<div id="controls"> |
|
<div class="control"> |
|
<div class="title">Alignment</div> |
|
<div class="range"> |
|
<input data-parameter="alignment" type="range" min="0" max="1" value=".5" step=".1" /> |
|
<div class="value">0.5</div> |
|
</div> |
|
<div class="description">Steer towards the average heading of local flockmates</div> |
|
</div> |
|
<div class="control"> |
|
<div class="title">Cohesion</div> |
|
<div class="range"> |
|
<input data-parameter="cohesion" type="range" min="0" max="1" value=".5" step=".1" /> |
|
<div class="value">0.5</div> |
|
</div> |
|
<div class="description">Steer to move toward the average position of local flockmates</div> |
|
</div> |
|
<div class="control"> |
|
<div class="title">Separation</div> |
|
<div class="range"> |
|
<input data-parameter="separation" type="range" min="0" max="1" value=".5" step=".1" /> |
|
<div class="value">0.5</div> |
|
</div> |
|
<div class="description">Steer to avoid crowding local flockmates</div> |
|
</div> |
|
<div class="control"> |
|
<div class="title">Distance</div> |
|
<div class="range"> |
|
<input data-parameter="distance" type="range" min="1" max="200" value="30" step="1" /> |
|
<div class="value">30</div> |
|
</div> |
|
<div class="description">Maximum distance of other boids to consider</div> |
|
</div> |
|
</div> |
|
<div id="simulation"></div> |
|
<body> |
|
<script src="https://unpkg.com/[email protected]/build/geometric.min.js"></script> |
|
<script src="https://d3js.org/d3-array.v2.min.js"></script> |
|
<script src="https://d3js.org/d3-random.v2.min.js"></script> |
|
<script> |
|
class Boids { |
|
init(opts){ |
|
this.width = opts && opts.width ? opts.width : innerWidth; |
|
this.height = opts && opts.height ? opts.height : innerHeight; |
|
this.center = [this.width / 2, this.height / 2]; |
|
|
|
this.data = []; |
|
|
|
this.separation = opts && isFinite(opts.separation) ? opts.separation : .5; |
|
this.alignment = opts && isFinite(opts.alignment) ? opts.alignment : 1; |
|
this.cohesion = opts && isFinite(opts.cohesion) ? opts.cohesion : 1; |
|
|
|
this.distance = opts && opts.distance ? opts.distance : 30; |
|
|
|
return this; |
|
} |
|
|
|
add(datum){ |
|
const d = datum || {}; |
|
d.angle = d.angle || 0; |
|
d.startAngle = d.angle; |
|
d.pos = d.pos || this.center; |
|
d.speed = d.speed || 1; |
|
|
|
this.data.push(d); |
|
|
|
return this; |
|
} |
|
|
|
tick(){ |
|
// Check if any of alignment, cohesion, or separation are greater than 0 |
|
const hasValue = this.alignment || this.cohesion || this.separation; |
|
|
|
if (hasValue){ |
|
// Loop through the boids to find the neighborhood of each |
|
for (let i = 0, l = this.data.length; i < l; i++){ |
|
const d = this.data[i]; |
|
|
|
d.neighborhood = []; |
|
|
|
// Find all boids within this.distance |
|
for (let i0 = 0, l0 = this.data.length; i0 < l0; i0++){ |
|
const d0 = this.data[i0]; |
|
|
|
if (geometric.lineLength([d.pos, d0.pos]) < this.distance) d.neighborhood.push(d0); |
|
} |
|
} |
|
} |
|
|
|
// Loop through the boids to calculate the new position |
|
for (let i = 0, l = this.data.length; i < l; i++){ |
|
const d = this.data[i]; |
|
|
|
if (d.neighborhood.length && hasValue){ |
|
const alignment = d3.mean(d.neighborhood, d0 => d0.angle), |
|
cohesion = geometric.lineAngle([ |
|
d.pos, |
|
geometric.polygonMean(d.neighborhood.map(d0 => d0.pos)) |
|
]), |
|
separation = d.startAngle; |
|
|
|
// The new angle. Alignment needs to be boosted by some coefficient |
|
d.angle = (cohesion * this.cohesion + |
|
separation * this.separation + |
|
alignment * this.alignment * 40) / |
|
(this.cohesion + this.separation + this.alignment * 40); |
|
} |
|
|
|
const [x, y] = geometric.pointTranslate(d.pos, d.angle, d.speed); |
|
d.pos = [x < 0 ? this.width : x > this.width ? 0 : x, y < 0 ? this.height : y > this.height ? 0 : y]; |
|
} |
|
|
|
return this; |
|
} |
|
} |
|
|
|
const myBoids = (_ => { |
|
const simulation = new Boids; |
|
simulation.init({ |
|
cohesion: .5, |
|
alignment: .5, |
|
separation: .5 |
|
}); |
|
|
|
// Add 500 boids |
|
for (let i = 0; i < 500; i++){ |
|
simulation.add({ |
|
angle: d3.randomUniform(-360, 360)(), |
|
pos: [ |
|
d3.randomUniform(0, simulation.width)(), |
|
d3.randomUniform(0, simulation.height)() |
|
] |
|
}); |
|
} |
|
|
|
return simulation; |
|
})(); |
|
|
|
// Draw the simulation |
|
const wrapper = document.getElementById("simulation"); |
|
const canvas = document.createElement("canvas"); |
|
canvas.width = myBoids.width; |
|
canvas.height = myBoids.height; |
|
wrapper.appendChild(canvas); |
|
const context = canvas.getContext("2d"); |
|
context.strokeStyle = "red"; |
|
context.fillStyle = "pink"; |
|
|
|
function tick(){ |
|
requestAnimationFrame(tick); |
|
context.clearRect(0, 0, myBoids.width, myBoids.height); |
|
|
|
// The simulation.tick method advances the simulation one tick |
|
myBoids.tick(); |
|
for (let i = 0, l = myBoids.data.length; i < l; i++){ |
|
const boid = myBoids.data[i], |
|
a = geometric.pointTranslate(boid.pos, boid.angle - 90, 3), |
|
b = geometric.pointTranslate(boid.pos, boid.angle, 9), |
|
c = geometric.pointTranslate(boid.pos, boid.angle + 90, 3); |
|
context.beginPath(); |
|
context.moveTo(...a); |
|
context.lineTo(...b); |
|
context.lineTo(...c); |
|
context.lineTo(...a); |
|
context.fill(); |
|
context.stroke(); |
|
} |
|
} |
|
tick(); |
|
|
|
let holding = false; |
|
canvas.addEventListener("mousedown", e => { holding = true; addBoidOnEvent(e); }); |
|
canvas.addEventListener("mouseup", e => { holding = false }); |
|
canvas.addEventListener("mousemove", e => { if (holding) addBoidOnEvent(e); }); |
|
|
|
function addBoidOnEvent(e){ |
|
myBoids.add({ |
|
angle: d3.randomUniform(-360, 360)(), |
|
pos: [e.pageX, e.pageY] |
|
}); |
|
} |
|
|
|
addEventListener("resize", _ => { |
|
myBoids.width = innerWidth; |
|
myBoids.height = innerHeight; |
|
canvas.width = myBoids.width; |
|
canvas.height = myBoids.height; |
|
context.strokeStyle = "red"; |
|
context.fillStyle = "pink"; |
|
}); |
|
|
|
const controls = document.querySelectorAll(".control"); |
|
controls.forEach(control => { |
|
control.addEventListener("input", _ => { |
|
_.target.nextElementSibling.innerHTML = +_.target.value |
|
myBoids[_.target.dataset.parameter] = +_.target.value; |
|
}); |
|
}); |
|
|
|
</script> |
|
</body> |
|
</html> |