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.
<!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) {
var cφ0 = 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() {
return [λ0, φ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 (!isNaN(λ02) && !isNaN(φ02)) { // 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>
!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}();