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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more