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.
// Forked from "Colliding Flow 6" by mathrabbit // https://turtletoy.net/turtle/55b055fc96 // Forked from "Colliding Flow 4" by mathrabbit // https://turtletoy.net/turtle/26c2a0352e // Forked from "Colliding Flow 3" by mathrabbit // https://turtletoy.net/turtle/5515eb16b3 // Forked from "Colliding Flow 2" by mathrabbit // https://turtletoy.net/turtle/36ead8f7d2 const RAD = 80; // min=10, max=100, step=5 const OUTER_BOUNDS = 95; // min=10, max=110, step=5 // kill particles that exceed this const N_PARTICLES = 2000; // min=10, max=300, step=1 const MIN_ITERS = 10; // length of a snake const MAX_ITERS = 50; const ITER_RAND_POW = 3; const STEPSIZE = 0.5; const COLLIDE_DIST = 1.25; // min=0.5, max=5, step=0.5 const BOUNDS_KILL_CHANCE = 1.0; const RAND_START_TIME = 100; // bools const GROW_HEADS = 1; // min=0, max=1, step=1 const GROW_TAILS = 0; // min=0, max=1, step=1 const SQUARE_BOUNDS = 1; // min=0, max=1, step=1 //========================================================================= // MAIN FUNCTIONS TO PLAY WITH // 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=5 let field = (x, y, t) => ({ x: -x/50 + Math.sin(y / 10 + t/40) / 2 - y * SWIRL/1000, y: -y/50 + 1 - t/5000 + Math.sin(x/10) / 10 + x* SWIRL/1000, }) let inBoundsCircle = (p) => dist(0, 0, p.x, p.y) < OUTER_BOUNDS; let inBoundsSquare = (p) => Math.abs(p.x) < OUTER_BOUNDS && Math.abs(p.y) < OUTER_BOUNDS; //let inBounds = inBoundsCircle; let inBounds = SQUARE_BOUNDS ? inBoundsSquare : inBoundsCircle; let startNewParticle = () => { let theta = rand(0, 2 * Math.PI); let rad = rand(RAD, RAD * 0); return makeParticle( Math.sin(theta) * rad, Math.cos(theta) * rad, GROW_HEADS, GROW_TAILS ); } //========================================================================= // UTILS let rand = (min, max, pow) => { if (pow === undefined) { return Math.random() * (max - min) + min; } else { return Math.pow(Math.random(), pow) * (max - min) + min; } } let randInt = (min, max, pow) => Math.floor(rand(min, max, pow)); 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 chance = (prob) => Math.random() < prob; 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 } //========================================================================= // MAIN console.log('-------------------'); let PARTICLES = []; // add particles one at a time let fails = 0; // number of consecutive failures to place a particle while (PARTICLES.length < N_PARTICLES && fails < 100) { let particle = startNewParticle(); if (distToAny(particle, true, PARTICLES) < COLLIDE_DIST) { fails += 1; continue; } fails = 0; PARTICLES.push(particle); // iterate this particle let startTime = rand(0, RAND_START_TIME); let iters = randInt(MIN_ITERS, MAX_ITERS, ITER_RAND_POW); for (let iter = 0; iter < iters; iter++) { let t = iter * STEPSIZE + startTime; if (particle.headAlive) { let p = getHead(particle); if (!inBounds(p) && chance(BOUNDS_KILL_CHANCE)) { 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) { let p = getTail(particle); if (!inBounds(p) && chance(BOUNDS_KILL_CHANCE)) { 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 (!particle.headAlive && !particle.tailAlive) { break; } } } // remove long snakes //PARTICLES = PARTICLES.filter(part => part.points.length <= 12); //========================================================================= // 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; }