Skip to content

Instantly share code, notes, and snippets.

@tlfrd
Last active April 8, 2023 19:44
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save tlfrd/df1f1f705c7940a6a7c0dca47041fec8 to your computer and use it in GitHub Desktop.
Save tlfrd/df1f1f705c7940a6a7c0dca47041fec8 to your computer and use it in GitHub Desktop.
Destination Globe (Spinning + Versor Drag)
license: mit

An interactive (spinning) globe showing the cities that I have travelled to around the world. Hover over place names to highlight the line to that location and to calculate the distance from London (where I currently live). Drag to stop the globe spinning (this uses Versor dragging).

forked from dwtkns's block: Faux-3d Shaded Globe

Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.land {
fill: white;
opacity: .5;
stroke: #4f2291;
stroke-opacity: 1;
}
.countries path {
stroke: #4f2291;
stroke-linejoin: round;
stroke-width:.5;
fill: #4f2291;
opacity: .1;
pointer-events:none;
}
.lines path {
fill: none;
stroke: #4f2291;
stroke-opacity: 0.3;
stroke-dasharray: 5,5;
}
circle {
stroke: #4f2291;
}
.graticule {
fill: none;
stroke: #4f2291;
stroke-width:.5;
opacity:.2;
}
.labels {
font: 8px sans-serif;
fill: black;
opacity: 1;
cursor: pointer;
}
.noclicks {
pointer-events:none;
}
.point {
opacity:.6;
}
</style>
<body>
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="http://d3js.org/queue.v1.min.js"></script>
<script src="http://d3js.org/topojson.v0.min.js"></script>
<script src="versor.js"></script>
<script>
var width = 960,
height = 500;
var proj = d3.geoOrthographic()
.scale(230)
.translate([width / 2, height / 2])
// change this to 180 for transparent globe
.clipAngle(90);
var path = d3.geoPath().projection(proj).pointRadius(1.5);
var graticule = d3.geoGraticule();
var london = [-0.118667702475932, 51.5019405883275];
var time = Date.now();
var rotate = [39.666666666666664, -30];
var velocity = [.015, -0];
var lineToLondon = function(d) {
return path({"type": "LineString", "coordinates": [london, d.geometry.coordinates]});
}
function stripWhitespace(str) {
return str.replace(" ", "");
}
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
svg.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged));
queue()
.defer(d3.json, "world-110m.json")
.defer(d3.json, "destinations.json")
.await(ready);
function ready(error, world, places) {
svg.append("circle")
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("r", proj.scale())
.attr("class", "noclicks")
.attr("fill", "none");
svg.append("path")
.datum(topojson.object(world, world.objects.land))
.attr("class", "land")
.attr("d", path);
svg.append("path")
.datum(graticule)
.attr("class", "graticule noclicks")
.attr("d", path);
svg.append("g").attr("class","points")
.selectAll("text").data(places.features)
.enter().append("path")
.attr("class", "point")
.attr("d", path);
svg.append("g").attr("class","lines")
.selectAll(".lines").data(places.features)
.enter().append("path")
.attr("class", "lines")
.attr("id", d => stripWhitespace(d.properties.name))
.attr("d", d => lineToLondon(d));
svg.append("g").attr("class","labels")
.selectAll("text").data(places.features)
.enter().append("text")
.attr("class", "label")
.text(d => d.properties.name)
.on("mouseover", (d) => {
var distance = Math.round(d3.geoDistance(d.geometry.coordinates, london) * 6371);
d3.select("g.info").select("text.distance").text("Distance from London: ~" + distance + "km");
var name = stripWhitespace(d.properties.name);
d3.select("g.lines").select("#" + name).style("stroke-opacity", 1)
})
.on("mouseout", (d) => {
var name = stripWhitespace(d.properties.name);
d3.select("g.lines").select("#" + name).style("stroke-opacity", 0.3)
d3.select("g.info").select("text.distance").text("Distance from London: Hover Over A Location");
});
svg.append("g").attr("class","countries")
.selectAll("path")
.data(topojson.object(world, world.objects.countries).geometries)
.enter().append("path")
.attr("d", path);
position_labels();
svg.append("g").attr("class", "info")
.append("text")
.attr("class", "distance")
.attr("x", width / 20)
.attr("y", height * 0.9)
.attr("text-anchor", "start")
.style("font-size", "12px")
.text("Distance from London: Hover Over A Location");
refresh();
spin();
}
function position_labels() {
var centerPos = proj.invert([width/2,height/2]);
svg.selectAll(".label")
.attr("text-anchor", (d) => {
var x = proj(d.geometry.coordinates)[0];
return x < width/2-20 ? "end" :
x < width/2+20 ? "middle" :
"start"
})
.attr("transform", (d) => {
var loc = proj(d.geometry.coordinates),
x = loc[0],
y = loc[1];
var offset = x < width/2 ? -5 : 5;
return "translate(" + (x+offset) + "," + (y-2) + ")"
})
.style("display", (d) => {
var d = d3.geoDistance(d.geometry.coordinates, centerPos);
return (d > 1.57) ? 'none' : 'inline';
})
}
function refresh() {
svg.selectAll(".land").attr("d", path);
svg.selectAll(".countries path").attr("d", path);
svg.selectAll(".graticule").attr("d", path);
svg.selectAll(".point").attr("d", path);
svg.selectAll(".lines").attr("d", (d) => { if (d) { return lineToLondon(d); }});
position_labels();
}
var timer;
function spin() {
timer = d3.timer(function() {
var dt = Date.now() -time;
proj.rotate([rotate[0] + velocity[0] * dt, rotate[1] + velocity[1] * dt]);
refresh();
});
}
function dragstarted() {
timer.stop();
v0 = versor.cartesian(proj.invert(d3.mouse(this)));
r0 = proj.rotate();
q0 = versor(r0);
}
function dragged() {
var v1 = versor.cartesian(proj.rotate(r0).invert(d3.mouse(this))),
q1 = versor.multiply(q0, versor.delta(v0, v1)),
r1 = versor.rotation(q1);
proj.rotate(r1);
refresh();
}
</script>
// Version 0.0.0. Copyright 2017 Mike Bostock.
(function(global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.versor = factory());
}(this, (function() {'use strict';
var acos = Math.acos,
asin = Math.asin,
atan2 = Math.atan2,
cos = Math.cos,
max = Math.max,
min = Math.min,
PI = Math.PI,
sin = Math.sin,
sqrt = Math.sqrt,
radians = PI / 180,
degrees = 180 / PI;
// Returns the unit quaternion for the given Euler rotation angles [λ, φ, γ].
function versor(e) {
var l = e[0] / 2 * radians, sl = sin(l), cl = cos(l), // λ / 2
p = e[1] / 2 * radians, sp = sin(p), cp = cos(p), // φ / 2
g = e[2] / 2 * radians, sg = sin(g), cg = cos(g); // γ / 2
return [
cl * cp * cg + sl * sp * sg,
sl * cp * cg - cl * sp * sg,
cl * sp * cg + sl * cp * sg,
cl * cp * sg - sl * sp * cg
];
}
// Returns Cartesian coordinates [x, y, z] given spherical coordinates [λ, φ].
versor.cartesian = function(e) {
var l = e[0] * radians, p = e[1] * radians, cp = cos(p);
return [cp * cos(l), cp * sin(l), sin(p)];
};
// Returns the Euler rotation angles [λ, φ, γ] for the given quaternion.
versor.rotation = function(q) {
return [
atan2(2 * (q[0] * q[1] + q[2] * q[3]), 1 - 2 * (q[1] * q[1] + q[2] * q[2])) * degrees,
asin(max(-1, min(1, 2 * (q[0] * q[2] - q[3] * q[1])))) * degrees,
atan2(2 * (q[0] * q[3] + q[1] * q[2]), 1 - 2 * (q[2] * q[2] + q[3] * q[3])) * degrees
];
};
// Returns the quaternion to rotate between two cartesian points on the sphere.
versor.delta = function(v0, v1) {
var w = cross(v0, v1), l = sqrt(dot(w, w));
if (!l) return [1, 0, 0, 0];
var t = acos(max(-1, min(1, dot(v0, v1)))) / 2, s = sin(t); // t = θ / 2
return [cos(t), w[2] / l * s, -w[1] / l * s, w[0] / l * s];
};
// Returns the quaternion that represents q0 * q1.
versor.multiply = function(q0, q1) {
return [
q0[0] * q1[0] - q0[1] * q1[1] - q0[2] * q1[2] - q0[3] * q1[3],
q0[0] * q1[1] + q0[1] * q1[0] + q0[2] * q1[3] - q0[3] * q1[2],
q0[0] * q1[2] - q0[1] * q1[3] + q0[2] * q1[0] + q0[3] * q1[1],
q0[0] * q1[3] + q0[1] * q1[2] - q0[2] * q1[1] + q0[3] * q1[0]
];
};
function cross(v0, v1) {
return [
v0[1] * v1[2] - v0[2] * v1[1],
v0[2] * v1[0] - v0[0] * v1[2],
v0[0] * v1[1] - v0[1] * v1[0]
];
}
function dot(v0, v1) {
return v0[0] * v1[0] + v0[1] * v1[1] + v0[2] * v1[2];
}
return versor;
})));
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment