|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
svg { |
|
background-color: #cccccc; |
|
border: 2px solid #777777; |
|
margin: 20px; |
|
} |
|
</style> |
|
|
|
|
|
<script src="http://d3js.org/d3.v4.min.js"></script> |
|
|
|
<svg id="demo" width='900' height='900'></svg> |
|
|
|
<script> |
|
|
|
// Setup and Parameterization |
|
|
|
var svg = d3.select('svg#demo'); |
|
|
|
var width = svg.attr('width'); |
|
var height = svg.attr('height'); |
|
|
|
var hullPadding = 60; |
|
var pointRadius = 20; |
|
var margin = hullPadding + pointRadius; |
|
|
|
|
|
|
|
var vecScale = function (scale, v) { |
|
// Returns the vector 'v' scaled by 'scale'. |
|
return [ scale * v[0], scale * v[1] ]; |
|
} |
|
|
|
|
|
var vecSum = function (pv1, pv2) { |
|
// Returns the sum of two vectors, or a combination of a point and a vector. |
|
return [ pv1[0] + pv2[0], pv1[1] + pv2[1] ]; |
|
} |
|
|
|
|
|
var unitNormal = function (p0, p1) { |
|
// Returns the unit normal to the line segment from p0 to p1. |
|
var n = [ p0[1] - p1[1], p1[0] - p0[0] ]; |
|
var nLength = Math.sqrt (n[0]*n[0] + n[1]*n[1]); |
|
return [ n[0] / nLength, n[1] / nLength ]; |
|
}; |
|
|
|
|
|
var strictHull = function(polyPoints) { |
|
// This method returns a polygon given the specified points. The points are assumed to be |
|
// in polygon order. |
|
|
|
return ( |
|
'M ' + polyPoints[0] |
|
+ ' L ' |
|
+ d3.range(1,polyPoints.length) |
|
.map(function(i) {return polyPoints[i];}) |
|
.join(' L') |
|
+ ' Z' |
|
); |
|
}; |
|
|
|
|
|
var roundedHull = function (polyPoints) { |
|
// Returns the SVG path data string representing the polygon, expanded and rounded. |
|
|
|
// Handle special cases |
|
if (!polyPoints || polyPoints.length < 1) return ""; |
|
if (polyPoints.length === 1) return roundedHull1 (polyPoints); |
|
if (polyPoints.length === 2) return roundedHull2 (polyPoints); |
|
|
|
var segments = new Array (polyPoints.length); |
|
|
|
// Calculate each offset (outwards) segment of the convex hull. |
|
for (var segmentIndex = 0; segmentIndex < segments.length; ++segmentIndex) { |
|
var p0 = (segmentIndex === 0) ? polyPoints[polyPoints.length-1] : polyPoints[segmentIndex-1]; |
|
var p1 = polyPoints[segmentIndex]; |
|
|
|
// Compute the offset vector for the line segment, with length = hullPadding. |
|
var offset = vecScale (hullPadding, unitNormal (p0, p1)); |
|
|
|
segments[segmentIndex] = [ vecSum (p0, offset), vecSum (p1, offset) ]; |
|
} |
|
|
|
var arcData = 'A ' + [hullPadding, hullPadding, '0,0,0,'].join(','); |
|
|
|
segments = segments.map (function (segment, index) { |
|
var pathFragment = ""; |
|
if (index === 0) { |
|
var pathFragment = 'M ' + segments[segments.length-1][1] + ' '; |
|
} |
|
pathFragment += arcData + segment[0] + ' L ' + segment[1]; |
|
|
|
return pathFragment; |
|
}); |
|
|
|
return segments.join(' '); |
|
} |
|
|
|
|
|
var roundedHull1 = function (polyPoints) { |
|
// Returns the path for a rounded hull around a single point (a circle). |
|
|
|
var p1 = [polyPoints[0][0], polyPoints[0][1] - hullPadding]; |
|
var p2 = [polyPoints[0][0], polyPoints[0][1] + hullPadding]; |
|
|
|
return 'M ' + p1 |
|
+ ' A ' + [hullPadding, hullPadding, '0,0,0', p2].join(',') |
|
+ ' A ' + [hullPadding, hullPadding, '0,0,0', p1].join(','); |
|
}; |
|
|
|
|
|
var roundedHull2 = function (polyPoints) { |
|
// Returns the path for a rounded hull around two points (a "capsule" shape). |
|
|
|
var offsetVector = vecScale (hullPadding, unitNormal (polyPoints[0], polyPoints[1])); |
|
var invOffsetVector = vecScale (-1, offsetVector); |
|
|
|
var p0 = vecSum (polyPoints[0], offsetVector); |
|
var p1 = vecSum (polyPoints[1], offsetVector); |
|
var p2 = vecSum (polyPoints[1], invOffsetVector); |
|
var p3 = vecSum (polyPoints[0], invOffsetVector); |
|
|
|
return 'M ' + p0 |
|
+ ' L ' + p1 + ' A ' + [hullPadding, hullPadding, '0,0,0', p2].join(',') |
|
+ ' L ' + p3 + ' A ' + [hullPadding, hullPadding, '0,0,0', p0].join(','); |
|
}; |
|
|
|
|
|
// Initial Graphics Setup |
|
|
|
var convexHullPath = svg.append('path') |
|
.attr ('fill', '#aca') |
|
.attr ('stroke', '#888') |
|
.attr ('stroke-width', '6px'); |
|
|
|
|
|
function render (points) { |
|
var convexHull = (points.length < 3) ? points : d3.polygonHull(points); |
|
|
|
var points = svg |
|
.selectAll ('circle') |
|
.data (points, function pointId(d) { return d[2]; }); |
|
|
|
points.enter() |
|
.append ('circle') |
|
.attr ('fill', '#a8d') |
|
.attr ('stroke', 'black') |
|
.attr ('stroke-width', '2px') |
|
.attr ('r', pointRadius) |
|
.attr ('cx', function(d) { return d[0]; }) |
|
.attr ('cy', function(d) { return d[1]; }); |
|
|
|
points.exit() |
|
.remove(); |
|
|
|
convexHullPath.attr ('d', roundedHull(convexHull)); // Update the rounded hull |
|
} |
|
|
|
|
|
// Random point generators |
|
var pointGenX = d3.randomUniform (margin, width - margin); |
|
var pointGenY = d3.randomUniform (margin, height - margin); |
|
var pointGen = function (id) { return [pointGenX(), pointGenY(), id]; }; |
|
|
|
var minPointCount = 1; |
|
var maxPointCount = 12; |
|
var pointId = 0; |
|
var pointDelta = 1; |
|
var points = d3.range(minPointCount - 1).map(pointGen); |
|
|
|
var updateFunction = function() { |
|
if (pointDelta > 0) { |
|
// Add Points |
|
points.push (pointGen(++pointId)); |
|
if (points.length >= maxPointCount) pointDelta = -1; |
|
} else { |
|
// Retire Points |
|
var i = Number.parseInt (Math.random() * points.length); |
|
points.splice (i, 1); |
|
if (points.length <= minPointCount) pointDelta = 1; |
|
} |
|
|
|
render (points); |
|
window.setTimeout (updateFunction, 500); |
|
}; |
|
|
|
updateFunction(); // Launch the animation |
|
|
|
</script> |