← Examples
after Bridget Riley · chromatic · optical · art

Op-Art Earth: Spectrum

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.

How it's made

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.

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