Home

23. Rutt/Etra · 1973

Rutt-Etra scan

Colors and knobs

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

About

The Rutt-Etra scan processor took a video signal and bent each scanline in Z by brightness. This one synthesizes a cheap 'video' of blobs and rings, then draws it as a wire mesh. Drag to orbit. It keeps a slow auto-spin until you take over.

Drag on the preview to orbit. Auto-rotation stops after the first drag.

Browser APIs

  • Canvas 2D
  • 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",
    displace: 1,
    speed: 1
  };
  function hexToRgb(hex) {
    const n = parseInt(String(hex).replace("#", ""), 16);
    if (isNaN(n)) return [110, 231, 183];
    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 COLS = 72, ROWS = 48;
  let t = 0, yaw = 0.45, pitch = 0.55, auto = true, dragging = false, lastX = 0, lastY = 0, last = 0;
  const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  canvas.addEventListener("pointerdown", function (e) {
    dragging = true; auto = false; lastX = e.clientX; lastY = e.clientY;
    canvas.setPointerCapture(e.pointerId);
  });
  canvas.addEventListener("pointerup", function () { dragging = false; });
  canvas.addEventListener("pointermove", function (e) {
    if (!dragging) return;
    yaw += (e.clientX - lastX) * 0.008;
    pitch += (e.clientY - lastY) * 0.008;
    pitch = Math.max(0.15, Math.min(1.2, pitch));
    lastX = e.clientX; lastY = e.clientY;
  });

  function luma(u, v, time) {
    const x = u * 2 - 1, y = v * 2 - 1;
    let L = 0.42 + 0.22 * Math.sin(u * 9 + time) * Math.cos(v * 7 - time * 0.7);
    const b1 = Math.hypot(x - Math.sin(time * 0.7) * 0.45, y - Math.cos(time * 0.5) * 0.3);
    const b2 = Math.hypot(x + Math.cos(time * 0.4) * 0.4, y + Math.sin(time * 0.9) * 0.35);
    L += 0.55 * Math.exp(-b1 * b1 * 7) + 0.4 * Math.exp(-b2 * b2 * 9);
    const ring = Math.abs(Math.hypot(x, y) - (0.55 + 0.08 * Math.sin(time * 1.3)));
    L += 0.28 * Math.exp(-ring * ring * 40);
    return Math.max(0, Math.min(1, L));
  }

  function project(x, y, z, w, h) {
    const cy = Math.cos(yaw), sy = Math.sin(yaw);
    const cx = Math.cos(pitch), sx = Math.sin(pitch);
    const x1 = x * cy + z * sy;
    const z1 = -x * sy + z * cy;
    const y1 = y * cx - z1 * sx;
    const z2 = y * sx + z1 * cx + 3.4;
    if (z2 < 0.2) return null;
    const f = Math.min(w, h) * 0.9 / z2;
    return { x: w * 0.5 + x1 * f, y: h * 0.52 - y1 * f };
  }

  function draw(w, h) {
    ctx.fillStyle = "#050806";
    ctx.fillRect(0, 0, w, h);
    const rgb = hexToRgb(PARAMS.color);
    const amp = 1.15 * PARAMS.displace;
    for (let j = 0; j < ROWS; j++) {
      const v = j / (ROWS - 1);
      ctx.beginPath();
      let pen = false;
      for (let i = 0; i < COLS; i++) {
        const u = i / (COLS - 1);
        const L = luma(u, v, t);
        const p = project((u - 0.5) * 2.2, (0.5 - v) * 1.5, (L - 0.35) * amp, w, h);
        if (!p) { pen = false; continue; }
        if (!pen) { ctx.moveTo(p.x, p.y); pen = true; }
        else ctx.lineTo(p.x, p.y);
      }
      const a = 0.22 + 0.7 * (1 - v * 0.45);
      ctx.strokeStyle = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + a + ")";
      ctx.lineWidth = 1.15;
      ctx.stroke();
    }
  }

  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(now) {
    const dt = Math.min(0.05, last ? (now - last) * 0.001 : 0.016);
    last = now;
    if (!reduced) {
      t += dt * PARAMS.speed;
      if (auto) yaw += dt * 0.18;
    }
    resize();
    draw(canvas.width, canvas.height);
    if (!reduced) requestAnimationFrame(frame);
  }

  if (reduced) { resize(); draw(canvas.width, canvas.height); }
  else requestAnimationFrame(frame);
})();