<canvas>
body {
margin: 0;
overflow: hidden;
}
canvas {
display: block;
}
const STG = {
nbParticles: 100,
sensorsOffset: 20,
attractionStrength: 1,
}
const gui = new dat.GUI()
gui.add(STG, "sensorsOffset", 1, 40)
gui.add(STG, "attractionStrength", 0, 4)
gui.add(STG, "nbParticles", 50, 500, 1)
let W, H
const cvs = document.querySelector("canvas")
const ctx = cvs.getContext("2d")
cvs.width = W = window.innerWidth
cvs.height = H = window.innerHeight
// create an offscreen canvas which will be used to draw the subtrate from the particles
const offCvs = document.createElement("canvas")
const offCtx = offCvs.getContext("2d")
offCvs.width = W
offCvs.height = H
let offImgData
// sample the offscreen canvas at given x, y coordinates
const sampleEnv = (x, y) => {
const idx = ((x|0) + (y|0)*W) * 4
return offImgData.data[idx]
}
// initialize the particles
let particles = []
for (let i = 0; i < STG.nbParticles; i++) {
particles[i] = {
x: Math.random() * W,
y: Math.random() * H,
vx: (Math.random() - 0.5) * .5,
vy: (Math.random() - 0.5) * .5,
ax: 0,
ay: 0
}
}
const rst = {
reset: () => {
offCtx.fillStyle = "black"
offCtx.fillRect(0, 0, W, H)
particles = []
for (let i = 0; i < STG.nbParticles; i++) {
particles[i] = {
x: Math.random() * W,
y: Math.random() * H,
vx: (Math.random() - 0.5) * .5,
vy: (Math.random() - 0.5) * .5,
ax: 0,
ay: 0
}
}
}
}
gui.add(rst, "reset")
// toroidal topology (coordinates that loop on the edges)
const toroidal = (x, y) => {
if (x > W) x%= W
else if (x < 0) x = W + x
if (y > H) y%= H
else if (y < 0) y = H + y
return { x, y }
}
// a map of the angles for faster update loop
const angles = (new Array(8)).fill(0).map((_, i) => (i/8) * Math.PI*2)
const cosAngles = angles.map(a => Math.cos(a))
const sinAngles = angles.map(a => Math.sin(a))
const update = () => {
// first store the substrate data
offImgData = offCtx.getImageData(0, 0, W, H)
// particles acceleration is changed by sampling the substrate map
let ang, Sp, weight, ax, ay
for (const p of particles) {
ax = 0
ay = 0
// sample the env with the sensors
for (let i = 0; i < 8; i++) {
ang = angles[i]
// the sensor position
Sp = toroidal(
p.x + cosAngles[i] * STG.sensorsOffset,
p.y + sinAngles[i] * STG.sensorsOffset
)
// sample the environment
weight = sampleEnv(Sp.x, Sp.y)
ax+= cosAngles[i] * weight
ay+= sinAngles[i] * weight
}
p.ax = ax / 8
p.ay = ay / 8
}
// move the particles
for (const p of particles) {
p.vx+= p.ax * STG.attractionStrength * 0.01
p.vy+= p.ay * STG.attractionStrength * 0.01
const { x, y } = toroidal(p.x + p.vx, p.y + p.vy)
p.x = x
p.y = y
p.vx*= 0.98
p.vy*= 0.98
}
// apply some decay to the offscreen canvas
offCtx.globalCompositeOperation = "normal"
offCtx.globalAlpha = 0.08
offCtx.fillStyle = `rgba(0, 0, 0, 1)`
offCtx.fillRect(0, 0, W, H)
// draw to the substrate map (ie the offscreen canvas)
offCtx.globalAlpha = 0.1
offCtx.globalCompositeOperation = "lighter"
for (const p of particles) {
const grad = offCtx.createRadialGradient(p.x, p.y, 3, p.x, p.y, 50)
grad.addColorStop(0, "#555555")
grad.addColorStop(1, "transparent")
offCtx.beginPath()
offCtx.arc(p.x, p.y, 50, 0, 2 * Math.PI)
offCtx.fillStyle = grad
offCtx.fill()
}
}
const draw = () => {
// draw the substrate
ctx.drawImage(offCvs, 0, 0)
// draw the particles
ctx.fillStyle = "red"
for (const p of particles) {
ctx.beginPath()
ctx.arc(p.x, p.y, 4, 0, 2*Math.PI)
ctx.fill()
}
}
const loop = () => {
requestAnimationFrame(loop)
update()
draw()
}
loop()
This Pen doesn't use any external CSS resources.