Home

24. Amiga · 1990

Glenz vectors

Colors and knobs

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

About

Glenz vectors are the Amiga demo trick of drawing a solid with see-through faces so the back sides pile up as light. An icosahedron, painter-sorted, additive blend. Magenta against cyan unless you change it. Drag to orbit.

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: "#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;
  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.01;
    pitch += (e.clientY - lastY) * 0.01;
    pitch = Math.max(-0.8, Math.min(0.9, pitch));
    lastX = e.clientX; lastY = e.clientY;
  });

  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) requestAnimationFrame(frame);
  }

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