|
<!DOCTYPE html> |
|
<canvas width="960" height="500"></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="https://unpkg.com/complex.js"></script> |
|
<script> |
|
// Approximate \int _0 ^sm(z) dt / (1 - t^3)^(2/3) |
|
// sm maps a triangle to a disc, sm^-1 does the opposite |
|
function sm_1(s){ |
|
var z = Complex(s), |
|
w = Complex([-1/2, Math.sqrt(3)/2]), |
|
k = Complex(0); |
|
|
|
// rotate to have s ~= 1 |
|
var rot = w.clone().pow(d3.scan([0,1,2].map( |
|
i => -(z.clone().mul(w.clone().pow(i))).re |
|
))); |
|
|
|
var y = rot.clone().mul(z).mul(-1).add(1); |
|
|
|
// var n = 3 |
|
// w1 = gamma(1/n) * gamma(1 - 2/n) / n / gamma(1 - 1/n) |
|
// https://bl.ocks.org/Fil/852557838117687bbd985e4b38ff77d4 |
|
var w1 = 1.7666387502854533; |
|
|
|
// McIlroy formula 5 p6 and table for F3 page 16 |
|
var F0 = [ |
|
1.44224957030741, |
|
0.240374928384568, |
|
0.0686785509670194, |
|
0.0178055502507087, |
|
0.00228276285265497, |
|
-1.48379585422573e-3, |
|
-1.64287728109203e-3, |
|
-1.02583417082273e-3, |
|
-4.83607537673571e-4, |
|
-1.67030822094781e-4, |
|
-2.45024395166263e-5, |
|
2.14092375450951e-5, |
|
2.55897270486771e-5, |
|
1.73086854400834e-5, |
|
8.72756299984649e-6, |
|
3.18304486798473e-6, |
|
4.79323894565283e-7 |
|
-4.58968389565456e-7, |
|
-5.62970586787826e-7, |
|
-3.92135372833465e-7 |
|
]; |
|
|
|
var F = Complex(0); |
|
for (var i = F0.length; i--;) { |
|
F = Complex(F0[i]).add(F.mul(y)); |
|
} |
|
|
|
// /!\ mutates y |
|
var k = Complex(w1).add(y.pow(1-2/3).mul(-1).mul(F)).mul(rot.pow(2)) |
|
|
|
|
|
// when we are close to [0,0] we switch to another approximation: |
|
// https://www.wolframalpha.com/input/?i=(-2%2F3+choose+k)++*+(-1)%5Ek++%2F+(k%2B1)+with+k%3D0,1,2,3,4 |
|
// the difference is _very_ tiny but necessary |
|
// if we want projection(0,0) === [0,0] |
|
var n = z.abs(), m = 0.3; |
|
if (n < m) { |
|
var H0 = [ |
|
1, 1/3, 5/27, 10/81, 22/243 //… |
|
]; |
|
var z3 = z.clone().pow(3); |
|
var h = Complex(0); |
|
for (i = H0.length; i--;) { |
|
h = Complex(H0[i]).add(h.mul(z3)); |
|
} |
|
h = h.mul(z); |
|
k.mul(n / m).add(h.mul(1 - n / m)); |
|
} |
|
|
|
return k.toVector(); |
|
} |
|
|
|
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); |
|
|
|
function coxRaw(lambda, phi){ |
|
var s = d3.geoLagrangeRaw(0.5)(lambda, phi); |
|
var t = sm_1([s[1] / 2, s[0] / 2]); |
|
return [t[1], t[0]] |
|
} |
|
|
|
projection = d3.geoProjection(coxRaw) |
|
.rotate([-10.8,0]) |
|
.scale(188.68216) |
|
.translate([480, 500 * 2/3]) |
|
.precision(0.01); |
|
|
|
|
|
// the Sphere should go *exactly* to the vertices of the triangles |
|
// because they're singular points |
|
function sphere() { |
|
var degrees = 180 / Math.PI, |
|
c = 2 * Math.asin(1/Math.sqrt(5))*degrees; |
|
return { |
|
type: "Polygon", |
|
coordinates: [ |
|
[ [ 0,90 ], [ -180, -c ], [ 0, -90 ], [ 180, -c ], [ 0,90 ] ] |
|
] |
|
}; |
|
} |
|
|
|
var stream_ = projection.stream; |
|
projection.stream = function(stream) { |
|
var rotate = projection.rotate(), |
|
rotateStream = stream_(stream), |
|
sphereStream = (projection.rotate([0, 0]), stream_(stream)); |
|
projection.rotate(rotate); |
|
rotateStream.sphere = function() { d3.geoStream(sphere(), sphereStream); }; |
|
return rotateStream; |
|
}; |
|
|
|
var 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 land1 = topojson.merge(world, world.objects.countries.geometries |
|
.filter(d => d.id !== "010") // Antarctica |
|
); |
|
var land = topojson.merge(world, world.objects.countries.geometries |
|
//.filter(d => d.id !== "010") // Antarctica |
|
); |
|
projection.fitExtent([[1,1], [width-1, height-1]], land1); |
|
|
|
render = function() { |
|
context.fillStyle = "#fff"; |
|
context.fillRect(0, 0, width, height); |
|
|
|
context.beginPath(); |
|
path({type:"Sphere"}); |
|
context.strokeStyle = "black"; |
|
context.lineWidth = 1.5; |
|
context.stroke(), context.closePath(); |
|
|
|
context.beginPath(); |
|
path(d3.geoGraticule()()); |
|
context.strokeStyle = "#777"; |
|
context.lineWidth = 0.5; |
|
context.stroke(), context.closePath(); |
|
|
|
context.beginPath(); |
|
var now = performance.now() |
|
path(land); |
|
console.log(performance.now() - now) |
|
context.lineWidth = 2; |
|
context.strokeStyle = "#000"; |
|
context.stroke(); |
|
context.fillStyle = "rgba(240,240,240,0.9)"; |
|
context.fill(); |
|
context.closePath(); |
|
|
|
|
|
}; |
|
|
|
render(); |
|
}); |
|
|
|
</script> |