block by mbostock e2a20d86124693ef0f93

Pencil Sketch

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.

Updated Example →

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) {
  if (error) throw error;

  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}();