← Examples
contour · topographic · concentric · art

Op-Art Earth: Rings

The ring idea, made legible: instead of circles from a centre, every landmass is drawn as its own set of nested contour rings, each one stepping inward from the coast like the lines on a topographic map. Big continents fill with many rings; small islands get a few; the ocean stays blank. Concentric lines, but this time they are the world map. One /v1/geo call.

How it's made

One mapjson call is rasterized to a land mask on a coarse grid. A quick distance transform then measures, for every land cell, how far it sits from the nearest coast — a value that grows toward the interior of each continent. Running d3.contours over that distance field at evenly spaced levels yields the nested rings: the outermost is the coastline itself, each inner ring a fixed step deeper inland. Bigger landmasses simply hold more rings, so the map reads as relief.

API Calls

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

Code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Op-Art Earth: Rings — contour world map</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>
  </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 = 240, GH = 140, cw = W / GW, ch = H / GH;   // grid aspect matches the canvas
  let D = null;
  function buildField(world) {
    const proj = d3.geoEqualEarth().rotate([-11, 0]).fitExtent([[24, 20], [W - 24, H - 20]], { 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;
    const B = new Uint8Array(GW * GH);
    for (let gy = 0; gy < GH; gy++) for (let gx = 0; gx < GW; gx++) {
      const x = ((gx + 0.5) * cw) | 0, y = ((gy + 0.5) * ch) | 0;
      B[gy * GW + gx] = m[(y * W + x) * 4] > 128 ? 1 : 0;
    }
    // chamfer distance transform — distance into land from the coast
    D = new Float32Array(GW * GH);
    const BIG = 1e5;
    for (let i = 0; i < D.length; i++) D[i] = B[i] ? BIG : 0;
    for (let gy = 0; gy < GH; gy++) for (let gx = 0; gx < GW; gx++) {
      const i = gy * GW + gx; if (!B[i]) continue;
      let v = D[i]; if (gx > 0) v = Math.min(v, D[i - 1] + 1); if (gy > 0) v = Math.min(v, D[i - GW] + 1); D[i] = v;
    }
    for (let gy = GH - 1; gy >= 0; gy--) for (let gx = GW - 1; gx >= 0; gx--) {
      const i = gy * GW + gx; if (!B[i]) continue;
      let v = D[i]; if (gx < GW - 1) v = Math.min(v, D[i + 1] + 1); if (gy < GH - 1) v = Math.min(v, D[i + GW] + 1); D[i] = v;
    }
  }

  function paint() {
    svg.selectAll("*").remove();
    svg.append("rect").attr("width", W).attr("height", H).attr("fill", "#ffffff");
    const maxD = d3.max(D) || 1;
    const contours = d3.contours().size([GW, GH]).thresholds(d3.range(0.5, maxD, 2.0))(Array.from(D));
    const path = d3.geoPath(d3.geoIdentity().scale(W / GW));
    svg.append("g").attr("fill", "none").attr("stroke", "#141414")
      .selectAll("path").data(contours).join("path")
      .attr("d", path).attr("stroke-width", (d, i) => i === 0 ? 1.6 : 1.05);
  }

  d3.json("https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=high&format=geojson")
    .then((w) => { buildField(w); paint(); });
  </script>
</body>
</html>