Gradient path mutations 📿

Mutate your path!
In 8 directions! (or 4)
(...while maintaining cell size constraints)

Log in to post a comment.

// Forked from "Bounded path mutation 📿" by Jurgen
// https://turtletoy.net/turtle/4e696524dd

// Forked from "Path mutation" by reinder
// https://turtletoy.net/turtle/ead5b35378

// Path mutation. Created by Reinder Nijhoff 2024 - @reindernijhoff
//
// https://turtletoy.net/turtle/ead5b35378
//

const evolution = 4; //min=4 max=8 step=4 (In 4 directions, In 8 directions)
const mutationRate = .195; // min=0.0, max=0.5, step=0.001
const mutationDefect = 57.3; // min=0, max=100, step=0.1
const mutationCountMin = 10; // min=1, max=100, step=1
const mutationCountMax = 30; // min=1, max=150, step=1

const grid = 13; // min=3, max=25, step=2

const pathInput = `M0,-37 C-10,-37 -26,-22 -30,-14 C-32,-10 -38,-3 -35,2 C-28,17 -7,39 13,29
C14,28 17,30 18,29 C59,9 37,-36 -3,-36`; // type=path, bbox=-40,-40,80,80 Click here to redraw the path

let   seed = 1; // min=1, max=1000, step=1
let   tokens = pathInput.match(/([0-9.-]+|[MLC])/g);

const populatedGrid = runUglyCodeToPopulateTheGrid();

function Translate(x,y) { return p => [p[0]+x, p[1]+y]; }
function Scale(s) { return p => [p[0]*s, p[1]*s]; }

function lerpTokens(a, b, p) {
    return a.map((token, index) => {
        if (isNumber(token)) {
            return token * (1-p) + b[index] * p;
        } else return token;
    });
}

function mutation(tokens) {
    return tokens.map(token => {
       if (isNumber(token)) {
           return random() < mutationRate ? token - (random()-.5)*mutationDefect : token;
       } else return token;
    });
}

function walk(i) {
    const y = i/grid|0, x = i%grid;
    const path = Path(populatedGrid[x][y]);

    const steps = path.length() | 0;
    
    const turtle = new Tortoise(path.p(0));
    turtle.addTransform(Scale( 132 / Math.max(...path.size()) / grid));
    turtle.addTransform(Translate((x+.5)*180/grid-90, (y+.5)*180/grid-90));
    
    for (let i=0; i<steps; i++) {
        turtle.goto(path.p( i/steps ));
    }
    
    return i < grid**2-1;
}

function isNumber(n) {
    return !isNaN(parseFloat(n)) && isFinite(n);
}

function random() {
    let r = 1103515245 * ((++seed >> 1) ^ seed);
    r = 1103515245 * (r ^ (r>>3));
    r = r ^ (r >> 16);
    return r / 32768 % 1;	
}

////////////////////////////////////////////////////////////////
// Modified path utility code. Created by Reinder Nijhoff 2023
// Parses a single SVG path (only M, C and L statements are
// supported). The p-method will return
// [...position, ...derivative] for a normalized point t.
//
// https://turtletoy.net/turtle/46adb0ad70
//
// Modified by Jurgen Westerhof 2024, added bb() and size()
////////////////////////////////////////////////////////////////
function Path(tokens) {
    class MoveTo {
        constructor(p) { this.p0 = p; }
        p(t, s) { return [...this.p0, 1, 0]; }
        length() { return 0; }
    }
    class LineTo {
        constructor(p0, p1) { this.p0 = p0, this.p1 = p1; }
        p(t, s = 1) {
            const nt = 1 - t, p0 = this.p0, p1 = this.p1;
            return [ 
                nt*p0[0] + t*p1[0],
                nt*p0[1] + t*p1[1],
                (p1[0] - p0[0]) * s,
                (p1[1] - p0[1]) * s,
            ];
        }
        length() { 
            const p0 = this.p0, p1 = this.p1;
            return Math.hypot(p0[0]-p1[0], p0[1]-p1[1]);
        }
    }
    class BezierTo {
        constructor(p0, c0, c1, p1) { this.p0 = p0, this.c0 = c0, this.c1 = c1, this.p1 = p1; }
        p(t, s = 1) {
            const nt = 1 - t, p0 = this.p0, c0 = this.c0, c1 = this.c1, p1 = this.p1;
            return [ 
                nt*nt*nt*p0[0] + 3*t*nt*nt*c0[0] + 3*t*t*nt*c1[0] + t*t*t*p1[0],
                nt*nt*nt*p0[1] + 3*t*nt*nt*c0[1] + 3*t*t*nt*c1[1] + t*t*t*p1[1],
                (3*nt*nt*(c0[0]-p0[0]) + 6*t*nt*(c1[0]-c0[0]) + 3*t*t*(p1[0]-c1[0])) * s,
                (3*nt*nt*(c0[1]-p0[1]) + 6*t*nt*(c1[1]-c0[1]) + 3*t*t*(p1[1]-c1[1])) * s,
            ];
        }
        length() {
            return this._length || (
                this._length = Array.from({length:25}, (x, i) => this.p(i/25)).reduce( 
                    (a,c,i,v) => i > 0 ? a + Math.hypot(c[0]-v[i-1][0], c[1]-v[i-1][1]) : a, 0));
        }
    }
    class Path {
        constructor(tokens) {
            this.segments = [];
            this.parsePath(tokens);
        }
        parsePath(t) {
            for (let s, i=0; i<t.length;) {
                switch (t[i++]) {
                    case 'M': this.add(new MoveTo(s=[t[i++],t[i++]]));
                              break;
                    case 'L': this.add(new LineTo(s, s=[t[i++],t[i++]]));
                              break;
                    case 'C': this.add(new BezierTo(s, [t[i++],t[i++]], [t[i++],t[i++]], s=[t[i++],t[i++]]));
                              break;
                    default:  i++;
                }
            }
        }
        add(segment) {
            this.segments.push(segment);
            this._length = 0;
            
            this._bb = undefined;
            this._size = undefined;
        }
        length() {
            return this._length || (this._length = this.segments.reduce((a,c) => a + c.length(), 0));
        }
        bb(sampleRate = .01) {
            if(this._bb === undefined) {
                this._bb = Array.from({length: 1 / sampleRate + 1})
                                .map((v, i) => this.p(i * sampleRate))
                                .reduce((p, c) => [[Math.min(p[0][0], c[0]), Math.min(p[0][1], c[1])],[Math.max(p[1][0], c[0]), Math.max(p[1][1], c[1])]], [[Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], [Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER]]);
            }
            return this._bb;
        }
        size(sampleRate = .01) {
            if(this._size === undefined) {
                this._size = [this.bb(sampleRate)].map(v => [v[1][0] - v[0][0], v[1][1] - v[0][1]]).pop();
            }
            return this._size;
        }
        p(t) {
            t = Math.max(Math.min(t, 1), 0) * this.length();
            for (let l=0, i=0, sl=0; i<this.segments.length; i++, l+=sl) {
                sl = this.segments[i].length();
                if (t > l && t <= l + sl) {
                    return this.segments[i].p((t-l)/sl, sl/this.length());
                }
            }
            return this.segments[Math.min(1, this.segments.length-1)].p(0);
        }
    }
    return new Path(tokens);
}

////////////////////////////////////////////////////////////////
// Tortoise utility code. Created by Reinder Nijhoff 2019
// https://turtletoy.net/turtle/102cbd7c4d
////////////////////////////////////////////////////////////////

function Tortoise(x, y) {
    class Tortoise extends Turtle {
        constructor(x, y) {
            super(x, y);
            this.ps = Array.isArray(x) ? [...x] : [x || 0, y || 0];
            this.transforms = [];
        }
        addTransform(t) {
            this.transforms.push(t);
            this.jump(this.ps);
            return this;
        }
        applyTransforms(p) {
            if (!this.transforms) return p;
            let pt = [...p];
            this.transforms.map(t => { pt = t(pt); });
            return pt;
        }
        goto(x, y) {
            const p = Array.isArray(x) ? [...x] : [x, y];
            const pt = this.applyTransforms(p);
            if (this.isdown() && (this.pt[0]-pt[0])**2 + (this.pt[1]-pt[1])**2 > 4) {
               this.goto((this.ps[0]+p[0])/2, (this.ps[1]+p[1])/2);
               this.goto(p);
            } else {
                super.goto(pt);
                this.ps = p;
                this.pt = pt;
            }
        }
        position() { return this.ps; }
    }
    return new Tortoise(x,y);
}

// Way too ugly code stashed far away at the bottom so nobody will look at it
function runUglyCodeToPopulateTheGrid() {
    const roundsMin = Math.min(mutationCountMin, mutationCountMax);
    const roundsMax = Math.max(mutationCountMin, mutationCountMax);
    
    const keyCells = [];
    for(let i = 0; i < evolution; i++) {
        let rounds = roundsMin + ((roundsMax - roundsMin) * random() | 0);
        keyCells.push(Array.from({length: rounds}).reduce((p, c) => mutation(p), tokens));
    }
    if(evolution == 4) {
        keyCells.splice(1, 0, lerpTokens(keyCells[0], keyCells[1], .5));
        keyCells.splice(3, 0, lerpTokens(keyCells[0], keyCells[3], .5));
        keyCells.splice(4, 0, lerpTokens(keyCells[2], keyCells[5], .5));
        keyCells.splice(6, 0, lerpTokens(keyCells[5], keyCells[6], .5));
    }
    
    keyCells.splice(4, 0, tokens);
    
    const populatedGrid = Array.from({length: grid}).map((v, c) => Array.from({length: grid}));
    
    const beg = 0;
    const mid = grid / 2 | 0;
    const end = grid - 1;
    
    [beg, mid, end].forEach(r => [beg, mid, end].forEach(c => populatedGrid[c][r] = keyCells.pop()));
    
    for(let row = beg; row <= end; row += mid) {
        for(let i = 0; i < 2; i++) {
            for(let col = 1; col < mid; col++) {
                populatedGrid[col + i * mid][row] = lerpTokens(
                    populatedGrid[i * mid][row],
                    populatedGrid[(i + 1) * mid][row],
                    col / mid
                );
            }
        }
    }
    for(let col = beg; col <= end; col++) {
        for(let i = 0; i < 2; i++) {
            for(let row = 1; row < mid; row++) {
                populatedGrid[col][row + i * mid] = lerpTokens(
                    populatedGrid[col][i * mid],
                    populatedGrid[col][(i + 1) * mid],
                    row / mid
                );
            }
        }
    }
    
    return populatedGrid;
}