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.
Last active
June 23, 2017 05:47
-
-
Save alexmacy/6bb8cbdaa077a267a887aeceb8a064f6 to your computer and use it in GitHub Desktop.
Draggable Voronoi Circles - SVG
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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