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

  1. 1.

    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.

  2. 2.

    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.

  3. 3.

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