Skip to content

Instantly share code, notes, and snippets.

@alexmacy
Last active June 23, 2017 05:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexmacy/6bb8cbdaa077a267a887aeceb8a064f6 to your computer and use it in GitHub Desktop.
Save alexmacy/6bb8cbdaa077a267a887aeceb8a064f6 to your computer and use it in GitHub Desktop.
Draggable Voronoi Circles - SVG
license: gpl-3.0

A riff on mbostock's block: Voronoi Circles. I updated it to d3 v4, ported it to SVG, and added the ability to drag the polygons.

<!DOCTYPE html>
<meta charset="utf-8">
<script src="//d3js.org/d3.v4.min.js"></script>
<svg width="960" height="500"></svg>
<script>
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
voronoi = d3.voronoi().extent([[0.5, 0.5], [width - 0.5, height - 0.5]]);
const n = 100,
particles = Array.from(new Array(n), getInitData);
let activeCell,
cells;
const circles = svg.append("g")
.attr("class", "circles")
.selectAll("circle")
.data(particles)
.enter().append("circle")
.style("fill", "#ddd")
const points = svg.append("g")
.attr("class", "points")
.selectAll("circle")
.data(particles)
.enter().append("circle")
.style("fill", "black")
.attr("r", 1)
const polygons = svg.append("g")
.attr("class", "polygons")
.selectAll("polygon")
.data(particles)
.enter().append("polygon")
.style("fill", "white")
.style("fill-opacity", 0)
.style("stroke", "#aaa")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged));
d3.timer(function(elapsed) {
newData()
drawVoronoi()
});
function newData() {
for (let p of particles) {
p[0] += p.vx;
if (p[0] < 0) p[0] = p.vx *= -1;
else if (p[0] > width) p[0] = width + (p.vx *= -1);
p[1] += p.vy;
if (p[1] < 0) p[1] = p.vy *= -1;
else if (p[1] > height) p[1] = height + (p.vy *= -1);
p.vx += 0.1 * (Math.random() - .5) - 0.01 * p.vx;
p.vy += 0.1 * (Math.random() - .5) - 0.01 * p.vy;
}
}
function drawVoronoi() {
cells = voronoi(particles);
polygons.data(cells.polygons())
.attr("points", function(d) {return d})
circles.data(cells.polygons())
.style("fill", function(d, i) {
return i === activeCell ? "#f88" : "#ddd"
})
.each(function(p, j) {
var circle = polygonIncircle(p)
d3.select(this)
.attr("r", Math.max(circle.radius - 2.5, 0))
.attr("cx", circle[0])
.attr("cy", circle[1])
})
points.data(particles)
.attr("cx", function(d) {return d[0]})
.attr("cy", function(d) {return d[1]})
}
function getInitData() {
return {0: Math.random() * width, 1: Math.random() * height, vx: 0, vy: 0}
}
function dragstarted(d, i) {
particles[i][0] = d3.event.x
particles[i][1] = d3.event.y
activeCell = i
}
function dragged(d, i) {
if (0 > d3.event.x || d3.event.x > width ||
0 > d3.event.y || d3.event.y > height) return
particles[i][0] = d3.event.x
particles[i][1] = d3.event.y
activeCell = i
}
// A horrible brute-force algorithm for determining the largest circle that can
// fit inside a convex polygon. For each distinct set of three sides of the
// polygon, compute the tangent circle. Then reduce the circle’s radius against
// the remaining sides of the polygon.
function polygonIncircle(points) {
var circle = {radius: 0};
for (var i = 0, n = points.length; i < n; ++i) {
var pi0 = points[i],
pi1 = points[(i + 1) % n];
for (var j = i + 1; j < n; ++j) {
var pj0 = points[j],
pj1 = points[(j + 1) % n],
pij = j === i + 1 ? pj0 : lineLineIntersection(pi0[0], pi0[1], pi1[0], pi1[1], pj0[0], pj0[1], pj1[0], pj1[1]);
search: for (var k = j + 1; k < n; ++k) {
var pk0 = points[k],
pk1 = points[(k + 1) % n],
pik = lineLineIntersection(pi0[0], pi0[1], pi1[0], pi1[1], pk0[0], pk0[1], pk1[0], pk1[1]),
pjk = k === j + 1 ? pk0 : lineLineIntersection(pj0[0], pj0[1], pj1[0], pj1[1], pk0[0], pk0[1], pk1[0], pk1[1]),
candidate = triangleIncircle(pij[0], pij[1], pik[0], pik[1], pjk[0], pjk[1]),
radius = candidate.radius;
for (var l = 0; l < n; ++l) {
var pl0 = points[l],
pl1 = points[(l + 1) % n],
r = pointLineDistance(candidate[0], candidate[1], pl0[0], pl0[1], pl1[0], pl1[1]);
if (r < circle.radius) continue search;
if (r < radius) radius = r;
}
circle = candidate;
circle.radius = radius;
}
}
}
return circle;
}
// Returns the incircle of the triangle 012.
function triangleIncircle(x0, y0, x1, y1, x2, y2) {
var x01 = x0 - x1, y01 = y0 - y1,
x02 = x0 - x2, y02 = y0 - y2,
x12 = x1 - x2, y12 = y1 - y2,
l01 = Math.sqrt(x01 * x01 + y01 * y01),
l02 = Math.sqrt(x02 * x02 + y02 * y02),
l12 = Math.sqrt(x12 * x12 + y12 * y12),
k0 = l01 / (l01 + l02),
k1 = l12 / (l12 + l01),
center = lineLineIntersection(x0, y0, x1 - k0 * x12, y1 - k0 * y12, x1, y1, x2 + k1 * x02, y2 + k1 * y02);
center.radius = Math.sqrt((l02 + l12 - l01) * (l12 + l01 - l02) * (l01 + l02 - l12) / (l01 + l02 + l12)) / 2;
return center;
}
// Returns the intersection of the infinite lines 01 and 23.
function lineLineIntersection(x0, y0, x1, y1, x2, y2, x3, y3) {
var x02 = x0 - x2, y02 = y0 - y2,
x10 = x1 - x0, y10 = y1 - y0,
x32 = x3 - x2, y32 = y3 - y2,
t = (x32 * y02 - y32 * x02) / (y32 * x10 - x32 * y10);
return [x0 + t * x10, y0 + t * y10];
}
// Returns the signed distance from point 0 to the infinite line 12.
function pointLineDistance(x0, y0, x1, y1, x2, y2) {
var x21 = x2 - x1, y21 = y2 - y1;
return (y21 * x0 - x21 * y0 + x2 * y1 - y2 * x1) / Math.sqrt(y21 * y21 + x21 * x21);
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment