{"slug":"stargate","number":"22","title":"Slit-scan stargate","era":"Trumbull · 1968","summary":"A 2001 corridor from a moving slit. Pointer shifts the vanishing point.","description":"Douglas Trumbull's slit-scan: a one-dimensional strip of artwork is sampled while the camera dollies, so each pixel is a different moment. Here the slit is polar. Color streaks recede into a vanishing point. Move the pointer to shove the corridor.","apis":["Canvas 2D","ImageData","Pointer Events","requestAnimationFrame","matchMedia"],"tags":["optical","3d"],"hasGlsl":false,"interaction":"Move the pointer to shift the vanishing point.","url":"https://3d-retro.com/experiments/stargate","example_url":"https://3d-retro.com/examples/stargate.html","api_url":"https://3d-retro.com/api/v1/experiments/stargate","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=\"Trumbull slit-scan corridor. Pointer shifts the vanishing point.\" />\n  <link rel=\"canonical\" href=\"https://3d-retro.com/experiments/stargate\" />\n  <title>Slit-scan stargate · 3d retro graphics</title>\n  <style>\n    html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #050308; color: #e8e6e1; position: absolute; inset: 0; }\n    canvas { display: block; position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; image-rendering: pixelated; }\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: #050308; }\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: \"#ff6a2a\",\n    speed: 1,\n    streaks: 1\n  };\n  function hexToRgb(hex) {\n    const n = parseInt(String(hex).replace(\"#\", \"\"), 16);\n    if (isNaN(n)) return [255, 106, 42];\n    return [n >> 16 & 255, n >> 8 & 255, n & 255];\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 BW = 280, BH = 160;\n  const buf = document.createElement(\"canvas\");\n  buf.width = BW; buf.height = BH;\n  const bctx = buf.getContext(\"2d\");\n  const img = bctx.createImageData(BW, BH);\n  const px = img.data;\n\n  let t = 0, lookX = 0, lookY = 0, last = 0;\n  const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  canvas.addEventListener(\"pointermove\", function (ev) {\n    const r = canvas.getBoundingClientRect();\n    lookX = (ev.clientX - r.left) / r.width - 0.5;\n    lookY = (ev.clientY - r.top) / r.height - 0.5;\n  });\n\n  function slit(u, rgb) {\n    const s = (u % 1 + 1) % 1;\n    const band = Math.sin(s * Math.PI * 10) * 0.5 + 0.5;\n    const cool = s < 0.45;\n    if (cool) {\n      return [\n        20 + band * 40 + rgb[0] * 0.05,\n        30 + band * 90,\n        80 + band * 160\n      ];\n    }\n    return [\n      rgb[0] * (0.35 + band * 0.65),\n      rgb[1] * (0.15 + band * 0.5) + band * 80,\n      rgb[2] * 0.12 + (1 - band) * 40\n    ];\n  }\n\n  function draw() {\n    const rgb = hexToRgb(PARAMS.color);\n    const cx = BW * (0.5 + lookX * 0.35);\n    const cy = BH * (0.5 + lookY * 0.35);\n    const k = 7.5 * PARAMS.streaks;\n    for (let y = 0; y < BH; y++) {\n      for (let x = 0; x < BW; x++) {\n        const dx = x - cx, dy = y - cy;\n        const r = Math.hypot(dx, dy) / BH;\n        const a = Math.atan2(dy, dx);\n        const u = a / (Math.PI * 2) * 6 + t * 0.15;\n        const scan = t * 1.4 * PARAMS.speed - r * k;\n        const col = slit(u * 0.2 + scan, rgb);\n        const glow = Math.max(0, 1 - r * 1.15);\n        const i = (y * BW + x) * 4;\n        px[i] = Math.min(255, col[0] * (0.25 + glow) + glow * 40);\n        px[i + 1] = Math.min(255, col[1] * (0.25 + glow));\n        px[i + 2] = Math.min(255, col[2] * (0.2 + glow * 0.8));\n        px[i + 3] = 255;\n      }\n    }\n    bctx.putImageData(img, 0, 0);\n    ctx.imageSmoothingEnabled = false;\n    ctx.drawImage(buf, 0, 0, canvas.width, canvas.height);\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 (!reduced) t += dt;\n    resize();\n    draw();\n    if (!reduced) requestAnimationFrame(frame);\n  }\n\n  if (reduced) { resize(); draw(); }\n  else requestAnimationFrame(frame);\n})();\n</script>\n</body>\n</html>\n"}