|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="utf-8" /> |
|
<style> |
|
|
|
path { |
|
fill: none; |
|
stroke-width: 2px; |
|
stroke-linejoin: round; |
|
} |
|
|
|
.state { |
|
stroke: #999; |
|
stroke-width: 1px; |
|
fill: papayawhip; |
|
} |
|
|
|
.simplified { |
|
stroke: #de1e3d; |
|
stroke-width: 2px; |
|
stroke-dasharray: 8,8; |
|
} |
|
|
|
.zone { |
|
stroke: #0eb8ba; |
|
} |
|
|
|
.hidden { |
|
display: none; |
|
} |
|
|
|
</style> |
|
</head> |
|
<body> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script> |
|
<script src="warper.js"></script> |
|
<script src="simplify.js"></script> |
|
<script> |
|
|
|
var stripWidth = 80; |
|
|
|
var projection = d3.geo.conicConformal() |
|
.parallels([36, 37 + 15 / 60]) |
|
.rotate([119, -35 - 20 / 60]) |
|
.scale(3433) |
|
.translate([355, 498]); |
|
|
|
var line = d3.svg.line(); |
|
|
|
// Top point |
|
var origin = [50, 100]; |
|
|
|
d3.json("norway.geojson",function(err,ca){ |
|
|
|
// Preproject to screen coords |
|
ca.coordinates[0] = ca.coordinates[0].map(projection); |
|
|
|
// Get coastline |
|
var ls = ca.coordinates[0].slice(0, 155); |
|
|
|
// Get simplified vertices |
|
var simplified = simplify(ls, 1000); |
|
|
|
var zones = d3.select("body").append("svg") |
|
.attr("width", 960) |
|
.attr("height", 720) |
|
.selectAll("g") |
|
.data(getZones(simplified)) |
|
.enter() |
|
.append("g"); |
|
|
|
zones.append("defs") |
|
.append("clipPath") |
|
.attr("id",function(d, i){ |
|
return "clip" + i; |
|
}) |
|
.append("path"); |
|
|
|
var inner = zones.append("g") |
|
.attr("class",function(d, i) { |
|
return i ? "hidden" : null; |
|
}); |
|
|
|
inner.append("path") |
|
.attr("class", "state"); |
|
|
|
inner.append("line") |
|
.attr("class", "simplified fade hidden"); |
|
|
|
// Put boundary outside so it isn't clipped |
|
zones.append("path") |
|
.attr("class", "zone fade hidden"); |
|
|
|
zones.call(update); |
|
|
|
// Step-by-step for demo purposes |
|
d3.select("body") |
|
.transition() |
|
.duration(1000) |
|
.each("end", clipState) |
|
.transition() |
|
.each("end", showLine) |
|
.transition() |
|
.each("end", showZones) |
|
.transition() |
|
.each("end", move); |
|
|
|
// 1. Clip out the rest of CA |
|
function clipState() { |
|
inner.classed("hidden", false) |
|
.attr("clip-path",function(d, i){ |
|
return "url(#clip" + i + ")"; |
|
}); |
|
} |
|
|
|
// 2. Show the simplified line |
|
function showLine() { |
|
inner.select(".simplified") |
|
.classed("hidden", false); |
|
} |
|
|
|
// 3. Show the zone boundaries |
|
function showZones() { |
|
zones.select(".zone") |
|
.classed("hidden", false); |
|
} |
|
|
|
// 4. Rotate/translate all the zones |
|
function move() { |
|
|
|
warpZones(zones.data()); |
|
|
|
zones.transition() |
|
.duration(2000) |
|
.each("end",align) |
|
.call(update); |
|
|
|
} |
|
|
|
// 5. Warp the zones to rectangles |
|
function align(z) { |
|
|
|
z.project = function(d){ |
|
return z.warp(z.translate(d)); |
|
}; |
|
|
|
z.boundary = z.corners; |
|
|
|
d3.select(this) |
|
.transition() |
|
.duration(750) |
|
.call(update) |
|
.each("end",fade); |
|
|
|
} |
|
|
|
// 6. Fade out |
|
function fade() { |
|
|
|
d3.select(this).selectAll(".fade") |
|
.transition() |
|
.duration(500) |
|
.style("opacity", 0); |
|
|
|
} |
|
|
|
// Redraw |
|
function update(sel) { |
|
|
|
sel.select(".zone") |
|
.attr("d",function(d){ |
|
// TODO why does this not work? |
|
// return line(d.boundary); |
|
return line(d.boundary.slice(0,4)) + "Z"; |
|
}); |
|
|
|
sel.select(".state") |
|
.attr("d",function(d){ |
|
return d.path(ca); |
|
}); |
|
|
|
sel.select(".simplified") |
|
.attr("x1",function(d){ |
|
return d.ends[0][0]; |
|
}) |
|
.attr("x2",function(d){ |
|
return d.ends[1][0]; |
|
}) |
|
.attr("y1",function(d){ |
|
return d.ends[0][1]; |
|
}) |
|
.attr("y2",function(d){ |
|
return d.ends[1][1]; |
|
}); |
|
|
|
sel.select("clipPath path") |
|
.attr("d",function(d){ |
|
return line(d.boundary.slice(0,4)) + "Z"; |
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
// Turn a simplified LineString into one group per segment |
|
function getZones(simp) { |
|
|
|
return simp.slice(1).map(function(p, i){ |
|
|
|
return { |
|
boundary: getBoundary(simp[i - 1], simp[i], p, simp[i + 2]), |
|
ends: [simp[i], p], |
|
path: d3.geo.path().projection(null) |
|
}; |
|
|
|
}); |
|
|
|
} |
|
|
|
function warpZones(zones) { |
|
|
|
zones.forEach(function(z,i){ |
|
|
|
var angle = getAngle(z.ends[0], z.ends[1]), |
|
anchor = i ? zones[i - 1].ends[1] : origin; |
|
|
|
// Anchor points to end of prev segment |
|
var translate = [ |
|
anchor[0] - z.ends[0][0], |
|
anchor[1] - z.ends[0][1] |
|
]; |
|
|
|
// Get translation/rotation function |
|
z.translate = translateAndRotate(translate, z.ends[0], angle); |
|
|
|
// Warp the boundary line and the simplified segment |
|
z.ends = z.ends.map(z.translate); |
|
z.boundary = z.boundary.map(z.translate); |
|
|
|
var top = bisect(null, z.ends[0], z.ends[1]), |
|
bottom = bisect(z.ends[0], z.ends[1], null); |
|
|
|
z.corners = [top[0], top[1], bottom[1], bottom[0], top[0]]; |
|
|
|
z.corners.push(z.corners[0]); |
|
|
|
// See: http://bl.ocks.org/veltman/8f5a157276b1dc18ce2fba1bc06dfb48 |
|
z.warp = warper(z.boundary, z.corners); |
|
|
|
z.project = function(d){ |
|
return z.translate(d); |
|
}; |
|
|
|
z.path.projection(d3.geo.transform({ |
|
point: function(x, y) { |
|
var p = z.project([x, y]); |
|
this.stream.point(p[0], p[1]); |
|
} |
|
})); |
|
|
|
}); |
|
|
|
} |
|
|
|
function getBoundary(prev, first, second, next) { |
|
|
|
// if prev is undefined, top is perpendicular through first |
|
// otherwise top bisects the prev-first-second angle |
|
// if next is undefined, bottom is perpendicular through second |
|
// otherwise bottom bisects the first-second-next angle |
|
var top = bisect(prev, first, second), |
|
bottom = bisect(first, second, next); |
|
|
|
return [top[0], top[1], bottom[1], bottom[0], top[0]]; |
|
} |
|
|
|
function getAngle(a, b) { |
|
|
|
return Math.atan2(b[1] - a[1], b[0] - a[0]); |
|
|
|
} |
|
|
|
// Given an anchor point, initial translate, and angle rotation |
|
// Return a function to translate+rotate a point |
|
function translateAndRotate(translate, anchor, angle) { |
|
|
|
var cos = Math.cos(angle), |
|
sin = Math.sin(angle); |
|
|
|
return function(point) { |
|
|
|
return [ |
|
translate[0] + anchor[0] + ( cos * (point[0] - anchor[0]) + sin * (point[1] - anchor[1])), |
|
translate[1] + anchor[1] + ( -sin * (point[0] - anchor[0]) + cos * (point[1] - anchor[1])) |
|
]; |
|
|
|
}; |
|
|
|
} |
|
|
|
// Hacky angle bisector |
|
function bisect(start, vertex, end) { |
|
|
|
var at, |
|
bt, |
|
adjusted, |
|
right, |
|
left; |
|
|
|
if (start) { |
|
at = getAngle(start, vertex); |
|
} |
|
|
|
if (end) { |
|
bt = getAngle(vertex, end); |
|
} |
|
|
|
if (!start) { |
|
at = bt; |
|
} |
|
|
|
if (!end) { |
|
bt = at; |
|
} |
|
|
|
adjusted = bt - at; |
|
|
|
if (adjusted <= -Math.PI) { |
|
adjusted = 2 * Math.PI + adjusted; |
|
} else if (adjusted > Math.PI) { |
|
adjusted = adjusted - 2 * Math.PI; |
|
} |
|
|
|
right = (adjusted - Math.PI) / 2; |
|
left = Math.PI + right; |
|
|
|
left += at; |
|
right += at; |
|
|
|
return [ |
|
[vertex[0] + stripWidth * Math.cos(left) / 2, vertex[1] + stripWidth * Math.sin(left) / 2], |
|
[vertex[0] + stripWidth * Math.cos(right) / 2, vertex[1] + stripWidth * Math.sin(right) / 2] |
|
]; |
|
} |
|
|
|
function id(d) { |
|
return d; |
|
} |
|
|
|
d3.select(self.frameElement).style("height", "720px"); |
|
|
|
</script> |
|
</body> |
|
</html> |