Home

28. Scientific visualization · 2026

Spaghetti vortex

Colors and knobs

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

About

Parametric helices scaled by the 2026 OpenAI paper's leading laws: radius follows tau^(1/2), height tau^(1/2−h), with illustrative h = 0.005 and tau = 1−t/T*. Both shrink; radius shrinks faster. Drag to orbit, scrub toward T*, or follow the core with a labeled uniform zoom. Color shows normalized radius. Oscillatory corrections are omitted. Illustration of the proposed blowup construction; not a numerical reproduction of the proof.

Drag to orbit; auto-rotation stops after the first drag. Play, pause, scrub the approach, or follow the core.

Browser APIs

  • WebGL 1
  • GLSL ES 1.0
  • 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: "#6ee7b7",
    core: "#f2b45c",
    trail: 0.35,
    speed: 1
  };
  // Leading scales: OpenAI (2026), section 2.1. h is illustrative.
  // These scales do not define a velocity field or simulate its corrections.
  const H = 0.005, MAX_S = 12, COUNT = 960, SAMPLES = 24;
  function tau(s) { return Math.pow(10, -s); }
  function scales(s) {
    const t = tau(s);
    return { radial: Math.pow(t, 0.5), axial: Math.pow(t, 0.5 - H), speed: Math.pow(t, -0.5 - H) };
  }
  function seed(i) {
    // Fixed low-discrepancy seeds: reset and reverse seeking need no RNG state.
    return { phase: (i * 0.61803398875) % 1, angle: (i * 2.60258057) % (2 * Math.PI), side: i % 2 ? 1 : -1 };
  }
  function position(p, s) {
    const k = scales(s), cycle = p.phase + s * 0.65;
    const q = cycle - Math.floor(cycle);
    // An inward helix bends into two axial exits. Recycling is a schematic
    // seeding device; history segments across a recycle boundary are omitted.
    const r = 0.14 + 1.36 * Math.exp(-3.6 * q);
    const angle = p.angle + s * 6 + q * 10;
    return [k.radial * r * Math.cos(angle), k.axial * p.side * (0.08 + 2.1 * q * q), k.radial * r * Math.sin(angle), r / 1.5, Math.floor(cycle)];
  }
  function rgb(hex) {
    const n = parseInt(hex.slice(1), 16);
    return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
  }
  const canvas = document.getElementById("c"), fallback = document.getElementById("fallback");
  const play = document.getElementById("play"), slider = document.getElementById("approach");
  const follow = document.getElementById("follow"), reset = document.getElementById("reset");
  const timeText = document.getElementById("time"), zoomText = document.getElementById("zoom"), ruler = document.getElementById("ruler");
  document.getElementById("ramp").style.background = "linear-gradient(90deg," + PARAMS.core + "," + PARAMS.color + ")";
  let gl;
  try { gl = canvas.getContext("webgl", { alpha: false, antialias: true }); } catch (_) { /* Fallback below. */ }
  function fail(message) {
    fallback.hidden = false;
    fallback.textContent = message;
    for (const el of [play, slider, follow, reset]) el.disabled = true;
    // A failed WebGL creation still allows a Canvas 2D explanation.
    if (!gl) {
      const ctx = canvas.getContext("2d");
      if (ctx) {
        canvas.width = Math.max(1, canvas.clientWidth); canvas.height = Math.max(1, canvas.clientHeight);
        ctx.fillStyle = "#000"; ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = "#c8d7d0"; ctx.font = "12px monospace"; ctx.textAlign = "center";
        ctx.fillText("WebGL unavailable", canvas.width / 2, canvas.height / 2);
      }
    }
  }
  if (!gl) { fail("WebGL is unavailable. Enable WebGL to view this illustration."); return; }
  const VERT = `attribute vec3 a_pos;
attribute vec2 a_style;
uniform mat4 u_mvp;
uniform float u_zoom;
uniform float u_point;
varying vec2 v_style;
varying float v_depth;
void main() {
  gl_Position = u_mvp * vec4(a_pos * u_zoom, 1.0);
  gl_PointSize = u_point;
  v_style = a_style;
  v_depth = clamp(1.5 - gl_Position.w * 0.1, 0.25, 1.0);
}`;
  const FRAG = `precision mediump float;
uniform vec3 u_outer;
uniform vec3 u_core;
varying vec2 v_style;
varying float v_depth;
void main() {
  vec3 color = mix(u_core, u_outer, smoothstep(0.08, 0.85, v_style.x));
  gl_FragColor = vec4(color, v_style.y * v_depth);
}`;
  function compile(type, source) {
    const shader = gl.createShader(type);
    gl.shaderSource(shader, source); gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error("Could not compile the vortex shader.");
    return shader;
  }
  // Column-major orbit matrices shared with the gallery's lowpoly example.
  function mul(a, b) {
    const out = new Float32Array(16);
    for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) {
      out[c * 4 + r] = a[r] * b[c * 4] + a[4 + r] * b[c * 4 + 1] + a[8 + r] * b[c * 4 + 2] + a[12 + r] * b[c * 4 + 3];
    }
    return out;
  }
  function camera(aspect, pitch, yaw) {
    const f = 1 / Math.tan(Math.PI / 8), near = 0.1, far = 50;
    const proj = new Float32Array([f/aspect,0,0,0, 0,f,0,0, 0,0,(far+near)/(near-far),-1, 0,0,2*far*near/(near-far),0]);
    const cx = Math.cos(pitch), sx = Math.sin(pitch), cy = Math.cos(yaw), sy = Math.sin(yaw);
    const rx = new Float32Array([1,0,0,0, 0,cx,sx,0, 0,-sx,cx,0, 0,0,0,1]);
    const ry = new Float32Array([cy,0,-sy,0, 0,1,0,0, sy,0,cy,0, 0,0,0,1]);
    const view = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,-8.8*Math.max(1,0.8/aspect),1]);
    return mul(proj, mul(view, mul(rx, ry)));
  }
  let prog, buffer, loc;
  function setup() {
    prog = gl.createProgram();
    gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
    gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG)); gl.linkProgram(prog);
    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error("Could not link the vortex shader.");
    buffer = gl.createBuffer();
    loc = {};
    for (const key of ["mvp", "zoom", "point", "outer", "core"]) loc[key] = gl.getUniformLocation(prog, "u_" + key);
    gl.useProgram(prog); gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    for (const [name, size, offset] of [["a_pos", 3, 0], ["a_style", 2, 12]]) {
      const at = gl.getAttribLocation(prog, name);
      gl.enableVertexAttribArray(at); gl.vertexAttribPointer(at, size, gl.FLOAT, false, 20, offset);
    }
    gl.uniform3fv(loc.outer, rgb(PARAMS.color)); gl.uniform3fv(loc.core, rgb(PARAMS.core));
    gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
    gl.clearColor(0, 0, 0, 1);
  }
  try { setup(); } catch (err) { fail(err.message); return; }
  const motion = matchMedia("(prefers-reduced-motion: reduce)");
  let s = motion.matches ? 6 : 0.6, playing = !motion.matches, auto = !motion.matches;
  let yaw = 0.7, pitch = 0.34, pointer = null, lastX = 0, lastY = 0;
  let raf = 0, last = 0, lost = false, dirty = true;
  let count = COUNT, slow = 0;
  const seeds = Array.from({ length: COUNT }, (_, i) => seed(i));
  // Bounded world-space history ring, regenerated analytically on every seek.
  // Fixed sample spacing makes history independent of playback frame rate.
  const history = new Float32Array(COUNT * SAMPLES * 5);
  const vertices = new Float32Array(COUNT * (SAMPLES - 1) * 2 * 5 + COUNT * 5);
  let lineCount = 0, vertexCount = 0;
  function rebuild() {
    const step = PARAMS.trail / (SAMPLES - 1), tick = Math.floor(s / step), head = tick % SAMPLES;
    let n = 0;
    for (let i = 0; i < count; i++) {
      for (let age = 0; age < SAMPLES; age++) {
        const slot = (head - age + SAMPLES) % SAMPLES;
        const t = age === 0 ? s : Math.max(0, (tick - age + 1) * step);
        history.set(position(seeds[i], t), (i * SAMPLES + slot) * 5);
      }
      for (let age = SAMPLES - 1; age > 0; age--) {
        const a = (i * SAMPLES + (head - age + SAMPLES) % SAMPLES) * 5;
        const b = (i * SAMPLES + (head - age + 1 + SAMPLES) % SAMPLES) * 5;
        if (history[a + 4] !== history[b + 4]) continue;
        for (const at of [a, b]) {
          vertices[n++] = history[at]; vertices[n++] = history[at+1]; vertices[n++] = history[at+2];
          vertices[n++] = history[at+3]; vertices[n++] = 0.16 * (1 - age / SAMPLES);
        }
      }
    }
    lineCount = n / 5;
    for (let i = 0; i < count; i++) {
      const at = (i * SAMPLES + head) * 5;
      vertices[n++] = history[at]; vertices[n++] = history[at+1]; vertices[n++] = history[at+2];
      vertices[n++] = history[at+3]; vertices[n++] = 0.65;
    }
    vertexCount = n / 5;
    dirty = false;
  }
  function draw() {
    const dpr = Math.min(devicePixelRatio || 1, 2);
    const w = Math.max(1, Math.round(canvas.clientWidth * dpr)), h = Math.max(1, Math.round(canvas.clientHeight * dpr));
    if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
    gl.viewport(0, 0, canvas.width, canvas.height); gl.clear(gl.COLOR_BUFFER_BIT);
    gl.uniformMatrix4fv(loc.mvp, false, camera(canvas.width / canvas.height, pitch, yaw));
    const mag = follow.checked ? 1 / scales(s).radial : 1;
    gl.uniform1f(loc.zoom, 1); gl.uniform1f(loc.point, Math.min(2, dpr * 1.3));
    // Reference ticks are in view units; the label converts back to world units.
    const axes = [];
    function line(a, b) { for (const p of [a, b]) axes.push(...p, 1, 0.13); }
    line([0,-2.7,0], [0,2.7,0]);
    for (let i = -3; i <= 3; i++) {
      line([i*0.5,-2.5,-1.5], [i*0.5,-2.5,1.5]);
      line([-1.5,-2.5,i*0.5], [1.5,-2.5,i*0.5]);
    }
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(axes), gl.DYNAMIC_DRAW);
    gl.drawArrays(gl.LINES, 0, axes.length / 5);
    if (dirty) rebuild();
    gl.uniform1f(loc.zoom, mag);
    gl.bufferData(gl.ARRAY_BUFFER, vertices.subarray(0, vertexCount * 5), gl.DYNAMIC_DRAW);
    gl.drawArrays(gl.LINES, 0, lineCount);
    gl.drawArrays(gl.POINTS, lineCount, vertexCount - lineCount);
    timeText.textContent = "1−t/T* = " + tau(s).toExponential(2);
    zoomText.textContent = (follow.checked ? "FOLLOW · " : "FIXED · ") + mag.toExponential(2) + "×";
    ruler.textContent = "grid Δ = " + (0.5 / mag).toExponential(1) + " initial units";
    slider.value = String(s);
    slider.setAttribute("aria-valuetext", "s " + s.toFixed(2) + ", remaining time " + tau(s).toExponential(2));
    play.textContent = s >= MAX_S ? "Replay" : playing ? "Pause" : "Play";
    play.setAttribute("aria-pressed", String(playing));
  }
  function wake() { if (!raf && !lost && !document.hidden) raf = requestAnimationFrame(frame); }
  function frame(now) {
    raf = 0;
    const dt = last ? Math.min((now - last) / 1000, 0.05) : 0;
    last = now;
    if (playing) { s = Math.min(MAX_S, s + dt * 0.24 * PARAMS.speed); dirty = true; if (s >= MAX_S) playing = false; }
    if (auto) yaw += dt * 0.09;
    const start = performance.now();
    draw();
    // Reduce geometry only after sustained expensive frames, never change seeds.
    slow = performance.now() - start > 24 ? slow + 1 : 0;
    if (slow > 45 && count > 480) { count = 480; dirty = true; slow = 0; }
    if (playing || auto) wake(); else last = 0;
  }
  play.addEventListener("click", function () {
    if (s >= MAX_S) { s = 0.6; dirty = true; }
    playing = !playing; last = 0; wake();
  });
  slider.addEventListener("input", function () {
    s = Math.max(0, Math.min(MAX_S, Number(slider.value))); playing = false; dirty = true; last = 0; wake();
  });
  follow.addEventListener("change", wake);
  reset.addEventListener("click", function () {
    s = motion.matches ? 6 : 0.6; yaw = 0.7; pitch = 0.34; count = COUNT; slow = 0;
    playing = !motion.matches; auto = !motion.matches; follow.checked = true; pointer = null; dirty = true; last = 0; wake();
  });
  canvas.addEventListener("pointerdown", function (e) {
    if (pointer !== null) return;
    pointer = e.pointerId; lastX = e.clientX; lastY = e.clientY;
    canvas.setPointerCapture(e.pointerId);
  });
  canvas.addEventListener("pointermove", function (e) {
    if (e.pointerId !== pointer) return;
    auto = false; yaw += (e.clientX - lastX) * 0.008;
    pitch = Math.max(-1.2, Math.min(1.2, pitch + (e.clientY - lastY) * 0.008));
    lastX = e.clientX; lastY = e.clientY; wake();
  });
  for (const name of ["pointerup", "pointercancel", "lostpointercapture"]) canvas.addEventListener(name, function () { pointer = null; });
  motion.addEventListener("change", function () { if (motion.matches) { playing = false; auto = false; } wake(); });
  document.addEventListener("visibilitychange", function () {
    cancelAnimationFrame(raf); raf = 0; last = 0; if (!document.hidden) wake();
  });
  new ResizeObserver(wake).observe(document.getElementById("scene"));
  canvas.addEventListener("webglcontextlost", function (e) {
    e.preventDefault(); lost = true; cancelAnimationFrame(raf); raf = 0;
    fail("WebGL context lost. Waiting for the browser to restore it.");
  });
  canvas.addEventListener("webglcontextrestored", function () {
    try {
      setup(); lost = false; dirty = true; last = 0; fallback.hidden = true;
      for (const el of [play, slider, follow, reset]) el.disabled = false;
      wake();
    } catch (err) { fail(err.message); }
  });
  wake();
})();