|
<!DOCTYPE html> |
|
<canvas width="960" height="600"></canvas> |
|
<script src="https://d3js.org/d3.v4.js"></script> |
|
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script> |
|
<script src="https://d3js.org/topojson.v2.min.js"></script> |
|
<script src="versor.js"></script> |
|
|
|
<style> |
|
path {fill: none; stroke: #444; } |
|
</style> |
|
|
|
<script> |
|
var canvas = d3.select("canvas"), |
|
width = canvas.property("width"), |
|
height = canvas.property("height"), |
|
context = canvas.node().getContext("2d"); |
|
|
|
|
|
// retina display |
|
var devicePixelRatio = window.devicePixelRatio || 1; |
|
canvas.style('width', canvas.attr('width')+'px'); |
|
canvas.style('height', canvas.attr('height')+'px'); |
|
canvas.attr('width', canvas.attr('width') * devicePixelRatio); |
|
canvas.attr('height', canvas.attr('height') * devicePixelRatio); |
|
context.scale(devicePixelRatio,devicePixelRatio); |
|
|
|
var pi = Math.PI, degrees = 180 / pi, asin1_3 = Math.asin(1 / 3); |
|
|
|
var centers = [ |
|
[0, 90], |
|
[-180, -asin1_3 * degrees], |
|
[-60, -asin1_3 * degrees], |
|
[60, -asin1_3 * degrees] |
|
]; |
|
|
|
d3.geoTetrahedralGnomonic = function(faceProjection) { |
|
var tetrahedron = [[1, 2, 3], [0, 2, 1], [0, 3, 2], [0, 1, 3]].map(function( |
|
face |
|
) { |
|
return face.map(function(i) { |
|
return centers[i]; |
|
}); |
|
}); |
|
|
|
faceProjection = |
|
faceProjection || |
|
function(face) { |
|
var c = d3.geoCentroid({ type: "MultiPoint", coordinates: face }); |
|
return d3 |
|
.geoGnomonic() |
|
.scale(1) |
|
.translate([0, 0]) |
|
.rotate([-c[0] * (Math.abs(c[1]) != 90), -c[1]]); |
|
}; |
|
|
|
var faces = tetrahedron.map(function(face) { |
|
return { face: face, project: faceProjection(face) }; |
|
}); |
|
|
|
[-1, 0, 0, 0].forEach(function(d, i) { |
|
var node = faces[d]; |
|
node && (node.children || (node.children = [])).push(faces[i]); |
|
}); |
|
|
|
return d3 |
|
.geoPolyhedral( |
|
faces[0], |
|
function(lambda, phi) { |
|
lambda *= degrees; |
|
phi *= degrees; |
|
for (var i = 0; i < faces.length; i++) { |
|
if ( |
|
d3.geoContains( |
|
{ |
|
type: "Polygon", |
|
coordinates: [[...tetrahedron[i], tetrahedron[i][0]]] |
|
}, |
|
[lambda, phi] |
|
) |
|
) { |
|
return faces[i]; |
|
} |
|
} |
|
}, |
|
pi / 3 |
|
) |
|
.clipAngle(180) |
|
.rotate([-30, 180]) |
|
.fitExtent([[0, 0], [width, height]], { type: "Sphere" }); |
|
}; |
|
|
|
projection = d3.geoTetrahedralGnomonic(); |
|
|
|
var init_scale = projection.scale(), |
|
path = d3.geoPath().projection(projection).context(context); |
|
|
|
d3.json("https://unpkg.com/world-atlas@1/world/110m.json", function( |
|
error, |
|
world |
|
) { |
|
if (error) throw error; |
|
|
|
var land = topojson.feature(world, world.objects.countries); |
|
|
|
render = function() { |
|
context.fillStyle = "#fff"; |
|
context.fillRect(0, 0, width, height); |
|
|
|
context.beginPath(); |
|
path({type:"Sphere"}); |
|
context.strokeStyle = "black"; |
|
context.stroke(), context.clip(), context.closePath(); |
|
|
|
context.beginPath(); |
|
path(land); |
|
context.fillStyle = "#000"; |
|
context.fill(), context.closePath(); |
|
|
|
context.beginPath(); |
|
path(d3.geoGraticule()()); |
|
context.strokeStyle = "#777"; |
|
context.stroke(), context.closePath(); |
|
|
|
}; |
|
|
|
render(); |
|
}); |
|
|
|
canvas.call(d3.drag().on("start", dragstarted).on("drag", dragged)); |
|
|
|
var render = function() {}, |
|
v0, // Mouse position in Cartesian coordinates at start of drag gesture. |
|
r0, // Projection rotation as Euler angles at start. |
|
q0; // Projection rotation as versor at start. |
|
|
|
function dragstarted() { |
|
v0 = versor.cartesian(projection.invert(d3.mouse(this))); |
|
r0 = projection.rotate(); |
|
console.log("r0", r0); |
|
q0 = versor(r0); |
|
} |
|
|
|
function dragged() { |
|
var v1 = versor.cartesian(projection.rotate(r0).invert(d3.mouse(this))), |
|
q1 = versor.multiply(q0, versor.delta(v0, v1)), |
|
r1 = versor.rotation(q1); |
|
projection.rotate(r1); |
|
render(); |
|
} |
|
</script> |