214 lines, 17 of them comments · 4,947 tokens, written in 63 seconds for $0.125


  1  // Differential Growth — an organic curve that outgrows its own space
  2  // space = reseed, drag = push the membrane
  3  
  4  let nodes = [];
  5  let ghost;
  6  let grid;
  7  let hueBase = 190;
  8  const DESIRED = 9;      // preferred spacing between nodes
  9  const REPEL_R = 17;     // repulsion radius
 10  const MAX_EDGE = 13;    // split edges longer than this
 11  const MAX_NODES = 2600;
 12  
 13  function setup() {
 14    createCanvas(windowWidth, windowHeight);
 15    colorMode(HSB, 360, 100, 100, 1);
 16    ghost = createGraphics(width, height);
 17    ghost.colorMode(HSB, 360, 100, 100, 1);
 18    ghost.clear();
 19    reseed();
 20  }
 21  
 22  function windowResized() {
 23    resizeCanvas(windowWidth, windowHeight);
 24    ghost = createGraphics(width, height);
 25    ghost.colorMode(HSB, 360, 100, 100, 1);
 26    ghost.clear();
 27    reseed();
 28  }
 29  
 30  function reseed() {
 31    nodes = [];
 32    hueBase = random(360);
 33    const n = 36;
 34    const r = min(width, height) * 0.06;
 35    for (let i = 0; i < n; i++) {
 36      const a = (TWO_PI * i) / n;
 37      nodes.push(
 38        createVector(width / 2 + cos(a) * r, height / 2 + sin(a) * r * 0.9)
 39      );
 40    }
 41    if (ghost) ghost.clear();
 42  }
 43  
 44  function buildGrid() {
 45    grid = new Map();
 46    for (let i = 0; i < nodes.length; i++) {
 47      const cx = Math.floor(nodes[i].x / REPEL_R);
 48      const cy = Math.floor(nodes[i].y / REPEL_R);
 49      const k = cx + ',' + cy;
 50      let bucket = grid.get(k);
 51      if (!bucket) { bucket = []; grid.set(k, bucket); }
 52      bucket.push(i);
 53    }
 54  }
 55  
 56  function step() {
 57    const N = nodes.length;
 58    if (N < 3) return;
 59    buildGrid();
 60  
 61    const cx0 = width / 2;
 62    const cy0 = height / 2;
 63    const bound = min(width, height) * 0.46;
 64    const forces = new Array(N);
 65  
 66    for (let i = 0; i < N; i++) {
 67      const cur = nodes[i];
 68      const prev = nodes[(i - 1 + N) % N];
 69      const next = nodes[(i + 1) % N];
 70      const f = createVector(0, 0);
 71  
 72      // --- spring attraction toward neighbours ---
 73      for (const nb of [prev, next]) {
 74        const d = p5.Vector.sub(nb, cur);
 75        const len = d.mag();
 76        if (len > 0.0001) {
 77          d.mult(((len - DESIRED) * 0.22) / len);
 78          f.add(d);
 79        }
 80      }
 81  
 82      // --- smoothing toward the midpoint of neighbours ---
 83      const mid = p5.Vector.add(prev, next).mult(0.5);
 84      f.add(mid.sub(cur).mult(0.14));
 85  
 86      // --- repulsion from nearby nodes (spatial hash) ---
 87      const gx = Math.floor(cur.x / REPEL_R);
 88      const gy = Math.floor(cur.y / REPEL_R);
 89      for (let ox = -1; ox <= 1; ox++) {
 90        for (let oy = -1; oy <= 1; oy++) {
 91          const bucket = grid.get((gx + ox) + ',' + (gy + oy));
 92          if (!bucket) continue;
 93          for (let b = 0; b < bucket.length; b++) {
 94            const j = bucket[b];
 95            if (j === i) continue;
 96            const dx = cur.x - nodes[j].x;
 97            const dy = cur.y - nodes[j].y;
 98            const d2 = dx * dx + dy * dy;
 99            if (d2 > REPEL_R * REPEL_R || d2 < 0.0001) continue;
100            const d = Math.sqrt(d2);
101            const w = (1 - d / REPEL_R) * 1.35;
102            f.x += (dx / d) * w;
103            f.y += (dy / d) * w;
104          }
105        }
106      }
107  
108      // --- gentle containment ---
109      const dcx = cur.x - cx0;
110      const dcy = cur.y - cy0;
111      const dc = Math.sqrt(dcx * dcx + dcy * dcy);
112      if (dc > bound) {
113        const pull = (dc - bound) * 0.05;
114        f.x -= (dcx / dc) * pull;
115        f.y -= (dcy / dc) * pull;
116      }
117  
118      // --- mouse pressure ---
119      if (mouseIsPressed) {
120        const mx = cur.x - mouseX;
121        const my = cur.y - mouseY;
122        const md = Math.sqrt(mx * mx + my * my);
123        if (md < 90 && md > 0.001) {
124          const w = (1 - md / 90) * 3.2;
125          f.x += (mx / md) * w;
126          f.y += (my / md) * w;
127        }
128      }
129  
130      // --- a whisper of noise so growth stays irregular ---
131      const nz = noise(cur.x * 0.004, cur.y * 0.004, frameCount * 0.004);
132      const ang = nz * TWO_PI * 3;
133      f.x += cos(ang) * 0.11;
134      f.y += sin(ang) * 0.11;
135  
136      f.limit(1.6);
137      forces[i] = f;
138    }
139  
140    for (let i = 0; i < N; i++) nodes[i].add(forces[i]);
141  
142    // --- subdivision: edges that stretch too far spawn new nodes ---
143    if (nodes.length < MAX_NODES) {
144      for (let i = nodes.length - 1; i >= 0; i--) {
145        const a = nodes[i];
146        const b = nodes[(i + 1) % nodes.length];
147        if (p5.Vector.dist(a, b) > MAX_EDGE) {
148          nodes.splice(i + 1, 0, p5.Vector.add(a, b).mult(0.5));
149        }
150      }
151      // occasional spontaneous insertion keeps the growth asymmetric
152      if (random() < 0.5 && nodes.length < MAX_NODES) {
153        const i = floor(random(nodes.length));
154        const a = nodes[i];
155        const b = nodes[(i + 1) % nodes.length];
156        const m = p5.Vector.add(a, b).mult(0.5);
157        m.add(p5.Vector.random2D().mult(0.6));
158        nodes.splice(i + 1, 0, m);
159      }
160    }
161  }
162  
163  function traceShape(g, closeIt) {
164    g.beginShape();
165    g.curveVertex(nodes[nodes.length - 1].x, nodes[nodes.length - 1].y);
166    for (let i = 0; i < nodes.length; i++) g.curveVertex(nodes[i].x, nodes[i].y);
167    g.curveVertex(nodes[0].x, nodes[0].y);
168    g.curveVertex(nodes[1].x, nodes[1].y);
169    g.endShape(closeIt ? CLOSE : undefined);
170  }
171  
172  function draw() {
173    for (let s = 0; s < 2; s++) step();
174  
175    // stamp a faint memory of the current outline
176    if (frameCount % 14 === 0 && nodes.length > 4) {
177      ghost.noFill();
178      ghost.strokeWeight(1);
179      ghost.stroke((hueBase + nodes.length * 0.06) % 360, 55, 95, 0.075);
180      traceShape(ghost, true);
181    }
182  
183    background(222, 38, 7);
184    image(ghost, 0, 0);
185  
186    if (nodes.length > 4) {
187      // body: slightly darker than the ghosts so the loop reads as solid
188      noStroke();
189      fill(224, 45, 9, 0.82);
190      traceShape(this, true);
191  
192      // living edge
193      noFill();
194      strokeWeight(1.4);
195      stroke((hueBase + nodes.length * 0.06) % 360, 45, 100, 0.9);
196      traceShape(this, true);
197  
198      // nodes glimmer
199      strokeWeight(1);
200      stroke((hueBase + 40) % 360, 30, 100, 0.25);
201      for (let i = 0; i < nodes.length; i += 7) point(nodes[i].x, nodes[i].y);
202    }
203  
204    noStroke();
205    fill(0, 0, 100, 0.35);
206    textSize(11);
207    textFont('monospace');
208    text('nodes ' + nodes.length + '   [space] reseed   [drag] push', 14, height - 14);
209  }
210  
211  function keyPressed() {
212    if (key === ' ') reseed();
213  }
214