<canvas>
body {
margin: 0;
overflow: hidden;
}
const STG = {
sensorsOffset: 80,
nbSensors: 8,
noiseScale: 0.003,
noiseOffset: 0,
noiseExponent: 1.4,
invFriction: 0.98,
attrStrength: 1,
}
const gui = new dat.GUI()
gui.add(STG, "sensorsOffset", 0, 200)
const offsetCtrl = gui.add(STG, "noiseOffset", 0, 3)
const nsExponentCtrl = gui.add(STG, "noiseExponent", 0.1, 4)
gui.add(STG, "invFriction", 0.9, 0.9999)
gui.add(STG, "attrStrength", 0.5, 4)
const cvs = document.querySelector("canvas")
const ctx = cvs.getContext("2d")
let W, H
cvs.width = W = window.innerWidth
cvs.height = H = window.innerHeight
// generate a noise map
const simplex = new SimplexNoise("aaa")
const noiseMap = new Float32Array(W*H)
const noiseMapUint8 = new Uint8ClampedArray(W*H*4)
const index = (x, y) => x + y*W
// the ImageData which will store the noise to be drawn
let noiseMapImg
// fills the noise bases on the
const updateNoise = () => {
for (let x = 0; x < W; x++) {
for (let y = 0; y < H; y++) {
const idx = index(x, y)
let N = simplex.noise3D(x * STG.noiseScale, y * STG.noiseScale, STG.noiseOffset) * .5 + .5
N = Math.pow(N, STG.noiseExponent)
const Nint = (N*255)|0
noiseMap[idx] = N
noiseMapUint8[idx*4] = Nint
noiseMapUint8[idx*4+1] = Nint
noiseMapUint8[idx*4+2] = Nint
noiseMapUint8[idx*4+3] = 255
}
}
// put the noise map data into ImageData for quicker rendering
noiseMapImg = new ImageData(noiseMapUint8, W, H)
}
offsetCtrl.onChange(updateNoise)
nsExponentCtrl.onChange(updateNoise)
updateNoise()
// initialize the particle & sensors
const particle = {
x: Math.random()*W,
y: Math.random()*H,
vx: 0,
vy: 0,
ax: 0,
ay: 0
}
const sensors = new Array(STG.nbSensors)
for (let i = 0; i < sensors.length; i++) {
sensors[i] = {
angle: i/sensors.length * 2*Math.PI,
weight: 0,
x: 0,
y: 0,
fakeX: 0,
fakeY: 0
}
}
// add a button to reset the particle
const btnCtrl = {
resetParticle: () => {
particle.x = Math.random()*W
particle.y = Math.random()*H
particle.vx = 0
particle.vy = 0
particle.ax = 0
particle.ay = 0
}
}
gui.add(btnCtrl, "resetParticle")
// 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 }
}
const update = () => {
// update each sensor by sampling the noise map
for (const sensor of sensors) {
// get sensor position
sensor.fakeX = particle.x + Math.cos(sensor.angle) * STG.sensorsOffset
sensor.fakeY = particle.y + Math.sin(sensor.angle) * STG.sensorsOffset
const Spos = toroidal(sensor.fakeX, sensor.fakeY)
const idx = index(Spos.x|0, Spos.y|0)
sensor.x = Spos.x
sensor.y = Spos.y
sensor.weight = noiseMap[idx]
}
// compute the direction based on the weight of each sensor
let dirX = 0
let dirY = 0
for (const sensor of sensors) {
dirX+= Math.cos(sensor.angle) * sensor.weight
dirY+= Math.sin(sensor.angle) * sensor.weight
}
dirX/= STG.nbSensors
dirY/= STG.nbSensors
particle.ax = dirX
particle.ay = dirY
// now update the particle
particle.vx+= particle.ax * STG.attrStrength
particle.vy+= particle.ay * STG.attrStrength
particle.x+= particle.vx
particle.y+= particle.vy
particle.vx*= STG.invFriction
particle.vy*= STG.invFriction
// toroidal topology
const { x, y } = toroidal(particle.x, particle.y)
particle.x = x
particle.y = y
}
const drawVector = (cx, cy, x, y) => {
ctx.save()
ctx.translate(cx, cy)
const angle = Math.atan2(y, x)
const len = Math.sqrt(x**2 + y**2)
ctx.rotate(angle)
// the main line
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.translate(len, 0)
ctx.lineTo(0, 0)
ctx.stroke()
// the arrow
ctx.beginPath()
ctx.moveTo(-5, 5)
ctx.lineTo(0, 0)
ctx.lineTo(-5, -5)
ctx.stroke()
ctx.restore()
}
const draw = () => {
// clear the background
ctx.fillStyle = "black"
ctx.fillRect(0, 0, W, H)
// draw the simplex noise
ctx.putImageData(noiseMapImg, 0, 0)
// then the sensors
for (const sensor of sensors) {
// draw the link
ctx.strokeStyle = "blue"
ctx.lineWidth = 1.5
ctx.beginPath()
ctx.moveTo(particle.x, particle.y)
ctx.lineTo(sensor.fakeX, sensor.fakeY)
ctx.stroke()
// draw the sensor
ctx.lineWidth = 2
const val = (sensor.weight*255)|0
ctx.fillStyle = `rgb(${val}, ${val}, ${val})`
ctx.beginPath()
ctx.arc(sensor.x, sensor.y, 8, 0, 2*Math.PI)
ctx.fill()
ctx.stroke()
// draw the sensor weight, so that it's always visible
ctx.strokeStyle = "#00ff00"
drawVector(
particle.x,
particle.y,
Math.cos(sensor.angle) * sensor.weight * STG.sensorsOffset,
Math.sin(sensor.angle) * sensor.weight * STG.sensorsOffset
)
}
// draw the particle
ctx.fillStyle = "white"
ctx.beginPath()
ctx.arc(particle.x, particle.y, 18, 0, 2*Math.PI)
ctx.fill()
ctx.fillStyle = "red"
ctx.beginPath()
ctx.arc(particle.x, particle.y, 16, 0, 2*Math.PI)
ctx.fill()
// draw the direction of the particle
ctx.strokeStyle = "red"
ctx.lineWidth = 4
const mag = Math.sqrt(particle.ax**2 + particle.ay**2)
const dx = particle.ax / mag
const dy = particle.ay / mag
const L = Math.min(300, Math.max(30, mag * 500))
drawVector(particle.x, particle.y, dx * L, dy * L)
}
// main loop
const loop = () => {
requestAnimationFrame(loop)
update()
draw()
}
loop()
This Pen doesn't use any external CSS resources.