Genuary 2026.27: Lifeform

So this one really fits better for Genuary 2026.25: Organic Geometry and that one is a lifeform like this one, so… we’ll consider them swapped or something.

Anyways, spawn branching nodes and draw a bunch of squares. Not only an organic looking lifeform but creepy to boot! I do love it without borders and with fade.

Be careful with high child count without either a high segment length or death rate to compensate, it will get slow.


let gui;
let params = {
  spawnRate: 0.25, spawnRateMin: 0, spawnRateMax: 1, spawnRateStep: 0.01,
  deathRate: 0.0001, deathRateMin: 0, deathRateMax: 0.01, deathRateStep: 0.0001,
  segmentLength: 10, segmentLengthMin: 10, segmentLengthMax: 100,
  maxLength: 40, maxLengthMin: 10, maxLengthMax: 100,
  childrenCount: 2, childrenCountMin: 2, childrenCountMax: 10,
  wigglyness: 3, wigglynessMin: 1, wigglynessMax: 5, 
  straightenChance: 0.5, straightenChanceMin: 0, straightenChanceMax: 1, straightenChanceStep: 0.01,
  roots: 7, rootsMin: 1, rootsMax: 16,
  borders: false,
  colorful: true,
  fade: true,
};

let roots;

let lastParams;
let pauseUntil;

function setup() {
  createCanvas(400, 400);
  colorMode(HSB, 360, 100, 100, 100);
  
  gui = createGuiPanel("params");
  gui.addObject(params);
  gui.setPosition(420, 0);

  reset();
}

class Node {
  constructor(theta, hue) {
    this.hue = hue || random(360);
    this.theta = theta;
    this.children = [];
  }
  
  update(length = 0, segment = 0) {
    let spawn = false;
    
    // If we don't have at least 1 child, we can spawn
    spawn |= (
      length < params.maxLength
      && this.children.length < 1
      && random() < params.spawnRate
    );
    
    // If we have at least one but we're at the right segment length, we can also spawn
    spawn |= (
      length < params.maxLength
      && this.children.length <= params.childrenCount
      && segment == params.segmentLength
      && random() < params.spawnRate
    );
    
    if (spawn) {
      let nextHue = this.hue + (random() - random()) * 10;
      while (nextHue < 0) nextHue += 360;
      while (nextHue > 360) nextHue -= 360;
      this.children.push(new Node(this.theta, nextHue));
    }
    
    this.hue = this.hue + (random() - random());
    while (this.hue < 0) this.hue += 360;
    while (this.hue > 360) this.hue -= 360;
    
    this.theta += (random() - random()) / (10 ** (5 - params.wigglyness));
    while (this.theta < 0) this.theta += TWO_PI;
    while (this.theta > TWO_PI) this.theta -= TWO_PI;
    
    let nextSegment = this.children.length > 1 ? 0 : segment + 1;
    this.children.forEach((c) => c.update(length + 1, nextSegment));
    
    if (random() < params.straightenChance) {
      if (this.children.count == 1) {
        this.children.forEach((c) => c.theta = this.theta);
      }
    }
    
    for (let i = this.children.length - 1; i >= 0; i--) {
      if (random() < params.deathRate) {
        this.children.splice(i, 1);
      }
    }
  }
  
  draw() {
    if (params.borders) {
      stroke("black");
    } else {
      noStroke();
    }
    
    if (params.colorful) {
      fill(this.hue, 100, 100);
    } else {
      fill("white");
    }
    
    rect(0, 0, 10, 10);
    push();
    {
      rotate(this.theta);
      translate(4, 0);
      this.children.forEach((c) => c.draw());      
    }
    pop();
  }
}

function reset() {
  roots = [];
  for (let i = 0; i < params.roots; i++) {
    roots.push(new Node(0))
  }
}

function draw() {
  const paramsChanged = lastParams == undefined || Object.keys(params).some((k) => 
    params[k] !== lastParams[k]
  );
  
  if (paramsChanged) {
    reset();
    lastParams = {...params};
    pauseUntil = undefined;
  }

  if (pauseUntil) {
    if (millis() < pauseUntil) {
      return;
    } else {
      reset();
    }
  }
  
  if (params.fade) {
    fill(0, 0, 0, 3);
    rect(0, 0, width, height);
  } else {
    background("black");
  }
  
  roots.forEach((r) => r.update());
  
  push();
  {
    if (params.roots == 1) {
      translate(width / 2, 3 * height / 4);
    } else {
      translate(width / 2, height / 2);
    }
    rotate(-PI / 2);
    roots.forEach((r) => {
      rotate(TWO_PI / params.roots);
      r.draw();
    });
  }
  pop();
}