{
const iter = 12;
const gap = 4;
const xScale = d3.scaleLinear().domain([0,1]).range([0,width])
const yScale = d3.scaleLinear().domain([0,1]).range([0,height])
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const delaunay = d3.Delaunay.from(
pts,
d => xScale(d[0]),
d => yScale(d[1])
);
const voronoi = delaunay.voronoi([0, 0, width, height])
let cellCount = 0;
for (let k = 0; k < iter; k++) {
for (let i = 0; i < delaunay.points.length; i += 2) {
const cell = voronoi.cellPolygon(i >> 1);
if (cell === null) continue;
const x0 = delaunay.points[i];
const y0 = delaunay.points[i+1];
const [x1,y1] = d3.polygonCentroid(cell);
delaunay.points[i] = x0 + (x1 - x0) * 1; //move point towards voronoi cell centroid horiztonally
delaunay.points[i + 1] = y0 + (y1 - y0) * 1; //move point towards voronoi cell centroid vertically
}
voronoi.update();
}
//return array of final centroids
const centeredPts = [];
for (let p of voronoi.cellPolygons()) {
const centroid = d3.polygonCentroid(p);
centeredPts.push(centroid);
}
const fittingCircles = [];
for (let p of voronoi.cellPolygons()) { //for every polygon
const [cx, cy] = d3.polygonCentroid(p);
if (isNaN(cx) || isNaN(cy)) continue;
const edgeMidPoints = [];
for (let i = 0; i < p.length - 1; i++) { //for every point in polygon
const midpoint = midpointOfALine(...p[i], ...p[i+1]); // return middle point between each edge
edgeMidPoints.push(midpoint);
}
const r = edgeMidPoints.reduce((acc, pt) => {
const dist = distance(...pt, cx, cy); // distance between edgeMidPoint and voronoi polygon centroid
return dist < acc ? dist : acc; //return the minimum distance
}, width);
const freq = lerp(1/20000, 1/400, freqUV); //contrain the noise freq between this range
const angle = Math.PI * 2 * CSUtils.random.noise2D(cx, cy, freq); //pi * 2 = 360 deg in radians
fittingCircles.push([cx, cy, r, angle]);
}
svg.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", colorScheme.background);
if (debug) {
svg
.append("g")
.selectAll(".fitting-circle")
.data(fittingCircles)
.join("circle")
.attr("class", "fitting-circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", d => d[2])
svg
.append("g")
.selectAll(".voronoi")
.data(pts)
.join("path")
.attr("class", "voronoi")
.attr("d", (d,i) => voronoi.renderCell(i));
svg.append("g")
.attr("class", "original")
.selectAll("circle")
.data(pts)
.join("circle")
.attr("class","dot")
.attr("cx", d => xScale(d[0]))
.attr("cy", d => yScale(d[1]))
.attr("r", 3)
svg.append("g")
.attr("class", "centered")
.selectAll("circle")
.data(centeredPts)
.join("circle")
.attr("class", "dot")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 3)
}
svg.append("g")
.selectAll(".shape")
.data(fittingCircles)
.join("g")
.attr("class", d =>
Math.round(Math.random()) % 2 == 1? "shape shape-line" : "shape shape-circle"
);
svg.selectAll(".shape-circle")
.append("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", d => d[2] - gap / 2)
.attr("fill", d => shuffle(colorScheme.colors).slice().pop());
svg.selectAll(".shape-line")
.append("line")
.each(function (d) {
const [cx, cy, r, angle] = d;
const length = 2 * r - strokeWidth - gap;
const [x1, y1] = rotatePoint(cx + length / 2, cy, angle, cx, cy);
const [x2, y2] = rotatePoint(cx - length / 2, cy, angle, cx, cy);
d3.select(this)
.attr("x1", x1)
.attr("y1", y1)
.attr("x2", x2)
.attr("y2", y2);
})
.attr("stroke-width", strokeWidth)
.attr("stroke-linecap", "round")
.attr("stroke", d => shuffle(colorScheme.colors).slice().pop())
return svg.node()
}