Particles follow a vector field which changes over time. They stop when they collide with another particle's trail.
Log in to post a comment.
const RAD = 90; // min=10, max=100, step=1
const N_PARTICLES = 150; // min=10, max=300, step=1
const ITERS = 200; // min=10, max=300, step=1
const STEPSIZE = 1; // min=0.25, max=5, step=0.25
const COLLIDE_DIST = 1.5; // min=0.5, max=5, step=0.5
// x and y in the range -100 to 100
// t in the range 0 to ITERS * STEPSIZE
const SWIRL = 25; // min=-50, max=50, step=1
let field = (x, y, t) => ({
x: -x/50 + Math.sin(y / 10 + t/50) / 2 - y * SWIRL/1000,
y: -y/50 + 1 - t/20 + Math.sin(x/10) / 10 + x* SWIRL/1000,
})
let len = (pt) =>
dist(0, 0, pt.x, pt.y);
let normalize = (pt) => {
let length = len(pt);
if (length === 0) { return {x: 0, y: 0}; }
return {
x: pt.x / length,
y: pt.y / length,
};
}
//=========================================================================
Canvas.setpenopacity(1);
const turtle = new Turtle();
turtle.pendown();
let rand = (min, max) =>
Math.random() * (max - min) + min;
let randInt = (min, max) =>
Math.floor(rand(min, max));
let remap = (x, oldmin, oldmax, newmin, newmax) => {
let t = (x - oldmin) / (oldmax - oldmin);
return t * (newmax - newmin) + newmin;
}
let dist = (x1, y1, x2, y2) =>
Math.sqrt((x1-x2) * (x1-x2) + (y1-y2) * (y1-y2));
let particle = (x, y) => ({
point: {x:x, y:y},
history: [],
alive: true,
})
let moveto = (particle, x, y) => {
particle.history.push(particle.point);
particle.point = {x:x, y:y};
}
let moveby = (particle, dx, dy) => {
particle.history.push(particle.point);
particle.point = {x: particle.point.x + dx, y: particle.point.y + dy};
}
let FAR = 99999999;
let distToTrail = (p1, p2) => {
// closest distance from p1's current pos to p2's history
let lowestDist = FAR;
for (let hp of p2.history) {
lowestDist = Math.min(lowestDist, dist(
p1.point.x, p1.point.y,
hp.x, hp.y
));
}
return lowestDist;
}
let distToAny = (p, particles) => {
let lowestDist = FAR;
for (let p2 of particles) {
if (p === p2) { continue; }
lowestDist = Math.min(lowestDist, distToTrail(p, p2));
}
return lowestDist
}
//=========================================================================
// make initial circle of particles
let PARTICLES = [];
for (let ii = 0; ii < N_PARTICLES; ii++) {
let theta = remap(ii, 0, N_PARTICLES, 0, 2 * Math.PI);
let p = particle(
Math.sin(theta) * RAD,
Math.cos(theta) * RAD,
);
PARTICLES.push(p);
}
//=========================================================================
// iterate through vector field
// iterate!
for (let iter = 0; iter < ITERS; iter++) {
let t = iter * STEPSIZE;
let numAlive = 0;
//for (let particle of PARTICLES) {
for (let rr = 0; rr < N_PARTICLES; rr++) {
// instead of looping through each particle in the same order each time,
// update N particles each iteration (with replacement)
// to allow some to get ahead of others
let particle = PARTICLES[randInt(0, PARTICLES.length)];
if (particle.alive) {
numAlive += 1;
let force = field(particle.point.x, particle.point.y, t);
moveby(particle, force.x * STEPSIZE, force.y * STEPSIZE);
if (distToAny(particle, PARTICLES) < COLLIDE_DIST) {
particle.alive = false;
}
}
}
if (numAlive === 0) {
console.log('all particles stopped after ' + iter + ' iters');
break;
}
}
//=========================================================================
// draw
// The walk function will be called until it returns false.
console.log('-------------------');
function walk(ii) {
let p = PARTICLES[ii];
p.history.push(p.point);
turtle.jump(p.history[0].x, p.history[0].y);
for (let jj = 1; jj < p.history.length; jj++) {
turtle.goto(p.history[jj].x, p.history[jj].y);
}
return ii < PARTICLES.length - 1;
}