The optical idea of Op-Art Earth, in color — the mode Riley moved into with her chromatic stripe paintings. Flowing horizontal bands cycle through the spectrum; over open ocean they lie flat and even, but over land they swell and ripple, so the continents rise as bands of chromatic turbulence. One /v1/geo call.
The mapjson geometry is rasterized to a smooth land field. The canvas is filled top to bottom with solid color bands cycling through a spectral palette; each band's boundary is a sine wave whose amplitude is the land field, so the bands stay flat over water and bulge over land. Filled color instead of thin lines gives it weight — an optical map you read by where the color churns. Re-run to shift the flow.
https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=medium&format=geojson
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Op-Art Earth: Spectrum — after Bridget Riley</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { margin: 0; background: #fafafa; font-family: system-ui, sans-serif; }
.wrap { max-width: 980px; margin: 2rem auto; padding: 0 1rem; }
#map { background: #101318; border-radius: 4px; overflow: hidden; }
#map svg { width: 100%; display: block; }
button { margin-top: 0.7rem; font: 12px ui-monospace, monospace; padding: 6px 12px; background: #fff; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<div class="wrap">
<div id="map"></div>
<button id="repaint">↻ re-run</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<script>
const W = 960, H = 560;
const svg = d3.select("#map").append("svg").attr("viewBox", `0 0 ${W} ${H}`);
const off = document.createElement("canvas"); off.width = W; off.height = H;
const octx = off.getContext("2d");
const GW = 150, GH = 88, cw = W / GW, ch = H / GH;
let L = new Float32Array(GW * GH);
function buildField(world) {
const proj = d3.geoEqualEarth().rotate([-11, 0]).fitExtent([[26, 22], [W - 26, H - 22]], { type: "Sphere" });
const p = d3.geoPath(proj, octx);
octx.fillStyle = "#000"; octx.fillRect(0, 0, W, H);
octx.beginPath(); world.features.forEach((f) => p(f)); octx.fillStyle = "#fff"; octx.fill();
const m = octx.getImageData(0, 0, W, H).data;
for (let gy = 0; gy < GH; gy++) for (let gx = 0; gx < GW; gx++) {
let n = 0, t = 0;
for (let y = (gy * ch) | 0; y < ((gy + 1) * ch) | 0; y++) for (let x = (gx * cw) | 0; x < ((gx + 1) * cw) | 0; x++) { t++; if (m[(y * W + x) * 4] > 128) n++; }
L[gy * GW + gx] = t ? n / t : 0;
}
for (let pass = 0; pass < 2; pass++) {
const T = L.slice();
for (let gy = 0; gy < GH; gy++) for (let gx = 0; gx < GW; gx++) {
let s = 0, c = 0;
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) { const yy = gy + dy, xx = gx + dx; if (yy >= 0 && yy < GH && xx >= 0 && xx < GW) { s += T[yy * GW + xx]; c++; } }
L[gy * GW + gx] = s / c;
}
}
}
function land(x, y) {
if (x < 0 || y < 0 || x >= W || y >= H) return 0;
const fx = Math.max(0, Math.min(GW - 1.001, x / cw - 0.5)), fy = Math.max(0, Math.min(GH - 1.001, y / ch - 0.5));
const x0 = fx | 0, y0 = fy | 0, tx = fx - x0, ty = fy - y0;
const a = L[y0 * GW + x0], b = L[y0 * GW + x0 + 1], c = L[(y0 + 1) * GW + x0], d = L[(y0 + 1) * GW + x0 + 1];
return a + (b - a) * tx + (c - a) * ty + (a - b - c + d) * tx * ty;
}
const PAL = ["#d1394f", "#e17b3a", "#e8c33a", "#5fa85a", "#2f9c95", "#3f6fb0", "#7a4fa0"];
function paint() {
svg.selectAll("*").remove();
const bh = H / 24, k = (2 * Math.PI) / (58 + Math.random() * 30), ph = Math.random() * 6.283, A = bh * 0.46;
const by = (j, x) => j * bh + A * land(x, Math.max(0, Math.min(H - 1, j * bh))) * Math.sin(x * k + j * 0.4 + ph);
const n = Math.ceil(H / bh) + 1;
for (let j = 0; j < n; j++) {
let d = "";
for (let x = 0; x <= W; x += 4) d += (x === 0 ? "M" : "L") + x + "," + by(j, x).toFixed(1);
for (let x = W; x >= 0; x -= 4) d += "L" + x + "," + by(j + 1, x).toFixed(1);
svg.append("path").attr("d", d + "Z").attr("fill", PAL[((j % PAL.length) + PAL.length) % PAL.length]);
}
}
d3.json("https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=medium&format=geojson")
.then((w) => { buildField(w); paint(); });
document.getElementById("repaint").addEventListener("click", paint);
</script>
</body>
</html>