Trying out the scribble fill method from this old Apple patent by John B. Turner with a bit of spline embellishment at the corners. Seems to work pretty well for more convex shapes, but results can get weird otherwise (the Olympic Peninsula seems especially uncooperative).
Some possible tweaks:
xxxxxxxxxx
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
path {
fill: #ccc;
stroke: #444;
stroke-width: 1px;
}
line {
stroke-width: 1px;
stroke: black;
}
circle {
stroke: black;
stroke-width: 1px;
fill: rgba(255, 0, 255, 0.25);
}
.scribble {
fill: none;
}
</style>
</head>
<body>
<svg width="960" height="500"></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min.js"></script>
<script>
let svg = d3.select("svg"),
border = svg.append("path"),
line = d3.line(),
closedLine = d3.line().curve(d3.curveLinearClosed);
d3.json("us.topo.json", function(err, topo){
const states = topojson.feature(topo, topo.objects.states).features.map(d => d.geometry.coordinates[0]);
scribbleStates(d3.shuffle(states));
});
function scribbleStates(states) {
let points = states.shift(),
scribbleAngle = Math.PI * (1 / 16 + Math.random() * 3 / 8) * (Math.random() < 0.5 ? -1 : 1),
midpoint = getMidpoint(points),
rotator = rotateAround(midpoint, scribbleAngle),
rotated = points.map(rotator),
gridlines = getGridlines(getBounds(rotated)),
intersections = getIntersections(gridlines, rotated);
svg.selectAll(".scribble")
.remove();
border.style("opacity", 1)
.datum(points)
.attr("d", closedLine);
rotateShape()
.then(drawGridlines)
.then(drawScribbles)
.then(clearGuides)
.then(unrotate)
.then(wobble)
.then(function(){
scribbleStates([...states, points]);
});
function rotateShape() {
return new Promise(function(resolve) {
border.datum(rotated)
.transition()
.delay(500)
.duration(1000)
.attr("d", closedLine)
.on("end", resolve);
});
}
function drawGridlines() {
svg.selectAll("line")
.data(gridlines)
.enter()
.append("line")
.attr("x1", d => d[0][0])
.attr("x2", d => d[1][0])
.attr("y1", d => d[0][1])
.attr("y2", d => d[1][1]);
svg.selectAll("circle")
.data(d3.merge(intersections))
.enter()
.append("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 5);
}
function drawScribbles() {
let scribbles = getScribbles(intersections, rotated);
svg.selectAll(".scribble")
.data(scribbles)
.enter()
.append("path")
.attr("class", "scribble")
.attr("d", line);
return new Promise(resolve => d3.timeout(resolve, 500));
}
function clearGuides() {
return new Promise(function(resolve){
svg.selectAll("line, circle")
.transition()
.duration(500)
.style("opacity", 0)
.remove()
.on("end", resolve);
});
}
function unrotate() {
let unrotator = rotateAround(midpoint, -scribbleAngle),
paths = svg.selectAll("path");
paths.each(function(d){
d3.select(this).datum(d.map(unrotator));
});
return new Promise(function(resolve){
paths.transition()
.duration(1000)
.attr("d", (d, i) => i ? line(d) : closedLine(d))
.transition()
.filter((d, i) => !i)
.style("opacity", 0.1)
.on("end", resolve);
});
}
function wobble() {
let scribbles = svg.selectAll(".scribble")
.attr("d", d3.line().curve(d3.curveCardinal.tension(0.25)))
.style("stroke", d3.interpolateRainbow(Math.random()))
.style("stroke-width", 3);
return new Promise(function(resolve){
scribbles.transition()
.duration(1000)
.on("end", resolve);
});
}
}
function getScribbles(rows, ring) {
let top = 0,
bottom = 1,
i = j = 0,
p1 = rows[top][i],
p2 = rows[bottom][j],
scribbles = [];
checkSegment();
return scribbles;
function checkSegment() {
if (isInFront() && isContained()) {
addSegment();
} else {
nextBottom();
}
}
function addSegment() {
let found = scribbles.find(scribble => distanceBetween(scribble[scribble.length - 1], p1) < 1e-6);
if (found) {
found.push(p2);
} else {
scribbles.push([p1, p2]);
}
scribbles.sort((a, b) => scribbleLength(b) - scribbleLength(a));
nextTop();
}
function isInFront() {
return (top % 2 ? -1 : 1) * (p2[0] - p1[0]) > 0
}
function isContained() {
if (p1[2] === p2[2] || !d3.polygonContains(ring, pointBetween(p1, p2, 0.5))) {
return false;
}
return ring.every(function(a, segmentIndex){
const b = ring[segmentIndex + 1] || ring[0];
return segmentIndex === p1[2] || segmentIndex === p2[2] || !segmentsIntersect([a, b], [p1, p2]);
});
}
function nextRow() {
if (bottom + 1 < rows.length) {
p1 = rows[++top][i = 0];
p2 = rows[++bottom][j = 0];
checkSegment();
}
}
function nextTop() {
if (i + 1 >= rows[top].length) {
nextRow();
} else {
p1 = rows[top][++i];
checkSegment();
}
}
function nextBottom() {
if (j + 1 >= rows[bottom].length) {
nextTop();
} else {
p2 = rows[bottom][++j];
checkSegment();
}
}
function scribbleLength(points) {
return points.reduce(function(length, point, i){
return i ? length + distanceBetween(point, points[i - 1]) : 0;
}, 0);
}
}
function getGridlines(bounds) {
let lineFrequency = 7 + Math.random() * 3,
lineVariation = lineFrequency * 3 / 8;
i = bounds[0][1],
gridY = [],
space = lineFrequency - lineVariation / 2 + Math.random() * lineVariation;
while (i + space < bounds[1][1]) {
i += space;
gridY.push(i);
space = lineFrequency - lineVariation + Math.random() * lineVariation * 2;
}
return gridY.map(y => [[bounds[0][0] - 5, y], [bounds[1][0] + 5, y]]);
}
function getIntersections(gridlines, ring){
return gridlines.map(function(gridline, i){
const y = gridline[0][1],
row = [],
direction = i % 2 ? - 1 : 1;
ring.forEach(function(p1, j){
const p2 = ring[j + 1] || ring[0],
m = (p2[1] - p1[1]) / (p2[0] - p1[0]),
b = p2[1] - m * p2[0],
x = (y - b) / m;
if ((p1[1] <= y && p2[1] > y) || (p1[1] >= y && p2[1] < y)) {
row.push([x, y, j]);
}
});
row.sort((a, b) => direction * (a[0] - b[0]));
return row;
});
}
function rotateAround(center, angle) {
const cos = Math.cos(angle),
sin = Math.sin(angle);
return function(p) {
return [
center[0] + (p[0] - center[0]) * cos - (p[1] - center[1]) * sin,
center[1] + (p[0] - center[0]) * sin + (p[1] - center[1]) * cos
];
};
}
function getBounds(ring) {
let x0 = y0 = Infinity,
x1 = y1 = -Infinity;
ring.forEach(function(point){
if (point[0] < x0) x0 = point[0];
if (point[0] > x1) x1 = point[0];
if (point[1] < y0) y0 = point[1];
if (point[1] > y1) y1 = point[1];
});
return [
[x0, y0],
[x1, y1]
];
}
function getMidpoint(ring) {
const bounds = getBounds(ring);
return [
(bounds[1][0] + bounds[0][0]) / 2,
(bounds[1][1] + bounds[0][1]) / 2
];
}
function segmentsIntersect(a, b) {
if (orientation(a[0], a[1], b[0]) === orientation(a[0], a[1], b[1])) {
return false;
}
return orientation(b[0], b[1], a[0]) !== orientation(b[0], b[1], a[1]);
}
function orientation(p, q, r) {
const val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]);
return val > 0 ? 1 : val < 0 ? -1 : 0;
}
function pointBetween(a, b, pct) {
const point = [
a[0] + (b[0] - a[0]) * pct,
a[1] + (b[1] - a[1]) * pct
];
return point;
}
function distanceBetween(a, b) {
const dx = a[0] - b[0],
dy = a[1] - b[1];
return Math.sqrt(dx * dx + dy * dy);
}
</script>
https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js
https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min.js