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; }