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

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more