Voronoi study 🧽

My take on Voronoi diagrams using Delaunay triangulation.

(A look into debugging this thing: Voronoi study 🧽 (variation) and Voronoi study 🧽 (variation))

Log in to post a comment.

const seed = 10000; //min=10000 max=99999 step=1
const n = 200; //min=4 max=600 step=1
const padding = 0; //min=0 max=90 step=1
const mirrorEdges = 1; //min=0 max=1 step=1 (No, Yes)
const dSources = 0; //min=0 max=1 step=1 (No, Yes)
const dVoronoiLines = 1; //min=0 max=1 step=1 (No, Yes)
const drawMode = 1; //min=0 max=1 step=1 (dDelaunayX, dPolygonX)
const dDelaunayPoints = 0; //min=0 max=1 step=1 (No, Yes)
const dDelaunayCircles = 0; //min=0 max=1 step=1 (No, Yes)
const dDelaunayTriangles = 0; //min=0 max=1 step=1 (No, Yes)
const dPolygonShrink = .9; //min=0 max=1.5 step=.1
const dPolygonArtBorder = 5; //min=0 max=30 step=1
const dPolygonArtShape = 2; //min=2 max=10 step=1 (Circle, Triangle, Square, Pentagon, Hexagon, Heptagon, Octagon, Nonagon, Decagon)
const dPolygonHatching = 1; //min=0 max=5 step=1 (None, Random, Circular gradient, Circular gradient inverted, Gradient, Inverted gradient)
const dPolygonHatchDir = 0; //min=0 max=2 step=1 (Random, Centralized, Centralized othogonal)
const dPolygonHatchMin = .31; //min=.01 max=1.6 step=.05
const dPolygonHatchMax = .51; //min=.01 max=1.6 step=.05
const dPolygonBackground = 0; //min=0 max=2 step=1 (None, Gradient up, Gradient down)
const debug = false;

const dPolygonHatchMaxc = Math.max(dPolygonHatchMin, dPolygonHatchMax);

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

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

function normalize2(a) { const length = len2(a); return scale2(a,length<0.0001?1:1/length); }
function len2(a) { return Math.sqrt(lensq2(a)); }
function lensq2(a) { return dot2(a,a); }
function rot2(a) { return [Math.cos(a), -Math.sin(a), Math.sin(a), Math.cos(a)]; }
function trans2(m, a) { return [m[0]*a[0]+m[2]*a[1], m[1]*a[0]+m[3]*a[1]]; }
function scale2(a,b) { return [a[0]*b,a[1]*b]; }
function add2(a,b) { return [a[0]+b[0],a[1]+b[1]]; }
function sub2(a,b) { return [a[0]-b[0],a[1]-b[1]]; }
function dist2(a,b) { return Math.hypot(...sub2(a,b)); }
function lerp2(a,b,t) { return [a[0]*(1-t)+b[0]*t,a[1]*(1-t)+b[1]*t]; }
function dot2(a,b) { return a[0]*b[0]+a[1]*b[1]; }
function cross2(a,b) { return a[0]*b[1] - a[1]*b[0]; }
function mulf2(v, f) { return scale2(v,f); }
function multiply2(a2x2, a) { return [(a[0]*a2x2[0])+(a[1]*a2x2[1]),(a[0]*a2x2[2])+(a[1]*a2x2[3])]; }
function segment_intersect2(a,b,d,c, inclusive = true) {
    const e=(c[1]-d[1])*(b[0]-a[0])-(c[0]-d[0])*(b[1]-a[1]);
    if(0==e)return false;
    c=((c[0]-d[0])*(a[1]-d[1])-(c[1]-d[1])*(a[0]-d[0]))/e;
    d=((b[0]-a[0])*(a[1]-d[1])-(b[1]-a[1])*(a[0]-d[0]))/e;
    const t = inclusive? 0<=c&&1>=c&&0<=d&&1>=d: 0<c&&1>c&&0<d&&1>d;
    return t?[a[0]+c*(b[0]-a[0]),a[1]+c*(b[1]-a[1])]:false;
}
function eq2(a,b) { return a[0]==b[0]&&a[1]==b[1]; }

// 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('anything here' + seed);
// Add a seed in seedrandom, then Math.random will use this seed

const randomPoint = () => [2*Math.random()*(100-padding)-(100-padding),2*Math.random()*(100-padding)-(100-padding)];

//We start with one randomly chosen point in our points list
const pts = [randomPoint()];
//While there are not yet a certain number of points chosen
while(pts.length < n) {
    //add a point to the list
    pts.push(
        //using [length] candidate points
        Array.apply(null,{length: 10})
             //which are random points
             .map(i => randomPoint())
             //then add a distance to that candidate
             .map(i => [i[0], i[1], pts.map(j => [j[0], j[1], dist2(i, j)])
                                       //so that it is the smallest distance from the
                                       //candidate to any of the already chosen points
                                       .reduce((prev, current) => (current[2] < prev[2])? current: prev, [0,0,500])[2]
                ])
             //then pick the candidate that has the largest minimum distance from the group of candidates
             .reduce((prev, current) => (current[2] > prev[2])? current: prev, [0,0,0])
             //and remove the distance before promoting the candidate
             .filter((i, k) => k < 2)
    );

}

/*
turtle.jump([0,-100]);
turtle.goto([0, 100]);
turtle.jump([-100, 0]);
turtle.goto([100, 0]);
*/

/////////----------------- TEST CASES ------------------////////
//two outside circles
//const pts = [[0, -20], [70, 0], [0, 20], [-60, 20], [0, 50]];

//one inside circle, outside hull
//const pts = [[0, -20], [70, 0], [-30, 50], [30, 50]];
//one inside circle outside another, outside hull
//const pts = [[0, -10], [50, 0], [0, 25], [-50, 50], [25, 34]];

//point inside triangle, once circle
//const pts = [[0, -20], [70, 0], [0, 20], [25, 0]];
//point inside triangle, two circles
//const pts = [[0, -20], [70, 0], [0, 20], [25, 0], [20, 10]];
//point on a non-shared edge
//const pts = [[0, 50], [50, 50], [0, -50], [0,0]];

//point on a non-shared edge, but in 'another' circle
//const pts = [[20, -70], [70, 0], [0, 0], [20, 30]];
//pts.push(add2(pts[0], scale2(sub2(pts[1], pts[0]), .8)));

//point on a shared edge
//const pts = [[20, -70], [70, 0], [0, 0], [20, 30], [30, 0]];

//point on a shared edge and in 'another' circle
//const pts = [[20, -70], [70, 0], [0, 0], [20, 30], [0, 20], [30, 0]];
/////////----------------- END TEST CASES ------------------////////

class DelaunayTriangulation {
    constructor(pts) {
        if(pts.length < 3) {
            throw new Error('What u do?');
        }
        this.points = pts.filter((i, k) => k < 3);
        this.log('init points 1, 2, 3', ...this.points);
        this.circles = [];
        this.convexHullEdges = [[0, 1], [1, 2], [0, 2]];
        
        this.addCircle(0, 1, 2);

        pts.filter((i, k) => k > 2).forEach(i => this.add(i));
    }
    log(...msg) {
        if(debug) {
            console.log(...msg);
        }
    }
    addCircle(...idxs) {
        this.circles.push(this.getCircle(...idxs));
        return this.circles.length - 1;
    }
    getCircle(...idxs) {
        if(idxs.length != 3) throw new Error('Expecting 3 arguments at getCircle()');
        idxs.sort();
        const cc = this.getSmallestCircumscribedCircleCenter(...idxs.map(i => this.points[i]));
        const r = lensq2(sub2(cc, this.points[idxs[0]]));
        return [cc, r, [...idxs]];
    }
    // https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_2
    getSmallestCircumscribedCircleCenter(pt1, pt2, pt3) {
        const D = 2 * (
            (pt1[0] * (pt2[1] - pt3[1])) + 
            (pt2[0] * (pt3[1] - pt1[1])) + 
            (pt3[0] * (pt1[1] - pt2[1]))
        );
        
        return [(
                ((pt1[0]**2 + pt1[1]**2) * (pt2[1] - pt3[1])) +
                ((pt2[0]**2 + pt2[1]**2) * (pt3[1] - pt1[1])) +
                ((pt3[0]**2 + pt3[1]**2) * (pt1[1] - pt2[1]))
            ) / D,
            (
                ((pt1[0]**2 + pt1[1]**2) * (pt3[0] - pt2[0])) +
                ((pt2[0]**2 + pt2[1]**2) * (pt1[0] - pt3[0])) +
                ((pt3[0]**2 + pt3[1]**2) * (pt2[0] - pt1[0]))
            ) / D
        ];
    }
    // https://stackoverflow.com/questions/2049582/how-to-determine-if-a-point-is-in-a-2d-triangle#2049593
    isPointContainedInTriangle(trianglePointIndices, pt) {
        const sign = (p1, p2, p3) => (p1[0]-p3[0]) * (p2[1]-p3[1]) - (p2[0]-p3[0]) * (p1[1]-p3[1]);
        
        const d = trianglePointIndices.map(i => this.points[i]).map((p, idx, src) => sign(pt, p, src[(idx+1)%src.length]));
        
        return !(d.reduce((p, c) => p || (c < 0), false) && d.reduce((p, c) => p || (c > 0), false));
    }
    drawTriangles(t) {
        //const edges = this.circles.map((c, i) => [c[2][i], c[2][(i+1)%3]]);
        this.circles.forEach(c => {
            const edges = c[2].map((p, i) => [p, c[2][(i+1)%3]]).map(e => [e, this.edgeIsPartOfConvexHull(e)]);
            
            edges.forEach(e => {
                const pts = [this.points[e[0][0]], this.points[e[0][1]]];
                t.jump(pts[0]); t.goto(pts[1]);
                if(e[1]) {
                    [[1, 0], [0, 1], [-1, 0], [0, -1]].map(i => scale2(i, .2)).forEach(d => {
                        t.jump(add2(pts[0], d));
                        t.goto(add2(pts[1], d));
                    });
                }
            })
        });
    }
    drawCircles(t) {
        this.circles.forEach(c => {
            t.jump(c[0][0], c[0][1] - Math.sqrt(c[1]));
            t.circle(Math.sqrt(c[1]));
        });
    }
    drawPoints(t) {
        this.circles.forEach(c => {
            drawPoint(t, c[0], .5);
        });
    }
    edgeIsPartOfConvexHull(edge) {
        edge = edge[0] < edge[1]? [edge[0], edge[1]]: [edge[1], edge[0]];//sort();
        return this.convexHullEdges.filter(che => eq2(che, edge)).length > 0;
    }
    add(pt) {
        const arrayIntersection = (array1, array2) => array1.filter(value => array2.includes(value));
        const arrayDifference = (array1, array2) => array1.filter(value => !array2.includes(value)).concat(array2.filter(value => !array1.includes(value)));
        const flip = (idx1, idx2) => {
            const intersect = arrayIntersection(this.circles[idx1][2], this.circles[idx2][2])
            const uniques = [
                arrayDifference(this.circles[idx1][2], intersect)[0],
                arrayDifference(this.circles[idx2][2], intersect)[0]
            ];
            this.circles[idx1] = this.getCircle(intersect[0], ...uniques);
            this.circles[idx2] = this.getCircle(intersect[1], ...uniques);
            
            return this.edgeIsPartOfConvexHull(intersect);
        }

        //add the point to the list
        this.points.push(pt);
        let convexHullUpdateNeeded = false;
        
        const inCircleIndices = this.circles.map((i, idx) => [idx, i]).filter(c => lensq2(sub2(c[1][0], pt)) <= c[1][1]).map(i => i[0]);
        const inTriangleIndices = inCircleIndices.filter(c => this.isPointContainedInTriangle(this.circles[c][2], pt));
        if(inTriangleIndices.length == 0) {
            //the new point is outside of circles and by definition outside convex hull
            //   find convexhull edges that are not connecting to p that intersect pt-p
            //   and if there are, return false (exclude that point)
            const connectToPoints = this.points.filter((p, pIdx) => this.convexHullEdges
                .filter(che => che[0] != pIdx && che[1] != pIdx)
                .filter(che => false !== segment_intersect2(pt, p, this.points[che[0]], this.points[che[1]])).length === 0
            ).map(i => this.points.indexOf(i));

            //so because the new point is outside circles we can just add it to all convex hull edges where their points are all in the connectToPoints

            //get the combinations (length 2) of all points to connect to internally sorted by index in this.points
            const possibleEdges = connectToPoints.flatMap((i, idx) =>
                connectToPoints.filter((ii, iidx) => iidx > idx).map(ii => [i, ii])
            );
            
            //get all convex hull edges associated with the points (valid combinations)
            const hullEdges = this.convexHullEdges.filter(i => possibleEdges.filter(ii => eq2(ii, i)).length > 0);

            //create new circles for every new triangle
            hullEdges.forEach((he, idx) => {
                this.addCircle(...he, this.points.length - 1);
            });
            
            convexHullUpdateNeeded = true;
        } else {
            //is the point on an edge?
            const edge = [[0, 1], [1, 2], [0, 2]].map(i =>
                [this.circles[inTriangleIndices[0]][2][i[0]], this.circles[inTriangleIndices[0]][2][i[1]]]
            ).filter(edge => len2(sub2(this.points[edge[0]], this.points[edge[1]]))
                ==
                len2(sub2(this.points[edge[0]], this.points[this.points.length - 1])) +
                len2(sub2(this.points[edge[1]], this.points[this.points.length - 1]))
            );
            const onEdge = edge.length == 0? false: (
                edge[0][0] < edge[0][1]? [edge[0][0], edge[0][1]]: [edge[0][1], edge[0][0]]
            );
            
            if(onEdge === false) {
                [[0, 1], [1, 2], [0, 2]].map(i =>
                    [this.circles[inTriangleIndices[0]][2][i[0]], this.circles[inTriangleIndices[0]][2][i[1]], this.points.length - 1]
                ).forEach((i, k) => {
                    k == 0? this.circles[inTriangleIndices[0]] = this.getCircle(...i): this.addCircle(...i);
                });
            } else {
                convexHullUpdateNeeded = this.edgeIsPartOfConvexHull(onEdge);
                
                inTriangleIndices.forEach(iti => {
                    const unique = arrayDifference(this.circles[iti][2], onEdge)[0];
                    this.circles[iti] = this.getCircle(onEdge[0], unique, this.points.length - 1);
                    this.addCircle(onEdge[1], unique, this.points.length - 1);
                });
            }
        }

        while (true) {
            const inCircleIdxs = this.circles.map((i, idx) => [idx, i]).filter(c => lensq2(sub2(c[1][0], pt)) <= c[1][1]).map(i => i[0]);
            const ownCircleIdxs = this.circles.map((i, idx) => [idx, i]).filter(i => i[1][2].includes(this.points.length - 1)).map(i => i[0]);
            const otherCircleIdxs = inCircleIdxs.filter(i => !ownCircleIdxs.includes(i));

            if(otherCircleIdxs.length == 0) break;

            otherCircleIdxs.forEach(c => {
                const commonCircleIndex = ownCircleIdxs
                    .map(i => [i, arrayIntersection(this.circles[i][2], this.circles[c][2])])
                    .filter(i => i[1].length == 2)[0];

                if(commonCircleIndex === undefined) return;

                //switch triangles
                convexHullUpdateNeeded = flip(c, commonCircleIndex[0]) || convexHullUpdateNeeded;
            });
        };

        this.log('added point ' + this.points.length, this.points[this.points.length - 1]);

        //reset the convex hull
        if(convexHullUpdateNeeded) {
            this.refreshConvexHull();
        }
    }
    refreshConvexHull() {
        //get all the edges from triangles;
        const edges = this.circles.flatMap(c => [[c[2][0], c[2][1]], [c[2][1], c[2][2]], [c[2][0],c[2][2]]]).map(i => i[0] < i[1]? [i[0], i[1]]: [i[1], i[0]]);
        //see which are shared
        const duplicates = edges.filter((e, i) => edges.filter((ee, ii) => ii > i && eq2(ee, e)).length > 0);
        //the ones that are not shared make up the hull
        this.convexHullEdges = edges.filter(e => duplicates.filter(d => eq2(e, d)).length == 0).map(e => e[0] < e[1]? [e[0], e[1]]: [e[1], e[0]]);
    }
    finalize(width, height) {
        //get the unique points that are part of the convex hull
        const pts = this.points.filter(
            (p, i) => this.convexHullEdges.flatMap(i => i)
                          .filter((p, ii, s) => s.indexOf(p) === ii).includes(i)
        );
        //add those points mirrored in the edges of the canvas
        [
            ...pts.filter(p => p[0] < 0).map(p => [-p[0] - width - 5, p[1]]), //mirror left
            ...pts.filter(p => p[0] > 0).map(p => [-p[0] + width + 5, p[1]]), //mirror right
            ...pts.filter(p => p[1] < 0).map(p => [p[0], -p[1] - height - 5]), //mirror top
            ...pts.filter(p => p[1] > 0).map(p => [p[0], -p[1] + height + 5]), //mirror bottom
        ].forEach(p => this.add(p));
    }
}

const drawPoint = (t, p, r = 1) => { t.jump(p[0], p[1] - (r/2)); t.circle(r); }

class VoronoiDiagram {
    constructor(delaunayTriangulation) {
        this.dt = delaunayTriangulation;
        this.dtMap = this.dt.circles.
            map(c => [c[0], [[0, 1], [1, 2], [0, 2]].map(vte => [c[2][vte[0]], c[2][vte[1]]])]);
    }
    draw(t) {
        this.dtMap.forEach((s, ks) => {
            this.dtMap
                .filter((c, kc) => kc < ks) // not the same as source or previous
                .filter(c => //find any edge of c[2] in s[2]
                    c[1].filter(cc => s[1].filter(ss => eq2(ss, cc)).length > 0).length > 0
                )
                .forEach(c => {
                    t.jump(s[0]);
                    t.goto(c[0]);
                })
        });
    }
    getPolygonPoints(withPointIndex = false) {
        const arrayIntersection = (array1, array2) => array1.filter(value => array2.includes(value));

        //find all circles associated with each point [[pointIndex, [circles]], ...]
        const circlesPerPoint = this.dt.points.map(
            (p, i) => [i, this.dt.circles.filter(c => c[2].includes(i))]
        ).filter(p => p[1].length > 2); // for a complete polygon, it should at least have 3 edges
        
        //find all indices of circles that have a completed polygon
        // e.g. from the first circle, find circles that have an edge in common until the last one connects to the first one again
        // in other words, each circle should have two other circles that each have a Delaunay Triangle edge in common with it
        const pointIndicesWithFullVoronoiPolygon = circlesPerPoint.map(cpp => [cpp[0], cpp[1].map(cs => 
            cpp[1].filter(cppc => arrayIntersection(cs[2], cppc[2]).length == 2).length == 2
        ).reduce((p, c) => p && c, true)]).filter(c => c[1] == true).map(c => c[0]);

        // then order the points in such a way that that the path 'rotates' around the source point.
        return circlesPerPoint.filter(p => pointIndicesWithFullVoronoiPolygon.includes(p[0])).map(p => {
            return [p[0], p[1].map(c => [c[0], Math.atan2(...sub2(c[0], this.dt.points[p[0]]))])
                       .sort((a, b) => a[1] < b[1]? -1: 1)
                       .map(c => c[0])];
        }).map(p => withPointIndex? p: p[1]);
    }
}

const tri = new DelaunayTriangulation(pts);

if(mirrorEdges) tri.finalize(200, 200);

const vd = new VoronoiDiagram(tri);

const modClip = (val, lowerBound, upperBound) => { const diff = upperBound - lowerBound; while(val < lowerBound) val += diff; while(val > upperBound) val -= diff; return val; }
const regularPolygonPoint = (theta, polygonEdgeCount) => scale2([Math.cos(theta * Math.PI * 2), Math.sin(theta * Math.PI * 2)], Math.cos(Math.PI / polygonEdgeCount) / Math.cos(modClip(theta * Math.PI * 2, 0, Math.PI * 2 / polygonEdgeCount) - (Math.PI / polygonEdgeCount)))


if(drawMode === 0) {
    if(dDelaunayTriangles) tri.drawTriangles(turtle);
    if(dDelaunayPoints) tri.drawPoints(turtle);
    if(dDelaunayCircles) tri.drawCircles(turtle);
    if(dVoronoiLines) vd.draw(turtle);
    if(dSources) pts.forEach(i => drawPoint(turtle, i));
} else if(drawMode == 1) {
    if(dPolygonArtBorder > 0) {
        [
            [91, 92],
            [93, 95],
            [96, 99],
            [100, 400 + dPolygonArtBorder],
        ].map(i => i.map(ii => ii - dPolygonArtBorder)).forEach(r => {
            const pts = [];
            const max =  dPolygonArtShape == 2? 120: dPolygonArtShape;
            r.forEach((rr, rrIdx) => {
                Array.from({length: max + 1}).forEach((d, j) => {
                    let pt;
                    switch(dPolygonArtShape) {
                        case 2:
                            const u = (-rrIdx * 2) + 1;
                             pt = [rr * Math.sin(u * 2 * Math.PI * j / max),
                                   rr * Math.cos(u * 2 * Math.PI * j / max)];
                            break;
                        default:
                            pt = scale2(regularPolygonPoint(j/max, dPolygonArtShape), rr);
                    }
                    pts.push([pt[0], pt[1]]);
                    turtle[j==0?'jump':'goto']([pt[0], pt[1]]);
                });
            });
    
            const p = polygons.create();
            p.addPoints(...pts);
            polygons.draw(turtle, p);
        });
    }
    
    if(dSources) pts.forEach(pt => {
        const p = polygons.create();
        p.addPoints(...Array.from({length: 15}).map((p, i, a) => [.8 * Math.sin(2 * Math.PI * i / a.length), .8 * Math.cos(2 * Math.PI * i / a.length),]).map(i => add2(pt, i)));
        p.addHatching(0, .3);
        polygons.draw(turtle, p);
    });

    vd.getPolygonPoints(true).forEach(pts => {
        const p = polygons.create();
        if(dPolygonShrink > 0) {
            let shrunkPts = pts[1].map((p, i, a) =>
                add2(p, 
                    scale2(normalize2(
                        add2(
                            normalize2(sub2(pts[1][(i-1+a.length)%a.length], p)),
                            normalize2(sub2(pts[1][(i+1)%a.length], p))
                        )
                    ), dPolygonShrink)
                )
            );
            
            //the shrunken points may intersect now... that needs clearing.
            //for now, a naive check if edges from adjacent points intersect at the same point
            for(let i = 0; shrunkPts.length > 3 && i < shrunkPts.length; i++) {
                const iTarget = (i - 1 + shrunkPts.length) % shrunkPts.length;
                const next = (i + 1) % shrunkPts.length;
                const nextTarget = (i + 2) % shrunkPts.length;
                
                const intersect = segment_intersect2(shrunkPts[i], shrunkPts[iTarget], shrunkPts[next], shrunkPts[nextTarget], false);
                if(intersect !== false) {
                    shrunkPts[i] = intersect;
                    shrunkPts.splice(next, 1);
                }
            }
            
            p.addPoints(...shrunkPts);
        } else {
            p.addPoints(...pts[1]);
        }
        if(dPolygonHatching) {
            let hatchDistance = .15;
            let hatchDirection = 0;
            switch(dPolygonHatching) {
                case 1:
                    hatchDistance = dPolygonHatchMin + (Math.random() * (dPolygonHatchMaxc - dPolygonHatchMin));
                    break;
                case 2:
                case 3:
                    const maxx = ((100 - dPolygonArtBorder) * 2**.5)**2;
                    hatchDistance = dPolygonHatchMin + (
                        (dPolygonHatching == 2? lensq2(vd.dt.points[pts[0]]) / maxx: 1 - (lensq2(vd.dt.points[pts[0]]) / maxx)) 
                        
                        * (dPolygonHatchMaxc - dPolygonHatchMin)
                    );
                    break;
                case 4:
                case 5:
                    const maxxx = 200;
                    hatchDistance = dPolygonHatchMin + (
                        (dPolygonHatching == 4? (vd.dt.points[pts[0]][1] + 100) / maxxx: 1 - ((vd.dt.points[pts[0]][1] + 100) / maxxx) ) 
                        
                        * (dPolygonHatchMaxc - dPolygonHatchMin)
                    );
                    break;
                default:
            }
            switch(dPolygonHatchDir) {
                case 0:
                    hatchDirection = Math.random() * Math.PI;
                    break;
                case 1:
                    hatchDirection += Math.PI / 2;
                case 2:
                    hatchDirection += Math.atan2(...vd.dt.points[pts[0]]);
            }
            p.addHatching(hatchDirection, hatchDistance)
        }
        if(dVoronoiLines) p.addOutline();
        polygons.draw(turtle, p);
    });
    
    if(dPolygonBackground) {
        const grad = 75;
        for(let i = 1; i <= grad; i++) {
            const pp = polygons.create();
            pp.addPoints(
                [-100, 100 - (i * 200 / grad)], [100, 100 - (i * 200 / grad)],
                [100, 100], [-100, 100]
            );
            pp.addHatching(1, dPolygonHatchMin + (
                (dPolygonBackground == 1? (i/grad): 1 - (i/grad))
                
                * (dPolygonHatchMaxc - dPolygonHatchMin)
            ));
            polygons.draw(turtle, pp);
        }
    }
}

// The walk function will be called until it returns false.
function walk(i) {
    return false;
}


////////////////////////////////////////////////////////////////
// Polygon Clipping utility code - Created by Reinder Nijhoff 2019
// (Polygon binning by Lionel Lemarie 2021)
// https://turtletoy.net/turtle/a5befa1f8d
////////////////////////////////////////////////////////////////
function Polygons(){const t=[],s=25,e=Array.from({length:s**2},t=>[]),n=class{constructor(){this.cp=[],this.dp=[],this.aabb=[]}addPoints(...t){let s=1e5,e=-1e5,n=1e5,h=-1e5;(this.cp=[...this.cp,...t]).forEach(t=>{s=Math.min(s,t[0]),e=Math.max(e,t[0]),n=Math.min(n,t[1]),h=Math.max(h,t[1])}),this.aabb=[s,n,e,h]}addSegments(...t){t.forEach(t=>this.dp.push(t))}addOutline(){for(let t=0,s=this.cp.length;t<s;t++)this.dp.push(this.cp[t],this.cp[(t+1)%s])}draw(t){for(let s=0,e=this.dp.length;s<e;s+=2)t.jump(this.dp[s]),t.goto(this.dp[s+1])}addHatching(t,s){const e=new n;e.cp.push([-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);const h=Math.sin(t)*s,o=Math.cos(t)*s,a=200*Math.sin(t),i=200*Math.cos(t);for(let t=.5;t<150/s;t++)e.dp.push([h*t+i,o*t-a],[h*t-i,o*t+a]),e.dp.push([-h*t+i,-o*t-a],[-h*t-i,-o*t+a]);e.boolean(this,!1),this.dp=[...this.dp,...e.dp]}inside(t){let s=0;for(let e=0,n=this.cp.length;e<n;e++)this.segment_intersect(t,[.1,-1e3],this.cp[e],this.cp[(e+1)%n])&&s++;return 1&s}boolean(t,s=!0){const e=[];for(let n=0,h=this.dp.length;n<h;n+=2){const h=this.dp[n],o=this.dp[n+1],a=[];for(let s=0,e=t.cp.length;s<e;s++){const n=this.segment_intersect(h,o,t.cp[s],t.cp[(s+1)%e]);!1!==n&&a.push(n)}if(0===a.length)s===!t.inside(h)&&e.push(h,o);else{a.push(h,o);const n=o[0]-h[0],i=o[1]-h[1];a.sort((t,s)=>(t[0]-h[0])*n+(t[1]-h[1])*i-(s[0]-h[0])*n-(s[1]-h[1])*i);for(let n=0;n<a.length-1;n++)(a[n][0]-a[n+1][0])**2+(a[n][1]-a[n+1][1])**2>=.001&&s===!t.inside([(a[n][0]+a[n+1][0])/2,(a[n][1]+a[n+1][1])/2])&&e.push(a[n],a[n+1])}}return(this.dp=e).length>0}segment_intersect(t,s,e,n){const h=(n[1]-e[1])*(s[0]-t[0])-(n[0]-e[0])*(s[1]-t[1]);if(0===h)return!1;const o=((n[0]-e[0])*(t[1]-e[1])-(n[1]-e[1])*(t[0]-e[0]))/h,a=((s[0]-t[0])*(t[1]-e[1])-(s[1]-t[1])*(t[0]-e[0]))/h;return o>=0&&o<=1&&a>=0&&a<=1&&[t[0]+o*(s[0]-t[0]),t[1]+o*(s[1]-t[1])]}};return{list:()=>t,create:()=>new n,draw:(n,h,o=!0)=>{reducedPolygonList=function(n){const h={},o=200/s;for(var a=0;a<s;a++){const c=a*o-100,r=[0,c,200,c+o];if(!(n[3]<r[1]||n[1]>r[3]))for(var i=0;i<s;i++){const c=i*o-100;r[0]=c,r[2]=c+o,n[0]>r[2]||n[2]<r[0]||e[i+a*s].forEach(s=>{const e=t[s];n[3]<e.aabb[1]||n[1]>e.aabb[3]||n[0]>e.aabb[2]||n[2]<e.aabb[0]||(h[s]=1)})}}return Array.from(Object.keys(h),s=>t[s])}(h.aabb);for(let t=0;t<reducedPolygonList.length&&h.boolean(reducedPolygonList[t]);t++);h.draw(n),o&&function(n){t.push(n);const h=t.length-1,o=200/s;e.forEach((t,e)=>{const a=e%s*o-100,i=(e/s|0)*o-100,c=[a,i,a+o,i+o];c[3]<n.aabb[1]||c[1]>n.aabb[3]||c[0]>n.aabb[2]||c[2]<n.aabb[0]||t.push(h)})}(h)}}}