<canvas>
body {
margin: 0;
padding: 0;
overflow: hidden;
}
canvas {
display: block;
}
const NB_PARTICLES = 20
const ATTR_RANGE = 80
const ATTR_STRENGTH = 20
const REP_RANGE = 20
const REP_STRENGTH = 50
const INV_FRICTION = 0.95
const SETTINGS = {
attractionRange: 60,
attractionStrength: 20,
repulsionRange: 20,
repulsionStrength: 50,
invFriction: 0.95
}
// GUI
const gui = new dat.GUI()
gui.add(SETTINGS, "attractionRange", 0, 150)
gui.add(SETTINGS, "attractionStrength", 0, 100)
gui.add(SETTINGS, "repulsionRange", 0, 150)
gui.add(SETTINGS, "repulsionStrength", 0, 100)
let W = 300, H = 200
const cvs = document.querySelector("canvas")
const ctx = cvs.getContext("2d")
cvs.width = W
cvs.height = H
const distanceField = new Uint8ClampedArray(W*H*4)
const particles = []
// initialize particles
let { width, height } = cvs
for (let i = 0; i < NB_PARTICLES; i++) {
particles[i] = {
x: Math.random() * W,
y: Math.random() * H,
vx: 0,
vy: 0,
ax: 0,
ay: 0
}
}
const computeDistanceField = () => {
let idx, D
for (let x = 0; x < W; x++) {
for (let y = 0; y < H; y++) {
idx = (x + y*W) * 4
let closest = Infinity
for (const P of particles) {
D = Math.sqrt((P.x-x)**2 + (P.y-y)**2)
if (D < closest) {
closest = D
}
}
closest = (closest*2) | 0
distanceField[idx] = closest
distanceField[idx+1] = closest
distanceField[idx+2] = closest
distanceField[idx+3] = 255
}
}
}
// update function
const update = () => {
// compute the acceleration of each particle
for (let p of particles) {
// force reset the acceleration of the particle
p.ax = 0
p.ay = 0
// go through all the other particles
for (let p2 of particles) {
if (p !== p2) {
// the vector between the 2 particles
let M = {
x: p2.x - p.x,
y: p2.y - p.y
}
// the distance between the 2 particles
let D = Math.sqrt(M.x**2 + M.y**2)
if (D > 1) { // prevents extreme values because of division by D^2
// normalized p->p2 vector
M.x/= D
M.y/= D
// add attraction force
if (D < SETTINGS.attractionRange) {
p.ax+= M.x / D**2 * SETTINGS.attractionStrength
p.ay+= M.y / D**2 * SETTINGS.attractionStrength
}
// add rep force
if (D < SETTINGS.repulsionRange) {
p.ax+= -M.x / D**2 * SETTINGS.repulsionStrength
p.ay+= -M.y / D**2 * SETTINGS.repulsionStrength
}
}
}
}
}
// update the particles position based on their acceleration and velocity
for (const p of particles) {
p.vx+= p.ax
p.vy+= p.ay
p.vx*= INV_FRICTION
p.vy*= INV_FRICTION
p.x+= p.vx
p.y+= p.vy
}
}
// draw the particles
const draw = () => {
ctx.fillStyle = "black"
ctx.fillRect(0, 0, W, H)
// update and draw the distance field
computeDistanceField()
const imageData = new ImageData(distanceField, W, H)
ctx.putImageData(imageData, 0, 0)
ctx.fillStyle = "red"
for (const p of particles) {
ctx.beginPath()
ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI)
ctx.fill()
}
}
const loop = () => {
requestAnimationFrame(loop)
update()
draw()
}
loop()
This Pen doesn't use any external CSS resources.