About this technique →
mondrian/sketch.js
// Mondrian Composition — recursive rectangular subdivision
// Recursive rectangular subdivision in the style of Piet Mondrian's 1920s neoplasticism.
// Each rectangle is split horizontally or vertically at a random ratio until
// max depth or minimum size, then colored from a Mondrian palette.
//
// The composition tree is built ONCE in sketchSetup (using p.random()) and stored
// in a module-level variable. sketchDraw traverses the cached tree deterministically,
// so there is no flicker and sliders take effect immediately on the next frame.

export const PARAMS = {
  depth:      { value: 6, min: 2, max: 9, step: 1, label: "Max Depth", folder: "Structure", rebuildOnChange: true },
  splitProb:  { value: 0.85, min: 0.1, max: 1.0, step: 0.01, label: "Split Probability", folder: "Structure", rebuildOnChange: true },
  minRatio:   { value: 0.3, min: 0.1, max: 0.5, step: 0.01, label: "Min Cell Aspect", folder: "Structure", rebuildOnChange: true },
  strokeW:    { value: 8, min: 1, max: 24, step: 1, label: "Grid Thickness", folder: "Appearance" },
  colorChance:{ value: 0.18, min: 0.0, max: 0.6, step: 0.01, label: "Color Probability", folder: "Appearance", rebuildOnChange: true },
};

export const SHARE = {
  bookmarked: ["depth", "splitProb", "strokeW", "colorChance"],
};

// Mondrian palette: weighted toward white, occasional primaries
// Each entry: [r, g, b, weight]
const PALETTE = [
  [245, 243, 235, 6], // off-white (most common)
  [215, 40,  40,  1], // Mondrian red
  [30,  80,  165, 1], // Mondrian blue
  [245, 200, 30,  1], // Mondrian yellow
];

function weightedColorIndex(rand01) {
  const totalWeight = PALETTE.reduce((sum, c) => sum + c[3], 0);
  let r = rand01 * totalWeight;
  for (let i = 0; i < PALETTE.length; i++) {
    r -= PALETTE[i][3];
    if (r <= 0) return i;
  }
  return 0;
}

// Build the subdivision tree using p.random() — returns a node:
//   { leaf: true, colorIdx: number, x, y, w, h }
//   { leaf: false, children: [...] }
function buildTree(p, x, y, w, h, depth, params) {
  // Leaf condition: max depth reached, too small, or random stop
  if (depth === 0 || w < 40 || h < 40 || p.random() > params.splitProb) {
    // Decide color at build time — use colorChance to gate primaries
    let colorIdx = 0; // default: white
    if (p.random() < params.colorChance) {
      // Pick one of the 3 primaries (indices 1-3)
      colorIdx = 1 + Math.floor(p.random(3));
    }
    return { leaf: true, colorIdx, x, y, w, h };
  }

  // Decide split direction: prefer splitting the longer dimension
  const doVertical = (w > h * 1.4) ? true : (h > w * 1.4) ? false : (p.random() < 0.5);

  if (doVertical) {
    const lo = w * params.minRatio;
    const hi = w * (1 - params.minRatio);
    if (lo >= hi) {
      const colorIdx = p.random() < params.colorChance ? (1 + Math.floor(p.random(3))) : 0;
      return { leaf: true, colorIdx, x, y, w, h };
    }
    const splitX = p.random(lo, hi);
    return {
      leaf: false,
      children: [
        buildTree(p, x, y, splitX, h, depth - 1, params),
        buildTree(p, x + splitX, y, w - splitX, h, depth - 1, params),
      ],
    };
  } else {
    const lo = h * params.minRatio;
    const hi = h * (1 - params.minRatio);
    if (lo >= hi) {
      const colorIdx = p.random() < params.colorChance ? (1 + Math.floor(p.random(3))) : 0;
      return { leaf: true, colorIdx, x, y, w, h };
    }
    const splitY = p.random(lo, hi);
    return {
      leaf: false,
      children: [
        buildTree(p, x, y, w, splitY, depth - 1, params),
        buildTree(p, x, y + splitY, w, h - splitY, depth - 1, params),
      ],
    };
  }
}

// Render the cached tree — no p.random() calls here
function renderTree(p, node, strokeW) {
  if (node.leaf) {
    const half = strokeW / 2;
    const [r, g, b] = PALETTE[node.colorIdx];
    p.noStroke();
    p.fill(r, g, b);
    p.rect(node.x + half, node.y + half, node.w - strokeW, node.h - strokeW);
  } else {
    for (const child of node.children) {
      renderTree(p, child, strokeW);
    }
  }
}

// Module-level state — persists between draw calls
let state;

export function sketchSetup(p, w, h, params) {
  p.rectMode(p.CORNER);

  // Build the composition tree once using current LIVE params. Structural
  // params (depth, splitProb, minRatio, colorChance) are marked rebuildOnChange
  // so the runtime calls sketchSetup again whenever any of them change.
  const tree = buildTree(p, 0, 0, w, h, params.depth, {
    splitProb:   params.splitProb,
    minRatio:    params.minRatio,
    colorChance: params.colorChance,
  });

  state = { tree, w, h };
  return state;
}

export function sketchDraw(p, w, h, params) {
  if (!state) return;

  // Rebuild tree if canvas was resized
  if (state.w !== w || state.h !== h) {
    const tree = buildTree(p, 0, 0, w, h, params.depth, params);
    state = { tree, w, h };
  }

  // Black background becomes the grid lines (cells are inset by half stroke width)
  p.background(10, 10, 10);

  // Outer border to close canvas edges
  p.stroke(10, 10, 10);
  p.strokeWeight(params.strokeW);
  p.noFill();
  p.rect(0, 0, w, h);

  // Render the cached tree — deterministic, no p.random()
  renderTree(p, state.tree, params.strokeW);
}