// 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);
}