Skip to content

Instantly share code, notes, and snippets.

@hollasch
Last active April 19, 2017 08:19
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 hollasch/f70f1fe7700f092b5a505e3efd1d9232 to your computer and use it in GitHub Desktop.
Save hollasch/f70f1fe7700f092b5a505e3efd1d9232 to your computer and use it in GitHub Desktop.
Rounded Enclosing Hull
height: 960

This example illustrates a way to construct a loose bounding area around a set of points. The general technique is to use d3.polygonHull, then offset each segment of the resulting polygon outwards, and finally to bridge the gaps with circular arcs.

One could also envision using Bézier curves to get a blob with no straight segments.

See also Smooth Polygon Convex Hull.

<!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>
MIT License
Copyright © 2017 Steve Hollasch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.imitations under the License.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment