30. Martin · 1986 / 2026
Spiral stretch
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);
})();