Path repulsion 👣

Wandering walkers on a lonely stroll avoid crossing paths.

This is the first time I want to test plot with different colored pens. Hence I added 'pencils' to select the number of different colors to be used (or channels). Then there's also a slider to select the 'color' or channel to plot in the turtle. Be sure that, when you use these channels, you supply the same seed for every different channel to make them match.

Log in to post a comment.

// Path repulsion. Created by Jurgen Westerhof 2024

const seed = 'Empty seed is random every run.'; //type=string
const size = 90; //min=10 max=100 step=1
const population = 1; //min=0 max=4 step=.01 Multiplier of walker population per area
const pencils = 1; //min=1 max=10 step=1 The number of different colored pensils to use in a plot. Be sure to supply the same seed for different channels.
const drawPencil = 1; //min=1 max=10 step=1 The color to plot. Be sure to supply the same seed for different pencils.
const repulsion = 1; //min=1 max=2 step=.01

const config = {
    populationMultiplier: population**2,
    channels: pencils,
    drawChannel: Math.min(drawPencil, pencils),
    size: size,
    repulsion: repulsion,
};

// Seedable random number generator by David Bau: http://davidbau.com/archives/2010/01/30/random_seeds_coded_hints_and_quintillions.html
!function(a,b,c,d,e,f,g,h,i){function j(a){var b,c=a.length,e=this,f=0,g=e.i=e.j=0,h=e.S=[];for(c||(a=[c++]);d>f;)h[f]=f++;for(f=0;d>f;f++)h[f]=h[g=s&g+a[f%c]+(b=h[f])],h[g]=b;(e.g=function(a){for(var b,c=0,f=e.i,g=e.j,h=e.S;a--;)b=h[f=s&f+1],c=c*d+h[s&(h[f]=h[g=s&g+b])+(h[g]=b)];return e.i=f,e.j=g,c})(d)}function k(a,b){var c,d=[],e=typeof a;if(b&&"object"==e)for(c in a)try{d.push(k(a[c],b-1))}catch(f){}return d.length?d:"string"==e?a:a+"\0"}function l(a,b){for(var c,d=a+"",e=0;e<d.length;)b[s&e]=s&(c^=19*b[s&e])+d.charCodeAt(e++);return n(b)}function m(c){try{return o?n(o.randomBytes(d)):(a.crypto.getRandomValues(c=new Uint8Array(d)),n(c))}catch(e){return[+new Date,a,(c=a.navigator)&&c.plugins,a.screen,n(b)]}}function n(a){return String.fromCharCode.apply(0,a)}var o,p=c.pow(d,e),q=c.pow(2,f),r=2*q,s=d-1,t=c["seed"+i]=function(a,f,g){var h=[];f=1==f?{entropy:!0}:f||{};var o=l(k(f.entropy?[a,n(b)]:null==a?m():a,3),h),s=new j(h);return l(n(s.S),b),(f.pass||g||function(a,b,d){return d?(c[i]=a,b):a})(function(){for(var a=s.g(e),b=p,c=0;q>a;)a=(a+c)*d,b*=d,c=s.g(1);for(;a>=r;)a/=2,b/=2,c>>>=1;return(a+c)/b},o,"global"in f?f.global:this==c)};if(l(c[i](),b),g&&g.exports){g.exports=t;try{o=require("crypto")}catch(u){}}else h&&h.amd&&h(function(){return t})}(this,[],Math,256,6,52,"object"==typeof module&&module,"function"==typeof define&&define,"random");
Math.seedrandom(seed == ''? (new Date().getTime()): seed);

// You can find the Turtle API reference here: https://turtletoy.net/syntax
Canvas.setpenopacity(1);

// Global code will be evaluated once.
init();
const turtle = new Turtle();

const proximityThreshold = 3;

const MODE_INACTIVE = 0;
const MODE_NORMAL = 1;
const MODE_REVERSE = 2;

class Walker {
    constructor(turtle, position, direction, maxTurn = Math.PI * 2, turnResolution = Math.PI / 360) {
        this.t = turtle;
        this.p = position;
        this.d = direction;
        this.mt = maxTurn;
        this.tr = turnResolution;
        this.mode = MODE_NORMAL;
        this.startPosition = position.map(v => v);
        this.otherDirection = V.scale(direction, -1);
        
        this.turnPreference = Math.random() < .5? 1: -1;
        this.channel = (config.channels * Math.random() | 0) + 1;
    }
    step(scene) {
        if(this.mode === MODE_INACTIVE) return;
        this.t.jump(this.p);
        
        function tryCandidates(p, d, maxTurn, turnResolution, tp) {
            for(let sign = 1; sign >= -1; sign-=2) {
                for(let t = 0; t <= maxTurn; t += turnResolution) {
                    const rotatedD = V.trans(V.rot2d(tp*sign*t), d);
                    const candidateP = V.add(p, rotatedD);
                    if(!scene.proximityToRegistered(candidateP, p, config.repulsion)) {
                        return [candidateP, rotatedD];
                    }
                }
            }
            return false;
        }
        const candidateP_RotatedD = tryCandidates(this.p, this.d, this.mt, this.tr, this.turnPreference);
        
        if(candidateP_RotatedD === false) {
            if(this.mode == MODE_REVERSE) {
                this.mode = 0;
                return;
            }
            this.p = this.startPosition;
            this.d = this.otherDirection;
            this.mode = MODE_REVERSE;
            return this.step(scene);
        }
        this.p = candidateP_RotatedD[0];
        this.d = candidateP_RotatedD[1];
        if(this.channel == config.drawChannel) this.t.goto(this.p);
        scene.registerPoint(this.p);
    }
}

class WalkerScene {
    constructor(s, binSize = 2) {
        this.walkers = [];
        this.s = s;
        this.binSize = binSize;
        this.bins = [];
        for(let x = -120; x <= 120; x++) {
            const xBin = (x/binSize)|0;
            if(this.bins[xBin] == undefined) this.bins[xBin] = [];
            for(let y = -120; y <= 120; y++) {
                const yBin = (y/binSize)|0;
                if(this.bins[xBin][yBin] == undefined) this.bins[xBin][yBin] = [];
            }
        }
    }
    addWalker(walker) {
        this.walkers.push(walker);
        this.registerPoint(walker.p);
    }
    registerPoint(point) {
        const xBin = (point[0] / this.binSize) | 0;
        const yBin = (point[1] / this.binSize) | 0;
        this.bins[xBin - 1][yBin - 1].push(point.map(v => v));
        this.bins[xBin - 1][yBin    ].push(point.map(v => v));
        this.bins[xBin - 1][yBin + 1].push(point.map(v => v));
        this.bins[xBin    ][yBin - 1].push(point.map(v => v));
        this.bins[xBin    ][yBin    ].push(point.map(v => v));
        this.bins[xBin    ][yBin + 1].push(point.map(v => v));
        this.bins[xBin + 1][yBin - 1].push(point.map(v => v));
        this.bins[xBin + 1][yBin    ].push(point.map(v => v));
        this.bins[xBin + 1][yBin + 1].push(point.map(v => v));
    }
    proximityToRegistered(p, excluding, threshold = 1) {
        if(p[0] < -this.s || p[0] > this.s || p[1] < -this.s || p[1] > this.s) return true;
        const thresholdSq = threshold**2;

        const xBin = (p[0] / this.binSize) | 0;
        const yBin = (p[1] / this.binSize) | 0;

        for(let i = 0; i < this.bins[xBin][yBin].length; i++) {
            if(V.equals(this.bins[xBin][yBin][i], excluding)) continue;
            if(V.lenSq(V.sub(this.bins[xBin][yBin][i], p)) < thresholdSq) return true;
        }
        return false;
    }
    step() {
        this.walkers.forEach(walker => walker.step(this));
    }
    hasActive() {
        return this.walkers.some(w => w.mode !== MODE_INACTIVE);
    }
}
const ws = new WalkerScene(config.size);
for(let i = 0; i < config.populationMultiplier * config.size**2 / 10; i++) {
//    const newPt = [Math.random() * s * 2 - s, Math.random() * s * 2 - s];
//    ws.addWalker(new Walker(turtle, newPt, V.scale(V.norm(newPt), V.len(newPt) > 50?1: -1)));

    ws.addWalker(
        new Walker(
            turtle,
            [Math.random() * config.size * 2 - config.size, Math.random() * config.size * 2 - config.size],
            V.scale(V.norm([Math.random()-.5, Math.random()-.5]), 1)
        )
    );
}

// The walk function will be called until it returns false.
function walk(i) {
    ws.step();
    //for(let j = 0; j < 40000000; j++) {}
    return ws.hasActive();
}


function init() {
    ///////////////////////////////////////////////////////
    // Vector functions - Created by Jurgen Westerhof 2024
    // https://turtletoy.net/turtle/d068ad6040
    ///////////////////////////////////////////////////////
    class Vector {
        static add  (a,b) { return a.map((v,i)=>v+b[i]); }
        static sub  (a,b) { return a.map((v,i)=>v-b[i]); }
        static mul  (a,b) { return a.map((v,i)=>v*b[i]); }
        static div  (a,b) { return a.map((v,i)=>v/b[i]); }
        static scale(a,s) { return a.map(v=>v*s); }
    
        static det(m)                { return m.length == 1? m[0][0]: m.length == 2 ? m[0][0]*m[1][1]-m[0][1]*m[1][0]: m[0].reduce((r,e,i) => r+(-1)**(i+2)*e*this.det(m.slice(1).map(c => c.filter((_,j) => i != j))),0); }
        static angle(a)              { return Math.PI - Math.atan2(a[1], -a[0]); } //compatible with turtletoy heading
        static rot2d(angle)          { return [[Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]]; }
        static rot3d(yaw,pitch,roll) { return [[Math.cos(yaw)*Math.cos(pitch), Math.cos(yaw)*Math.sin(pitch)*Math.sin(roll)-Math.sin(yaw)*Math.cos(roll), Math.cos(yaw)*Math.sin(pitch)*Math.cos(roll)+Math.sin(yaw)*Math.sin(roll)],[Math.sin(yaw)*Math.cos(pitch), Math.sin(yaw)*Math.sin(pitch)*Math.sin(roll)+Math.cos(yaw)*Math.cos(roll), Math.sin(yaw)*Math.sin(pitch)*Math.cos(roll)-Math.cos(yaw)*Math.sin(roll)],[-Math.sin(pitch), Math.cos(pitch)*Math.sin(roll), Math.cos(pitch)*Math.cos(roll)]]; }
        static trans(matrix,a)       { return a.map((v,i) => a.reduce((acc, cur, ci) => acc + cur * matrix[ci][i], 0)); }
        //Mirror vector a in a ray through [0,0] with direction mirror
        static mirror2d(a,mirror)    { return [Math.atan2(...mirror)].map(angle => this.trans(this.rot2d(angle), this.mul([-1,1], this.trans(this.rot2d(-angle), a)))).pop(); }
        
        static equals(a,b)   { return !a.some((e, i) => e != b[i]); }
        static approx(a,b,p) { return this.len(this.sub(a,b)) < (p === undefined? .001: p); }
        static norm  (a)     { return this.scale(a,1/this.len(a)); }
        static len   (a)     { return Math.hypot(...a); }
        static lenSq (a)     { return a.reduce((a,c)=>a+c**2,0); }
        static lerp  (a,b,t) { return a.map((v, i) => v*(1-t) + b[i]*t); }
        static dist  (a,b)   { return Math.hypot(...this.sub(a,b)); }
        
        static dot  (a,b)   { return a.reduce((a,c,i) => a+c*b[i], 0); }
        static cross(...ab) { return ab[0].map((e, i) => ab.map(v => v.filter((ee, ii) => ii != i))).map((m,i) => (i%2==0?-1:1)*this.det(m)); }
    }
    this.V = Vector;
    
    class Intersection2D {
        //a-start, a-direction, b-start, b-direction
        //returns false on no intersection or [[intersection:x,y], scalar a-direction, scalar b-direction
        static info(as, ad, bs, bd) { const d = V.sub(bs, as), det = -V.det([bd, ad]); if(det === 0) return false; const res = [V.det([d, bd]) / det, V.det([d, ad]) / det]; return [V.add(as, V.scale(ad, res[0])), ...res]; }
        static ray(a, b, c, d) { return this.info(a, b, c, d); }
        static segment(a,b,c,d, inclusiveStart = true, inclusiveEnd = true) { const i = this.info(a, V.sub(b, a), c, V.sub(d, c)); return i === false? false: ( (inclusiveStart? 0<=i[1] && 0<=i[2]: 0<i[1] && 0<i[2]) && (inclusiveEnd?   i[1]<=1 && i[2]<=1: i[1]<1 && i[2]<1) )?i[0]:false;}
        static tour(tour, segmentStart, segmentDirection) { return tour.map((e, i, a) => [i, this.info(e, V.sub(a[(i+1)%a.length], e), segmentStart, segmentDirection)]).filter(e => e[1] !== false && 0 <= e[1][1] && e[1][1] <= 1).filter(e => 0 <= e[1][2]).map(e => ({position: e[1][0],tourIndex: e[0],tourSegmentPortion: e[1][1],segmentPortion: e[1][2],}));}
        static inside(tour, pt) { return tour.map((e,i,a) => this.segment(e, a[(i+1)%a.length], pt, [Number.MAX_SAFE_INTEGER, 0], true, false)).filter(e => e !== false).length % 2 == 1; }
        static circles(centerA, radiusA, centerB, radiusB) {const result = {intersect_count: 0,intersect_occurs: true,one_is_in_other: false,are_equal: false,point_1: [null, null],point_2: [null, null],};const dx = centerB[0] - centerA[0];const dy = centerB[1] - centerA[1];const dist = Math.hypot(dy, dx);if (dist > radiusA + radiusB) {result.intersect_occurs = false;}if (dist < Math.abs(radiusA - radiusB) && !N.approx(dist, Math.abs(radiusA - radiusB))) {result.intersect_occurs = false;result.one_is_in_other = true;}if (V.approx(centerA, centerB) && radiusA === radiusB) {result.are_equal = true;}if (result.intersect_occurs) {const centroid = (radiusA**2 - radiusB**2 + dist * dist) / (2.0 * dist);const x2 = centerA[0] + (dx * centroid) / dist;const y2 = centerA[1] + (dy * centroid) / dist;const prec = 10000;const h = (Math.round(radiusA**2 * prec)/prec - Math.round(centroid**2 * prec)/prec)**.5;const rx = -dy * (h / dist);const ry = dx * (h / dist);result.point_1 = [x2 + rx, y2 + ry];result.point_2 = [x2 - rx, y2 - ry];if (result.are_equal) {result.intersect_count = null;} else if (result.point_1.x === result.point_2.x && result.point_1.y === result.point_2.y) {result.intersect_count = 1;} else {result.intersect_count = 2;}}return result;}
    }
    this.Intersection = Intersection2D;
    
    class PathTools {
        static bezier(p1, cp1, cp2, p2, steps = null) {steps = (steps === null? (V.len(V.sub(cp1, p1)) + V.len(V.sub(cp2, cp1)) + V.len(V.sub(p2, cp2))) | 0: steps) - 1;return Array.from({length: steps + 1}).map((v, i, a, f = i/steps) => [[V.lerp(p1, cp1, f),V.lerp(cp1, cp2, f),V.lerp(cp2, p2, f)]].map(v => V.lerp(V.lerp(v[0], v[1], f), V.lerp(v[1], v[2], f), f))[0]);}
        // https://stackoverflow.com/questions/18655135/divide-bezier-curve-into-two-equal-halves#18681336
        static splitBezier(p1, cp1, cp2, p2, t=.5) {const e = V.lerp(p1, cp1, t);const f = V.lerp(cp1, cp2, t);const g = V.lerp(cp2, p2, t);const h = V.lerp(e, f, t);const j = V.lerp(f, g, t);const k = V.lerp(h, j, t);return [[p1, e, h, k], [k, j, g, p2]];}
        static circular(radius,verticeCount,rotation=0) {return Array.from({length: verticeCount}).map((e,i,a,f=i*2*Math.PI/verticeCount+rotation) => [radius*Math.cos(f),radius*Math.sin(f)])}
        static circle(r){return this.circular(r,Math.max(12, r*2*Math.PI|0));}
        static arc(radius, extend = 2 * Math.PI, clockWiseStart = 0, steps = null, includeLast = false) { return [steps == null? (radius*extend+1)|0: steps].map(steps => Array.from({length: steps}).map((v, i, a) => [radius * Math.cos(clockWiseStart + extend*i/(a.length-(includeLast?1:0))), radius * Math.sin(clockWiseStart + extend*i/(a.length-(includeLast?1:0)))])).pop(); }
        static draw(turtle, path) {path.forEach((pt, i) => turtle[i==0?'jump':'goto'](pt));}
        static drawTour(turtle, path) {this.draw(turtle, path.concat([path[0]]));}
        static drawPoint(turtle, pt, r = .1) {this.drawTour(turtle, this.circle(r).map(e => V.add(e, pt)));}
        static drawArrow(turtle, s, d, width = 6, length = 3) {turtle.jump(s);const arrowHeadBase = V.add(s,d);turtle.goto(arrowHeadBase);turtle.goto(V.add(arrowHeadBase, V.trans(V.rot2d(-V.angle(d)), [-length, width/2])));turtle.jump(V.add(arrowHeadBase, V.trans(V.rot2d(-V.angle(d)), [-length, -width/2])));turtle.goto(arrowHeadBase);}
        static circlesTangents(c1_center, c1_radius, c2_center, c2_radius, internal = false) {let middle_circle = [V.scale(V.sub(c1_center, c2_center), .5)].map(hwp => [V.add(c2_center, hwp), V.len(hwp)]).pop();if(!internal && c1_radius == c2_radius) {let target = V.sub(c2_center, c1_center);let scaledTarget = V.scale(target, c1_radius/V.len(target));let partResult = [V.add(c1_center, V.trans(V.rot2d(Math.PI/2), scaledTarget)),V.add(c1_center, V.trans(V.rot2d(Math.PI/-2), scaledTarget))];return [partResult,partResult.map(pt => V.add(pt, target))]}let swap = !internal && c2_radius > c1_radius;if(swap) {let t = [[...c1_center], c1_radius];c1_center = c2_center;c1_radius = c2_radius;c2_center = t[0];c2_radius = t[1];}let internal_waypoints = intersectCircles2(c1_center, c1_radius + (internal?c2_radius:-c2_radius), ...middle_circle);if(internal_waypoints.length == 0) return [];const circlePointAtDirection2 = (circle_center, radius, direction) => V.add(circle_center, V.scale(direction, radius/V.len(direction)));const result = [[circlePointAtDirection2(c1_center, c1_radius, V.sub(internal_waypoints[0], c1_center)),circlePointAtDirection2(c1_center, c1_radius, V.sub(internal_waypoints[1], c1_center))],[circlePointAtDirection2(c2_center, c2_radius, internal? V.sub(c1_center, internal_waypoints[0]): V.sub(internal_waypoints[0], c1_center)),circlePointAtDirection2(c2_center, c2_radius, internal? V.sub(c1_center, internal_waypoints[1]): V.sub(internal_waypoints[1], c1_center))]];return swap? [[result[1][1],result[1][0]],[result[0][1],result[0][0]]]: result;}
    }

    this.PT = PathTools;
    
    class Complex {
        static add(a,b)     { return V.add(a,b); }
        static sub(a,b)     { return V.sub(a,b); }
        static scale(a,s)   { return V.scale(a,s); }
        static mult(a,b)    { return [a[0]*b[0]-a[1]*b[1],a[0]*b[1]+a[1]*b[0]]; }
        static sqrt(a)      { return [[Math.hypot(...a)**.5, Math.atan2(...a.reverse()) / 2]].map(ra => [ra[0]*Math.cos(ra[1]), ra[0]*Math.sin(ra[1])]).pop(); }
    }
    this.C = Complex;
    
    class Numbers {
        static approx(a,b,p)        { return Math.abs(a-b) < (p === undefined? .001: p); }
        static clamp(a, min, max)   { return Math.min(Math.max(a, min), max); }
    }
    this.N = Numbers;
    
    class Matrix {
        static bayer(order) { return [...Array(1<<order)].map((_,y,a) => { const g = (k=order,x)=>k--&&4*g(k,x)|2*(x>>k)+3*(y>>k&1)&3; return a.map(g); }); }
        static rotate(m) { return m[0].map((e, i) => m.map(r => r[i]).reverse()); }
        static rotateCCW(m) { return m[0].map((e, i) => m.map(r => r[r.length-1-i])); }
        static add(a,b) { return a.map((e, c) => e.map((e, r) => a[c][r] + b[c][r])); }
        static sub(a,b) { return a.map((e, c) => e.map((e, r) => a[c][r] - b[c][r])); }
        static multiply(a,b) { return Array.from({length: b.length}, (e,resCol) => Array.from({length: a[0].length}, (e,resRow) => b[resCol].reduce((acc, c, bRow) => acc + a[bRow][resRow] * b[resCol][bRow], 0)));}
        static scale(a,s) { return a.map((e, c) => e.map((e, r) => a[c][r] * s)); }
        static random(c,r,fillFn = Math.random) { return Array.from({length: c}, (e,i) => Array.from({length: r}, e => fillFn(c, r))); }
        static identity(d) { return Array.from({length: d}, (e,c) => Array.from({length: d}, (e, r) => c==r?1:0 )); }
        static log(m, name, logFn = console.log) { if(name != undefined) logFn(name); if(m === undefined || (typeof m == 'object' && (m[0] === undefined || m[0][0] === undefined))) { return logFn(`Failed to log matrix:`, m); } logFn(m[0].map((e,r) => m.map((e,c) => m[c][r]).join(', ')).join('\n')); }
        static invert(m) { let _A = m.map(col => col.map(cell => cell));/*clone matrix*/let temp;const N = _A.length;const E = Array.from({length: N}, (e,i) => Array.from({length: _A[0].length}, (e,j) => i==j?1:0));for (let k = 0; k < N; k++) {temp = _A[k][k];for (let j = 0; j < N; j++) {_A[k][j] /= temp;E[k][j] /= temp;}for (let i = k + 1; i < N; i++) {temp = _A[i][k];for (let j = 0; j < N; j++) {_A[i][j] -= _A[k][j] * temp;E[i][j] -= E[k][j] * temp;}}}for (let k = N - 1; k > 0; k--) {for (let i = k - 1; i >= 0; i--) {temp = _A[i][k];for (let j = 0; j < N; j++) {_A[i][j] -= _A[k][j] * temp;E[i][j] -= E[k][j] * temp;}}}return E; }
        static determinant(m) { return m.length == 1 ?m[0][0] :m.length == 2 ? m[0][0]*m[1][1]-m[0][1]*m[1][0] :m[0].reduce((r,e,i) => r+(-1)**(i+2)*e*this.determinant(m.slice(1).map(c => c.filter((_,j) => i != j))), 0)}
        static flip(m) { return Array.from({length: m[0].length}, (_, r) => Array.from({length: m.length}, (e, c) => m[c][r])); }
        static sum(m) { return m.reduce((a, c) => a + c.reduce((aa, cc) => aa + cc, 0), 0); }
    }
    this.M = Matrix;
    
    class Algorithms {
        static nthTriangular(n) { return ((n * n) + n) / 2; }
    }
    this.A = Algorithms;
}