block by mbostock 3057239

Geodesic Rainbow

Full Screen

Use the range slider to change the degree of subdivision in this geodesic sphere. The base shape, visible when subdivision is disabled, is the icosahedron. Built with the d3.geodesic plugin.

Updated Example →

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>

#subdivision {
  position: absolute;
  top: 20px;
  left: 20px;
}

#subdivision input {
  width: 200px;
}

</style>
<div id="subdivision">
  <input type="range" min="1" max="12" value="8">
  <output name="subdivision"></output>
</div>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="d3.geodesic.min.js"></script>
<script>

var width = 960,
    height = 500;

var velocity = [.010, .005],
    t0 = Date.now();

var projection = d3.geo.orthographic()
    .scale(height / 2 - 10);

var canvas = d3.select("body").append("canvas")
    .attr("width", width)
    .attr("height", height);

var context = canvas.node().getContext("2d");

context.strokeStyle = "#000";
context.lineWidth = .5;

var faces;

var output = d3.select("output");

var input = d3.select("input")
    .on("input", function() { geodesic(+this.value); })
    .on("change", function() { geodesic(+this.value); })
    .each(function() { geodesic(+this.value); });

d3.timer(function() {
  var time = Date.now() - t0;
  projection.rotate([time * velocity[0], time * velocity[1]]);
  redraw();
});

function redraw() {
  context.clearRect(0, 0, width, height);

  faces.forEach(function(d) {
    d.polygon[0] = projection(d[0]);
    d.polygon[1] = projection(d[1]);
    d.polygon[2] = projection(d[2]);
    if (d.visible = d.polygon.area() > 0) {
      context.fillStyle = d.fill;
      context.beginPath();
      drawTriangle(d.polygon);
      context.fill();
    }
  });

  context.beginPath();
  faces.forEach(function(d) {
    if (d.visible) {
      drawTriangle(d.polygon);
    }
  });
  context.stroke();
}

function drawTriangle(triangle) {
  context.moveTo(triangle[0][0], triangle[0][1]);
  context.lineTo(triangle[1][0], triangle[1][1]);
  context.lineTo(triangle[2][0], triangle[2][1]);
  context.closePath();
}

function geodesic(subdivision) {
  output.text(subdivision);

  faces = d3.geodesic.polygons(subdivision).map(function(d) {
    d = d.coordinates[0];
    d.pop(); // use an open polygon
    d.fill = d3.hsl(d[0][0], 1, .5) + "";
    d.polygon = d3.geom.polygon(d.map(projection));
    return d;
  });

  redraw();
}

</script>

d3.geodesic.min.js

!function(){function n(n){return d3.merge(e.map(function(t){var o=r(t[0],t[1]),u=r(t[0],t[2]),i=[];i.push([t[0],o(1/n),u(1/n)]);for(var a=1;n>a;++a){for(var e=r(o(a/n),u(a/n)),c=r(o((a+1)/n),u((a+1)/n)),f=0;a>=f;++f)i.push([e(f/a),c(f/(a+1)),c((f+1)/(a+1))]);for(var f=0;a>f;++f)i.push([e(f/a),c((f+1)/(a+1)),e((f+1)/a)])}return i}))}function t(t){function r(n,t){var r;(n[0]<t[0]||n[0]==t[0]&&(n[1]<t[1]||n[1]==t[1]&&n[2]<t[2]))&&(r=n,n=t,t=r),u[n.map(o)+" "+t.map(o)]=[n,t]}function o(n){return d3.round(n,4)}var u={};return n(t).forEach(function(n){r(n[0],n[1]),r(n[1],n[2]),r(n[2],n[0])}),d3.values(u)}function r(n,t){var r=n[0],o=n[1],u=n[2],i=t[0]-r,a=t[1]-o,e=t[2]-u;return function(n){return[r+n*i,o+n*a,u+n*e]}}function o(n){var t=n[0],r=n[1],o=n[2];return[Math.atan2(r,t)*i,Math.acos(o/Math.sqrt(t*t+r*r+o*o))*i-90]}var u=1.618033988749895,i=180/Math.PI,a=[[1,u,0],[-1,u,0],[1,-u,0],[-1,-u,0],[0,1,u],[0,-1,u],[0,1,-u],[0,-1,-u],[u,0,1],[-u,0,1],[u,0,-1],[-u,0,-1]],e=[[0,1,4],[1,9,4],[4,9,5],[5,9,3],[2,3,7],[3,2,5],[7,10,2],[0,8,10],[0,4,8],[8,2,10],[8,4,5],[8,5,2],[1,0,6],[11,1,6],[3,9,11],[6,10,7],[3,11,7],[11,6,7],[6,0,10],[9,1,11]].map(function(n){return n.map(function(n){return a[n]})});d3.geodesic={multipolygon:function(t){return{type:"MultiPolygon",coordinates:n(~~t).map(function(n){return n=n.map(o),n.push(n[0]),n=[n]})}},polygons:function(n){return d3.geodesic.multipolygon(~~n).coordinates.map(function(n){return{type:"Polygon",coordinates:n}})},multilinestring:function(n){return{type:"MultiLineString",coordinates:t(~~n).map(function(n){return n.map(o)})}}}}();