Source of: Chladni Drift, by Claude Opus 5
129 lines, 8 of them comments · 5,179 tokens, written in 72 seconds for $0.131
1 // Chladni Drift — particles settling on nodal lines of a vibrating plate
2
3 let parts = [];
4 const NUM = 14000;
5 const MODES = [[1,2],[2,3],[3,4],[1,4],[2,5],[3,5],[4,5],[1,3],[2,4],[3,6],[5,6],[4,7],[2,7],[5,7],[1,6]];
6 let mi = 0;
7 let tick = 0;
8 const PERIOD = 420, HOLD = 300;
9
10 let plate, px0, py0;
11 let buckets = [];
12 const COLS = [
13 'rgba(255,248,226,0.55)',
14 'rgba(255,206,130,0.30)',
15 'rgba(236,126,96,0.17)',
16 'rgba(138,100,206,0.11)',
17 'rgba(62,76,165,0.07)'
18 ];
19
20 function setup() {
21 createCanvas(windowWidth, windowHeight);
22 pixelDensity(1);
23 layout();
24 for (let i = 0; i < NUM; i++) parts.push({ x: Math.random(), y: Math.random() });
25 for (let b = 0; b < COLS.length; b++) buckets.push([]);
26 background(6, 7, 12);
27 textFont('monospace');
28 }
29
30 function layout() {
31 plate = Math.min(width, height) * 0.86;
32 px0 = (width - plate) / 2;
33 py0 = (height - plate) / 2;
34 }
35
36 function windowResized() {
37 resizeCanvas(windowWidth, windowHeight);
38 layout();
39 background(6, 7, 12);
40 }
41
42 function mousePressed() {
43 mi = (mi + 1) % MODES.length;
44 tick = HOLD;
45 }
46
47 function draw() {
48 const ctx = drawingContext;
49
50 // slow fade so figures accumulate as light
51 ctx.globalCompositeOperation = 'source-over';
52 noStroke();
53 fill(6, 7, 12, 26);
54 rect(0, 0, width, height);
55
56 // ---- mode interpolation ----
57 let k = constrain(map(tick, HOLD, PERIOD, 0, 1), 0, 1);
58 k = k * k * (3 - 2 * k); // smoothstep
59 const a = MODES[mi], b = MODES[(mi + 1) % MODES.length];
60 const n = lerp(a[0], b[0], k);
61 const m = lerp(a[1], b[1], k);
62 const trans = (k > 0 && k < 1) ? Math.sin(Math.PI * k) : 0;
63
64 tick++;
65 if (tick >= PERIOD) { tick = 0; mi = (mi + 1) % MODES.length; }
66
67 // ---- update grains ----
68 for (let i = 0; i < buckets.length; i++) buckets[i].length = 0;
69
70 const kn = Math.PI * n, km = Math.PI * m;
71 const mx = (mouseX - px0) / plate, my = (mouseY - py0) / plate;
72 const mouseOn = mouseX > 0 && mouseY > 0 && mouseX < width && mouseY < height;
73
74 for (let i = 0; i < NUM; i++) {
75 const p = parts[i];
76 const v = Math.sin(kn * p.x) * Math.sin(km * p.y) +
77 Math.sin(km * p.x) * Math.sin(kn * p.y);
78 const amp = Math.abs(v) * 0.5;
79
80 const s = 0.0005 + amp * 0.042 * (1 + 2.2 * trans);
81 const ang = Math.random() * Math.PI * 2;
82 p.x += Math.cos(ang) * s;
83 p.y += Math.sin(ang) * s;
84
85 if (mouseOn) {
86 const dx = p.x - mx, dy = p.y - my;
87 const d2 = dx * dx + dy * dy;
88 if (d2 < 0.0125 && d2 > 1e-7) {
89 const f = (0.0028 / (d2 + 0.0012));
90 p.x += dx * f; p.y += dy * f;
91 }
92 }
93
94 // reflect at plate edges
95 if (p.x < 0) p.x = -p.x; else if (p.x > 1) p.x = 2 - p.x;
96 if (p.y < 0) p.y = -p.y; else if (p.y > 1) p.y = 2 - p.y;
97
98 let bi = amp < 0.02 ? 0 : amp < 0.07 ? 1 : amp < 0.2 ? 2 : amp < 0.45 ? 3 : 4;
99 const arr = buckets[bi];
100 arr.push(px0 + p.x * plate, py0 + p.y * plate);
101 }
102
103 // ---- render additively, grouped by brightness ----
104 ctx.globalCompositeOperation = 'lighter';
105 for (let bi = buckets.length - 1; bi >= 0; bi--) {
106 const arr = buckets[bi];
107 if (!arr.length) continue;
108 ctx.fillStyle = COLS[bi];
109 const sz = bi === 0 ? 1.6 : 1.2;
110 for (let j = 0; j < arr.length; j += 2) ctx.fillRect(arr[j], arr[j + 1], sz, sz);
111 }
112
113 // ---- frame & label ----
114 ctx.globalCompositeOperation = 'source-over';
115 noFill();
116 stroke(255, 255, 255, 22);
117 strokeWeight(1);
118 rect(px0 - 0.5, py0 - 0.5, plate + 1, plate + 1);
119
120 noStroke();
121 fill(255, 236, 200, 150);
122 textSize(12);
123 textAlign(LEFT, TOP);
124 text('mode n=' + nf(n, 1, 2) + ' m=' + nf(m, 1, 2), px0, py0 - 22);
125 textAlign(RIGHT, TOP);
126 fill(255, 236, 200, 80);
127 text('click: next harmonic', px0 + plate, py0 - 22);
128 }
129