block by wboykinm 002b77ea237231567e28

002b77ea237231567e28

Full Screen

This stylized globe uses D3’s in-development geometry pipeline to jitter points before rendering. Points are sampled along great arcs, jittered, and then smoothed using basis-spline interpolation to achieve a hand-drawn effect. The resulting spherical geometry is computed once, allowing faster rendering with arbitrary rotation!

This techinque was developed in collaboration with Derek Watkins for Norway the Slow Way.

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<canvas width="960" height="500"></canvas>
<script src="d3.js"></script>
<script src="topojson.js"></script>
<script>

var canvas = d3.select("canvas"),
    canvasNode = canvas.node(),
    context = canvasNode.getContext("2d");

var width = canvasNode.width,
    height = canvasNode.height,
    scale = width * .6;

var π = Math.PI,
    τ = 2 * π,
    halfπ = π / 2,
    asin = function(x) { return x > 1 ? halfπ : x < -1 ? -halfπ : Math.asin(x); },
    atan2 = Math.atan2;

d3.json("/mbostock/raw/4090846/world-110m.json", function(error, world) {
  var sketch = d3.geo.pipeline()
      .source(d3.geo.jsonSource)
      .pipe(resample, .020)
      .pipe(jitter, .004)
      .pipe(smooth, .005)
      .sink(d3.geo.jsonSink);

  var land = topojson.feature(world, world.objects.land),
      lands = {type: "GeometryCollection", geometries: [sketch(land), sketch(land), sketch(land)]};

  d3.timer(function(elapsed) {
    context.clearRect(0, 0, width, height);
    context.save();
    context.translate(width / 2, height * 1.2);
    context.scale(scale, -scale);

    context.beginPath();
    d3.geo.pipeline()
        .source(d3.geo.jsonSource)
        .pipe(d3.geo.rotate, elapsed * .00005, -.7, .26)
        .pipe(d3.geo.clipCircle, Math.PI / 2)
        .pipe(d3.geo.project, d3.geo.orthographic)
        .sink(d3.geom.contextSink, context)
        (lands);
    context.lineWidth = 1 / scale;
    context.globalAlpha = .5;
    context.strokeStyle = "#000";
    context.stroke();

    context.restore();
  });
});

function haversin(θ) {
  return (θ = Math.sin(θ / 2)) * θ;
}

function interpolateArc(λ0, φ0, λ1, φ1) {
  var0 = Math.cos(φ0),
      sφ0 = Math.sin(φ0),
      cφ1 = Math.cos(φ1),
      sφ1 = Math.sin(φ1),
      kλ0 = cφ0 * Math.cos(λ0),
      kφ0 = cφ0 * Math.sin(λ0),
      kλ1 = cφ1 * Math.cos(λ1),
      kφ1 = cφ1 * Math.sin(λ1),
      d = 2 * Math.asin(Math.sqrt(haversin(φ1 - φ0) + cφ0 * cφ1 * haversin(λ1 - λ0))),
      k = 1 / Math.sin(d);

  var interpolate = d ? function(t) {
    var B = Math.sin(t *= d) * k,
        A = Math.sin(d - t) * k,
        x = A * kλ0 + B * kλ1,
        y = A * kφ0 + B * kφ1,
        z = A * sφ0 + B * sφ1;
    return [
      atan2(y, x),
      atan2(z, Math.sqrt(x * x + y * y))
    ];
  } : function() {
    return0, φ0];
  };

  interpolate.distance = d;

  return interpolate;
}

function resample(δ, sink) {
  var λ00,
      φ00,
      λ0,
      φ0;

  function lineStart() {
    θ = 0;
    resample.point = lineFirstPoint;
    sink.lineStart();
  }

  function lineFirstPoint(λ, φ) {
    λ00 = λ0 = λ, φ00 = φ0 = φ;
    resample.point = linePoint;
  }

  function linePoint(λ1, φ1) {
    sink.point(λ0, φ0);
    var a = interpolateArc(λ0, φ0, λ0 = λ1, φ0 = φ1),
        n = Math.floor(a.distance / δ) + 1;
    for (var i = 1, p; i < n; ++i) p = a(i / n), sink.point(p[0], p[1]);
  }

  function lineEnd() {
    linePoint(λ00, φ00);
    sink.lineEnd();
    λ00 = φ00 = λ0 = φ0 = undefined;
    resample.point = null;
  }

  var resample = {
    sphere: function() { sink.sphere(); },
    polygonStart: function() { sink.polygonStart(); },
    polygonEnd: function() { sink.polygonEnd(); },
    lineStart: lineStart,
    lineEnd: lineEnd,
    point: null
  };

  return resample;
}

function jitter(δ, sink) {
  var random = d3.random.normal(0, δ);

  return {
    sphere: function() { sink.sphere(); },
    polygonStart: function() { sink.polygonStart(); },
    polygonEnd: function() { sink.polygonEnd(); },
    lineStart: function() { sink.lineStart(); },
    lineEnd: function() { sink.lineEnd(); },
    point: function(λ, φ) {
      var p = rotation(random(), random())(λ, φ);
      sink.point(p[0], p[1]);
    }
  };
}

function rotation(δλ, δφ) {
  var cosδφ = Math.cos(δφ),
      sinδφ = Math.sin(δφ);
  return function(λ, φ) {
    λ += δλ; if (λ > π) λ -= τ; else if (λ < -π) λ += τ;
    var cosφ = Math.cos(φ),
        x = Math.cos(λ) * cosφ,
        y = Math.sin(λ) * cosφ,
        z = Math.sin(φ);
    return [atan2(y, x * cosδφ - z * sinδφ), asin(z * cosδφ + x * sinδφ)];
  };
}

function inverseRotation(δλ, δφ) {
  var cosδφ = Math.cos(δφ),
      sinδφ = Math.sin(δφ);
  return function(λ, φ) {
    var cosφ = Math.cos(φ),
        x = Math.cos(λ) * cosφ,
        y = Math.sin(λ) * cosφ,
        z = Math.sin(φ);
    λ = atan2(y, x * cosδφ + z * sinδφ) - δλ;
    if (λ > π) λ -= τ; else if (λ < -π) λ += τ;
    return [λ, asin(z * cosδφ - x * sinδφ)];
  };
}

function smooth(δ, sink) {
  var θ,
      λ00,
      φ00,
      λ01,
      φ01,
      λ02,
      φ02,
      λ0,
      φ0,
      λ1,
      φ1,
      λ2,
      φ2;

  function lineStart() {
    θ = 0;
    smooth.point = lineFirstPoint;
    sink.lineStart();
  }

  function lineFirstPoint(λ, φ) {
    λ00 = λ0 = λ, φ00 = φ0 = φ;
    smooth.point = lineSecondPoint;
  }

  function lineSecondPoint(λ, φ) {
    λ01 = λ1 = λ, φ01 = φ1 = φ;
    smooth.point = lineThirdPoint;
  }

  function lineThirdPoint(λ, φ) {
    λ02 = λ2 = λ, φ02 = φ2 = φ;
    smooth.point = linePoint;
  }

  function linePoint(λ3, φ3) {
    var segment = interpolateArc(λ1, φ1, λ2, φ2),
        origin = segment(.5),
        n = Math.ceil(segment.distance / δ),
        r = rotation(-origin[0], -origin[1]),
        ri = inverseRotation(-origin[0], -origin[1]),
        p0 = r(λ0, φ0),
        p1 = r(λ1, φ1),
        p2 = r(λ2, φ2),
        p3 = r(λ3, φ3);
    for (var i = 0; i < n; ++i) {
      var t = i / n,
          k0 = (1 - t) * (1 - t) * (1 - t) / 6,
          k1 = (3 * t * t * t - 6 * t * t + 4) / 6,
          k2 = (-3 * t * t * t + 3 * t * t + 3 * t + 1) / 6,
          k3 = t * t * t / 6,
          x = k0 * p0[0] + k1 * p1[0] + k2 * p2[0] + k3 * p3[0],
          y = k0 * p0[1] + k1 * p1[1] + k2 * p2[1] + k3 * p3[1],
          p = ri(x, y);
      sink.point(p[0], p[1]);
    }
    λ0 = λ1, φ0 = φ1, λ1 = λ2, φ1 = φ2, λ2 = λ3, φ2 = φ3;
  }

  function lineEnd() {
    if (!isNaN02) && !isNaN02)) { // skip polygons with 3 or fewer points
      linePoint(λ00, φ00);
      linePoint(λ01, φ01);
      linePoint(λ02, φ02);
    }
    sink.lineEnd();
    λ00 = φ00 = λ01 = φ01 = λ02 = φ02 = λ0 = φ0 = λ1 = φ1 = λ2 = φ2 = undefined;
    smooth.point = null;
  }

  var smooth = {
    sphere: function() { sink.sphere(); },
    polygonStart: function() { sink.polygonStart(); },
    polygonEnd: function() { sink.polygonEnd(); },
    lineStart: lineStart,
    lineEnd: lineEnd,
    point: null
  };

  return smooth;
}

</script>

topojson.js

!function(){function t(n,t){function r(t){var r,e=n.arcs[0>t?~t:t],o=e[0];return n.transform?(r=[0,0],e.forEach(function(n){r[0]+=n[0],r[1]+=n[1]})):r=e[e.length-1],0>t?[r,o]:[o,r]}function e(n,t){for(var r in n){var e=n[r];delete t[e.start],delete e.start,delete e.end,e.forEach(function(n){o[0>n?~n:n]=1}),f.push(e)}}var o={},i={},u={},f=[],c=-1;return t.forEach(function(r,e){var o,i=n.arcs[0>r?~r:r];i.length<3&&!i[1][0]&&!i[1][1]&&(o=t[++c],t[c]=r,t[e]=o)}),t.forEach(function(n){var t,e,o=r(n),f=o[0],c=o[1];if(t=u[f])if(delete u[t.end],t.push(n),t.end=c,e=i[c]){delete i[e.start];var a=e===t?t:t.concat(e);i[a.start=t.start]=u[a.end=e.end]=a}else i[t.start]=u[t.end]=t;else if(t=i[c])if(delete i[t.start],t.unshift(n),t.start=f,e=u[f]){delete u[e.end];var s=e===t?t:e.concat(t);i[s.start=e.start]=u[s.end=t.end]=s}else i[t.start]=u[t.end]=t;else t=[n],i[t.start=f]=u[t.end=c]=t}),e(u,i),e(i,u),t.forEach(function(n){o[0>n?~n:n]||f.push([n])}),f}function r(n,r,e){function o(n){var t=0>n?~n:n;(s[t]||(s[t]=[])).push({i:n,g:a})}function i(n){n.forEach(o)}function u(n){n.forEach(i)}function f(n){"GeometryCollection"===n.type?n.geometries.forEach(f):n.type in l&&(a=n,l[n.type](n.arcs))}var c=[];if(arguments.length>1){var a,s=[],l={LineString:i,MultiLineString:u,Polygon:u,MultiPolygon:function(n){n.forEach(u)}};f(r),s.forEach(arguments.length<3?function(n){c.push(n[0].i)}:function(n){e(n[0].g,n[n.length-1].g)&&c.push(n[0].i)})}else for(var h=0,p=n.arcs.length;p>h;++h)c.push(h);return{type:"MultiLineString",arcs:t(n,c)}}function e(r,e){function o(n){n.forEach(function(t){t.forEach(function(t){(f[t=0>t?~t:t]||(f[t]=[])).push(n)})}),c.push(n)}function i(n){return l(u(r,{type:"Polygon",arcs:[n]}).coordinates[0])>0}var f={},c=[],a=[];return e.forEach(function(n){"Polygon"===n.type?o(n.arcs):"MultiPolygon"===n.type&&n.arcs.forEach(o)}),c.forEach(function(n){if(!n._){var t=[],r=[n];for(n._=1,a.push(t);n=r.pop();)t.push(n),n.forEach(function(n){n.forEach(function(n){f[0>n?~n:n].forEach(function(n){n._||(n._=1,r.push(n))})})})}}),c.forEach(function(n){delete n._}),{type:"MultiPolygon",arcs:a.map(function(e){var o=[];if(e.forEach(function(n){n.forEach(function(n){n.forEach(function(n){f[0>n?~n:n].length<2&&o.push(n)})})}),o=t(r,o),(n=o.length)>1)for(var u,c=i(e[0][0]),a=0;n>a;++a)if(c===i(o[a])){u=o[0],o[0]=o[a],o[a]=u;break}return o})}}function o(n,t){return"GeometryCollection"===t.type?{type:"FeatureCollection",features:t.geometries.map(function(t){return i(n,t)})}:i(n,t)}function i(n,t){var r={type:"Feature",id:t.id,properties:t.properties||{},geometry:u(n,t)};return null==t.id&&delete r.id,r}function u(n,t){function r(n,t){t.length&&t.pop();for(var r,e=s[0>n?~n:n],o=0,i=e.length;i>o;++o)t.push(r=e[o].slice()),a(r,o);0>n&&f(t,i)}function e(n){return n=n.slice(),a(n,0),n}function o(n){for(var t=[],e=0,o=n.length;o>e;++e)r(n[e],t);return t.length<2&&t.push(t[0].slice()),t}function i(n){for(var t=o(n);t.length<4;)t.push(t[0].slice());return t}function u(n){return n.map(i)}function c(n){var t=n.type;return"GeometryCollection"===t?{type:t,geometries:n.geometries.map(c)}:t in l?{type:t,coordinates:l[t](n)}:null}var a=v(n.transform),s=n.arcs,l={Point:function(n){return e(n.coordinates)},MultiPoint:function(n){return n.coordinates.map(e)},LineString:function(n){return o(n.arcs)},MultiLineString:function(n){return n.arcs.map(o)},Polygon:function(n){return u(n.arcs)},MultiPolygon:function(n){return n.arcs.map(u)}};return c(t)}function f(n,t){for(var r,e=n.length,o=e-t;o<--e;)r=n[o],n[o++]=n[e],n[e]=r}function c(n,t){for(var r=0,e=n.length;e>r;){var o=r+e>>>1;n[o]<t?r=o+1:e=o}return r}function a(n){function t(n,t){n.forEach(function(n){0>n&&(n=~n);var r=o[n];r?r.push(t):o[n]=[t]})}function r(n,r){n.forEach(function(n){t(n,r)})}function e(n,t){"GeometryCollection"===n.type?n.geometries.forEach(function(n){e(n,t)}):n.type in u&&u[n.type](n.arcs,t)}var o={},i=n.map(function(){return[]}),u={LineString:t,MultiLineString:r,Polygon:r,MultiPolygon:function(n,t){n.forEach(function(n){r(n,t)})}};n.forEach(e);for(var f in o)for(var a=o[f],s=a.length,l=0;s>l;++l)for(var h=l+1;s>h;++h){var p,g=a[l],v=a[h];(p=i[g])[f=c(p,v)]!==v&&p.splice(f,0,v),(p=i[v])[f=c(p,g)]!==g&&p.splice(f,0,g)}return i}function s(n,t){function r(n){i.remove(n),n[1][2]=t(n),i.push(n)}var e=v(n.transform),o=m(n.transform),i=g();return t||(t=h),n.arcs.forEach(function(n){for(var u,f,c=[],a=0,s=0,l=n.length;l>s;++s)f=n[s],e(n[s]=[f[0],f[1],1/0],s);for(var s=1,l=n.length-1;l>s;++s)u=n.slice(s-1,s+2),u[1][2]=t(u),c.push(u),i.push(u);for(var s=0,l=c.length;l>s;++s)u=c[s],u.previous=c[s-1],u.next=c[s+1];for(;u=i.pop();){var h=u.previous,p=u.next;u[1][2]<a?u[1][2]=a:a=u[1][2],h&&(h.next=p,h[2]=u[2],r(h)),p&&(p.previous=h,p[0]=u[0],r(p))}n.forEach(o)}),n}function l(n){for(var t,r=-1,e=n.length,o=n[e-1],i=0;++r<e;)t=o,o=n[r],i+=t[0]*o[1]-t[1]*o[0];return.5*i}function h(n){var t=n[0],r=n[1],e=n[2];return Math.abs((t[0]-e[0])*(r[1]-t[1])-(t[0]-r[0])*(e[1]-t[1]))}function p(n,t){return n[1][2]-t[1][2]}function g(){function n(n,t){for(;t>0;){var r=(t+1>>1)-1,o=e[r];if(p(n,o)>=0)break;e[o._=t]=o,e[n._=t=r]=n}}function t(n,t){for(;;){var r=t+1<<1,i=r-1,u=t,f=e[u];if(o>i&&p(e[i],f)<0&&(f=e[u=i]),o>r&&p(e[r],f)<0&&(f=e[u=r]),u===t)break;e[f._=t]=f,e[n._=t=u]=n}}var r={},e=[],o=0;return r.push=function(t){return n(e[t._=o]=t,o++),o},r.pop=function(){if(!(0>=o)){var n,r=e[0];return--o>0&&(n=e[o],t(e[n._=0]=n,0)),r}},r.remove=function(r){var i,u=r._;if(e[u]===r)return u!==--o&&(i=e[o],(p(i,r)<0?n:t)(e[i._=u]=i,u)),u},r}function v(n){if(!n)return y;var t,r,e=n.scale[0],o=n.scale[1],i=n.translate[0],u=n.translate[1];return function(n,f){f||(t=r=0),n[0]=(t+=n[0])*e+i,n[1]=(r+=n[1])*o+u}}function m(n){if(!n)return y;var t,r,e=n.scale[0],o=n.scale[1],i=n.translate[0],u=n.translate[1];return function(n,f){f||(t=r=0);var c=0|(n[0]-i)/e,a=0|(n[1]-u)/o;n[0]=c-t,n[1]=a-r,t=c,r=a}}function y(){}var d={version:"1.6.18",mesh:function(n){return u(n,r.apply(this,arguments))},meshArcs:r,merge:function(n){return u(n,e.apply(this,arguments))},mergeArcs:e,feature:o,neighbors:a,presimplify:s};"function"==typeof define&&define.amd?define(d):"object"==typeof module&&module.exports?module.exports=d:this.topojson=d}();