<canvas>
body {
margin: 0;
padding: 0;
}
canvas {
display: block;
}
// system settings
const NB_AGENTS = 2000
// sim settings
const SETTINGS = {
stepSize: 4,
sensorOffset: 20,
sensorAngle: 0.8,
deposit: 0.5,
decay: 0.9,
turnAngle: 0.4,
randomAngle: 0.2,
renderAgents: true
}
// weights for the blur pass
const weight = [
1/16, 1/8, 1/16,
1/8, 1/4, 1/8,
1/16, 1/8, 1/16,
];
// GUI setup
const GUI = new dat.GUI()
GUI.add(SETTINGS, "stepSize", 0, 10)
GUI.add(SETTINGS, "sensorOffset", 0, 40)
GUI.add(SETTINGS, "sensorAngle", 0, Math.PI)
GUI.add(SETTINGS, "turnAngle", 0, Math.PI)
GUI.add(SETTINGS, "randomAngle", 0, Math.PI)
GUI.add(SETTINGS, "deposit", 0, 1)
GUI.add(SETTINGS, "decay", 0.9, 1)
GUI.add(SETTINGS, "renderAgents")
// document setup
const cvs = document.querySelector("canvas")
const ctx = cvs.getContext("2d")
let W, H
cvs.width = W = window.innerWidth
cvs.height = H = window.innerHeight
// toroidal coordinates from any 2D coordinates
// not ANY coordinates, but faster and enough for this purpose
const toroidalCoords = (x, y) => {
if (x < 0) x = W + x
else if (x >= W) x-= W
if (y < 0) y = H + y
else if (y >= H) y-= H
return { x, y }
}
// 2d toroidail coordinates to 1D coordinates, for the substrate lookup
const coords2dTo1d = (x, y) => (x + y * W)
// initialisation of the system
const agents = []
for (let i = 0; i < NB_AGENTS; i++) {
agents.push({
x: Math.random() * W,
y: Math.random() * H,
ang: Math.random() * 2 * Math.PI
})
}
const substrate = new Float32Array(W*H)
const substrateImageArray = new Uint8ClampedArray(W*H*4)
// performs a blur pass on the substrate array [diffusion]
const diffusion = () => {
let old = Float32Array.from(substrate)
for (let y = 1; y < H-1; y++) {
for (let x = 1; x < W-1; ++x) {
const diffused_value = (
old[coords2dTo1d(x-1, y-1)] * weight[0] +
old[coords2dTo1d(x , y-1)] * weight[1] +
old[coords2dTo1d(x+1, y-1)] * weight[2] +
old[coords2dTo1d(x-1, y )] * weight[3] +
old[coords2dTo1d(x , y )] * weight[4] +
old[coords2dTo1d(x+1, y )] * weight[5] +
old[coords2dTo1d(x-1, y+1)] * weight[6] +
old[coords2dTo1d(x , y+1)] * weight[7] +
old[coords2dTo1d(x+1, y+1)] * weight[8]
)
substrate[coords2dTo1d(x, y)] = diffused_value * SETTINGS.decay
}
}
}
const sensor = (agent, ang) => {
const sx = agent.x + Math.cos(agent.ang + ang) * SETTINGS.sensorOffset
const sy = agent.y + Math.sin(agent.ang + ang) * SETTINGS.sensorOffset
const { x, y } = toroidalCoords(sx, sy)
return substrate[coords2dTo1d(x|0, y|0)]
}
// update of the system
const update = () => {
// diffusion / evaporation step
diffusion()
// agents update
let c1d, SL, SF, SR
for (const agent of agents) {
// agent angle based on substrate sampling
SL = sensor(agent, -SETTINGS.sensorAngle)
SF = sensor(agent, 0)
SR = sensor(agent, SETTINGS.sensorAngle)
if (SF > SL && SF > SR) {
// no change
}
else if (SL > SR) {
agent.ang-= SETTINGS.turnAngle
}
else if (SR > SL) {
agent.ang+= SETTINGS.turnAngle
}
else {
agent.ang+= (Math.random()-0.5) * SETTINGS.randomAngle
}
// update agent position based on its heading
let dir = {
x: Math.cos(agent.ang),
y: Math.sin(agent.ang)
}
const { x, y } = toroidalCoords(
agent.x + dir.x * SETTINGS.stepSize,
agent.y + dir.y * SETTINGS.stepSize
)
agent.x = x
agent.y = y
// agent deposit substrate
if ((x|0) !== 0 && (x|0) < W-1 && (y|0) !== 0 && (y|0) < H-1) {
c1d = coords2dTo1d(x|0, y|0)
substrate[c1d]+= SETTINGS.deposit
}
}
}
// draw the system
const draw = () => {
ctx.fillStyle = "black"
ctx.fillRect(0, 0, W, H)
// draw the substrate
let idx, brightness
for (let x = 0; x < W; x++) {
for (let y = 0; y < H; y++) {
idx = coords2dTo1d(x, y)
brightness = (substrate[idx] * 255) | 0
idx*=4
substrateImageArray[idx] = brightness
substrateImageArray[idx+1] = brightness
substrateImageArray[idx+2] = brightness
substrateImageArray[idx+3] = 255
}
}
let substrateImage = new ImageData(substrateImageArray, W, H)
ctx.putImageData(substrateImage, 0, 0)
// draw the agents
if (SETTINGS.renderAgents) {
ctx.fillStyle = "red"
for (const agent of agents) {
ctx.beginPath()
ctx.arc(agent.x, agent.y, 1, 0, Math.PI*2)
ctx.fill()
}
}
}
const loop = () => {
requestAnimationFrame(loop)
update()
draw()
}
loop()
This Pen doesn't use any external CSS resources.