My take on Voronoi diagrams using Delaunay triangulation.
(A look into debugging this thing: Voronoi study 🧽 (variation) and Voronoi study 🧽 (variation))
#Delaunay #triangulation #voronoi
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)}}}