Differential Growth

click to restart sim

let startingNodes = 10;
let nodes = [];
let maxForce = 0.1;
let maxNodes;
let edgeBreak = 10;
let iter = 1;
let qtree;
let c1, c2, c3, c4, c5;

function setup() {
  createCanvas(400, 400);
  frameRate(60);
  init();

  // # Color Palette
  c1 = color(255, 205, 178);
  c2 = color(255, 180, 162);
  c3 = color(229, 152, 155);
  c4 = color(181, 131, 141);
  c5 = color(109, 104, 117);

  maxNodes = height * 2;
}

function init() {
  nodes = [];
  let radius = (edgeBreak * startingNodes) / TWO_PI; // calculate radius to position points right before edgebreak

  for (var i = 0; i < TWO_PI; i += TWO_PI / startingNodes) {
    let x = width / 2 + cos(i) * radius;
    let y = height / 2 + sin(i) * radius;
    nodes.push(createVector(x, y));
  }
}

function draw() {
  let boundary = new Rectangle(width / 2, height / 2, width, height);
  qtree = new QuadTree(boundary, 4);

  for (var i = 0; i < nodes.length; i++) {
    qtree.insert(new Particle(nodes[i].x, nodes[i].y));
  }


  background(c5);
  display();

  if (nodes.length < maxNodes) {

    for (let l = 0; l < iter; l++) {
      //addRandom();
      subdivide();
      repulsion();
    }
  }
}

function mouseClicked() {
  init();
}


function addRandom() {
  if (frameCount % getFrameRate() == 0) // 1 random per second
  {
    let r = int(random(0, nodes.length));
    let m = midpoint(nodes[fixId(r)], nodes[fixId(r + 1)]);
    splice(nodes, m, r + 1);
  }
}

function getEdgeBreakNoise(x, y) {
  return noise(x / 100, y / 100) * 30 + 1;
}

function subdivide() {
  for (var i = nodes.length - 1; i >= 0; i--) {
    edgeBreak = getEdgeBreakNoise(nodes[i].x, nodes[i].y);

    let n1 = nodes[fixId(i)];
    let n2 = nodes[fixId(i + 1)];
    if (distSq(n1, n2) > edgeBreak ** 2) {
      splice(nodes, midpoint(n1, n2), i + 1);
    }

  }
}

function repulsion() {
  let repulsionForces = [];
  for (var i = 0; i < nodes.length; i++) {
    edgeBreak = getEdgeBreakNoise(nodes[i].x, nodes[i].y);
    let seek = createVector();
    // quadtree request
    let request = qtree.query(new Circle(nodes[i].x, nodes[i].y, edgeBreak));
    for (let other of request) {
      if (nodes[i] != other.pos) {
        let d = nodes[i].dist(other.pos);
        let diff = p5.Vector.sub(nodes[i], other.pos);
        diff.mult(exp(edgeBreak - d)); // invertly proportional
        // this makes it so that the influence inside the edgebreak radius is big
        // the influence at the edgebreak radius is one
        // the influence outside the edgebreak radius is decreasingly proportional
        seek.add(diff);
      }
    }
    repulsionForces[i] = seek;
  }
  for (let i = 0; i < nodes.length; i++) {
    nodes[i].add(repulsionForces[i].limit(maxForce));
  }
}

function display() {

  //beginShape();
  for (var i = 0; i < nodes.length; i++) {
    noStroke();


    let colorSwitch = getEdgeBreakNoise(nodes[i].x, nodes[i].y) / 30
    let c = lerpColorFive(c1, c2, c3, c4, c5, colorSwitch);
    fill(c);
    //circle(nodes[i].x, nodes[i].y, colorSwitch*7);
    stroke(c);
    strokeWeight(3);
    noFill();
    //curveVertex(nodes[i].x, nodes[i].y);
    line(nodes[i].x, nodes[i].y, nodes[fixId(i + 1)].x, nodes[fixId(i + 1)].y);
  }
  //endShape(CLOSE);
}

// ##### Quadtree Helper Classes

class Rectangle {
  constructor(x, y, w, h) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
  }

  contains(point) {
    return (point.pos.x >= this.x - this.w &&
      point.pos.x <= this.x + this.w &&
      point.pos.y >= this.y - this.h &&
      point.pos.y <= this.y + this.h);
  }

  intersects(range) {
    return !(range.x - range.w > this.x + this.w ||
      range.x + range.w < this.x - this.w ||
      range.y - range.h > this.y + this.h ||
      range.y + range.h < this.y - this.h);
  }
}

class Circle {
  constructor(x, y, r) {
    this.x = x;
    this.y = y;
    this.r = r;
  }

  contains(point) {
    //let d = distSq(point.pos, createVector(this.x,this.y));
    let d = (point.pos.x - this.x) ** 2 + (point.pos.y - this.y) ** 2;
    return d <= this.r ** 2;
  }

  intersects(range) {

    var xDist = Math.abs(range.x - this.x);
    var yDist = Math.abs(range.y - this.y);

    // radius of the circle
    var r = this.r;

    var w = range.w;
    var h = range.h;

    var edges = (xDist - w) ** 2 + (yDist - h) ** 2;

    // no intersection
    if (xDist > (r + w) || yDist > (r + h))
      return false;

    // intersection within the circle
    if (xDist <= w || yDist <= h)
      return true;

    // intersection on the edge of the circle
    return edges <= this.r ** 2;
  }
}

// ##### Quadtree Class

class QuadTree {
  constructor(boundary, capacity) {
    if (!boundary) {
      throw TypeError('boundary is null or undefined');
    }
    if (!(boundary instanceof Rectangle)) {
      throw TypeError('boundary should be a Rectangle');
    }
    if (typeof capacity !== 'number') {
      throw TypeError(`capacity should be a number but is a ${typeof capacity}`);
    }
    if (capacity < 1) {
      throw RangeError('capacity must be greater than 0');
    }
    this.boundary = boundary;
    this.capacity = capacity;
    this.points = [];
    this.divided = false;
  }

  subdivide() {
    let x = this.boundary.x;
    let y = this.boundary.y;
    let w = this.boundary.w / 2;
    let h = this.boundary.h / 2;

    let ne = new Rectangle(x + w, y - h, w, h);
    this.northeast = new QuadTree(ne, this.capacity);
    let nw = new Rectangle(x - w, y - h, w, h);
    this.northwest = new QuadTree(nw, this.capacity);
    let se = new Rectangle(x + w, y + h, w, h);
    this.southeast = new QuadTree(se, this.capacity);
    let sw = new Rectangle(x - w, y + h, w, h);
    this.southwest = new QuadTree(sw, this.capacity);

    this.divided = true;
  }

  insert(point) {
    if (!this.boundary.contains(point)) {
      return false;
    }

    if (this.points.length < this.capacity) {
      this.points.push(point);
      return true;
    }

    if (!this.divided) {
      this.subdivide();
    }

    if (this.northeast.insert(point) || this.northwest.insert(point) ||
      this.southeast.insert(point) || this.southwest.insert(point)) {
      return true;
    }
  }

  query(range, found) {
    if (!found) {
      found = [];
    }

    if (!range.intersects(this.boundary)) {
      return found;
    }

    for (let p of this.points) {
      if (range.contains(p)) {
        found.push(p);
      }
    }
    if (this.divided) {
      this.northwest.query(range, found);
      this.northeast.query(range, found);
      this.southwest.query(range, found);
      this.southeast.query(range, found);
    }

    return found;
  }

  renderPoints(){
    
    for(let p of this.points){
      let l = lerpColorFive(c1,c2,c3,c4,c5, p.col);
      fill(l,255);
      circle(p.pos.x,p.pos.y,diameter);
    }

    if (this.divided) {
      this.northwest.renderPoints();
      this.northeast.renderPoints();
      this.southwest.renderPoints();
      this.southeast.renderPoints();
    }
  }
  
  renderQuads(){
    if (this.divided) {
      this.northwest.renderQuads();
      this.northeast.renderQuads();
      this.southwest.renderQuads();
      this.southeast.renderQuads();
    }
    else{        
      rect(this.boundary.x, this.boundary.y, this.boundary.w*2-3, this.boundary.h*2-3); 
    }
  }
}


class Particle {
  constructor(x,y) {

      this.pos = createVector(x,y);

    }
  }

function fixId(i) {
  if (i >= 0) {
    return i % nodes.length;
  } else if (i < 0) {
    return nodes.length + i
  }
}

function midpoint(v1, v2) {
  return (createVector((v1.x + v2.x) / 2, (v1.y + v2.y) / 2));
}

function distSq(v1, v2) {
  return (v2.x - v1.x) ** 2 + (v2.y - v1.y) ** 2;
}

function lerpColorFive(c1, c2, c3, c4, c5, i)
{
  if(i <= 0.25)       {return lerpColor(c1,c2, map(i,0,0.25,0,1));}
  else if(i <= 0.5)   {return lerpColor(c2,c3, map(i,0.25,0.5,0,1));}
  else if(i <= 0.75)  {return lerpColor(c3,c4, map(i,0.5,0.75,0,1));}
  else                {return lerpColor(c4,c5, map(i,0.75,1,0,1));}
}