Seed 23Click for a new seed · keys 1–4, n, b, s

The code

Paste this into a new Processing 4 sketch and press Run. Keys 1–4 switch mode, space or n gives a new seed, b goes back one, g toggles the title guide and s saves a 3600 × 5400 PNG and an SVG to out/.

sop_fractal.pde · 366 lines Download
// SoP cover experiment: generative fractal in the cover's palette.
//
// Two ideas, alone or together:
//   1  runaway   a branching network grows out of the control loop's node and
//                forks more the further it gets from the loop (runaway complexity)
//   2  recursion every feedback loop contains two smaller feedback loops
//                (Beer's recursive system theorem), cascading along the diagonal
//   3  both
//   4  civilisation: 3, plus tapering organic routes, settlements at the forks,
//      street grids round the old towns, lights along the roads and
//      cross-links that turn the tree into a network
//
// Keys: 1-4 mode, space or n = new seed, b = back one seed,
//       g = toggle title-zone guide, s = save PNG (3600x5400) + SVG to out/
//
// Headless: SOP_EXPORT=1 SOP_MODE=3 SOP_SEED=7 Processing cli --sketch=... --run

import processing.svg.*;

final int W = 1200, H = 1800;          // design units, 2:3 like the 6x9in cover
final color RED = #B8141A, CREAM = #F4F1EA, INK = #141010;

int MODE = 4;
long SEED = 23;                        // the book cover (adopted 2026-11-07)
boolean guide = true;
boolean exporting = false;
// SOP_BOLD=<factor>: heavier main loop, symbols and roads so the cover reads from
// a distance (thumbnail, shelf). Weights and sizes only; geometry and seed unchanged.
// Default 2.5 = the cover since 2026-11-12; SOP_BOLD=1 gives the earlier thin lines.
float BOLD = 2.5;

// main loop box, same placement as the current cover
float bx0, by0, bx1, by1;
float TITLE_Y = H * 0.30;              // keep the network below the title block
int segs;                              // runaway guard
final int MAX_SEGS = 40000;

PGraphics view;

void settings() {
  size(600, 900);
  pixelDensity(displayDensity());
}

void setup() {
  bx0 = W * 0.21; bx1 = W * 0.79; by0 = H * 0.51; by1 = H * 0.77;
  // Square main loop by default (the cover since 2026-11-12): same area as the old
  // rectangle, node (bottom-left, where the network starts) kept in place.
  // SOP_SQUARE=0 brings back the rectangle; SOP_SQUARE=<side in design units> sets the side.
  String sq = System.getenv("SOP_SQUARE");
  if (sq == null) sq = "1";
  if (!sq.equals("0")) {
    float side = sq.equals("1") ? sqrt((bx1 - bx0) * (by1 - by0)) : float(sq);
    bx1 = bx0 + side; by0 = by1 - side;
  }
  String bo = System.getenv("SOP_BOLD");
  if (bo != null) BOLD = float(bo);
  String m = System.getenv("SOP_MODE"), s = System.getenv("SOP_SEED");
  if (m != null) MODE = int(m);
  if (s != null) SEED = Long.parseLong(s);
  if (System.getenv("SOP_EXPORT") != null) {
    guide = false;
    exportAll();
    exit();
    return;
  }
  redrawView();
}

void draw() {
  image(view, 0, 0, width, height);
}

void redrawView() {
  view = createGraphics(W, H);
  render(view, 1);
  surface.setTitle("SoP fractal  mode " + MODE + "  seed " + SEED);
}

void keyPressed() {
  if (key >= '1' && key <= '4') MODE = key - '0';
  else if (key == ' ' || key == 'n') SEED++;
  else if (key == 'b') SEED = Math.max(0L, SEED - 1);
  else if (key == 'g') guide = !guide;
  else if (key == 's') { boolean gg = guide; guide = false; exportAll(); guide = gg; }
  else return;
  redrawView();
}

void exportAll() {
  String base = sketchPath("out/sop_fractal_m" + MODE + "_s" + SEED);
  PGraphics png = createGraphics(W * 3, H * 3);
  render(png, 3);
  png.save(base + ".png");
  PGraphics svg = createGraphics(W, H, SVG, base + ".svg");
  render(svg, 1);
  svg.dispose();
  println("saved " + base + ".png/.svg");
}

// ------------------------------------------------------------------ scene

void render(PGraphics g, float s) {
  randomSeed(SEED);
  noiseSeed(SEED);
  segs = 0;
  g.beginDraw();
  g.scale(s);
  g.background(RED);
  g.strokeCap(ROUND);
  g.strokeJoin(ROUND);
  g.noFill();

  civ = MODE == 4;
  if (MODE >= 2) {
    loopSystem(g, bx0, by0, bx1, by1, 0, MODE == 2 ? 4 : 3);
  } else {
    feedbackLoop(g, bx0, by0, bx1, by1, 0);
  }
  if (MODE != 2) runaway(g);

  if (guide) {
    g.noStroke();
    g.fill(255, 30);
    g.rect(0, 0, W, TITLE_Y);
    g.fill(255, 120);
    g.textSize(18);
    g.text("title zone", 40, TITLE_Y - 20);
    g.noFill();
  }
  g.endDraw();
}

// ------------------------------------------------------------------ runaway

// settlements (forks) and frontier ends (tips), collected while growing;
// z holds the branch depth, so low z = old trunk road = big town
ArrayList<PVector> forks = new ArrayList<PVector>();
ArrayList<PVector> tips = new ArrayList<PVector>();
boolean civ;                            // mode 4 extras on/off

void runaway(PGraphics g) {
  forks.clear();
  tips.clear();
  // a few trunks fanned between horizontal and steep up-right, like the cover
  int trunks = civ ? 6 : 5;
  for (int i = 0; i < trunks; i++) {
    float t = i / float(trunks - 1);
    float target = lerp(-0.08, -1.05, t) + random(-0.06, 0.06);
    float a = lerp(-0.25, -0.75, t);
    branch(g, bx0, by1, a, target, 7, 0, (civ ? 3.4 : 2.6) * pow(BOLD, 0.4), int(random(150, 220)));
  }
  if (civ) civilisation(g);
}

float fadeAt(float y) {                 // dissolve under the title
  return constrain((y - TITLE_Y) / 140.0, 0, 1);
}

void branch(PGraphics g, float x, float y, float a, float target,
            float step, int depth, float w, int life) {
  float off = random(1000);             // each branch meanders on its own
  float wig = civ ? 0.46 : 0.32;        // more wander = more organic
  for (int i = 0; i < life; i++) {
    if (++segs > MAX_SEGS) break;
    // meander with noise, pulled back toward this branch's own heading
    a += (noise(off + i * 0.09) - 0.5) * wig + (target - a) * 0.14;
    float nx = x + cos(a) * step, ny = y + sin(a) * step;
    if (nx > W + 10 || nx < -10 || ny > H * 0.94 || ny < TITLE_Y) break;

    boolean inside = insideBox((x + nx) / 2, (y + ny) / 2);
    float fade = fadeAt(ny);
    // taper along the branch, like a root or a river
    float ww = civ ? w * lerp(1.0, 0.55, i / float(life)) : w;
    g.stroke(inside ? INK : CREAM, 255 * fade);
    g.strokeWeight(max(0.4, ww * (inside ? 1.0 : 0.8)));
    g.line(x, y, nx, ny);

    float d = dist(nx, ny, bx0, by1) / W;
    // lights along the routes, thicker the further out (sprawl)
    if (civ && !inside && random(1) < 0.05 + 0.3 * d) {
      float r = random(0.35, 1.0);
      float ox = random(-6, 6), oy = random(-6, 6);
      g.noStroke();
      g.fill(CREAM, 255 * fade * random(0.45, 1));
      g.ellipse(nx + ox, ny + oy, r * 2, r * 2);
      g.noFill();
    }

    // forking gets likelier with distance from the loop: runaway complexity
    float p = civ ? 0.012 + 0.11 * d * d : 0.01 + 0.09 * d * d;
    int maxD = civ ? 9 : 8;
    if (depth < maxD && i > 5 && random(1) < p) {
      float side = random(1) < 0.5 ? -1 : 1;
      float da = side * random(0.25, 0.65);
      float nt = constrain(target + da * 0.5, -1.35, 0.25);
      forks.add(new PVector(nx, ny, depth));
      branch(g, nx, ny, a + da, nt, step * 0.93,
             depth + 1, ww * 0.72, int((life - i) * random(0.5, 0.95)));
    }
    x = nx; y = ny;
  }
  tips.add(new PVector(x, y, depth));
}

// ------------------------------------------------------------------ civilisation

void civilisation(PGraphics g) {
  ArrayList<PVector> all = new ArrayList<PVector>(forks);
  all.addAll(tips);

  // cross-links: frontier ends join up with nearby places, the tree becomes a network
  for (PVector t : tips) {
    if (insideBox(t.x, t.y) || random(1) < 0.25) continue;
    PVector best = null;
    float bd = 60;
    for (PVector o : all) {
      float dd = dist(t.x, t.y, o.x, o.y);
      if (dd > 14 && dd < bd) { bd = dd; best = o; }
    }
    if (best == null) continue;
    g.stroke(CREAM, 170 * min(fadeAt(t.y), fadeAt(best.y)));
    g.strokeWeight(0.55);
    wobblyLine(g, t.x, t.y, best.x, best.y);
  }

  // street grids around a handful of the old, big towns
  for (PVector f : forks) {
    if (f.z > 1 || insideBox(f.x, f.y) || random(1) < 0.5) continue;
    streetGrid(g, f.x, f.y, random(9, 15), random(TWO_PI), fadeAt(f.y));
  }

  // settlements: a hierarchy, few towns, more villages, mostly unmarked forks
  for (PVector f : forks) {
    boolean inside = insideBox(f.x, f.y);
    if (inside && f.z > 0) continue;            // keep the loop readable
    float keep = f.z <= 1 ? 1.0 : f.z <= 3 ? 0.45 : 0.18;
    if (random(1) > keep) continue;
    float r = (f.z == 0 ? 4.2 : f.z <= 2 ? 2.6 : 1.5) * (f.z <= 2 ? pow(BOLD, 0.4) : 1);
    float fade = fadeAt(f.y);
    g.noStroke();
    g.fill(inside ? INK : CREAM, 255 * fade);
    g.ellipse(f.x, f.y, r * 2, r * 2);
    if (f.z == 0 && !inside) {
      g.noFill();
      g.stroke(CREAM, 200 * fade);
      g.strokeWeight(0.7);
      g.ellipse(f.x, f.y, r * 4.2, r * 4.2);
    }
  }
  g.noFill();
}

// a small irregular block plan, rotated to the local road
void streetGrid(PGraphics g, float cx, float cy, float s, float rot, float fade) {
  g.pushMatrix();
  g.translate(cx, cy);
  g.rotate(rot);
  g.stroke(CREAM, 120 * fade);
  g.strokeWeight(0.4);
  int n = int(random(3, 5));
  for (int k = 0; k < n; k++) {
    float u = map(k, 0, n - 1, -s, s) + random(-2, 2);
    float a0 = -s * random(0.5, 1), a1 = s * random(0.5, 1);
    g.line(u, a0, u + random(-1.5, 1.5), a1);             // one direction
    float v = map(k, 0, n - 1, -s, s) + random(-2, 2);
    float b0 = -s * random(0.5, 1), b1 = s * random(0.5, 1);
    g.line(b0, v, b1, v + random(-1.5, 1.5));             // the other
  }
  g.popMatrix();
  g.noFill();
}

boolean insideBox(float x, float y) {
  return x > bx0 && x < bx1 && y > by0 && y < by1;
}

// ------------------------------------------------------------------ recursion

// A loop containing two smaller loops, each containing two more ...
void loopSystem(PGraphics g, float x0, float y0, float x1, float y1,
                int level, int maxLevel) {
  feedbackLoop(g, x0, y0, x1, y1, level);
  float w = x1 - x0, h = y1 - y0;
  if (level >= maxLevel || w < 40) return;
  float k = 0.40;                       // child scale
  float pad = w * 0.09;
  float cw = w * k, ch = h * k;
  // lower-left child and upper-right child, along the diagonal of growth
  float jx = random(-0.02, 0.02) * w, jy = random(-0.02, 0.02) * h;
  loopSystem(g, x0 + pad + jx, y1 - pad - ch + jy, x0 + pad + cw + jx, y1 - pad + jy,
             level + 1, maxLevel);
  loopSystem(g, x1 - pad - cw - jx, y0 + pad * 1.6 - jy, x1 - pad - jx, y0 + pad * 1.6 + ch - jy,
             level + 1, maxLevel);
  // the two subsystems talk: a thin channel between their nodes
  g.stroke(INK);
  g.strokeWeight(max(0.5, 1.3 - level * 0.3));
  wobblyLine(g, x0 + pad + cw + jx, y1 - pad - ch * 0.5 + jy,
             x1 - pad - cw - jx, y0 + pad * 1.6 + ch * 0.5 - jy);
}

// One hand-drawn control loop: node, forward path, amplifier, comparator.
void feedbackLoop(PGraphics g, float x0, float y0, float x1, float y1, int level) {
  float bf = level == 0 ? BOLD : level == 1 ? sqrt(BOLD) : 1;
  float sw = max(0.5, 2.6 * pow(0.62, level)) * bf;
  float u = (x1 - x0) / (bx1 - bx0) * pow(bf, 0.6);  // symbol scale relative to the main loop
  g.stroke(INK);
  g.strokeWeight(sw);
  g.noFill();
  wobblyLine(g, x0, y1, x1, y1);        // bottom, left to right
  wobblyLine(g, x1, y1, x1, y0);        // right, upward
  wobblyLine(g, x1, y0, x0, y0);        // top, right to left
  wobblyLine(g, x0, y0, x0, y1);        // left, downward
  float ah = max(3, 16 * u);
  arrow(g, lerp(x0, x1, 0.5), y1, 0, ah);
  arrow(g, x1, lerp(y1, y0, 0.25), -HALF_PI, ah);
  arrow(g, x1, lerp(y1, y0, 0.75), -HALF_PI, ah);
  arrow(g, lerp(x1, x0, 0.25), y0, PI, ah);
  arrow(g, lerp(x1, x0, 0.75), y0, PI, ah);
  arrow(g, x0, lerp(y0, y1, 0.5), HALF_PI, ah);
  // amplifier triangle on the right
  float ts = 26 * u, ty = lerp(y1, y0, 0.55);
  g.fill(RED);
  g.triangle(x1, ty - ts, x1 - ts * 0.75, ty + ts * 0.5, x1 + ts * 0.75, ty + ts * 0.5);
  // comparator on the top
  float r = 30 * u, cx = lerp(x0, x1, 0.5);
  g.ellipse(cx, y0, r * 2, r * 2);
  g.noFill();
  g.ellipse(cx, y0, r * 0.9, r * 0.9);
  g.line(cx - r * 0.3, y0 - r * 0.1, cx + r * 0.3, y0 - r * 0.1);
  g.line(cx - r * 0.3, y0 + r * 0.12, cx + r * 0.3, y0 + r * 0.12);
  // node
  g.noStroke();
  g.fill(INK);
  g.ellipse(x0, y1, max(3, 14 * u), max(3, 14 * u));
  g.noFill();
}

void arrow(PGraphics g, float x, float y, float a, float s) {
  g.pushMatrix();
  g.translate(x, y);
  g.rotate(a);
  g.noStroke();
  g.fill(INK);
  g.triangle(s * 0.6, 0, -s * 0.6, -s * 0.38, -s * 0.6, s * 0.38);
  g.popMatrix();
  g.noFill();
  g.stroke(INK);
}

// hand-drawn line: low-frequency noise wobble, pinned near the ends
void wobblyLine(PGraphics g, float x1, float y1, float x2, float y2) {
  float d = dist(x1, y1, x2, y2);
  if (d < 1) return;
  int n = max(2, int(d / 5));
  float nx = -(y2 - y1) / d, ny = (x2 - x1) / d;
  float off = random(1000);
  float amp = min(3.2, d * 0.012);
  g.beginShape();
  for (int i = 0; i <= n; i++) {
    float t = i / float(n);
    float o = (noise(off + t * d * 0.012) - 0.5) * 2 * amp * sin(PI * t);
    g.vertex(lerp(x1, x2, t) + nx * o, lerp(y1, y2, t) + ny * o);
  }
  g.endShape();
}