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 /mbostock/4183330
The plane flies from startCountry
to endCountry
, and you can rotate the globe around once the animation is done
xxxxxxxxxx
<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 https://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>
https://unpkg.com/d3@4.12.0/build/d3.js
https://d3js.org/topojson.v1.min.js