|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="utf-8" /> |
|
<style> |
|
path { |
|
fill: #e3e3e3; |
|
stroke-width: 1px; |
|
stroke: #666; |
|
} |
|
|
|
circle { |
|
stroke: none; |
|
fill: #fc0; |
|
} |
|
|
|
.added { |
|
fill: #f0f; |
|
} |
|
</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> |
|
|
|
var svg = d3.select("svg"), |
|
path = svg.append("path"), |
|
circles = svg.append("g"); |
|
|
|
d3.json("us.topo.json", function(err, topo){ |
|
|
|
var states = topojson.feature(topo, topo.objects.states).features.map(function(d){ |
|
return d.geometry.coordinates[0]; |
|
}); |
|
|
|
d3.shuffle(states); |
|
|
|
draw(); |
|
|
|
function draw() { |
|
|
|
var a = states[0].slice(0), |
|
b = states[1].slice(0); |
|
|
|
// Same number of points on each ring |
|
if (a.length < b.length) { |
|
addPoints(a, b.length - a.length); |
|
} else if (b.length < a.length) { |
|
addPoints(b, a.length - b.length); |
|
} |
|
|
|
// Pick optimal winding |
|
a = wind(a, b); |
|
|
|
path.attr("d", join(a)); |
|
|
|
// Redraw points |
|
circles.datum(a) |
|
.call(updateCircles); |
|
|
|
// Morph |
|
var t = d3.transition() |
|
.duration(800); |
|
|
|
path.transition(t) |
|
.on("end", function(){ |
|
states.push(states.shift()); |
|
setTimeout(draw, 100); |
|
}) |
|
.attr("d", join(b)); |
|
|
|
circles.selectAll("circle").data(b) |
|
.transition(t) |
|
.attr("cx",function(d){ |
|
return d[0]; |
|
}) |
|
.attr("cy",function(d){ |
|
return d[1]; |
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
function updateCircles(sel) { |
|
|
|
var circles = sel.selectAll("circle") |
|
.data(function(d){ return d; }); |
|
|
|
var merged = circles.enter() |
|
.append("circle") |
|
.attr("r", 2) |
|
.merge(circles); |
|
|
|
merged.classed("added", function(d){ |
|
return d.added; |
|
}) |
|
.attr("cx",function(d){ |
|
return d[0]; |
|
}) |
|
.attr("cy",function(d){ |
|
return d[1]; |
|
}); |
|
|
|
circles.exit().remove(); |
|
|
|
} |
|
|
|
function addPoints(ring, numPoints) { |
|
|
|
var desiredLength = ring.length + numPoints, |
|
step = d3.polygonLength(ring) / numPoints; |
|
|
|
var i = 0, |
|
cursor = 0, |
|
insertAt = step / 2; |
|
|
|
do { |
|
|
|
var a = ring[i], |
|
b = ring[(i + 1) % ring.length]; |
|
|
|
var segment = distanceBetween(a, b); |
|
|
|
if (insertAt <= cursor + segment) { |
|
ring.splice(i + 1, 0, pointBetween(a, b, (insertAt - cursor) / segment)); |
|
insertAt += step; |
|
continue; |
|
} |
|
|
|
cursor += segment; |
|
i++; |
|
|
|
} while (ring.length < desiredLength); |
|
|
|
} |
|
|
|
function pointBetween(a, b, pct) { |
|
|
|
var point = [ |
|
a[0] + (b[0] - a[0]) * pct, |
|
a[1] + (b[1] - a[1]) * pct |
|
]; |
|
|
|
point.added = true; |
|
return point; |
|
|
|
} |
|
|
|
function distanceBetween(a, b) { |
|
return Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2)); |
|
} |
|
|
|
function join(d) { |
|
return "M" + d.join("L") + "Z"; |
|
} |
|
|
|
function wind(ring, vs) { |
|
|
|
var len = ring.length, |
|
min = Infinity, |
|
bestOffset, |
|
sum; |
|
|
|
for (var offset = 0, len = ring.length; offset < len; offset++) { |
|
|
|
var sum = d3.sum(vs.map(function(p, i){ |
|
var distance = distanceBetween(ring[(offset + i) % len], p); |
|
return distance * distance; |
|
})); |
|
|
|
if (sum < min) { |
|
min = sum; |
|
bestOffset = offset; |
|
} |
|
|
|
} |
|
|
|
return ring.slice(bestOffset).concat(ring.slice(0, bestOffset)); |
|
|
|
} |
|
|
|
</script> |
Just curious... could you explain this possible improvement in a bit more detail? I wasn't sure what you meant.