← Examples
after Mondrian · De Stijl · subdivision · art

Composition with Continents

The world reduced to Mondrian's grammar: the plane recursively split into rectangles, ruled off in thick black, and painted in three primaries on an off-white ground — with the catch that red, blue, and yellow only pool where the land is. Ocean cells stay white. Squint and the continents are still there, hiding inside a Composition in Red, Blue and Yellow. One /v1/geo call.

How it's made

The mapjson geometry is rasterized once to an offscreen canvas as a land mask. The canvas is then split by recursive guillotine cuts — each rectangle divided in two, mostly across its longer side, at an off-centre line — until the cells reach Mondrian-ish proportions. For each cell the land coverage is measured against the mask: a mostly-water cell stays off-white, while land cells are flooded with a primary (redder where the land is densest). Thick black rules and a heavy frame finish it. Every recompose reseeds the cuts, so the same continents fall into a new arrangement each time.

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>Composition with Continents — after Mondrian</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: #f4f1e9; 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">↻ recompose</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
  <script>
  const W = 960, H = 620;
  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 WHITE = "#f6f2e8", BLACK = "#141414";
  const RED = "#d1332a", BLUE = "#1f4fa3", YEL = "#f2c33d";

  // recursive guillotine subdivision → Mondrian-ish rectangles
  function split(x, y, w, h, depth, out) {
    const MIN = 68;
    const stop = depth <= 0 || (w < MIN * 1.7 && h < MIN * 1.7) || (Math.random() < 0.22 && w < MIN * 3.2 && h < MIN * 3.2);
    if (stop) { out.push({ x, y, w, h }); return; }
    const horiz = w > h ? Math.random() < 0.78 : Math.random() < 0.22;   // cut across the longer side, mostly
    const t = 0.32 + Math.random() * 0.36;
    if (horiz) { const c = Math.round(w * t); split(x, y, c, h, depth - 1, out); split(x + c, y, w - c, h, depth - 1, out); }
    else { const c = Math.round(h * t); split(x, y, w, c, depth - 1, out); split(x, y + c, w, h - c, depth - 1, out); }
  }

  let land = null;
  function paint(world) {
    if (world) {
      const proj = d3.geoEqualEarth().rotate([-11, 0]).fitExtent([[30, 40], [W - 30, H - 40]], { 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();
      land = octx.getImageData(0, 0, W, H).data;
    }
    const isLand = (x, y) => { x |= 0; y |= 0; return x >= 0 && y >= 0 && x < W && y < H && land[(y * W + x) * 4] > 128; };
    const landFrac = (c) => {
      let n = 0, t = 0;
      for (let sy = c.y + 5; sy < c.y + c.h - 5; sy += 7) for (let sx = c.x + 5; sx < c.x + c.w - 5; sx += 7) { t++; if (isLand(sx, sy)) n++; }
      return t ? n / t : 0;
    };

    const cells = [];
    split(0, 0, W, H, 6, cells);
    for (const c of cells) {
      const lf = landFrac(c);
      if (lf < 0.12 ? Math.random() < 0.04 : Math.random() < 0.72) {
        c.fill = lf > 0.55 ? RED : lf > 0.28 ? (Math.random() < 0.6 ? BLUE : RED) : (Math.random() < 0.6 ? YEL : BLUE);
      } else c.fill = WHITE;
    }

    svg.selectAll("*").remove();
    svg.append("rect").attr("width", W).attr("height", H).attr("fill", WHITE);
    svg.append("g").selectAll("rect").data(cells).join("rect")
      .attr("x", (c) => c.x).attr("y", (c) => c.y).attr("width", (c) => c.w).attr("height", (c) => c.h)
      .attr("fill", (c) => c.fill).attr("stroke", BLACK).attr("stroke-width", 7);
    svg.append("rect").attr("x", 3).attr("y", 3).attr("width", W - 6).attr("height", H - 6)
      .attr("fill", "none").attr("stroke", BLACK).attr("stroke-width", 11);
  }

  d3.json("https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=medium&format=geojson").then((w) => paint(w));
  document.getElementById("repaint").addEventListener("click", () => paint(null));
  </script>
</body>
</html>