Home

30. Martin · 1986 / 2026

Spiral stretch

Colors and knobs

These rewrite the live file. Copy HTML picks up whatever you set.

About

No fluid solver. Each iterate is r ← k r, θ ← θ + ω, z ← z + c/r. Plot (r cos θ, r sin θ + z) and you get the cartoon of inward spiral plus axial stretch — the 2026 Navier–Stokes construction as a CRT orbit. Teal is early, orange is stretched. Pointer nudges k. When a walker's radius collapses it reseeds on the ring.

Move the pointer left or right to change how fast the radius shrinks.

Browser APIs

  • Canvas 2D
  • ImageData
  • Pointer Events
  • requestAnimationFrame
  • matchMedia

If WebGL is missing, the demo draws a message on the canvas instead of a blank frame. prefers-reduced-motion freezes the first still.

Source

(function () {
  const PARAMS = {
    color: "#ff6a2a",
    accent: "#3ce0c8",
    shrink: 0.992
  };
  function hexToRgb(hex) {
    const n = parseInt(String(hex).replace("#", ""), 16);
    if (isNaN(n)) return [255, 106, 42];
    return [n >> 16 & 255, n >> 8 & 255, n & 255];
  }

  const canvas = document.getElementById("c");
  const fallback = document.getElementById("fallback");
  const ctx = canvas.getContext("2d");
  if (!ctx) {
    fallback.hidden = false;
    fallback.textContent = "Canvas 2D is not available in this browser.";
    return;
  }

  const BW = 420, BH = 240;
  const buf = document.createElement("canvas");
  buf.width = BW; buf.height = BH;
  const bctx = buf.getContext("2d");
  const img = bctx.createImageData(BW, BH);
  const d = img.data;
  bctx.fillStyle = "#07060a";
  bctx.fillRect(0, 0, BW, BH);
  d.set(bctx.getImageData(0, 0, BW, BH).data);

  const N = 64;
  const pts = [];
  function seed() {
    pts.length = 0;
    for (let i = 0; i < N; i++) {
      const th = (i / N) * Math.PI * 2;
      const r = 1.05 + (i % 7) * 0.04;
      pts.push({ r: r, th: th, z: (i % 5) * 0.02 });
    }
  }
  seed();

  let look = 0;
  const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  canvas.addEventListener("pointermove", function (ev) {
    const rec = canvas.getBoundingClientRect();
    look = (ev.clientX - rec.left) / rec.width - 0.5;
  });

  function fade() {
    for (let i = 0; i < d.length; i += 4) {
      d[i] *= 0.965; d[i + 1] *= 0.965; d[i + 2] *= 0.965;
    }
  }

  function plot(px, py, k, hot, cool) {
    const x = px | 0, y = py | 0;
    if (x < 0 || y < 0 || x >= BW || y >= BH) return;
    const o = (y * BW + x) * 4;
    const r = cool[0] + (hot[0] - cool[0]) * k;
    const g = cool[1] + (hot[1] - cool[1]) * k;
    const b = cool[2] + (hot[2] - cool[2]) * k;
    d[o] = Math.min(255, d[o] + r * 0.22);
    d[o + 1] = Math.min(255, d[o + 1] + g * 0.22);
    d[o + 2] = Math.min(255, d[o + 2] + b * 0.22);
    d[o + 3] = 255;
  }

  function step() {
    fade();
    const hot = hexToRgb(PARAMS.color);
    const cool = hexToRgb(PARAMS.accent);
    const shrink = Math.min(0.999, Math.max(0.96, PARAMS.shrink + look * 0.012));
    const cx = BW * 0.5, cy = BH * 0.62;
    const s = Math.min(BW, BH) * 0.28;
    const inner = 4;
    for (let k = 0; k < inner; k++) {
      for (let i = 0; i < pts.length; i++) {
        const p = pts[i];
        p.th += 0.19;
        p.r *= shrink;
        p.z += 0.085 / (p.r + 0.04);
        if (p.r < 0.03 || p.z > 14) {
          p.r = 1.08 + (i % 7) * 0.05;
          p.th = (i / N) * Math.PI * 2 + p.z;
          p.z = 0;
        }
        const x = p.r * Math.cos(p.th);
        const y = p.r * Math.sin(p.th);
        const px = cx + x * s;
        const py = cy - (y * 0.32 + p.z * 0.085) * s;
        const heat = Math.min(1, p.z / 8);
        plot(px, py, heat, hot, cool);
        plot(px + 1, py, heat * 0.5, hot, cool);
      }
    }
    bctx.putImageData(img, 0, 0);
    ctx.imageSmoothingEnabled = false;
    ctx.drawImage(buf, 0, 0, canvas.width, canvas.height);
    ctx.font = Math.max(10, (canvas.height * 0.03) | 0) + "px ui-monospace, 'IBM Plex Mono', monospace";
    ctx.fillStyle = "rgba(7,6,10,0.5)";
    ctx.fillRect(6, 6, canvas.width * 0.46, canvas.height * 0.1);
    ctx.fillStyle = "#c8d2dc";
    ctx.textBaseline = "top";
    ctx.fillText("r shrinks   z grows", 12, 10);
    ctx.fillText("inward spiral + axial stretch", 12, 10 + canvas.height * 0.04);
  }

  function resize() {
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));
    const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));
    if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
  }

  function frame() {
    resize();
    step();
    if (!reduced) requestAnimationFrame(frame);
  }

  if (reduced) {
    for (let i = 0; i < 80; i++) step();
    resize();
    ctx.drawImage(buf, 0, 0, canvas.width, canvas.height);
  } else requestAnimationFrame(frame);
})();