Skip to content

Instantly share code, notes, and snippets.

@LuisSevillano
Created December 4, 2017 18:41
Show Gist options
  • Save LuisSevillano/085a4df16fe715dd7fa78ab925c7d09a to your computer and use it in GitHub Desktop.
Save LuisSevillano/085a4df16fe715dd7fa78ab925c7d09a to your computer and use it in GitHub Desktop.
From Hong Kong to Brussels
border: none
height: 960

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

<!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>
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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