{"slug":"glenz","number":"24","title":"Glenz vectors","era":"Amiga · 1990","summary":"Translucent filled polygons, additive. Drag to orbit.","description":"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.","apis":["Canvas 2D","Pointer Events","requestAnimationFrame","matchMedia"],"tags":["demoscene","3d"],"hasGlsl":false,"interaction":"Drag on the preview to orbit. Auto-rotation stops after the first drag.","url":"https://3d-retro.com/experiments/glenz","example_url":"https://3d-retro.com/examples/glenz.html","api_url":"https://3d-retro.com/api/v1/experiments/glenz","html":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n  <meta name=\"description\" content=\"Amiga Glenz: translucent filled polygons. Drag to orbit.\" />\n  <link rel=\"canonical\" href=\"https://3d-retro.com/experiments/glenz\" />\n  <title>Glenz vectors · 3d retro graphics</title>\n  <style>\n    html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #05040a; color: #e8e6e1; position: absolute; inset: 0; }\n    canvas { display: block; position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; }\n    .fallback { position: absolute; inset: 0; display: grid; place-items: center; padding: 24px; font: 14px/1.5 ui-monospace, \"IBM Plex Mono\", monospace; text-align: center; color: #e8e6e1; background: #05040a; }\n    .fallback[hidden] { display: none; }\n  </style>\n</head>\n<body>\n<canvas id=\"c\"></canvas>\n<div id=\"fallback\" class=\"fallback\" hidden></div>\n<script>\n(function () {\n  const PARAMS = {\n    color: \"#ff4ad2\",\n    accent: \"#3ce0ff\",\n    speed: 1\n  };\n  function hexToRgb(hex) {\n    const n = parseInt(String(hex).replace(\"#\", \"\"), 16);\n    if (isNaN(n)) return [255, 74, 210];\n    return [n >> 16 & 255, n >> 8 & 255, n & 255];\n  }\n  function mix(a, b, t) {\n    return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];\n  }\n\n  const canvas = document.getElementById(\"c\");\n  const fallback = document.getElementById(\"fallback\");\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) {\n    fallback.hidden = false;\n    fallback.textContent = \"Canvas 2D is not available in this browser.\";\n    return;\n  }\n\n  const G = (1 + Math.sqrt(5)) / 2;\n  const raw = [\n    [-1, G, 0], [1, G, 0], [-1, -G, 0], [1, -G, 0],\n    [0, -1, G], [0, 1, G], [0, -1, -G], [0, 1, -G],\n    [G, 0, -1], [G, 0, 1], [-G, 0, -1], [-G, 0, 1]\n  ];\n  const verts = raw.map(function (p) {\n    const L = Math.hypot(p[0], p[1], p[2]);\n    return [p[0] / L, p[1] / L, p[2] / L];\n  });\n  const faces = [\n    [0,11,5],[0,5,1],[0,1,7],[0,7,10],[0,10,11],\n    [1,5,9],[5,11,4],[11,10,2],[10,7,6],[7,1,8],\n    [3,9,4],[3,4,2],[3,2,6],[3,6,8],[3,8,9],\n    [4,9,5],[2,4,11],[6,2,10],[8,6,7],[9,8,1]\n  ];\n\n  let yaw = 0.4, pitch = 0.35, auto = true, dragging = false, lastX = 0, lastY = 0, last = 0;\n  const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n  canvas.addEventListener(\"pointerdown\", function (e) {\n    dragging = true; auto = false; lastX = e.clientX; lastY = e.clientY;\n    canvas.setPointerCapture(e.pointerId);\n  });\n  canvas.addEventListener(\"pointerup\", function () { dragging = false; });\n  canvas.addEventListener(\"pointermove\", function (e) {\n    if (!dragging) return;\n    yaw += (e.clientX - lastX) * 0.01;\n    pitch += (e.clientY - lastY) * 0.01;\n    pitch = Math.max(-0.8, Math.min(0.9, pitch));\n    lastX = e.clientX; lastY = e.clientY;\n  });\n\n  function rot(p) {\n    const cx = Math.cos(pitch), sx = Math.sin(pitch);\n    const cy = Math.cos(yaw), sy = Math.sin(yaw);\n    let x = p[0], y = p[1], z = p[2];\n    const y2 = y * cx - z * sx; z = y * sx + z * cx; y = y2;\n    const x2 = x * cy + z * sy; z = -x * sy + z * cy;\n    return [x2, y, z];\n  }\n\n  function draw(w, h) {\n    ctx.fillStyle = \"#05040a\";\n    ctx.fillRect(0, 0, w, h);\n    const A = hexToRgb(PARAMS.color);\n    const B = hexToRgb(PARAMS.accent);\n    const fov = Math.min(w, h) * 0.48;\n    const pts = verts.map(function (v) {\n      const r = rot(v);\n      const z = r[2] + 3.2;\n      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 };\n    });\n    const drawn = [];\n    for (let f = 0; f < faces.length; f++) {\n      const a = pts[faces[f][0]], b = pts[faces[f][1]], c = pts[faces[f][2]];\n      drawn.push({ a: a, b: b, c: c, z: (a.z + b.z + c.z) / 3, i: f });\n    }\n    drawn.sort(function (p, q) { return q.z - p.z; });\n    ctx.globalCompositeOperation = \"lighter\";\n    for (let i = 0; i < drawn.length; i++) {\n      const f = drawn[i];\n      const col = mix(A, B, (f.i % 7) / 6);\n      ctx.fillStyle = \"rgba(\" + (col[0]|0) + \",\" + (col[1]|0) + \",\" + (col[2]|0) + \",0.22)\";\n      ctx.strokeStyle = \"rgba(\" + (col[0]|0) + \",\" + (col[1]|0) + \",\" + (col[2]|0) + \",0.55)\";\n      ctx.lineWidth = 1;\n      ctx.beginPath();\n      ctx.moveTo(f.a.x, f.a.y);\n      ctx.lineTo(f.b.x, f.b.y);\n      ctx.lineTo(f.c.x, f.c.y);\n      ctx.closePath();\n      ctx.fill();\n      ctx.stroke();\n    }\n    ctx.globalCompositeOperation = \"source-over\";\n  }\n\n  function resize() {\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));\n    const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));\n    if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }\n  }\n\n  function frame(now) {\n    const dt = Math.min(0.05, last ? (now - last) * 0.001 : 0.016);\n    last = now;\n    if (auto && !reduced) { yaw += dt * 0.55 * PARAMS.speed; pitch = Math.sin(now * 0.0004) * 0.25; }\n    resize();\n    draw(canvas.width, canvas.height);\n    if (!reduced) requestAnimationFrame(frame);\n  }\n\n  if (reduced) { resize(); draw(canvas.width, canvas.height); }\n  else requestAnimationFrame(frame);\n})();\n</script>\n</body>\n</html>\n"}