block by patricksurry 354eb778dc70d25eb55b685eda8f013d

Showing depth with a 3D globe

Full Screen

Forked from Canvas globe with cities to illustrate how to simulate depth using the d3.geoDistance() function, using an interactive globe (zoom & rotate).

Also demonstrates additive blending via context.globalCompositeOperation = 'lighter', where drawing pixels on top of each other adds the RGB values. Starting with any base color that has non-zero red, blue and green components will ultimately saturate to white, giving a glow effect. This is different than opacity-based blending where continually over-drawing using a partially transparent color eventually converges to the non-transparent version of that same color.

Here we use a blue => red color shift and simulate a see-through world, but we could equally hide points where distance > π/2. We need to dim colors based on the normal vector at the globe’s surface to avoid a “glow” effect around the edges where points tend to overlap more often.

Built with blockbuilder.org

index.html

<!DOCTYPE html>
<!-- from https://bl.ocks.org/larsvers/dab7c2d6ea5ab964d10df0ef1470b90e -->
<head>
  <meta charset="utf-8">
  <script src="https://d3js.org/d3.v4.min.js"></script>
  <script src="https://d3js.org/topojson.v1.min.js"></script>

  <style>
    body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0;background: #000; }

  </style>
</head>

<body>
<script>


/* Set up */
/* ====== */

var width = 900,
    height = 500,
    originalScale = 200,
    scale = originalScale,
    scaleChange,
    rotation;

var sphere = {type: 'Sphere'};

var graticule = d3.geoGraticule();

// set up the main canvas and the projection

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

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

context.imageSmoothingEnabled = true;
// blend overlapping colors towards white (vs default source-over)
context.globalCompositeOperation = 'lighter';  

context.translate(width / 2, height / 2);

var projection = d3.geoOrthographic()
    .scale(scale)
    .rotate([42, -23])
    .translate([0, 0])
    .clipAngle(90);

var path = d3.geoPath()
    .projection(projection)
    .context(context);

// interpolate color from front to back of globe, 
// accounting for normal vector at globe surface
function globeColor(frontColor, backColor) {
  	// simple interpolation from front to back
    var redShift = d3.scaleLinear()
    		.domain([1, -1])
    		.range([frontColor, backColor]);

  	// with additive blending, pixels tend to overlap more near globe edges
    // so we dim based on the angle (in radians) from the point nearest viewer
    // allow a secondary brightness to increase (>1) or decrease (<1) brightness
    function angularDim(angle, brightness) {
      brightness = brightness || 1;
      const z = Math.cos(angle),
          c = d3.rgb(redShift(z)),
          f = Math.abs(z) * brightness;
      return d3.rgb(c.r*f, c.g*f, c.b*f, c.opacity).toString();
    }
    return angularDim;
}

/* Data load */
/* ========= */

d3.queue()
  .defer(d3.csv, 'cities_all.csv')
  .await(load);


function load(error, cities) {
  if (error) { console.log(error); }

  cities.sort((a, b) => d3.ascending(+a.Population, +b.Population));

  var grid = graticule();

  var popScale = d3.scaleSqrt()
      .domain([0, d3.median(cities, d => +d.Population)])
      .clamp(true)
      .range([0, 1]);

  var cityColor = globeColor('#080880', '#100404');

  // Draw the world

  function drawWorld() {

    context.clearRect(-width/2, -height/2, width, height);

    // draw grid(s)
    context.save();

    context.lineWidth = 0.5;
    context.strokeStyle = '#202020';

    context.beginPath();
    path(sphere);
    context.stroke();

    context.beginPath();
    path(grid);
    context.stroke();

    // draw cities
    // find the center point facing the viewer
    const c = projection.invert([0, 0]);
    cities.forEach(p => {
      const point = [+p.Longitude, +p.Latitude];
      // measure distance from the center, so d in [0, Math.PI]
      d = d3.geoDistance(c, point);
      context.fillStyle = cityColor(d, popScale(+p.Population));
      xy = projection(point);
      context.fillRect(xy[0], xy[1], 1, 1);
    })

    context.restore();

  } // drawWorld()

  // First draw

  requestAnimationFrame(drawWorld);

  var zoom = d3.zoom()
    .scaleExtent([0.5, 4])
    .on("zoom", zoomed)

  canvas.call(zoom);

  var previousScaleFactor = 1;

  function zoomed() {

    var dx = d3.event.sourceEvent.movementX;
    var dy = d3.event.sourceEvent.movementY;

    var event = d3.event.sourceEvent.type;

    context.save();
    context.clearRect(0, 0, width, height);

    if (event === 'wheel') {

      scaleFactor = d3.event.transform.k;
      scaleChange = scaleFactor - previousScaleFactor;
      scale = scale + scaleChange * originalScale;
      projection.scale(scale);
      previousScaleFactor = scaleFactor;

    } else {

      var r = projection.rotate();
      rotation = [r[0] + dx * 0.4, r[1] - dy * 0.5, r[2]];
      projection.rotate(rotation);

    }

    requestAnimationFrame(drawWorld);

    context.restore();

  } // zoomed()

} // load()


</script>
</body>