Source of: Differential Growth, by Claude Opus 5
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