Genuary 2026.22: Pen plotter ready

Split a polygon into triangles and then recursively split those triangles over and over again!


let gui;
let params = {
  initialSides: 6, initialSidesMin: 3, initialSidesMax: 12, initialSidesStep: 1,
  splitsPerFrame: 10, splitsPerFrameMin: 1, splitsPerFrameMax: 10, splitsPerFrameStep: 1,
  minArea: 50, minAreaMin: 1, minAreaMax: 2000, minAreaStep: 1,
  minSegment: 5, minSegmentMin: 1, minSegmentMax: 100, minSegmentStep: 1,
  colorize: false,
  hueVariation: 30, hueVariationMin: 0, hueVariationMax: 100, hueVariationStep: 1,
  pauseOnResetFor: 500, pauseOnResetForMin: 0, pauseOnResetForMax: 2000,
  stopOnReset: false,
};

let lastParams;
let triangles;
let pauseStart;
let stopped = false;

function setup() {
  createCanvas(400, 400);
  colorMode(HSB, 360, 100, 100, 100);

  gui = createGuiPanel("params");
  gui.addObject(params);
  gui.setPosition(420, 0);

  reset();
}

function reset() {
  triangles = [];

  let r = width * 0.4;
  let cx = width / 2;
  let cy = height / 2;

  for (let i = 0; i < params.initialSides; i++) {
    let a1 = (TWO_PI * i) / params.initialSides;
    let a2 = (TWO_PI * (i + 1)) / params.initialSides;

    let p1 = createVector(cx, cy);
    let p2 = createVector(
      cx + cos(a1) * r,
      cy + sin(a1) * r
    );
    let p3 = createVector(
      cx + cos(a2) * r,
      cy + sin(a2) * r
    );

    triangles.push({
      a: p1,
      b: p2,
      c: p3,
      hue: random(360)
    });
  }
}

function draw() {
  if (lastParams == undefined || Object.keys(params).some((k) => params[k] !== lastParams[k])) {
    reset();
    stopped = false;
    pauseStart = undefined;
    lastParams = { ...params };
  }

  if (stopped) return;

  background(0, 0, 95);

  let splits = params.splitsPerFrame;
  let updatesMade = false;

  for (let i = 0; i < splits; i++) {
    for (let retry = 0; retry < 100; retry++) {
      let index = floor(random(triangles.length));

      let t = triangles.splice(index, 1)[0];
      let g = p5.Vector.add(t.a, t.b).add(t.c).div(3);

      let corners = [
        { v: t.a, e1: t.b, e2: t.c },
        { v: t.b, e1: t.c, e2: t.a },
        { v: t.c, e1: t.a, e2: t.b },
      ];
      let choice = random(corners);

      let p = intersect(choice.v, g, choice.e1, choice.e2);
      if (!p) {
        triangles.push(t);
        continue;
      }

      if (p5.Vector.dist(choice.e1, choice.e2) < params.minSegment) {
        triangles.push(t);
        continue;
      }

      if (triangleArea(choice.v, choice.e1, choice.e2) / 2 < params.minArea ||
        triangleArea(choice.v, p, choice.e1) < params.minArea ||
        triangleArea(choice.v, p, choice.e2) < params.minArea) {
        triangles.push(t);
        continue;
      }
    
      triangles.push(
        {
          a: choice.v.copy(),
          b: choice.e1.copy(),
          c: p.copy(),
          hue: t.hue + random(-params.hueVariation, params.hueVariation)
        },
        {
          a: choice.v.copy(),
          b: p.copy(),
          c: choice.e2.copy(),
          hue: t.hue + random(-params.hueVariation, params.hueVariation)
        }
      );

      updatesMade = true;
      break;
    }
  }

  stroke(0);
  noFill();

  for (let t of triangles) {
    if (params.colorize) {
      fill(t.hue, 80, 80, 20);
    }
    t.hue += random(-params.hueVariation / 10, params.hueVariation / 10);
    while (t.hue < 0) t.hue += 360;
    while (t.hue >= 360) t.hue -= 360;

    beginShape();
    vertex(t.a.x, t.a.y);
    vertex(t.b.x, t.b.y);
    vertex(t.c.x, t.c.y);
    endShape(CLOSE);
  }

  if (!updatesMade) {
    if (params.stopOnReset) {
      stopped = true;
    } else if (pauseStart == undefined) {
      pauseStart = millis();
    } else if (millis() - pauseStart >= params.pauseOnResetFor) {
      pauseStart = undefined;
      reset();
    }
  }
}

function intersect(a, b, c, d) {
  let r = p5.Vector.sub(b, a);
  let s = p5.Vector.sub(d, c);

  let denom = r.x * s.y - r.y * s.x;
  if (abs(denom) < 1e-6) return null;

  let t = ((c.x - a.x) * s.y - (c.y - a.y) * s.x) / denom;
  let u = ((c.x - a.x) * r.y - (c.y - a.y) * r.x) / denom;

  if (t >= 0 && u >= 0 && u <= 1) {
    return createVector(
      a.x + t * r.x,
      a.y + t * r.y
    );
  }

  return null;
}

function triangleArea(a, b, c) {
  return abs(
    (a.x * (b.y - c.y) +
     b.x * (c.y - a.y) +
     c.x * (a.y - b.y)) / 2
  );
}

If you’d rather something a bit more stained glass, you can colorize it: