Published
Edited
Nov 27, 2019
1 fork
Importers
20 stars
Insert cell
Insert cell
canvas = {
const context = DOM.context2d(width, height);
context.lineCap = "round";
context.lineJoin = "round";
context.lineWidth = 4;
context.strokeStyle = "#fff";

while (true) {
context.clearRect(0, 0, width, height);

for (let i = 0; i < n; ++i) {
const cell = voronoi.cellPolygon(i);
if (cell === null) continue;
context.beginPath();
drawRoundedPolygon(context, cell, radius);
context.fillStyle = d3.schemeTableau10[i % 10];
context.fill();
context.stroke();
}

context.beginPath();
voronoi.delaunay.renderPoints(context);
context.fillStyle = "#000";
context.fill();
yield context.canvas;

for (let i = 0; i < n; ++i) {
const x = i << 1;
const y = x + 1;
positions[x] += velocities[x];
positions[y] += velocities[y];
if (positions[x] < -margin) positions[x] += width + margin * 2;
else if (positions[x] > width + margin) positions[x] -= width + margin * 2;
if (positions[y] < -margin) positions[y] += height + margin * 2;
else if (positions[y] > height + margin) positions[y] -= height + margin * 2;
velocities[x] += 0.1 * (Math.random() - 0.5) - 0.01 * velocities[x];
velocities[y] += 0.1 * (Math.random() - 0.5) - 0.01 * velocities[y];
}
voronoi.update();
}
}
Insert cell
positions = Float64Array.from({length: n * 2}, (_, i) => Math.random() * (i & 1 ? height : width))
Insert cell
velocities = new Float64Array(n * 2)
Insert cell
voronoi = new d3.Delaunay(positions).voronoi([0, 0, width, height])
Insert cell
function drawRoundedPolygon(context, points, r) {

// The rounding radius can’t be bigger than the polygon’s incircle.
// TODO Abort the search for the incircle if one is found larger than r.
const [cx, cy, cr] = polygonIncircle(points);
r = Math.min(r, cr);
if (r <= 0) return;

// Build a linked list from the array of vertices so we can splice.
let n = points.length - 1, p0, p1, p2, p3;
points = points.slice(1).map(([x, y]) => [x, y]);
p1 = points[n - 2], p2 = points[n - 1];
for (let i = 0; i < n; ++i) {
p0 = p1, p1 = p2, p2 = points[i];
p1.previous = p0;
p1.next = p2;
}

// TODO do we need to make all these extra passes?
p3 = p2.next;
for (let i = 0; i <= n; ++i) {
p0 = p1, p1 = p2, p2 = p3, p3 = p3.next;
const t012 = cornerTangent(p0, p1, p2, r);
const t123 = 1 - cornerTangent(p3, p2, p1, r);

// If the following corner’s tangent is before this corner’s tangent,
// replace p1 and p2 with the intersection of the lines 01 and 23.
if (!(t012 + 1e-6 < t123)) {
p2 = p0.next = p3.previous = lineLineIntersect(p0, p1, p2, p3);
p2.previous = p0;
p2.next = p3;
p3 = p2;
p2 = p3.previous;
p1 = p2.previous;
p0 = p1.previous;
if (--n < 3) break;
i = 0;
}
}

// If we removed too many points, just draw the previously computed incircle.
if (n < 3) {
context.moveTo(cx + cr, cy);
context.arc(cx, cy, cr, 0, 2 * Math.PI);
return;
}

// Draw the rounded polygon, compting the corner tangents.
for (let i = 0, moved = false; i <= n; ++i) {
p0 = p1, p1 = p2, p2 = p3, p3 = p3.next;
const t012 = cornerTangent(p0, p1, p2, r);
const t123 = 1 - cornerTangent(p3, p2, p1, r);
const x21 = p2[0] - p1[0], y21 = p2[1] - p1[1];
const x4 = p1[0] + t012 * x21, y4 = p1[1] + t012 * y21;
const x5 = p1[0] + t123 * x21, y5 = p1[1] + t123 * y21;
if (moved) context.arcTo(p1[0], p1[1], x4, y4, r);
else moved = true, context.moveTo(x4, y4);
context.lineTo(x5, y5);
}
}
Insert cell
function polygonIncircle(polygon) {
let circle = [NaN, NaN, 0];
for (let i = 0, n = polygon.length - 1; i < n; ++i) {
const pi0 = polygon[i], pi1 = polygon[i + 1];
for (let j = i + 1; j < n; ++j) {
const pj0 = polygon[j], pj1 = polygon[j + 1];
search: for (let k = j + 1; k < n; ++k) {
const pk0 = polygon[k], pk1 = polygon[k + 1];
const c = circleTangent(pi0, pi1, pj0, pj1, pk0, pk1);
if (!(c[2] > circle[2])) continue;
for (let l = 0; l < n; ++l) {
if (l === i || l === j || l === k) continue;
const d = pointLineDistance(c, polygon[l], polygon[l + 1]);
if (d + 1e-6 < c[2]) continue search;
}
circle = c;
}
}
}
return circle;
}
Insert cell
// Given a circle of radius r that is tangent to the line segments 01 and 12,
// returns the parameter t of the tangent along the line segment 12.
function cornerTangent([x0, y0], [x1, y1], [x2, y2], r) {
const theta = innerAngle([x0, y0], [x1, y1], [x2, y2]);
const x21 = x2 - x1, y21 = y2 - y1;
const l21 = Math.sqrt(x21 * x21 + y21 * y21);
const l14 = r / Math.tan(theta / 2);
return l14 / l21;
}
Insert cell
// Returns the angle between segments 01 and 12.
function innerAngle([x0, y0], [x1, y1], [x2, y2]) {
const x01 = x0 - x1, y01 = y0 - y1, l01_2 = x01 * x01 + y01 * y01;
const x12 = x1 - x2, y12 = y1 - y2, l12_2 = x12 * x12 + y12 * y12;
const x02 = x0 - x2, y02 = y0 - y2, l02_2 = x02 * x02 + y02 * y02;
return Math.acos((l12_2 + l01_2 - l02_2) / (2 * Math.sqrt(l12_2 * l01_2)));
}
Insert cell
function circleTangent(p0, p1, p2, p3, p4, p5) {
const b0 = lineLineBisect(p0, p1, p3, p2);
const b1 = lineLineBisect(p2, p3, p5, p4);
const i = lineLineIntersect(...b0, ...b1);
return [...i, pointLineDistance(i, p0, p1)];
}
Insert cell
function pointLineDistance([x0, y0], [x2, y2], [x1, y1]) {
const x21 = x2 - x1, y21 = y2 - y1;
return (y21 * x0 - x21 * y0 + x2 * y1 - y2 * x1) / Math.sqrt(y21 * y21 + x21 * x21);
}
Insert cell
function lineLineBisect([x0, y0], [x1, y1], [x2, y2], [x3, y3]) {
const x02 = x0 - x2, y02 = y0 - y2;
const x10 = x1 - x0, y10 = y1 - y0, l10 = Math.sqrt(x10 ** 2 + y10 ** 2);
const x32 = x3 - x2, y32 = y3 - y2, l32 = Math.sqrt(x32 ** 2 + y32 ** 2);
const ti = (x32 * y02 - y32 * x02) / (y32 * x10 - x32 * y10);
const xi = x0 + ti * x10, yi = y0 + ti * y10;
return [[xi, yi], [xi + x10 / l10 + x32 / l32, yi + y10 / l10 + y32 / l32]];
}
Insert cell
function lineLineIntersect([x0, y0], [x1, y1], [x2, y2], [x3, y3]) {
const x02 = x0 - x2, y02 = y0 - y2;
const x10 = x1 - x0, y10 = y1 - y0;
const x32 = x3 - x2, y32 = y3 - y2;
const t = (x32 * y02 - y32 * x02) / (y32 * x10 - x32 * y10);
return [x0 + t * x10, y0 + t * y10];
}
Insert cell
height = 600
Insert cell
margin = 60
Insert cell
radius = 12
Insert cell
n = 100
Insert cell
d3 = require("d3-delaunay@5", "d3-scale-chromatic@1")
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more