A field of parallel black lines: where there's ocean they run nearly straight; where there's land they ripple into tight optical waves. Nothing but black lines on white — no coastline is ever drawn — in the spirit of Bridget Riley's Current. One /v1/geo call.
One mapjson call is rasterized to an offscreen land mask, then box-blurred into a smooth field so the effect fades in gently at the coasts. The canvas is filled with evenly spaced horizontal lines; each line's vertical wobble is a sine wave whose amplitude is driven by that land field — almost zero over water, strong over land. The result is an optical moiré that traces the continents without ever drawing a coastline. Re-run to shift the phase and wavelength.
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 — 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: #ffffff; 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");
// smooth land field: rasterize → coarse grid → box-blur → bilinear sample
const GW = 128, GH = 76, 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 += 2) for (let x = (gx * cw) | 0; x < ((gx + 1) * cw) | 0; x += 2) { t++; if (m[(y * W + x) * 4] > 128) n++; }
L[gy * GW + gx] = t ? n / t : 0;
}
for (let pass = 0; pass < 3; pass++) { // box blur
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) {
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;
}
function paint() {
svg.selectAll("*").remove();
svg.append("rect").attr("width", W).attr("height", H).attr("fill", "#ffffff");
const g = svg.append("g").attr("fill", "none").attr("stroke", "#141414");
const spacing = 7.2, wl = 34 + Math.random() * 22, k = (2 * Math.PI) / wl;
const phase0 = Math.random() * Math.PI * 2, A = spacing * 0.92;
for (let y0 = spacing; y0 < H; y0 += spacing) {
let dstr = "";
for (let x = 0; x <= W; x += 3) {
const amp = A * (0.05 + 0.95 * land(x, y0)); // calm on water, strong on land
const y = y0 + amp * Math.sin(x * k + phase0 + y0 * 0.10);
dstr += (x === 0 ? "M" : "L") + x.toFixed(1) + "," + y.toFixed(1);
}
g.append("path").attr("d", dstr).attr("stroke-width", 1.25);
}
}
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>