Plastic surgery 👨🏽‍⚕️

This turtle uses a different 'technique' to go from one path to another than Path evolution.

This one does not use the naive genetic algorithm from Natural selection to evolve an initial path to one that matches the 'fittest' path (targetInput) as much as possible. It will instead exactly match the 'fittest' path, but no 'evolution' is used to modify a path. It samples both paths with the same resolution and for each index a lerp is used to go from the startInput to the targetInput.

('Naive' is quoted from Path evolution, not an opinion)

Log in to post a comment.

// Forked from "Path evolution" by reinder
// https://turtletoy.net/turtle/d5e96736b9

// Path evolution. Created by Reinder Nijhoff 2019
// @reindernijhoff
//
// https://turtletoy.net/turtle/d5e96736b9
//

const grid = 13; // min=2, max=15, step=1

const startInput = `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

const targetInput = `M0,-38 C0,-16 15,-2 20,18 C21,20 28,36 28,36 L26,36 C21,34 18,28 14,25
C3,17 -7,5 -20,2 C-22,1 -33,-5 -33,-5 L-32,-5 C-27,-5 -22,-5 -17,-5 C-3,-5 11,-6 25,-6 C26,-6 38,-4 38,-4
L23,1 C11,5 -7,18 -18,25 C-22,28 -28,30 -31,33 L-35,37 C-35,36 -26,22 -25,19 C-22,13 -21,6 -18,0
C-12,-12 -2,-24 -2,-37`; // type=path, bbox=-40,-40,80,80 Click here to redraw the path

const turtle = new Turtle();

const startPath = new Path(startInput.match(/([0-9.-]+|[MLC])/g));
const targetPath = new Path(targetInput.match(/([0-9.-]+|[MLC])/g));

const maxLength = Math.max(startPath.length(), targetPath.length()) | 0;

function add2(a, b) { return [a[0]+b[0], a[1]+b[1]]; }
function scale2(a, s) { return [a[0]*s,a[1]*s]; }
function lerp2(a,b,t) { return [a[0]*(1-t) + b[0]*t, a[1]*(1-t) + b[1]*t]; }

const startBB = startPath.bb();
const targetBB = targetPath.bb();

const startPathPts = Array.from({length: maxLength + 1}).map((v, i) => startPath.p(i/maxLength))
                          .map(pt => add2(pt, scale2(lerp2(...startBB, .5), -1))) //center
                          .map(pt => scale2(pt, (160/grid) / Math.max(startBB[1][0] - startBB[0][0], startBB[1][1] - startBB[0][1])));
const targetPathPts = Array.from({length: maxLength + 1}).map((v, i) => targetPath.p(i/maxLength))
                          .map(pt => add2(pt, scale2(lerp2(...targetBB, .5), -1))) //center
                          .map(pt => scale2(pt, (160/grid) / Math.max(targetBB[1][0] - targetBB[0][0], targetBB[1][1] - targetBB[0][1])));

function drawPath(turtle, pts) { return pts.forEach((pt, i) => turtle[i == 0? 'jump':'goto'](pt)); }
function lerpPaths(a, b, p) { return a.map((pt, i) => lerp2(pt, b[i], p)); }

function walk(i) {
    const y = i/grid|0, x = i%grid;
    
    drawPath(turtle, lerpPaths(startPathPts, targetPathPts, i / (grid**2 - 1)).map(pt => add2(pt, [
        -90 + (x + .5) * (180/grid),
        -90 + (y + .5) * (180/grid)
    ])));
    
    return i < grid*grid-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);
}