Home

24. Glenz vectors · Amiga demoscene, c. 1989 · Retro techniques

Glenz vectors

Demos are animating. Following your system setting.
Colors and knobs

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

About

Glenz vectors were the Amiga demo trick of drawing a solid with see-through faces so the back sides pile up as light. The original effect faked transparency with a purpose-built palette and bit-plane tricks on a 16- or 32-colour screen. This one does not: it is an icosahedron, painter-sorted and drawn with ordinary canvas alpha in additive mode, which gives the glow without the period technique. Magenta against cyan unless you change it. Drag to orbit.

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

Reference

Interpretation: this evokes a period look and does not reproduce one specific original.

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. By default a demo holds a still frame when your system asks for reduced motion; the Motion buttons override that, and a standalone copy animates with #motion=play on its URL.

Source

(function () {
  const PARAMS = {
    color: "#ff4ad2",
    accent: "#3ce0ff",
    speed: 1
  };
  function hexToRgb(hex) {
    const n = parseInt(String(hex).replace("#", ""), 16);
    if (isNaN(n)) return [255, 74, 210];
    return [n >> 16 & 255, n >> 8 & 255, n & 255];
  }
  function mix(a, b, t) {
    return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
  }

  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 G = (1 + Math.sqrt(5)) / 2;
  const raw = [
    [-1, G, 0], [1, G, 0], [-1, -G, 0], [1, -G, 0],
    [0, -1, G], [0, 1, G], [0, -1, -G], [0, 1, -G],
    [G, 0, -1], [G, 0, 1], [-G, 0, -1], [-G, 0, 1]
  ];
  const verts = raw.map(function (p) {
    const L = Math.hypot(p[0], p[1], p[2]);
    return [p[0] / L, p[1] / L, p[2] / L];
  });
  const faces = [
    [0,11,5],[0,5,1],[0,1,7],[0,7,10],[0,10,11],
    [1,5,9],[5,11,4],[11,10,2],[10,7,6],[7,1,8],
    [3,9,4],[3,4,2],[3,2,6],[3,6,8],[3,8,9],
    [4,9,5],[2,4,11],[6,2,10],[8,6,7],[9,8,1]
  ];

  let yaw = 0.4, pitch = 0.35, auto = true, dragging = false, lastX = 0, lastY = 0, last = 0;
  let raf = 0;
  const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
  let reduced = motionQuery.matches;

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

  function rot(p) {
    const cx = Math.cos(pitch), sx = Math.sin(pitch);
    const cy = Math.cos(yaw), sy = Math.sin(yaw);
    let x = p[0], y = p[1], z = p[2];
    const y2 = y * cx - z * sx; z = y * sx + z * cx; y = y2;
    const x2 = x * cy + z * sy; z = -x * sy + z * cy;
    return [x2, y, z];
  }

  function draw(w, h) {
    ctx.fillStyle = "#05040a";
    ctx.fillRect(0, 0, w, h);
    const A = hexToRgb(PARAMS.color);
    const B = hexToRgb(PARAMS.accent);
    const fov = Math.min(w, h) * 0.48;
    const pts = verts.map(function (v) {
      const r = rot(v);
      const z = r[2] + 3.2;
      return { x: w * 0.5 + r[0] / z * fov * 3.1, y: h * 0.5 - r[1] / z * fov * 3.1, z: z, n: r };
    });
    const drawn = [];
    for (let f = 0; f < faces.length; f++) {
      const a = pts[faces[f][0]], b = pts[faces[f][1]], c = pts[faces[f][2]];
      drawn.push({ a: a, b: b, c: c, z: (a.z + b.z + c.z) / 3, i: f });
    }
    drawn.sort(function (p, q) { return q.z - p.z; });
    ctx.globalCompositeOperation = "lighter";
    for (let i = 0; i < drawn.length; i++) {
      const f = drawn[i];
      const col = mix(A, B, (f.i % 7) / 6);
      ctx.fillStyle = "rgba(" + (col[0]|0) + "," + (col[1]|0) + "," + (col[2]|0) + ",0.22)";
      ctx.strokeStyle = "rgba(" + (col[0]|0) + "," + (col[1]|0) + "," + (col[2]|0) + ",0.55)";
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(f.a.x, f.a.y);
      ctx.lineTo(f.b.x, f.b.y);
      ctx.lineTo(f.c.x, f.c.y);
      ctx.closePath();
      ctx.fill();
      ctx.stroke();
    }
    ctx.globalCompositeOperation = "source-over";
  }

  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 (auto && !reduced) { yaw += dt * 0.55 * PARAMS.speed; pitch = Math.sin(now * 0.0004) * 0.25; }
    resize();
    draw(canvas.width, canvas.height);
    if (!reduced) raf = requestAnimationFrame(frame);
  }

  // Motion. The system's reduced-motion preference decides by default. A
  // standalone copy animates anyway with "#motion=play" in its URL (or holds a
  // frame with "#motion=still"); an embedding page can post {motion: "play"}.
  let motionMode = "auto";
  function applyMotion() {
    const next = motionMode === "still" || (motionMode === "auto" && motionQuery.matches);
    if (next === reduced) return;
    reduced = next;
    last = 0;
    if (reduced) { cancelAnimationFrame(raf); raf = 0; frame(performance.now()); }
    else if (!raf) raf = requestAnimationFrame(frame);
  }
  function setMotion(mode) {
    if (mode === "play" || mode === "still" || mode === "auto") { motionMode = mode; applyMotion(); }
  }
  function readMotionHash() {
    const m = /motion=(play|still|auto)/.exec(location.hash);
    let mode = m ? m[1] : "auto";
    // Embedded in the gallery, the page sets window.__demoMotion before any frame loads.
    try { if (!m && window.parent !== window && typeof window.parent.__demoMotion === "string") mode = window.parent.__demoMotion; } catch (e) { /* cross-origin parent */ }
    setMotion(mode);
  }
  function wake() { if (reduced) frame(performance.now()); }
  if (motionQuery.addEventListener) motionQuery.addEventListener("change", applyMotion);
  else if (motionQuery.addListener) motionQuery.addListener(applyMotion);
  window.addEventListener("hashchange", readMotionHash);
  window.addEventListener("message", function (e) { if (e.data && typeof e.data.motion === "string") setMotion(e.data.motion); });
  window.addEventListener("resize", wake);

  if (reduced) frame(performance.now());
  else raf = requestAnimationFrame(frame);
  readMotionHash();
})();