Path input type

Turtletoy supports an extra adjustable variable type 'path' by adding 'type=path' in a comment behind the variable declaration. This variable type contains a simple SVG path.

When you click on the 'Draw path' toggle in the UI, you can redraw the path on the canvas.

Log in to post a comment.

// Path input. Created by Reinder Nijhoff 2023 - @reindernijhoff
// The MIT License
//
// Turtletoy supports an extra adjustable variable type 'path' by adding 'type=path' in a comment 
// behind the variable declaration. This variable type contains a simple SVG path. 
//
// When you click on the 'Draw path' toggle in the UI, you can redraw the path on the canvas.
//
// https://turtletoy.net/turtle/46adb0ad70
//
const pathInput = `M-6,-84 C-6,-55 13,-26 25,0 C33,17 38,34 45,51 C50,62 53,73 58,83
C59,84 60,91 60,91 C60,91 57,86 55,84 C50,79 44,75 38,70 C26,61 13,53 2,43 C-13,30 -26,14 -44,3
C-52,-2 -59,-7 -67,-13 L-77,-18 C-77,-18 -72,-18 -70,-18 C-65,-18 -59,-18 -54,-18 
C-25,-18 4,-21 33,-21 C42,-21 51,-21 60,-21 C63,-21 66,-21 69,-21 C71,-21 76,-23 75,-22
C64,-11 48,-5 35,3 C10,20 -12,39 -34,58 C-35,59 -63,84 -63,84 C-64,84 -63,81 -62,80
C-60,76 -57,71 -55,66 C-47,50 -41,37 -36,20 C-29,-6 -18,-32 -11,-59
C-10,-62 -8,-84 -5,-84`; // type=path, Click here to redraw the path

const path = Path(pathInput);

const turtle = new Turtle();
turtle.jump(path.p(0));

let spawnSpiralChange = 0;
function walk(i) {
    const steps = path.length() | 0;
    const pos = path.p( i/steps );
        
    turtle.goto(pos);
    
    if (Math.random() < (spawnSpiralChange+=1/50)) {
        spawnSpiralChange = -0.05;
        spawnSpiral([pos[0], pos[1]], [pos[2], pos[3]]);
    }
    return i < steps;
}

let toggle = 0;
function spawnSpiral(point, derivative) {
    const t = new Turtle();
    t.radians();
    t.jump(point);
    t.setheading(Math.atan2(derivative[1], derivative[0]));

    const dangle = (0.2 + Math.random()) * .05 * Math.sign((toggle++ % 2) - .5);        

    for (let j=0; j<1; j+=1/1000) {
        t.left(dangle);
        t.forward((1-j)**3 * 0.3);
    }
}

////////////////////////////////////////////////////////////////
// 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
////////////////////////////////////////////////////////////////
function Path(svg) {
    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(svg) {
            this.segments = [];
            this.parsePath(svg);
        }
        parsePath(svg) {
            const t = svg.match(/([0-9.-]+|[MLC])/g);
            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;
        }
        length() {
            return this._length || (this._length = this.segments.reduce((a,c) => a + c.length(), 0));
        }
        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(svg);
}