Distance to path

I added a method to the path utility class so you can calculate the distance to a path. This function is slow: it divides all bezier curves into segments and calculates the distance to each segment separately.

If you use this distance function in your turtle, you should probably implement some cache.

```// Distance to path. Created by Reinder Nijhoff 2023 - @reindernijhoff
//
// https://turtletoy.net/turtle/b8caf5b1f5
//
const pathInput = `M100,-80 C-120,-80 -120,0 0,0 C120,0 120,80 -100,80`; // type=path, Click here to redraw the path

const grid = 11; // min=5, max=50, step=1
const drawmode = 2;  // min=0, max=2, step=1, (Gradient, Curl, Flow field)
const radius = 1.3; // min=0.1, max=5, step=0.01
const maxPathLength = 50;  // min=1, max=100, step=0.1
const maxTries = 1000;
const precision = .75;
const eps = .01;

const path = Path(pathInput);
const turtle = new Turtle();

function walk(i) {
if (drawmode <= 1) {
const x = i % grid;
const y = i / grid | 0;

const p = [(x+1) * 200 / (grid+1) - 100, (y+1) * 200 / (grid+1) - 100];

const d = path.distance(p);
const dx = (path.distance([p[0]+eps, p[1]]) - d)/eps;
const dy = (path.distance([p[0], p[1]+eps]) - d)/eps;

if (drawmode == 0) {
drawArrow(p, [dx, dy]);
} else {
drawArrow(p, [-dy, dx]);
}
return i < grid * grid - 1;
} else {
const p = turtle.pos();

const curl = flowField(p);
const dest = [p[0]+curl[0], p[1]+curl[1]];

if (turtle.traveled < maxPathLength && Math.abs(dest[0]) < 110 && Math.abs(dest[1]) < 110 && pGrid.insert(dest)) {
turtle.goto(dest);
turtle.traveled += Math.hypot(curl[0], curl[1]);
} else {
turtle.traveled = 0;
let r, i = 0;
do {
r =[Math.random()*200-100, Math.random()*200-100];
i ++;
} while(!pGrid.insert(r) && i < maxTries);
if (i >= maxTries) {
return false;
}
turtle.jump(r);
}
return true;
}
}

function drawArrow(p, dir) {
dir = scale(dir, 100/grid);
const hw  = lerp(p, end, .6);

turtle.jump(p);
turtle.goto(end);
turtle.jump(end);
}

function flowField(p) {
const [x, y] = p;

const dx = (path.distance([x, y + eps]) - path.distance([x, y - eps]))/(2 * eps);
const dy = (path.distance([x + eps, y]) - path.distance([x - eps, y]))/(2 * eps);

const l = Math.hypot(dx, dy) / radius * .99;
return [dx / l, -dy / l];
}

function scale(a,b) { return [a[0]*b,a[1]*b]; }
function add(a,b) { return [a[0]+b[0],a[1]+b[1]]; }
function lerp(a,b,t) { return [a[0]*(1-t)+b[0]*t,a[1]*(1-t)+b[1]*t]; }

////////////////////////////////////////////////////////////////
// Poisson-Disc utility code. Created by Reinder Nijhoff 2019
// https://turtletoy.net/turtle/b5510898dc
////////////////////////////////////////////////////////////////
class PoissonDiscGrid {
this.cells = [];
}
insert(p) {
const x = p[0]*this.cellSize|0, y=p[1]*this.cellSize|0;
for (let xi = x-1; xi<=x+1; xi++) {
for (let yi = y-1; yi<=y+1; yi++) {
const ps = this.cell(xi,yi);
for (let i=0; i<ps.length; i++) {
if ((ps[i][0]-p[0])**2 + (ps[i][1]-p[1])**2 < this.radius2) {
return false;
}
}
}
}
this.cell(x, y).push(p);
return true;
}
cell(x,y) {
const c = this.cells;
return (c[x]?c[x]:c[x]=[])[y]?c[x][y]:c[x][y]=[];
}
}
}

////////////////////////////////////////////////////////////////
// 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.
//
////////////////////////////////////////////////////////////////
function Path(svg) {
function lenSquare(a) { return a[0]**2 + a[1]**2; }
function dot(a, b) { return a[0]*b[0]+a[1]*b[1]; }
function distSegmentSquare(p, a, b ) {
const pa = [p[0]-a[0], p[1]-a[1]], ba = [b[0]-a[0], b[1]-a[1]];
const h = Math.max(0, Math.min(1, dot(pa,ba)/dot(ba,ba)));
return lenSquare( [pa[0] - ba[0]*h, pa[1] - ba[1]*h] );
}

class MoveTo {
constructor(p) { this.p0 = p; }
p(t, s) { return [...this.p0, 1, 0]; }
length() { return 0; }
distSquare(p, res) { return lenSquare([p[0]-this.p0[0], p[1]-this.p0[1]]); }
}
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]);
}
distSquare(p, res) {
return distSegmentSquare(p, this.p0, this.p1);
}
}
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));
}
distSquare(p, res) {
// super slow, probably better to use an analytical solution if exists
const segments = Math.max(1, this.length()/res | 0);
const p0 = this.p0, c0 = this.c0, c1 = this.c1, p1 = this.p1;

let a = [...p0];
let dist = 1e10;

for (let i=1; i<=segments; i++) {
const t = i/segments, nt = 1 - t;
const b = [
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]
];
dist = Math.min(dist, distSegmentSquare(p, a, b));
a = [...b];
}
return dist;
}
}
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++]) {
break;
break;
case 'C': this.add(new BezierTo(s, [t[i++],t[i++]], [t[i++],t[i++]], s=[t[i++],t[i++]]));
break;
default:  i++;
}
}
}
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);
}
distance(p, res = 3) {
return Math.sqrt(this.segments.reduce((a,c) => Math.min(a, c.distSquare(p, res)), 1e10));
}
}
return new Path(svg);
}```