block by kenpenn 2a21754aeb6006cd5e9e

d3 with three world tour

Full Screen

textures for three mesh are generated with topojson and d3

d3 is used for transitions and interpolation

three.js renders the globe

adapted from Mike Bostock’s World Tour

and Steve Hall’s Interactive WebGL Globes with THREE.js and D3

Tested on os x Mavericks Chrome, Safari, Firefox.

Borks in Chrome Beta on my KitKat Android phone.

index.html

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>d3 with three world tour</title>
  <link rel="stylesheet" href="globe-tour.css">
</head>
<body>
  <h3>Touring with d3 and three</h3>
  <div id="three-box">
    <h2 id="slug">Loading...</h2>
  </div>
  <canvas id="d3-canvas" width="2048px" height="1024px" style="display: none;"></canvas>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r71/three.min.js"></script>
  <script src="globe-tour.js"></script>
</body>
</html>

globe-tour.css


* { box-sizing : border-box; }

body {
  background: #111;
  color: #fff;
  font-family: sans-serif;
  font-weight:normal;
  font-size: 13px;
  margin:0;
  position: absolute;
  top:0;
  bottom:0;
  width: 100%;
}

h2::selection,
h3::selection {
  background: #ff619b; /* a nod to mr. irish */
}

h2, h3 {
  position: absolute;
  z-index: 1;
}

h3 { left : 0.769em; }

#slug {
  top : 75%;
  width: 100%;
  height: 1.5em;
  text-align: center;
}

#loading {
  position: relative;
  transform: translateY(-50%);
  text-align: center;
}

#three-box {
  position: absolute;
  top: 0.769em;
  right: 0.769em;
  bottom: 0.769em;
  left: 0.769em;
}

globe-tour.js

/*
 *  demonstrates
 *    genning textures with topojson and d3
 *    using d3 for transitions and interpolation,
 *    and three.js for rendering the globe
 *
 *  adapted from Mike Bostock's World Tour, http://bl.ocks.org/mbostock/4183330
 *  and Steve Hall's Interactive WebGL Globes with THREE.js and D3,
 *    http://www.delimited.io/blog/2015/5/16/interactive-webgl-globes-with-threejs-and-d3
 *  all cruft and smells are mine.
 */
(function() {

  var genKey = function (arr) {
    var key = '';
    arr.forEach(function (str) {
      key += str.toLowerCase().replace(/[^a-z0-9]/g, '');
    });
    return key;
  };

  // used for working with three.js globe and lat/lon
  var twoPI = Math.PI * 2;
  var halfPI = Math.PI / 2;;

  var world = {
        glEl : {},
        sunColor     : '#fbfccc',
        countryColor : d3.rgb('orange').darker().darker().toString(),
        waterColor   : '#0419a0',
        gratiColor   : '',
        landColor    : '#185f18',
        borderColor  : '',
        d3Canvas     : d3.select('#d3-canvas'),
        projection   : d3.geo.equirectangular().translate([1024, 512]).scale(325),

        geoCache : {
          keys : {},
          textures: [],
          init :  function (countries, names) {
            var self = this;
            this.countries = countries.filter(function(d) {
              return names.some(function(n) {
                if (d.id == n.name) {
                  d.name = d.id;
                  return d.id = n.id;
                }
              });
            }).sort(function(a, b) {
              return a.name.localeCompare(b.name);
            });
            this.countries.forEach(function(country, cx) {
              country.key = genKey([country.name, country.id])
              self.keys[country.id] = { name: country.name, idx : cx };
            });
          }
        },

        init : function (opts) {
          this.glEl = d3.select(opts.selector);
          this.slug = d3.select('#slug');

          this.gratiColor = d3.rgb(this.sunColor).darker().toString();
          this.borderColor = d3.rgb(this.landColor).darker().toString();

          var countries = topojson.feature(opts.data, opts.data.objects.countries).features;

          this.geoCache.init(countries, opts.names);

          this.initD3(opts);
        },

        initD3 : function(opts) {
          // will create textures for three.js globe
          var land = topojson.feature(opts.data, opts.data.objects.countries);
          var borders = topojson.mesh(
            opts.data, opts.data.objects.countries, function(a, b) { return a !== b; }
          );
          this.initThree({ selector: opts.selector, land : land, borders : borders });
        },

        scene : new THREE.Scene(),
        globe : new THREE.Object3D(),
        initThree : function(opts) {
          var segments = 155; // number of vertices. Higher = better mouse accuracy, slower loading

          // Set up cache for country textures
          var glRect = this.glEl.node().getBoundingClientRect();
          var canvas = this.glEl.append('canvas')
                                .attr('width', glRect.width)
                                .attr('height', glRect.height);

          canvas.node().getContext('webgl');

          this.renderer = new THREE.WebGLRenderer({ canvas: canvas.node(), antialias: true });
          this.renderer.setSize(glRect.width, glRect.height);
          this.renderer.setClearColor( 0x000000 );
          this.glEl.node().appendChild(this.renderer.domElement);

          this.camera = new THREE.PerspectiveCamera(70, glRect.width / glRect.height, 1, 5000);
          this.camera.position.z = 1000;

          var ambientLight = new THREE.AmbientLight(this.sunColor);
          this.scene.add(ambientLight);

          var light = new THREE.DirectionalLight( this.sunColor, .85 );
          light.position.set(this.camera.position.x, this.camera.position.y + glRect.height/2, this.camera.position.z);
          this.scene.add( light );

          // base globe with 'water'
          var waterMaterial = new THREE.MeshPhongMaterial({ color: this.waterColor, transparent: true });
          var sphere = new THREE.SphereGeometry(200, segments, segments);
          var baseGlobe = new THREE.Mesh(sphere, waterMaterial);
          baseGlobe.rotation.y = Math.PI + halfPI; // centers inital render at lat 0, lon 0

          // base map with land, borders, graticule
          var baseMap = this.genMesh({ land: opts.land, borders: opts.borders });

          // add the two meshes to the container object
          this.globe.scale.set(2.5, 2.5, 2.5);
          this.globe.add(baseGlobe);
          this.globe.add(baseMap);
          this.scene.add(this.globe);
          this.renderer.render(this.scene, this.camera);

          this.rotateTo(this.geoCache.countries, 0, this.geoCache.countries.length);

          var self = this;
          window.addEventListener('resize', function(evt) {
            requestAnimationFrame(function () {
              var glRect = self.glEl.node().getBoundingClientRect();
              self.camera.aspect = glRect.width / glRect.height;
              self.camera.updateProjectionMatrix();
              self.renderer.setSize(glRect.width, glRect.height);
              self.renderer.render(self.scene, self.camera);
            });
          });
        },

        rotateTo : function (countries, cx, cLen) {
          var self = this;
          var globe = this.globe;
          var country = countries[cx];
          var mesh = this.genMesh({country : country});
          var from = {
            x: globe.rotation.x,
            y: globe.rotation.y
          };
          var centroid = d3.geo.centroid(country)
          var to = {
            x: this.latToX3(centroid[1]),
            y: this.lonToY3(centroid[0])
          }
          globe.add(mesh);

          var hasta = globe.getObjectByName(this.currentId);
          this.setSlug(country.name);
          if (hasta) {
            globe.remove(hasta)
            requestAnimationFrame(function() { self.renderer.render(self.scene, self.camera); });
          }
          this.currentId = country.key;

          requestAnimationFrame(function() { self.renderer.render(self.scene, self.camera); });

          d3.transition()
              .delay(500)
              .duration(1250)
              .each('start', function() {
                self.terpObj = d3.interpolateObject(from, to);
              })
              .tween('rotate', function() {
                  return function (t) {
                    globe.rotation.x = self.terpObj(t).x;
                    globe.rotation.y = self.terpObj(t).y;
                    requestAnimationFrame(function() { self.renderer.render(self.scene, self.camera); });
                  };
              })
            .transition()
              .each('end', function () {
                cx += 1;
                if (cx >= cLen) { cx = 0; }
                self.rotateTo(countries, cx, cLen);
              });
        },

        genMesh : function (opts) {
          var rotation;
          var segments = 155;
          var texture = this.genTexture(opts);
          var material = new THREE.MeshPhongMaterial({ map: texture, transparent: true });
          var mesh = new THREE.Mesh(new THREE.SphereGeometry(200, segments, segments), material);

          if ( opts.land ) {
            mesh.name = 'land';
            mesh.rotation.y = Math.PI + halfPI;
          } else {
            mesh.name = opts.country.key;
            rotation = this.globe.getObjectByName('land').rotation;
            mesh.rotation.x = rotation.x;
            mesh.rotation.y = rotation.y;
          }
          return mesh;
        },

        setSlug : function (countryname) {
          var self = this;
          this.slug.transition()
                    .duration(500)
                    .style('opacity', 0)
                    .each('end', function () {
                      self.slug.text(countryname);
                    })
                    .transition()
                    .duration(1250)
                    .style('opacity', 1);
        },

        genTexture : function(opts) {
          var graticule;

          var ctx = this.d3Canvas.node().getContext('2d');
          ctx.clearRect(0, 0, 2048, 1024);
          var path = d3.geo.path()
                           .projection(this.projection)
                           .context(ctx);

          if (opts.land) {
            graticule = d3.geo.graticule();
            ctx.fillStyle = this.landColor, ctx.beginPath(), path(opts.land), ctx.fill();
            ctx.strokeStyle = this.borderColor, ctx.lineWidth = .5, ctx.beginPath(), path(opts.borders), ctx.stroke();
            ctx.strokeStyle = this.gratiColor, ctx.lineWidth = .25, ctx.beginPath(), path(graticule()), ctx.stroke();
          }
          if (opts.country) {
            ctx.fillStyle = this.countryColor, ctx.beginPath(), path(opts.country), ctx.fill();
          }

          // DEBUGGING, disable when done.
          // testImg(canvas.node().toDataURL());

          var texture = new THREE.Texture(this.d3Canvas.node());
          texture.needsUpdate = true;

          return texture;
        },

        /*
          x3ToLat & y3ToLon adapted from Peter Lux,
          http://www.plux.co.uk/converting-radians-in-degrees-latitude-and-longitude/
          convert three.js rotation.x & rotation.y (radians) to lat/lon

          globe.rotation.x + blah === northward
          globe.rotation.y - blah === southward
          globe.rotation.y + blah === westward
          globe.rotation.y - blah === eastward
        */
        x3ToLat : function(rad) {
          // convert radians into latitude
          // 90 to -90

          // first, get everthing into the range -2pi to 2pi
          rad = rad % (Math.PI*2);

          // convert negatives to equivalent positive angle
          if (rad < 0) {
            rad = twoPI + rad;
          }

          // restrict to 0 - 180
          var rad180 = rad % (Math.PI);

          // anything above 90 is subtracted from 180
          if (rad180 > Math.PI/2) {
            rad180 = Math.PI - rad180;
          }
          // if greater than 180, make negative
          if (rad > Math.PI) {
            rad = -rad180;
          } else {
            rad = rad180;
          }

          return (rad/Math.PI*180);
        },

        latToX3 : function(lat) {
          return (lat / 90) * halfPI;
        },

        y3ToLon : function(rad) {
          // convert radians into longitude
          // 180 to -180
          // first, get everything into the range -2pi to 2pi
          rad = rad % twoPI;
          if (rad < 0) {
            rad = twoPI + rad;
          }
          // convert negatives to equivalent positive angle
          var rad360 = rad % twoPI;

          // anything above 90 is subtracted from 360
          if (rad360 > Math.PI) {
            rad360 = twoPI - rad360;
          }

          // if greater than 180, make negative
          if (rad > Math.PI) {
            rad = -rad360;
          } else {
              rad = rad360;
          }
          return rad / Math.PI * 180;
        },

        lonToY3 : function(lon) {
          return -(lon / 180) * Math.PI;
        }
  };

  function testImg(dataURI) {
    var img = document.createElement('img');
    img.src = dataURI;
    img.width = 2048;
    img.height = 1024;
    document.body.appendChild(img);
  }

  var loaded = function (error, geojson, names) {
    var worldOpts = {
          selector : '#three-box',
          data     : geojson,
          names    : names
        };
    world.init(worldOpts);
  };

  window.addEventListener('DOMContentLoaded', function () {
    queue()
      .defer(d3.json, 'world.json')
      .defer(d3.tsv, 'world-country-names.tsv')
      .await(loaded);
  });

}());

world-country-names.tsv

id	name
-1	Northern Cyprus
-2	Kosovo
-3	Somaliland
4	Afghanistan
8	Albania
10	Antarctica
12	Algeria
16	American Samoa
20	Andorra
24	Angola
28	Antigua and Barbuda
31	Azerbaijan
32	Argentina
36	Australia
40	Austria
44	Bahamas
48	Bahrain
50	Bangladesh
51	Armenia
52	Barbados
56	Belgium
60	Bermuda
64	Bhutan
68	Bolivia
70	Bosnia and Herzegovina
72	Botswana
74	Bouvet Island
76	Brazil
84	Belize
86	British Indian Ocean Territory
90	Solomon Islands
92	British Virgin Islands
96	Brunei
100	Bulgaria
104	Myanmar
108	Burundi
112	Belarus
116	Cambodia
120	Cameroon
124	Canada
132	Cape Verde
136	Cayman Islands
140	Central African Republic
144	Sri Lanka
148	Chad
152	Chile
156	China
158	Taiwan
162	Christmas Island
166	Cocos (Keeling) Islands
170	Colombia
174	Comoros
175	Mayotte
178	Congo
180	Democratic Republic of the Congo
184	Cook Islands
188	Costa Rica
191	Croatia
192	Cuba
196	Cyprus
203	Czech Republic
204	Benin
208	Denmark
212	Dominica
214	Dominican Republic
218	Ecuador
222	El Salvador
226	Equatorial Guinea
231	Ethiopia
232	Eritrea
233	Estonia
234	Faroe Islands
238	Falkland Islands
239	South Georgia and the South Sandwich Islands
242	Fiji
246	Finland
248	Aaland Islands
250	France
254	French Guiana
258	French Polynesia
260	French Southern Territories
262	Djibouti
266	Gabon
268	Georgia
270	Gambia
275	Palestine
276	Germany
288	Ghana
292	Gibraltar
296	Kiribati
300	Greece
304	Greenland
308	Grenada
312	Guadeloupe
316	Guam
320	Guatemala
324	Guinea
328	Guyana
332	Haiti
334	Heard Island and McDonald Islands
336	Vatican City
340	Honduras
344	Hong Kong
348	Hungary
352	Iceland
356	India
360	Indonesia
364	Iran
368	Iraq
372	Ireland
376	Israel
380	Italy
384	Ivory Coast
388	Jamaica
392	Japan
398	Kazakhstan
400	Jordan
404	Kenya
408	North Korea
410	South Korea
414	Kuwait
417	Kyrgyzstan
418	Laos
422	Lebanon
426	Lesotho
428	Latvia
430	Liberia
434	Libya
438	Liechtenstein
440	Lithuania
442	Luxembourg
446	Macao
450	Madagascar
454	Malawi
458	Malaysia
462	Maldives
466	Mali
470	Malta
474	Martinique
478	Mauritania
480	Mauritius
484	Mexico
492	Monaco
496	Mongolia
498	Moldova
499	Montenegro
500	Montserrat
504	Morocco
508	Mozambique
512	Oman
516	Namibia
520	Nauru
524	Nepal
528	Netherlands
531	Curaçao
533	Aruba
534	Sint Maarten
535	Bonaire, Sint Eustatius and Saba
540	New Caledonia
548	Vanuatu
554	New Zealand
558	Nicaragua
562	Niger
566	Nigeria
570	Niue
574	Norfolk Island
578	Norway
580	Northern Mariana Islands
581	United States Minor Outlying Islands
583	Micronesia
584	Marshall Islands
585	Palau
586	Pakistan
591	Panama
598	Papua New Guinea
600	Paraguay
604	Peru
608	Philippines
612	Pitcairn
616	Poland
620	Portugal
624	Guinea-Bissau
626	Timor-Leste
630	Puerto Rico
634	Qatar
638	Reunion
642	Romania
643	Russia
646	Rwanda
652	Saint Bartholomew
654	Saint Helena, Ascension and Tristan da Cunha
659	Saint Kitts and Nevis
660	Anguilla
662	Saint Lucia
663	French Saint Martin
666	Saint Pierre and Miquelon
670	Saint Vincent and the Grenadines
674	San Marino
678	Sao Tome and Principe
682	Saudi Arabia
686	Senegal
688	Serbia
690	Seychelles
694	Sierra Leone
702	Singapore
703	Slovakia
704	Viet Nam
705	Slovenia
706	Somalia
710	South Africa
716	Zimbabwe
724	Spain
728	South Sudan
729	Sudan
732	Western Sahara
740	Suriname
744	Svalbard and Jan Mayen
748	Swaziland
752	Sweden
756	Switzerland
760	Syria
762	Tajikistan
764	Thailand
768	Togo
772	Tokelau
776	Tonga
780	Trinidad and Tobago
784	United Arab Emirates
788	Tunisia
792	Turkey
795	Turkmenistan
796	Turks and Caicos Islands
798	Tuvalu
800	Uganda
804	Ukraine
807	Macedonia
818	Egypt
826	United Kingdom
831	Guernsey
832	Jersey
833	Isle of Man
834	Tanzania
840	United States
850	US Virgin Islands
854	Burkina Faso
858	Uruguay
860	Uzbekistan
862	Venezuela
876	Wallis and Futuna
882	Samoa
887	Yemen
894	Zambia