Colliding Flow 2

Snakes grow at their heads and tails following a vector field that changes over time. They stop when they hit another snake.

Log in to post a comment.

const RAD = 80; // min=10, max=100, step=1
const N_PARTICLES = 100; // 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.25; // min=0.5, max=5, step=0.5
const OUTER_BOUNDS = 92; // kill particles that exceed this
const GROW_HEADS = true;
const GROW_TAILS = true;

// 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 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 choose = (arr) =>
    arr[randInt(0, arr.length)];
let dist = (x1, y1, x2, y2) =>
    Math.sqrt((x1-x2) * (x1-x2) + (y1-y2) * (y1-y2));
let len = (p) =>
    dist(0, 0, p.x, p.y);
let normalize = (p) => {
    let length = len(p);
    if (length === 0) { return {x: 0, y: 0}; }
    return {
        x: p.x / length,
        y: p.y / length,
    };
}

let makeParticle = (x, y, headAlive, tailAlive) => ({
    points: [{x:x, y:y}], // [head, ... history ..., tail]
    headAlive: headAlive,
    tailAlive: tailAlive,
})
let getHead = (particle) =>
    particle.points[0];
let getTail = (particle) =>
    particle.points[particle.points.length-1];

let moveTo = (particle, isHead, x, y) => {
    let p2 = {x: x, y: y};
    isHead ? particle.points.unshift(p2) : particle.points.push(p2);
}
let moveBy = (particle, isHead, dx, dy) => {
    let p = isHead ? getHead(particle) : getTail(particle);
    let p2 = {x: p.x + dx, y: p.y + dy};
    isHead ? particle.points.unshift(p2) : particle.points.push(p2);
}

let FAR = 99999999;
let distToTrail = (part1, isHead, part2) => {
    // closest distance from p1's current head or tail to p2's whole trail
    if (part1 === part2) { return FAR; }
    let lowestDist = FAR;
    let p1 = isHead ? getHead(part1) : getTail(part1);
    for (let p2 of part2.points) {
        lowestDist = Math.min(lowestDist, dist(
            p1.x, p1.y, p2.x, p2.y
        ));
    }
    return lowestDist;
}
let distToAny = (particle, isHead, particles) => {
    let lowestDist = FAR;
    for (let p2 of particles) {
        lowestDist = Math.min(lowestDist, distToTrail(particle, isHead, p2));
    }
    return lowestDist
}

//=========================================================================
// make initial circle of particles

console.log('-------------------');

let PARTICLES = [];
for (let ii = 0; ii < N_PARTICLES; ii++) {
    let theta = remap(ii, 0, N_PARTICLES, 0, 2 * Math.PI);
    let particle = makeParticle(
        Math.sin(theta) * RAD,
        Math.cos(theta) * RAD,
        GROW_HEADS, GROW_TAILS
    );
    PARTICLES.push(particle);
}

//=========================================================================
// 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++) {
        let particle = choose(PARTICLES);
        // 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
        if (particle.headAlive) {
            numAlive += 1;
            let p = getHead(particle);
            if (Math.abs(p.x) > OUTER_BOUNDS || Math.abs(p.y) > OUTER_BOUNDS) {
                particle.headAlive = false;
                continue;
            }
            let force = field(p.x, p.y, t);
            moveBy(particle, true, force.x * STEPSIZE, force.y * STEPSIZE);
            if (distToAny(particle, true, PARTICLES) < COLLIDE_DIST) {
                particle.headAlive = false;
            }
        }
        if (particle.tailAlive) {
            numAlive += 1;
            let p = getTail(particle);
            if (Math.abs(p.x) > OUTER_BOUNDS || Math.abs(p.y) > OUTER_BOUNDS) {
                particle.tailAlive = false;
                continue;
            }
            let force = field(p.x, p.y, t);
            moveBy(particle, false, -force.x * STEPSIZE, -force.y * STEPSIZE);
            if (distToAny(particle, false, PARTICLES) < COLLIDE_DIST) {
                particle.tailAlive = false;
            }
        }
    }
    if (numAlive === 0) {
        console.log('all particles stopped after ' + iter + ' iters');
        break;
    }
}

//=========================================================================
// draw
// The walk function will be called until it returns false.

let numSegs = 0;
for (let particle of PARTICLES) {
    numSegs += particle.points.length - 1;
}
console.log('' + numSegs + ' line segments');

Canvas.setpenopacity(1);
const turtle = new Turtle();
turtle.pendown();
function walk(ii) {
    let points = PARTICLES[ii].points;
    turtle.jump(points[0].x, points[0].y);
    for (let jj = 1; jj < points.length; jj++) {
        turtle.goto(points[jj].x, points[jj].y);
        numSegs += 1;
    }
    return ii < PARTICLES.length - 1;
}