by Philippe Rivière and Martin Hardouin-Duparc
forked from Fil's block: Vanishing Earth
forked from Fil's block: Translucent Earth
forked from Fil's block: Projection Paint [UNLISTED]
| license: gpl-3.0 |
by Philippe Rivière and Martin Hardouin-Duparc
forked from Fil's block: Vanishing Earth
forked from Fil's block: Translucent Earth
forked from Fil's block: Projection Paint [UNLISTED]
| <!DOCTYPE html> | |
| <meta charset="utf-8"> | |
| <body> | |
| <script src="https://unpkg.com/d3@4"></script> | |
| <script src="https://unpkg.com/d3-geo-projection"></script> | |
| <script src="https://unpkg.com/topojson"></script> | |
| <script> | |
| var width = 960, | |
| height = 500; | |
| var radius = height / 2 - 20, | |
| scale = radius, | |
| velocity = .015; | |
| var projection = d3.geoOrthographic() | |
| .fitExtent([[0,0],[300,300]], {type:"Sphere"}); | |
| var canvas = d3.select("body").append("canvas") | |
| .attr("width", width) | |
| .attr("height", height); | |
| var 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 path = d3.geoPath() | |
| .projection(projection) | |
| .context(context); | |
| var backprojection = d3.geoProjection(function(a,b) { | |
| return d3.geoOrthographicRaw(-a,b); | |
| }).clipAngle(90) | |
| .translate(projection.translate()) | |
| .scale(projection.scale()) | |
| var backpath = d3.geoPath() | |
| .projection(backprojection) | |
| .context(context); | |
| var projection2, path2; | |
| var projname = 'Bertin1953'; | |
| var projs = ['Bertin1953', 'Mercator', 'Gnomonic', 'Equirectangular']; | |
| newproj = function() { | |
| projection2 = d3['geo' + projname]().fitExtent([[300,0],[960,500]], {type:"Sphere"}); | |
| path2 = d3.geoPath() | |
| .projection(projection2) | |
| .context(context); | |
| projname = projs[Math.random() * projs.length | 0]; | |
| }; | |
| newproj() | |
| d3.json("https://unpkg.com/world-atlas/world/110m.json", function(error, world) { | |
| if (error) throw error; | |
| var land = topojson.feature(world, world.objects.land); | |
| var points = (d3.merge(d3.merge(land.features[0].geometry.coordinates))) | |
| .map((d,i) => (d.rank = i, d)) | |
| //.filter((d,i)=> i%2) | |
| // random order | |
| //points = points.sort((a,b) => Math.random() - Math.random()) | |
| // by longitude | |
| //points = points.sort((a,b) => -a[0]+b[0]) | |
| // by latitude | |
| //points = points.sort((a,b) => -a[1]+b[1]) | |
| points.forEach(function(point) { | |
| point.vecteur = Math.random() < 0.04; | |
| }) | |
| render = function(elapsed) { | |
| context.clearRect(0, 0, width, height); | |
| var rotate = [velocity * elapsed, -40]; | |
| // if (elapsed > 10000) projection2 = d3.geoEquirectangular().fitExtent([[300,0],[960,500]], {type:"Sphere"}), path2 = d3.geoPath().projection(projection2); | |
| projection.rotate(rotate); | |
| if (projection2.rotate) projection2.rotate([rotate[0], 0]); | |
| var t_intro = 3000, | |
| t_projection = 5000, | |
| t_lancer = 2800, | |
| t_pleine = t_lancer + 3000, | |
| t_disparition = 1500; | |
| var periode = (t_intro + t_projection + t_lancer + t_pleine + t_disparition), | |
| N = points.length, | |
| t = elapsed % periode; | |
| if (t < 40) newproj(); | |
| var tt = (t - (t_intro + t_projection + t_lancer))/(t_pleine + t_disparition); | |
| if (tt > 0 && tt < 1) { | |
| context.beginPath(); | |
| path2(land); | |
| context.fillStyle = '#f4f4f4f4'; | |
| context.globalAlpha = Math.min(1, 7 * tt * (1-tt)); | |
| context.fill(); | |
| context.globalAlpha = 1; | |
| } | |
| points.forEach(function(point,i){ | |
| point.A = projection(point); | |
| point.B = projection2(point); | |
| point.segment = [point.A,point.B]; | |
| point.bout = 1; | |
| point.ti = t - i / N * t_projection - t_intro; | |
| point.td = t - i / N * t_projection * 0.5 - t_intro; | |
| if (point.ti < 0) return point.bout = 0; | |
| if (point.ti > t_projection + t_lancer + t_pleine + t_disparition) return point.bout = 0; | |
| if (point.ti < 0.4 * t_lancer) { | |
| var p = 1 - point.ti / (0.4 * t_lancer), | |
| C = [p * point.A[0] + (1-p) * point.B[0], p * point.A[1] + (1-p)*point.B[1]]; | |
| point.segment = [point.A,C]; | |
| point.bout = 0.5; | |
| } else if (point.ti < 0.7 * t_lancer) { | |
| var p = -(point.ti - 0.7 * t_lancer) / ((0.7 - 0.4) * t_lancer), | |
| C = [p * point.A[0] + (1-p) * point.B[0], p * point.A[1] + (1-p)*point.B[1]]; | |
| point.segment = [C,point.B]; | |
| point.bout = 1; | |
| } | |
| else if (point.td < t_lancer + t_pleine) { | |
| point.segment = [point.B,point.B]; | |
| point.bout = 1; | |
| } | |
| else { | |
| // disparition | |
| point.segment = [point.B,point.B]; | |
| point.bout = Math.max(0,1 - (point.td - t_lancer - t_pleine) / t_disparition); | |
| } | |
| }); | |
| points.forEach(point => { | |
| var p = projection.invert(projection(point)); | |
| point.derriere = (d3.geoDistance(p, point) > 0.01); | |
| }) | |
| context.beginPath(); | |
| points.filter(point => point.bout) | |
| .filter(point => point.vecteur) | |
| .filter(point => point.derriere) | |
| .forEach(function(point){ | |
| context.moveTo(...point.segment[0]); | |
| context.lineTo(...point.segment[1]); | |
| }); | |
| context.lineWidth = .5; | |
| context.strokeStyle = 'rgba(100,100,100,0.6)'; | |
| context.stroke(); | |
| backprojection.rotate([rotate[0] + 180, -rotate[1]]); | |
| context.beginPath(); | |
| path({type:"Sphere"}); | |
| context.fillStyle = 'white'; | |
| context.fill(); | |
| context.beginPath(); | |
| backpath(land); | |
| context.fillStyle = '#f4f4f4f4'; | |
| context.fill(); | |
| context.beginPath(); | |
| path(d3.geoGraticule()()); | |
| path2(d3.geoGraticule()()); | |
| context.lineWidth = 0.05; | |
| context.strokeStyle = 'black'; | |
| context.stroke(); | |
| context.beginPath(); | |
| path(land); | |
| //path2(land); | |
| context.lineWidth = 1; | |
| context.strokeStyle = 'black'; | |
| context.stroke(); | |
| context.fillStyle = '#eee'; | |
| context.fill(); | |
| context.beginPath(); | |
| points.filter(point => point.bout) | |
| .filter(point => point.vecteur) | |
| .filter(point => !point.derriere) | |
| .forEach(function(point){ | |
| context.moveTo(...point.segment[0]); | |
| context.lineTo(...point.segment[1]); | |
| }); | |
| context.lineWidth = .5; | |
| context.strokeStyle = 'rgba(100,100,100,0.7)'; | |
| context.stroke(); | |
| context.beginPath(); | |
| var last = [0,0]; | |
| points | |
| .slice() | |
| .sort((a,b) => a.rank - b.rank) | |
| .filter(point => point.bout).forEach(function(point){ | |
| context.lineWidth = 1; | |
| // context.moveTo(...point.segment[1]); | |
| // context.lineTo(point.segment[1][0]+point.bout*2, point.segment[1][1]+point.bout); | |
| var p = point.segment[1]; | |
| if (Math.max(Math.abs(p[0] - last[0]), Math.abs(p[1] - last[1])) < 10) | |
| context.lineTo(p[0], p[1]); | |
| else { | |
| context.moveTo(p[0]-1, p[1]); | |
| context.lineTo(p[0], p[1]); | |
| } | |
| last = p; | |
| }) | |
| context.lineWidth = 1.; | |
| context.strokeStyle = '#333'; | |
| context.stroke(); | |
| context.beginPath(); | |
| path({type: "Sphere"}); | |
| path2({type: "Sphere"}); | |
| context.lineWidth = 1.; | |
| context.strokeStyle = 'black'; | |
| context.stroke(); | |
| }; | |
| render(0); | |
| d3.timer(render) | |
| }); | |
| d3.select(self.frameElement).style("height", height + "px"); | |
| </script> |