Published
Edited
Apr 29, 2019
1 fork
34 stars
Insert cell
Insert cell
map = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
// essentially, a drop shadow on the roads layer
const shadowCanvas = DOM.canvas(width, height);
const shadowCtx = shadowCanvas.getContext('2d');
shadowCtx.fillStyle = '#ead0b6';
shadowCtx.fillRect(0, 0, width, height);
shadowCtx.globalCompositeOperation = 'destination-in';
shadowCtx.filter = 'blur(7px)'
shadowCtx.drawImage(roadCanvas, 0, 0);
shadowCtx.filter = 'none'
shadowCtx.drawImage(landCanvas, 0, 0);
const landLayers = DOM.canvas(width, height);
const landCtx = landLayers.getContext('2d');
landCtx.drawImage(landCanvas, 0, 0);
landCtx.globalCompositeOperation = 'multiply';
landCtx.drawImage(shadowCanvas, 0, 0);
landCtx.globalCompositeOperation = 'source-atop';
landCtx.drawImage(getRandomNoise('#d6b18d', .4), 0, 0);
ctx.drawImage(waterCanvas, 0, 0);
ctx.drawImage(landLayers, 0, 0);
ctx.drawImage(borderCanvas, 0, 0);
ctx.drawImage(roadCanvas, 0, 0);
return canvas;
}
Insert cell
Insert cell
waterCanvas = {
const waterCanvas = DOM.canvas(width, height);
const waterCtx = waterCanvas.getContext('2d');
waterCtx.fillStyle = '#6fdaf2';
waterCtx.fillRect(0, 0, width, height);
const noiseCanvas = DOM.canvas(width, height);
const noiseCtx = noiseCanvas.getContext('2d');
noiseCtx.fillStyle = '#188ba5';
noiseCtx.fillRect(0, 0, width, height);
noiseCtx.globalCompositeOperation = 'destination-in';
noiseCtx.drawImage(noise, 0, 0);
waterCtx.globalAlpha = .75;
waterCtx.drawImage(noiseCanvas, 0, 0);
const shadowCanvas = DOM.canvas(width, height);
const shadowCtx = shadowCanvas.getContext('2d');
shadowCtx.fillStyle = '#1c81c1';
shadowCtx.fillRect(0, 0, width, height);
shadowCtx.globalCompositeOperation = 'destination-in';
shadowCtx.filter = 'blur(25px)'
shadowCtx.drawImage(landBuffer, 0, 0);
waterCtx.drawImage(shadowCanvas, 0, 0);
waterCtx.globalAlpha = 1;
waterCtx.globalCompositeOperation = 'source-atop';
waterCtx.drawImage(getRandomNoise('#19647c', .1), 0, 0);
return waterCanvas;
}
Insert cell
// for use in water layer (above) land polygon including a sizable buffer, i.e. a big fat stroke
landBuffer = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'black';
ctx.strokeStyle = 'black';
ctx.lineWidth = 20;
ctx.filter = 'blur(2px)';
ctx.beginPath();
path.context(ctx)(states);
ctx.fill();
ctx.stroke();
ctx.filter = 'none';
const land = threshold(canvas, 170);
ctx.clearRect(0, 0, width, height);
ctx.drawImage(land, 0, 0);
return canvas;
}
Insert cell
Insert cell
landCanvas = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'black';
ctx.filter = 'blur(2px)';
ctx.beginPath();
path.context(ctx)(states);
ctx.fill();
ctx.filter = 'none';
const land = threshold(canvas, 170);
ctx.clearRect(0, 0, width, height);
ctx.drawImage(land, 0, 0);
ctx.fillStyle = '#fffef5';
ctx.globalCompositeOperation = 'source-atop';
ctx.fillRect(0, 0, width, height);
const noiseCanvas = DOM.canvas(width, height);
const noiseCtx = noiseCanvas.getContext('2d');
noiseCtx.fillStyle = '#d3a487';
noiseCtx.fillRect(0, 0, width, height);
noiseCtx.globalCompositeOperation = 'destination-in';
noiseCtx.drawImage(noise, 0, 0);
noiseCtx.drawImage(canvas, 0, 0);

ctx.drawImage(noiseCanvas, 0, 0);
return canvas;
}
Insert cell
Insert cell
borderCanvas = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
ctx.lineWidth = 7;
ctx.strokeStyle = 'black';
ctx.lineJoin = 'round';
ctx.filter = 'blur(4px)';
ctx.beginPath();
path.context(ctx)(states);
ctx.stroke();
ctx.globalAlpha = .75;
ctx.drawImage(noise, 0, 0);
const strokeCanvas = threshold(canvas, 170);
const strokeCtx = strokeCanvas.getContext('2d');
strokeCtx.globalCompositeOperation = 'source-in';
strokeCtx.fillStyle = 'rgba(255,255,255,.75)';
strokeCtx.fillRect(0, 0, width, height);
ctx.filter = 'blur(1px)';
ctx.globalAlpha = 1;
ctx.clearRect(0, 0, width, height);
ctx.lineWidth = 3;
ctx.beginPath();
path(states);
ctx.stroke();
ctx.globalAlpha = .75;
ctx.drawImage(noise, 0, 0)
const fillCanvas = threshold(canvas, 175);
const fillCtx = fillCanvas.getContext('2d');
fillCtx.globalCompositeOperation = 'source-atop';
fillCtx.fillStyle = '#ccc';
fillCtx.fillRect(0, 0, width, height);
ctx.filter = 'none';
ctx.clearRect(0, 0, width, height);
ctx.drawImage(strokeCanvas, 0, 0);
ctx.drawImage(fillCanvas, 0, 0);
ctx.globalCompositeOperation = 'source-atop';
ctx.drawImage(getRandomNoise('#999', .35), 0, 0);
ctx.globalAlpha = 1;
// a final masking by land polygon fill, to reduce outer stroke along the coast
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(landCanvas, 0, 0);
return canvas;
}
Insert cell
Insert cell
roadCanvas = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
// starting with the bigger, white stroke
ctx.lineWidth = 7;
ctx.strokeStyle = 'black';
ctx.lineJoin = 'round';
ctx.filter = 'blur(3px)';
// draw the roads
ctx.beginPath();
path.context(ctx)(roads);
ctx.stroke();
// draw Perlin noise
ctx.globalAlpha = .75;
ctx.drawImage(noise, 0, 0);
// do threshold, draw to a new canvas
const strokeCanvas = threshold(canvas, 170); // 170 seemed to work well here
const strokeCtx = strokeCanvas.getContext('2d');
// using threshold image as a mask, fill the image with white to get the final appearance
strokeCtx.globalCompositeOperation = 'source-in';
strokeCtx.fillStyle = 'white';
strokeCtx.fillRect(0, 0, width, height);
// repeat for the smaller orange stroke
ctx.filter = 'blur(2px)';
ctx.globalAlpha = 1;
ctx.clearRect(0, 0, width, height);
ctx.lineWidth = 4;
ctx.beginPath();
path(roads);
ctx.stroke();
ctx.globalAlpha = .75;
ctx.drawImage(noise, 0, 0)
const fillCanvas = threshold(canvas, 170);
const fillCtx = fillCanvas.getContext('2d');
fillCtx.globalCompositeOperation = 'source-atop';
fillCtx.fillStyle = '#ff9400';
fillCtx.fillRect(0, 0, width, height);
// lil random noise helps too
fillCtx.drawImage(getRandomNoise('#bc590d', .35), 0, 0);
// draw both layers to the final resulting canvas
ctx.filter = 'none';
ctx.clearRect(0, 0, width, height);
ctx.drawImage(strokeCanvas, 0, 0);
ctx.drawImage(fillCanvas, 0, 0);
return canvas;
}
Insert cell
// simple random noise
function getRandomNoise(color = '#999', maxAlpha = .3) {
const c = d3.color(color);
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
for (let x = 0; x < ctx.canvas.width; x += 2) {
for (let y = 0; y < ctx.canvas.height; y += 2) {
const a = Math.random() * maxAlpha;
ctx.fillStyle = `rgba(${c.r}, ${c.g}, ${c.b}, ${a})`;
ctx.fillRect(x, y, 2, 2);
}
}
return canvas;
}
Insert cell
// this threshold function is a little weird and particular to this map process
// it assumes only black pixels that vary by transparency
function threshold (canvas, value) {
const ctx = canvas.getContext('2d');
const d = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
const imageData = ctx.createImageData(width, height);
const newData = imageData.data;
for (let i = 0; i < d.length; i += 4) {
var r = d[i];
var g = d[i+1];
var b = d[i+2];
var a = d[i+3];
var v = a >= value ? 0 : 255;
newData[i] = newData[i+1] = newData[i+2] = v;
newData[i+3] = 255 - v;
}
const newCanvas = DOM.canvas(canvas.width, canvas.height);
newCanvas.getContext('2d').putImageData(imageData, 0, 0);
return newCanvas;
}
Insert cell
projection = d3.geoEqualEarth()
.translate([width/2, height/2])
.rotate([70.9, 0])
.center([0, 42.361445])
.scale(13000);
Insert cell
path = d3.geoPath()
.projection(projection)
Insert cell
height = width * .75
Insert cell
noise = {
const canvas = DOM.canvas(width, height);
const context = canvas.getContext('2d');
const image = context.createImageData(width, height);
const noise = octave(perlin2, 8);
for (let z = 0, y = 0, i = 0; y < height; ++y) {
for (let x = 0; x < width; ++x, i += 4) {
image.data[i + 3] = (noise(x / 64, y / 64) + 1) * 128;
}
}
context.putImageData(image, 0, 0);
return canvas;
}
Insert cell
import { perlin2, octave } from '@mbostock/perlin-noise'
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more