Modular Visions

An interactive generative art project that produces modular geometric mosaics inspired by mid-century graphic design, constructivism, and the surrealist visual iconography of rock album covers.

Visual & Conceptual Identity

The artwork arranges dozens of distinct graphic panels into a structured, harmonious tapestry. It bridges retro-modernist print aesthetics with procedural generation:

Color Palette: A curated scheme of warm cream, mustard yellow, olive green, burnt orange, steel blue, rose pink, and plum purple, echoing 1960s–70s silkscreen and vinyl packaging.

Iconographic Vocabulary: Includes 22 distinct algorithmic motifs spanning optical patterns (hypnotic offset spirals, spoke wheels, sine-wave ribbons), structural forms (isometric cubes, Mondrian-style blocks, parabolic string art), and thematic emblems (facing silhouettes, industrial towers, geometric handshakes, retro automata).

Framing & Balance: Consistent 2px outlines and uniform cream-toned gutters anchor the composition, giving it the feel of a physical collector’s print or graphic index.

Technical & Algorithmic Architecture

Square-Only Packing System
The canvas is mapped to a discrete $12 \times 12$ unit occupancy matrix. The algorithm scans empty units and probabilistically merges them into multi-unit square tiles ($1 \times 1$, $2 \times 2$, $3 \times 3$, and $4 \times 4$). This guarantees an asymmetrical yet strictly square-aligned layout without rectangular distortion.

Hardware-Level Clipping Masks
Each square executes within an isolated HTML5 Canvas clipping context (drawingContext.clip()). Mathematical curves, diagonal stripes, and optical arcs can be drawn freely without bleeding beyond their cell borders, preserving pristine gutters.

Scale-Independent Motif Rendering
Every visual motif calculates its geometry relative to its container square ($s$) and local center ($cx, cy$), allowing any symbol to render legibly whether assigned to a compact $1 \times 1$ block or an expansive $4 \times 4$ hero panel.

// Expanded Modular Geometric Mosaic in p5.js
// Features 20+ distinct mid-century, optical, and constructivist motifs
// Interactive: 'R' = Reroll, 'S' = Save PNG

const PALETTE = [
'#2B5F8C', // Steel Blue
'#E58334', // Orange / Rust
'#F3C13A', // Mustard Yellow
'#809B48', // Olive Green
'#3D8E5A', // Emerald Teal
'#674172', // Purple / Plum
'#E898A5', // Rose Pink
'#F5ECD7', // Cream
'#247BA0', // Cyan Blue
'#D9534F', // Crimson Red
'#4A6984' // Slate Grey
];

const STROKE_COL = '#222222';
const STROKE_WEIGHT = 2;
const GUTTER = 7; // Strict pixel gap between square boxes
const MARGIN = 16; // Outer margin of the canvas
const BASE_COLS = 12; // 12x12 discrete grid units
const TOTAL_MOTIFS = 22; // Extended library of designs

let cells = [];
let unitSize;

function setup() {
createCanvas(800, 800);
noLoop();
rerollMosaic();
}

function draw() {
// Clear the canvas with the background/gutter tone
background('#F2EDE2');

for (let cell of cells) {
push();
translate(cell.x, cell.y);

// 1. Fill base background for the square
stroke(STROKE_COL);
strokeWeight(STROKE_WEIGHT);
fill(cell.bgCol);
rect(0, 0, cell.size, cell.size);

// 2. Hardware-level clipping path to prevent spillover into gutters
drawingContext.save();
drawingContext.beginPath();
drawingContext.rect(0, 0, cell.size, cell.size);
drawingContext.clip();

// 3. Draw internal motifs safely within clipped boundaries
drawCellMotif(cell);

// 4. Restore canvas state
drawingContext.restore();

// 5. Redraw clean outer border of the square
noFill();
stroke(STROKE_COL);
strokeWeight(STROKE_WEIGHT);
rect(0, 0, cell.size, cell.size);

pop();
}
}

// Keyboard shortcuts for interaction
function keyPressed() {
if (key === 'r' || key === 'R') {
rerollMosaic();
} else if (key === 's' || key === 'S') {
let timestamp = year() + nf(month(), 2) + nf(day(), 2) + '_' + nf(hour(), 2) + nf(minute(), 2) + nf(second(), 2);
saveCanvas('expanded_mosaic_' + timestamp, 'png');
}
}

function rerollMosaic() {
generateSquareGrid();
redraw();
}

// Square-only packing routine
function generateSquareGrid() {
cells = [];
let availableWidth = width - MARGIN * 2;
unitSize = (availableWidth - (BASE_COLS - 1) * GUTTER) / BASE_COLS;

let grid = Array(BASE_COLS).fill(null).map(() => Array(BASE_COLS).fill(false));

for (let r = 0; r < BASE_COLS; r++) {
for (let c = 0; c < BASE_COLS; c++) {
if (grid[r][c]) continue;

let maxSpan = 1;
while (c + maxSpan < BASE_COLS && r + maxSpan < BASE_COLS && maxSpan < 4) {
let fits = true;
for (let i = 0; i <= maxSpan; i++) {
for (let j = 0; j <= maxSpan; j++) {
if (grid[r + j][c + i]) {
fits = false;
break;
}
}
if (!fits) break;
}
if (fits) {
maxSpan++;
} else {
break;
}
}

let chosenSpan = 1;
if (maxSpan >= 4 && random() < 0.18) {
chosenSpan = 4;
} else if (maxSpan >= 3 && random() < 0.28) {
chosenSpan = 3;
} else if (maxSpan >= 2 && random() < 0.45) {
chosenSpan = 2;
}

for (let j = 0; j < chosenSpan; j++) {
for (let i = 0; i < chosenSpan; i++) {
grid[r + j][c + i] = true;
}
}

let px = MARGIN + c * (unitSize + GUTTER);
let py = MARGIN + r * (unitSize + GUTTER);
let pSize = chosenSpan * unitSize + (chosenSpan - 1) * GUTTER;

cells.push({
x: px,
y: py,
size: pSize,
span: chosenSpan,
type: floor(random(TOTAL_MOTIFS)),
bgCol: random(PALETTE),
fgCol1: random(PALETTE),
fgCol2: random(PALETTE),
fgCol3: random(PALETTE),
fgCol4: random(PALETTE)
});
}
}
}

// Expanded Motif Router
function drawCellMotif(c) {
let s = c.size;
let cx = s / 2;
let cy = s / 2;

stroke(STROKE_COL);
strokeWeight(STROKE_WEIGHT);

switch (c.type) {
case 0: drawConcentricRings(cx, cy, s); break;
case 1: drawSpokeWheel(cx, cy, s, c); break;
case 2: drawPrism(cx, cy, s, c); break;
case 3: drawDiagonalStripes(s, c); break;
case 4: drawConcentricDiamonds(cx, cy, s, c); break;
case 5: drawHourglass(cx, cy, s, c); break;
case 6: drawWavyRibbons(s); break;
case 7: drawMiniGrid(s, c); break;
case 8: drawProfiles(cx, cy, s, c); break;
case 9: drawChimneys(s, c); break;
case 10: drawEyeMotif(cx, cy, s, c); break;
case 11: drawCornerArcs(s, c); break;
// New Motifs
case 12: drawWindmill(cx, cy, s, c); break;
case 13: drawHypnoticSpiral(cx, cy, s, c); break;
case 14: drawMondrianBlocks(s, c); break;
case 15: drawIsometricCube(cx, cy, s, c); break;
case 16: drawModularHandshake(cx, cy, s, c); break;
case 17: drawFlamePrism(cx, cy, s, c); break;
case 18: drawOpticalLoom(s, c); break;
case 19: drawRetroRobot(cx, cy, s, c); break;
case 20: drawNestedPolygonStar(cx, cy, s, c); break;
case 21: drawAudioOscillator(s, c); break;
}
}

// -------------------------------------------------------------
// MOTIF IMPLEMENTATIONS
// -------------------------------------------------------------

function drawConcentricRings(cx, cy, s) {
let steps = floor(random(3, 6));
for (let r = steps; r >= 1; r--) {
fill(random(PALETTE));
let d = (s * 0.88) * (r / steps);
ellipse(cx, cy, d, d);
}
}

function drawSpokeWheel(cx, cy, s, c) {
let count = 16;
let r = s * 0.44;
for (let i = 0; i < count; i++) {
let a1 = map(i, 0, count, 0, TWO_PI);
let a2 = map(i + 1, 0, count, 0, TWO_PI);
fill(i % 2 === 0 ? c.fgCol1 : c.fgCol2);
arc(cx, cy, r * 2, r * 2, a1, a2, PIE);
}
fill(c.fgCol3);
ellipse(cx, cy, r * 0.45, r * 0.45);
}

function drawPrism(cx, cy, s, c) {
fill(c.fgCol1);
let d = s * 0.38;
triangle(cx, cy - d, cx - d * 0.86, cy + d * 0.6, cx + d * 0.86, cy + d * 0.6);
fill(c.fgCol2);
ellipse(cx, cy + d * 0.1, d * 0.35, d * 0.35);
}

function drawDiagonalStripes(s, c) {
let stripes = 6;
let step = (s * 2) / stripes;
strokeCap(SQUARE);
for (let i = 0; i <= stripes + 2; i++) {
fill(i % 2 === 0 ? c.fgCol1 : c.fgCol2);
let pos = i * step - s;
beginShape();
vertex(pos, 0);
vertex(pos + step, 0);
vertex(pos + step - s, s);
vertex(pos - s, s);
endShape(CLOSE);
}
}

function drawConcentricDiamonds(cx, cy, s, c) {
let steps = 3;
for (let i = steps; i >= 1; i--) {
fill(i % 2 === 0 ? c.fgCol1 : c.fgCol2);
let d = (s * 0.44) * (i / steps);
quad(cx, cy - d, cx + d, cy, cx, cy + d, cx - d, cy);
}
}

function drawHourglass(cx, cy, s, c) {
let w = s * 0.25;
let h = s * 0.35;
fill(c.fgCol1);
triangle(cx - w, cy - h, cx + w, cy - h, cx, cy);
triangle(cx - w, cy + h, cx + w, cy + h, cx, cy);
line(cx - w - 2, cy - h, cx + w + 2, cy - h);
line(cx - w - 2, cy + h, cx + w + 2, cy + h);
}

function drawWavyRibbons(s) {
let lines = 4;
let bandH = s / lines;
for (let i = 0; i < lines; i++) {
fill(random(PALETTE));
beginShape();
vertex(-5, i * bandH);
for (let x = -5; x <= s + 5; x += 4) {
let y = i * bandH + sin(x * 0.08 + i) * (bandH * 0.25);
vertex(x, y);
}
vertex(s + 5, (i + 1) * bandH);
for (let x = s + 5; x >= -5; x -= 4) {
let y = (i + 1) * bandH + sin(x * 0.08 + i) * (bandH * 0.25);
vertex(x, y);
}
endShape(CLOSE);
}
}

function drawMiniGrid(s, c) {
let n = 3;
let step = s / n;
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
fill((i + j) % 2 === 0 ? c.fgCol1 : c.fgCol2);
rect(i * step, j * step, step, step);
}
}
}

function drawProfiles(cx, cy, s, c) {
let d = s * 0.35;
fill(c.fgCol1);
beginShape();
vertex(cx - d, cy - d);
vertex(cx - 5, cy - d);
vertex(cx - 10, cy);
vertex(cx - 3, cy + 5);
vertex(cx - 15, cy + d);
vertex(cx - d, cy + d);
endShape(CLOSE);

fill(c.fgCol2);
beginShape();
vertex(cx + d, cy - d);
vertex(cx + 5, cy - d);
vertex(cx + 10, cy);
vertex(cx + 3, cy + 5);
vertex(cx + 15, cy + d);
vertex(cx + d, cy + d);
endShape(CLOSE);
}

function drawChimneys(s, c) {
fill(c.fgCol1);
let baseH = s * 0.35;
rect(0, s - baseH, s, baseH);

fill(c.fgCol2);
let count = 4;
let w = s / (count * 2 + 1);
for (let i = 0; i < count; i++) {
let x = w + i * (w * 2);
rect(x, s * 0.2, w, s - baseH - s * 0.2);
}
}

function drawEyeMotif(cx, cy, s, c) {
let w = s * 0.7;
let h = s * 0.35;
fill(c.fgCol1);
arc(cx, cy, w, h * 2, 0, PI, CHORD);
arc(cx, cy, w, h * 2, PI, TWO_PI, CHORD);
fill(c.fgCol2);
ellipse(cx, cy, h, h);
fill(STROKE_COL);
ellipse(cx, cy, h * 0.45, h * 0.45);
}

function drawCornerArcs(s, c) {
fill(c.fgCol1);
arc(0, 0, s * 1.6, s * 1.6, 0, HALF_PI);
fill(c.fgCol2);
rect(s * 0.5, s * 0.5, s * 0.4, s * 0.4);
}

// --- NEW MOTIFS ---

function drawWindmill(cx, cy, s, c) {
let blades = 4;
let len = s * 0.4;
let wid = s * 0.12;
push();
translate(cx, cy);
for (let i = 0; i < blades; i++) {
rotate(TWO_PI / blades);
fill(i % 2 === 0 ? c.fgCol1 : c.fgCol2);
quad(0, 0, -wid / 2, -len * 0.7, 0, -len, wid / 2, -len * 0.7);
}
fill(c.fgCol3);
ellipse(0, 0, s * 0.18, s * 0.18);
pop();
}

function drawHypnoticSpiral(cx, cy, s, c) {
let arcs = 8;
for (let i = arcs; i >= 1; i--) {
fill(i % 2 === 0 ? c.fgCol1 : c.fgCol2);
let rad = (s * 0.9) * (i / arcs);
let offset = (i % 2 === 0) ? -s * 0.04 : s * 0.04;
ellipse(cx + offset, cy, rad, rad);
}
}

function drawMondrianBlocks(s, c) {
let splitX = s * 0.6;
let splitY = s * 0.45;

fill(c.fgCol1);
rect(0, 0, splitX, splitY);
fill(c.fgCol2);
rect(splitX, 0, s - splitX, splitY * 0.6);
fill(c.fgCol3);
rect(splitX, splitY * 0.6, s - splitX, s - splitY * 0.6);
fill(c.fgCol4);
rect(0, splitY, splitX * 0.4, s - splitY);
fill(c.bgCol);
rect(splitX * 0.4, splitY, splitX * 0.6, s - splitY);
}

function drawIsometricCube(cx, cy, s, c) {
let r = s * 0.35;
let h = r * 0.866; // cos(30 deg)

// Top face
fill(c.fgCol1);
quad(cx, cy - r, cx + h, cy - r * 0.5, cx, cy, cx - h, cy - r * 0.5);
// Left face
fill(c.fgCol2);
quad(cx - h, cy - r * 0.5, cx, cy, cx, cy + r, cx - h, cy + r * 0.5);
// Right face
fill(c.fgCol3);
quad(cx, cy, cx + h, cy - r * 0.5, cx + h, cy + r * 0.5, cx, cy + r);
}

function drawModularHandshake(cx, cy, s, c) {
let w = s * 0.28;
let h = s * 0.18;
// Left cuff
fill(c.fgCol1);
rect(cx - w * 1.5, cy - h / 2, w * 0.7, h);
// Right cuff
fill(c.fgCol2);
rect(cx + w * 0.8, cy - h / 2, w * 0.7, h);
// Central interlock
fill(c.fgCol3);
beginShape();
vertex(cx - w * 0.8, cy - h / 2);
vertex(cx + 0.2 * w, cy - h / 2);
vertex(cx + 0.4 * w, cy);
vertex(cx + 0.8 * w, cy);
vertex(cx + 0.8 * w, cy + h / 2);
vertex(cx - 0.2 * w, cy + h / 2);
vertex(cx - 0.4 * w, cy);
vertex(cx - 0.8 * w, cy);
endShape(CLOSE);
}

function drawFlamePrism(cx, cy, s, c) {
let w = s * 0.32;
let h = s * 0.42;
fill(c.fgCol1);
beginShape();
vertex(cx, cy - h);
bezierVertex(cx + w * 1.2, cy - h * 0.2, cx + w, cy + h * 0.8, cx, cy + h);
bezierVertex(cx - w, cy + h * 0.8, cx - w * 1.2, cy - h * 0.2, cx, cy - h);
endShape(CLOSE);

// Inner flame core
fill(c.fgCol2);
ellipse(cx, cy + h * 0.35, w * 0.8, h * 0.6);
}

function drawOpticalLoom(s, c) {
let lines = 7;
for (let i = 0; i <= lines; i++) {
let t = map(i, 0, lines, 0, s);
line(0, t, t, s);
line(t, 0, s, t);
}
fill(c.fgCol1);
ellipse(s * 0.5, s * 0.5, s * 0.28, s * 0.28);
}

function drawRetroRobot(cx, cy, s, c) {
let headW = s * 0.5;
let headH = s * 0.38;

// Antenna
line(cx, cy - headH / 2, cx, cy - headH * 0.9);
fill(c.fgCol3);
ellipse(cx, cy - headH * 0.9, s * 0.08, s * 0.08);

// Head
fill(c.fgCol1);
rect(cx - headW / 2, cy - headH / 2, headW, headH, 4);

// Eyes
fill(c.fgCol2);
ellipse(cx - headW * 0.25, cy - headH * 0.05, s * 0.1, s * 0.1);
ellipse(cx + headW * 0.25, cy - headH * 0.05, s * 0.1, s * 0.1);

// Mouth slit
fill(STROKE_COL);
rect(cx - headW * 0.25, cy + headH * 0.2, headW * 0.5, headH * 0.12);
}

function drawNestedPolygonStar(cx, cy, s, c) {
let rad = s * 0.42;
fill(c.fgCol1);
beginShape();
for (let i = 0; i < 8; i++) {
let a = map(i, 0, 8, 0, TWO_PI);
let r = (i % 2 === 0) ? rad : rad * 0.45;
vertex(cx + cos(a) * r, cy + sin(a) * r);
}
endShape(CLOSE);

fill(c.fgCol2);
ellipse(cx, cy, rad * 0.4, rad * 0.4);
}

function drawAudioOscillator(s, c) {
let steps = 6;
let barH = s / (steps * 2);
for (let i = 0; i < steps; i++) {
let barW = map(sin(i * 0.7), -1, 1, s * 0.3, s * 0.85);
fill(i % 2 === 0 ? c.fgCol1 : c.fgCol2);
rect((s - barW) / 2, (i * 2 + 0.5) * barH, barW, barH);
}
}