repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

5.2 KB · 165 lines · JavaScript Raw History
  1// Physarum. Each agent senses three points ahead, steers toward the strongest
  2// trail and deposits its own, and the grid is blurred and decayed every frame.
  3// It runs at half resolution and upscales on draw, or 6000 agents drop frames.
  4
  5export const slimemold = (cvs, { numAgents = 6000 } = {}) => {
  6  const ctx = cvs.getContext("2d");
  7
  8  const resize = () => {
  9    cvs.width = cvs.offsetWidth;
 10    cvs.height = cvs.offsetHeight;
 11  };
 12  resize();
 13  window.addEventListener("resize", resize);
 14
 15  const scale = 2;
 16  const tw = Math.max(1, Math.floor(cvs.width / scale));
 17  const th = Math.max(1, Math.floor(cvs.height / scale));
 18  let trail = new Float32Array(tw * th);
 19  let trailNext = new Float32Array(tw * th);
 20
 21  // Seeded in a disc, all facing inward. Starting them uniformly at random
 22  // gives a mush that takes far longer to organise into anything.
 23  const agents = new Array(numAgents);
 24  for (let i = 0; i < numAgents; i++) {
 25    const cx = tw / 2;
 26    const cy = th / 2;
 27    const r = Math.sqrt(Math.random()) * Math.min(tw, th) * 0.4;
 28    const a = Math.random() * Math.PI * 2;
 29    const x = cx + Math.cos(a) * r;
 30    const y = cy + Math.sin(a) * r;
 31    agents[i] = { x, y, heading: Math.atan2(cy - y, cx - x) };
 32  }
 33
 34  const sensorAngle = Math.PI / 4;
 35  const sensorDistance = 9;
 36  const rotationAngle = Math.PI / 8;
 37  const moveSpeed = 1;
 38  const decay = 0.96;
 39  const depositAmount = 0.5;
 40
 41  const sense = (x, y, heading, offset) => {
 42    const angle = heading + offset;
 43    const sx = Math.floor(x + Math.cos(angle) * sensorDistance);
 44    const sy = Math.floor(y + Math.sin(angle) * sensorDistance);
 45    if (sx < 0 || sx >= tw || sy < 0 || sy >= th) return -1;
 46    return trail[sy * tw + sx];
 47  };
 48
 49  const stops = [
 50    [0.0, 0, 0, 0],
 51    [0.35, 14, 63, 244],
 52    [0.7, 132, 43, 255],
 53    [1.0, 255, 255, 255],
 54  ];
 55  const colorLUT = new Uint8ClampedArray(256 * 3);
 56  for (let i = 0; i < 256; i++) {
 57    const t = i / 255;
 58    let s = 0;
 59    while (s < stops.length - 2 && t > stops[s + 1][0]) s++;
 60    const [t0, r0, g0, b0] = stops[s];
 61    const [t1, r1, g1, b1] = stops[s + 1];
 62    const k = (t - t0) / (t1 - t0);
 63    colorLUT[i * 3] = Math.round(r0 + (r1 - r0) * k);
 64    colorLUT[i * 3 + 1] = Math.round(g0 + (g1 - g0) * k);
 65    colorLUT[i * 3 + 2] = Math.round(b0 + (b1 - b0) * k);
 66  }
 67
 68  const offCanvas = document.createElement("canvas");
 69  offCanvas.width = tw;
 70  offCanvas.height = th;
 71  const offCtx = offCanvas.getContext("2d");
 72  const offImageData = offCtx.createImageData(tw, th);
 73  const offData = offImageData.data;
 74
 75  let frame = null;
 76  let running = false;
 77
 78  const step = () => {
 79    for (let i = 0; i < numAgents; i++) {
 80      const agent = agents[i];
 81      const f = sense(agent.x, agent.y, agent.heading, 0);
 82      const l = sense(agent.x, agent.y, agent.heading, -sensorAngle);
 83      const r = sense(agent.x, agent.y, agent.heading, sensorAngle);
 84
 85      if (f > l && f > r) {
 86        // strongest straight ahead: hold course
 87      } else if (f < l && f < r) {
 88        agent.heading += (Math.random() - 0.5) * 2 * rotationAngle;
 89      } else if (l > r) {
 90        agent.heading -= rotationAngle;
 91      } else if (r > l) {
 92        agent.heading += rotationAngle;
 93      }
 94
 95      agent.x += Math.cos(agent.heading) * moveSpeed;
 96      agent.y += Math.sin(agent.heading) * moveSpeed;
 97
 98      if (agent.x < 0 || agent.x >= tw || agent.y < 0 || agent.y >= th) {
 99        agent.x = Math.max(0, Math.min(tw - 1, agent.x));
100        agent.y = Math.max(0, Math.min(th - 1, agent.y));
101        agent.heading = Math.random() * Math.PI * 2;
102      }
103
104      const idx = Math.floor(agent.y) * tw + Math.floor(agent.x);
105      trail[idx] = Math.min(1, trail[idx] + depositAmount);
106    }
107
108    // 3x3 box blur plus decay, clamped at the edges.
109    for (let y = 0; y < th; y++) {
110      for (let x = 0; x < tw; x++) {
111        const x0 = x === 0 ? x : x - 1;
112        const x2 = x === tw - 1 ? x : x + 1;
113        const y0 = y === 0 ? y : y - 1;
114        const y2 = y === th - 1 ? y : y + 1;
115        const sum =
116          trail[y0 * tw + x0] +
117          trail[y0 * tw + x] +
118          trail[y0 * tw + x2] +
119          trail[y * tw + x0] +
120          trail[y * tw + x] +
121          trail[y * tw + x2] +
122          trail[y2 * tw + x0] +
123          trail[y2 * tw + x] +
124          trail[y2 * tw + x2];
125        trailNext[y * tw + x] = (sum / 9) * decay;
126      }
127    }
128    const tmp = trail;
129    trail = trailNext;
130    trailNext = tmp;
131
132    const len = tw * th;
133    for (let i = 0; i < len; i++) {
134      const v = trail[i];
135      const ci =
136        (v >= 1 ? 255 : v <= 0 ? 0 : Math.floor(Math.pow(v, 0.6) * 255)) * 3;
137      const di = i * 4;
138      offData[di] = colorLUT[ci];
139      offData[di + 1] = colorLUT[ci + 1];
140      offData[di + 2] = colorLUT[ci + 2];
141      offData[di + 3] = 255;
142    }
143    offCtx.putImageData(offImageData, 0, 0);
144
145    ctx.imageSmoothingEnabled = true;
146    ctx.imageSmoothingQuality = "high";
147    ctx.drawImage(offCanvas, 0, 0, cvs.width, cvs.height);
148
149    if (running) frame = window.requestAnimationFrame(step);
150  };
151
152  return {
153    start() {
154      if (running) return;
155      running = true;
156      frame = window.requestAnimationFrame(step);
157    },
158    stop() {
159      running = false;
160      if (frame) window.cancelAnimationFrame(frame);
161      frame = null;
162    },
163  };
164};