Forked from Wendy Mak's block in reference to this tweet.
Sample code to make an animated globe with d3 on canvas. Borrowing heavily from http://bl.ocks.org/mbostock/4183330
The plane flies from startCountry
to endCountry
, and you can rotate the globe around once the animation is done
Created
December 4, 2017 18:41
-
-
Save LuisSevillano/085a4df16fe715dd7fa78ab925c7d09a to your computer and use it in GitHub Desktop.
From Hong Kong to Brussels
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
border: none | |
height: 960 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<script src="https://unpkg.com/d3@4.12.0/build/d3.js"></script> | |
<script src="https://d3js.org/topojson.v1.min.js"></script> | |
<meta charset="UTF-8"> | |
<title>From Hong Kong to Brussels</title> | |
</head> | |
<body> | |
<div id="globeParent"> | |
<div style="display:none;"> | |
<img id="plane" src="plane-2_03-black.png"> | |
</div> | |
</div> | |
<script> | |
var width = 960, | |
height = 960; | |
var projection = d3.geoOrthographic() | |
.scale(475) | |
.translate([width / 2, height / 2]) | |
.clipAngle(90) | |
.precision(.1); | |
var graticule = d3.geoGraticule(); | |
var canvas = d3.select("#globeParent").append("canvas") | |
.attr('width', width) | |
.attr('height', height) | |
.style("cursor", "move"); | |
var c = canvas.node().getContext("2d"); | |
var path = d3.geoPath() | |
.projection(projection) | |
.context(c); | |
var selectedCountryFill = "#007ea3", | |
flightPathColor = "#c7254e", | |
landFill = "#b9b5ad", | |
seaFill = "rgb(245, 245, 245)", | |
gridStroke = "#9a9a9a"; | |
gridWidth = .5; | |
var startCountry = "Hong Kong"; | |
var endCountry = "Belgium"; | |
//interpolator from http://bl.ocks.org/jasondavies/4183701 | |
var d3_geo_greatArcInterpolator = function() { | |
var d3_radians = Math.PI / 180; | |
var x0, y0, cy0, sy0, kx0, ky0, | |
x1, y1, cy1, sy1, kx1, ky1, | |
d, | |
k; | |
function interpolate(t) { | |
var B = Math.sin(t *= d) * k, | |
A = Math.sin(d - t) * k, | |
x = A * kx0 + B * kx1, | |
y = A * ky0 + B * ky1, | |
z = A * sy0 + B * sy1; | |
return [ | |
Math.atan2(y, x) / d3_radians, | |
Math.atan2(z, Math.sqrt(x * x + y * y)) / d3_radians | |
]; | |
} | |
interpolate.distance = function() { | |
if (d == null) k = 1 / Math.sin(d = Math.acos(Math.max(-1, Math.min(1, sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0))))); | |
return d; | |
}; | |
interpolate.source = function(_) { | |
var cx0 = Math.cos(x0 = _[0] * d3_radians), | |
sx0 = Math.sin(x0); | |
cy0 = Math.cos(y0 = _[1] * d3_radians); | |
sy0 = Math.sin(y0); | |
kx0 = cy0 * cx0; | |
ky0 = cy0 * sx0; | |
d = null; | |
return interpolate; | |
}; | |
interpolate.target = function(_) { | |
var cx1 = Math.cos(x1 = _[0] * d3_radians), | |
sx1 = Math.sin(x1); | |
cy1 = Math.cos(y1 = _[1] * d3_radians); | |
sy1 = Math.sin(y1); | |
kx1 = cy1 * cx1; | |
ky1 = cy1 * sx1; | |
d = null; | |
return interpolate; | |
}; | |
return interpolate; | |
} | |
function ready(error, world, names) { | |
if (error) throw error; | |
var globe = {type: "Sphere"}, | |
land = topojson.feature(world, world.objects.land), | |
countries = topojson.feature(world, world.objects.countries).features, | |
i = -1; | |
touch = "ontouchstart" in window; | |
canvas.on(touch ? "touchmove" : "mousemove", redraw); | |
grid = graticule(); | |
countries = countries.filter(function(d) { | |
return names.some(function(n) { | |
if (d.id == n.id) return d.name = n.name; | |
}); | |
}).sort(function(a, b) { | |
return a.name.localeCompare(b.name); | |
}); | |
var startIDObj = names.filter(function(d){ | |
return (d.name).toLowerCase() == (startCountry).toLowerCase(); | |
})[0]; | |
var endIDObj = names.filter(function(d){ | |
return (d.name).toLowerCase() == (endCountry).toLowerCase(); | |
})[0]; | |
var startGeom = countries.filter(function(d){ | |
return d.id == startIDObj.id | |
}), | |
endGeom = countries.filter(function(d){ | |
return d.id == endIDObj.id | |
}) | |
var journey = []; | |
journey[0] = startGeom[0]; | |
journey[1] = endGeom[0]; | |
var n = countries.length; | |
var startCoord = d3.geoCentroid(journey[0]), | |
endCoord = d3.geoCentroid(journey[1]) | |
var coords = [-startCoord[0], -startCoord[1]] | |
var flightPath ={} | |
flightPath.type = "LineString"; | |
flightPath.coordinates = [startCoord, endCoord]; | |
var plane = document.getElementById('plane'); | |
var sphere = {type: "Sphere"}; | |
projection.rotate(coords); | |
redrawGlobeOnly(); | |
//redraw(flightPathDynamic) | |
customTransition(journey) | |
function redrawGlobeOnly(){ | |
c.clearRect(0, 0, width, height); | |
c.setLineDash([]); | |
//base globe | |
c.shadowBlur = 0, c.shadowOffsetX = 0, c.shadowOffsetY = 0; | |
c.fillStyle = seaFill, c.beginPath(), path(globe), c.fill(); | |
c.fillStyle = landFill, c.beginPath(), path(land), c.fill(); | |
c.strokeStyle = gridStroke, c.lineWidth = gridWidth, c.beginPath(), path(grid), c.stroke(); | |
// shpere | |
c.beginPath(), path(sphere), c.stroke(); | |
//fills for start and end countries | |
c.fillStyle = flightPathColor, c.beginPath(), path(journey[0]), c.fill(); | |
c.fillStyle = flightPathColor, c.beginPath(), path(journey[1]), c.fill(); | |
} | |
function redraw(){ | |
c.clearRect(0, 0, width, height); | |
c.setLineDash([]); | |
//base globe | |
c.shadowBlur = 0, c.shadowOffsetX = 0, c.shadowOffsetY = 0; | |
c.fillStyle = seaFill, c.beginPath(), path(globe), c.fill(); | |
c.fillStyle = landFill, c.beginPath(), path(land), c.fill(); | |
c.strokeStyle = gridStroke, c.lineWidth = gridWidth, c.beginPath(), path(grid), c.stroke(); | |
//fills for start and end countries | |
c.fillStyle = selectedCountryFill, c.beginPath(), path(journey[0]), c.fill(); | |
c.fillStyle = selectedCountryFill, c.beginPath(), path(journey[1]), c.fill(); | |
// shpere | |
c.beginPath(), path(sphere), c.stroke(); | |
//flight path | |
c.strokeStyle = flightPathColor, c.lineWidth = 2, c.setLineDash([10, 10]) | |
c.beginPath(), path(flightPath), | |
//c.shadowColor = "#373633", | |
//c.shadowBlur = 20, c.shadowOffsetX = 5, c.shadowOffsetY = 20, | |
c.stroke(); | |
} | |
function redraw3(flightPath, angle, planeSize){ | |
c.setLineDash([]); | |
var pt = projection.rotate(); | |
var planeCartesianCoord = projection([-pt[0], -pt[1], 0]); | |
c.clearRect(0, 0, width, height); | |
c.shadowBlur = 0, c.shadowOffsetX = 0, c.shadowOffsetY = 0; | |
c.fillStyle = seaFill, c.beginPath(), path(globe), c.fill(); | |
c.fillStyle = landFill, c.beginPath(), path(land), c.fill(); | |
c.strokeStyle = gridStroke, c.lineWidth = gridWidth, c.beginPath(), path(grid), c.stroke(); | |
c.fillStyle = selectedCountryFill, c.beginPath(), path(journey[0]), c.fill(); | |
c.fillStyle = selectedCountryFill, c.beginPath(), path(journey[1]), c.fill(); | |
// shpere | |
c.beginPath(), path(sphere), c.stroke(); | |
c.strokeStyle = flightPathColor, c.lineWidth = 2, c.setLineDash([10, 10]) | |
c.beginPath(), path(flightPath), | |
//c.shadowColor = "#373633", | |
//c.shadowBlur = 20, c.shadowOffsetX = 5, c.shadowOffsetY = 20, | |
c.stroke(); | |
drawPlane(c, plane, planeCartesianCoord[0], planeCartesianCoord[1], angle, planeSize,planeSize) | |
} | |
//letting you drag the globe around but setting it so you can't tilt the globe over | |
var dragBehaviour = d3.drag() | |
.on('drag', function(){ | |
var dx = d3.event.dx; | |
var dy = d3.event.dy; | |
var rotation = projection.rotate(); | |
var radius = projection.scale(); | |
var scale = d3.scaleLinear() | |
.domain([-1 * radius, radius]) | |
.range([-90, 90]); | |
var degX = scale(dx); | |
var degY = scale(dy); | |
rotation[0] += degX; | |
rotation[1] -= degY; | |
if (rotation[1] > 90) rotation[1] = 90; | |
if (rotation[1] < -90) rotation[1] = -90; | |
if (rotation[0] >= 180) rotation[0] -= 360; | |
projection.rotate(rotation); | |
redraw(); | |
}) | |
//make the plane always align with the direction of travel | |
function calcAngle(originalRotate, newRotate){ | |
var deltaX = newRotate[0] - originalRotate[0], | |
deltaY = newRotate[1] - originalRotate[1] | |
return Math.atan2(deltaY, deltaX); | |
} | |
//this is to make the globe rotate and the plane fly along the path | |
function customTransition(journey){ | |
var rotateFunc = d3_geo_greatArcInterpolator(); | |
d3.transition() | |
.delay(250) | |
.duration(5050) | |
.tween("rotate", function() { | |
var point = d3.geoCentroid(journey[1]) | |
rotateFunc.source(projection.rotate()).target([-point[0], -point[1]]).distance(); | |
var pathInterpolate = d3.geoInterpolate(projection.rotate(), [-point[0], -point[1]]); | |
var oldPath = startCoord; | |
return function (t) { | |
projection.rotate(rotateFunc(t)); | |
var newPath = [-pathInterpolate(t)[0], -pathInterpolate(t)[1]]; | |
var planeAngle = calcAngle(projection(oldPath), projection(newPath)); | |
var flightPathDynamic = {} | |
flightPathDynamic.type = "LineString"; | |
flightPathDynamic.coordinates = [startCoord, [-pathInterpolate(t)[0], -pathInterpolate(t)[1]]]; | |
var maxPlaneSize = 0.05 * projection.scale(); | |
//this makes the plane grows and shrinks at the takeoff, landing | |
if (t <0.1){ | |
redraw3(flightPathDynamic, planeAngle, Math.pow(t/0.1, 0.5) * maxPlaneSize); | |
}else if(t > 0.9){ | |
redraw3(flightPathDynamic, planeAngle, Math.pow((1-t)/0.1, 0.5) * maxPlaneSize ); | |
}else{ | |
redraw3(flightPathDynamic, planeAngle, maxPlaneSize); | |
} | |
//redraw3(flightPathDynamic, (planeAngle)) | |
}; | |
//} | |
}).on("end", function(){ | |
//make the plane disappears after it's reached the destination | |
//also enable the drag interaction at this point | |
redraw(); | |
canvas.call(dragBehaviour); | |
}) | |
} | |
//add the plane to the canvas and rotate it | |
function drawPlane(context, image, xPos, yPos, angleInRad, imageWidth, imageHeight){ | |
context.save(); | |
context.translate(xPos, yPos); | |
// rotate around that point, converting our | |
// angle from degrees to radians | |
context.rotate(angleInRad); | |
// draw it up and to the left by half the width | |
// and height of the image, plus add some shadow | |
//context.shadowColor = "#373633", context.shadowBlur = 20, context.shadowOffsetX = 5, context.shadowOffsetY = 10; | |
context.drawImage(image, -(imageWidth/2), -(imageHeight/2), imageWidth, imageHeight); | |
// and restore the co-ords to how they were when we began | |
context.restore(); | |
} | |
} | |
d3.queue() | |
.defer(d3.json, "world-50m.json") | |
.defer(d3.tsv, "world-country-names.tsv") | |
.await(ready); | |
</script> | |
</body> | |
</html> |
View raw
(Sorry about that, but we can’t show files that are this big right now.)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment