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