|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
.graticule { |
|
fill: none; |
|
stroke: #777; |
|
stroke-width: 1px; |
|
} |
|
|
|
.graticule:nth-child(1) { |
|
fill: none; |
|
stroke-opacity: 0.6; |
|
stroke: #000; |
|
stroke-width: 6px; |
|
} |
|
.graticule:nth-child(2) { |
|
fill: none; |
|
stroke-opacity: 0.6; |
|
stroke: #00f; |
|
stroke-width: 6px; |
|
} |
|
.graticule:nth-child(3) { |
|
fill: none; |
|
stroke-opacity: 0.6; |
|
stroke: #f00; |
|
stroke-width: 6px; |
|
} |
|
.graticule:nth-child(4) { |
|
fill: none; |
|
stroke-opacity: 0.6; |
|
stroke: #f70; |
|
stroke-width: 6px; |
|
} |
|
|
|
.spiral { |
|
fill: lightblue; |
|
stroke: #000; |
|
stroke-width: 0.5px; |
|
} |
|
|
|
</style> |
|
|
|
<body></body> |
|
|
|
<script src="http://d3js.org/d3.v3.min.js"></script> |
|
<script src="http://d3js.org/d3.geo.projection.v0.min.js"></script> |
|
<script> |
|
|
|
var width = 960, |
|
height = 500; |
|
|
|
// Graticule to indicate time of period |
|
var graticule = d3.geo.graticule() |
|
.majorStep([90,90]) // Major step indicates 6-hour intervals |
|
.minorStep([15,0]) // Minor step indicates 1-hour intervals |
|
.majorExtent([[-180,-70], [180,65]]) |
|
.minorExtent([[-180,-60], [180,55]]) |
|
|
|
// Spiral function derived from https://www.jasondavies.com/maps/spiral/ |
|
var n = 1e4, dy = 3, rot=50; |
|
var deadArea = 0.2 |
|
var spiral = d3.range(0+deadArea, 1 + 1 / n - deadArea, 1 / n).map(function(t) { |
|
return [(360 * rot * t) % 360 - 180, -90 + dy |
|
- Math.random() |
|
+ (Math.cos(100 * Math.PI * t) - 1) / 2 |
|
+ (Math.cos(3 * Math.PI * t) - 1) / 2 |
|
+ (90 - dy) * 2 * t]; |
|
}).concat(d3.range(1-deadArea, 0+ deadArea, -1 / n).map(function(t) { |
|
return [(360 * rot * t) % 360 - 180, -90 + (90 - dy ) * 2 * t]; |
|
})); |
|
|
|
var spiralPoly = {type: "Polygon", coordinates: [spiral]}; |
|
|
|
// Interpolating projection from Michael Bostock's http://bl.ocks.org/mbostock/5731632 |
|
function interpolatedProjection(a, b) { |
|
var projection = d3.geo.projection(raw).scale(1), |
|
translate = projection.translate, |
|
α; |
|
function raw(λ, φ) { |
|
var pa = a([λ *= 180 / Math.PI, φ *= 180 / Math.PI]), pb = b([λ, φ]); |
|
return [(1 - α) * pa[0] + α * pb[0], (α - 1) * pa[1] - α * pb[1]]; |
|
} |
|
projection.alpha = function(_) { |
|
if (!arguments.length) return α; |
|
α = +_; |
|
var ta = a.translate(), tb = b.translate(); |
|
translate([(1 - α) * ta[0] + α * tb[0], (1 - α) * ta[1] + α * tb[1]]); |
|
return projection; |
|
}; |
|
delete projection.scale; |
|
delete projection.translate; |
|
return projection.alpha(0); |
|
} |
|
|
|
|
|
// Projection transforms |
|
|
|
function pause(proj, directionForward) { |
|
return function(tweenArg) { |
|
}; |
|
} |
|
|
|
function conicCartesianToSpiral(proj, directionForward, features, path) { |
|
return function(tweenArg) { |
|
var _ = directionForward?tweenArg:(1-tweenArg); |
|
proj.parallels([_*89.99, _*89.99]); |
|
proj.scale((1-_)*(1-_)*110+40); |
|
proj.translate([width / 2 - .5, height / 2 + Math.sqrt(_)*87]) |
|
features.attr("d", path); |
|
}; |
|
} |
|
|
|
function interpolationSpiralToSphere(proj, directionForward, features, path) { |
|
return function(tweenArg) { |
|
var _ = directionForward?tweenArg:(1-tweenArg); |
|
proj.alpha(_); |
|
features.attr("d", path); |
|
}; |
|
} |
|
|
|
function ortographicSpin(proj, directionForward, features, path) { |
|
var originalRotation = proj.rotate(); |
|
return function(tweenArg) { |
|
var _ = directionForward?tweenArg:(1-tweenArg); |
|
proj.clipAngle(95); |
|
proj.rotate([_*360, _*-90, _*-90]); |
|
features.attr("d", path); |
|
}; |
|
} |
|
|
|
|
|
// Showreel with projection/transform pairs |
|
// Items are out of order so one projection can refer to another (e.g. next one) |
|
|
|
showReel = [] |
|
showReel[0] = { |
|
projection: d3.geo.conicConformal().parallels([0,0]).scale(150).translate([width / 2, height / 2 ]), |
|
transform: pause |
|
}; |
|
showReel[1] = { |
|
projection: showReel[0].projection, |
|
transform: conicCartesianToSpiral |
|
}; |
|
showReel[3] = { |
|
projection: d3.geo.orthographic().rotate([0,0,0]).scale(250).translate([width / 2 , height / 2 ]), |
|
transform: ortographicSpin |
|
}; |
|
showReel[2] = { |
|
projection: interpolatedProjection(showReel[1].projection, showReel[3].projection), |
|
transform: interpolationSpiralToSphere |
|
}; |
|
|
|
|
|
// Animation and initial output |
|
|
|
function animate(index, directionForward) { |
|
var delta = directionForward?1:-1, |
|
nextIndex = Math.min(Math.max(index+delta,0), showReel.length-1), |
|
nextDirectionForward = nextIndex===index+delta?directionForward:!directionForward, |
|
keyframe = showReel[index], |
|
path = d3.geo.path().projection(keyframe.projection), |
|
features = render(path); |
|
svg.transition() |
|
.duration(2000) |
|
.tween("projection", function() { |
|
return keyframe.transform(keyframe.projection, directionForward, features, path); |
|
}) |
|
.transition() |
|
.duration(0) |
|
.each('end', animate.bind(this, nextIndex, nextDirectionForward)); |
|
} |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
function render(path) { |
|
svg.selectAll(".graticule") |
|
.data(graticule.lines) |
|
.enter().append("path") |
|
.attr("class", "graticule") |
|
.attr("d", path); |
|
svg.selectAll(".spiral") |
|
.data([spiralPoly]) |
|
.enter().append("path") |
|
.attr("class", "spiral") |
|
.attr("d", path); |
|
return svg.selectAll("path"); |
|
} |
|
|
|
animate(0, true); |
|
|
|
</script> |