last christmas I offered a series of prints and plots as gifts to my family & relatives. for some of them, I used vector maps as a way to seed a graphic post-processor. it looked like this:
rue Lecourbe, Paris.
rue Pelleport, Paris.
l’opéra de Paris.
detail
Notre-Dame de Paris
these are the steps I followed:
- collect a set of points
- triangulate the set
- compute the triangles’ edges’ lengths
- draw the gradient. for a given height:
- translate the canvas vertically and give it a small opacity
- render only the edges which length is below a given threshold
- draw a given amount of extra wireframes as 4 with a higher opacity
- overlay an additive picture
- render white wireframes & glow
it may seem complicated but apart from the triangulation, it is mostly a canvas work.
the first 3 steps work together in a function called process(), the 4 last steps are grouped under render().
first the code of the process() function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//#1 we collected some points function process( points ) { //not enough points: bail out if (points.length < 3)return; //there are enough points to compute something //first make sure there are no duplicates ( create a point set ) points = cleanup(points); //#2 second compute the delaunay triangulation on the set var tris = delaunay.compute(points); //#3 compute the edges lengths and associate the endpoints edges = []; for (var i = 0; i < tris.length; i += 3) { var p0 = points[tris[i]]; var p1 = points[tris[i+1]]; var p2 = points[tris[i+2]]; edges.push( [distance(p0, p1), p0, p1], [distance(p1, p2), p1, p2], [distance(p2, p0), p2, p0] ); } render(); } |
1 collect a set of points
this is a variably trivial task, in the demo below I collect the X/Y corrdinates of the mouse/finger. for the actual pieces, I used the vector data of the Mapzen API, it could basically be anything in 2D. One restriction though, because of the next step, we need a set and not only a list ; the difference being that a set doesn’t contain duplicates. cleanup() is a method that removes duplicate points from a list.
2 triangulation
the heavy lifting, I used this delaunay triangulation lib, even though it’s not extra robust, it works in most cases.
3 compute the triangles’ edges’ lengths
again, not much to say, only storing the lengths of each edge and the 2 vertices they’re made of in an array to batch draw them without having to compute their length each time (computing edges’ lengths all the time is not necessary as they don’t change from a render to the next).
when process() is over, we call the render() method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
function render() { //reset the canvas ctx.restore(); ctx.save(); ctx.globalAlpha = 1; ctx.fillStyle = "#FFF"; ctx.fillRect(0, 0, w, h); //#4 draw the gradient ctx.save(); var max = config.height; for (i = 0; i < max; i++) { //#4.1 ctx.translate(0, 1); ctx.globalAlpha = ( 1 - i / max ) * 0.05; //#4.2 renderEdges(edges, i); } ctx.restore(); //#5 render the wireframes if (config.wireframe) { var m = config.wireCount; for (i = 0; i < m; i++) { var t = ( i / m ); ctx.save(); ctx.translate(0, max * ( 1 - t )); ctx.globalAlpha = .05 + .15 * t; renderEdges(edges, (i * 10) ); ctx.restore(); } } |
steps 4 & 5
both work the same : the loops will translate the canvas by a certain amount, change the opacity and call the renderEdges() method many times.
renderEdges( edges, min )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//the method to render edges function renderEdges(edges, min ){ if( edges.length == 0 )return; ctx.beginPath(); //for each edge of the list for( var i=0; i < edges.length; i++ ){ var edge = edges[ i ]; //if the edge's length is inferior to the given threshold if( edge[ 0 ] < min ){ //draw a line between endpoints ctx.moveTo( edge[ 1 ].x, edge[ 1 ].y); ctx.lineTo( edge[ 2 ].x, edge[ 2 ].y); } } ctx.stroke(); } |
for each computed edge, we check its length against a min value and, if the edge is shorter than the value, we draw it otherwise, we continue.
in the #4 loop, min is slowly incremented while in #5 it jumps from 10 to 10. the smaller the value of min, the denser the mesh, the bigger the value of min the sparser the mesh.
this does most of the work, the rest is a cosmetic layer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//#6 draw a color overlay var cw = canvas.width; var ch = canvas.height; ctx.save(); ctx.globalAlpha = 1; ctx.globalCompositeOperation = "screen"; //draw the image over the whole image ctx.drawImage(img, 0, 0, img.width, img.height, 0,0,cw,ch); ctx.restore(); //#7 white wireframe if( config.glow ){ ctx.globalCompositeOperation = "source-over"; ctx.strokeStyle = "#FFF"; ctx.globalAlpha = .2; renderEdges(edges, config.glowSize / 2); //white glow ctx.globalCompositeOperation = "screen"; ctx.globalAlpha = 1; ctx.filter = "blur( 6px )"; renderEdges(edges, config.glowSize ); } } |
6 overlay
this is a single context.drawImage() call with the context’s globalCompositeOperation set to “screen”. the 9 arguments of the drawImage call are: the image to draw, the 4 value of the source rectangle and the 4 values of the destination rectangle. I’m using a variety of images from this license free website and consequently downscale them. this is the one I use in the demo below . as the image is very small and is stretched over a big area, the bilinear interpolation used by drawImage() prevents the end result from pixelating (it could be nice though…)
7 glow
the glow can be obtained by drawing 2 versions of the edges: one regular, the other with a wider strokeStyle & a blur filter.
here’s a demo if you want to try it out:
you can download a zip here and check the demo page here.
despite it’s simplicty and the limited amount of parameters, it’s quite a versatile process. here are some doodles obtained with the above demo.
and that was … no wait!
there are some prints for sale here https://society6.com/barradeau/collection/woven-maps if you would like a place to appear in the list, let me know :)
NOW that was it :)
Nik
Love this! Particularly the idea of overlaying an image with a screen blend mode – great way to organically add splashes of color from a natural color palette. The gradient strokes remind me of Jared Tarbell’s Sandstroke algorithm. Thanks for sharing some code snippets! I might need a custom map print : )
philippe-l
Je te redécouvre avec délectation.
C’est toujours aussi brillant. Bravo !
Jul
Impressive !