← Examples
after Bridget Riley · optical · line field · art

Op-Art Earth

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.

How it's made

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.

API Calls

https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=medium&format=geojson

Code

<!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>