Ring tunnel

Scrolling flies you forward through a tunnel of glowing rings.

How it works

Rings are spaced along the Z axis inside a preserve-3d scene. Scroll progress pushes the whole scene towards the camera with translateZ; rings that pass the camera fade out, and the loop wraps them to the back.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ring tunnel</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; }
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; }

body { background: #05010f; color: #fff; }
.track { height: 500vh; }
.view { position: sticky; top: 0; height: 100vh; overflow: hidden; perspective: 600px; display: grid; place-items: center; }
.scene { position: relative; width: 0; height: 0; transform-style: preserve-3d; }
.ring {
  position: absolute; left: -30vmin; top: -30vmin; width: 60vmin; height: 60vmin; border-radius: 50%;
  border: 3px solid hsl(var(--h), 90%, 65%); box-shadow: 0 0 30px hsl(var(--h), 90%, 55%), inset 0 0 30px hsl(var(--h), 90%, 55%);
}
.hint { position: absolute; bottom: 8vh; margin: 0; letter-spacing: .3em; opacity: .5; font-size: .9rem; }
</style>
</head>
<body>
<section class="track"><div class="view"><div class="scene" id="scene"></div><p class="hint">Fly forward</p></div></section>
<script>
const scene = document.getElementById('scene');
const track = document.querySelector('.track');
const N = 18, GAP = 260;
const rings = [];
for (let i = 0; i < N; i++) {
  const el = document.createElement('div');
  el.className = 'ring';
  el.style.setProperty('--h', (i * 22) % 360);
  scene.appendChild(el);
  rings.push(el);
}
function update() {
  const r = track.getBoundingClientRect();
  const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
  const travel = p * GAP * N;
  rings.forEach((el, i) => {
    let z = ((i * GAP - travel) % (GAP * N) + GAP * N) % (GAP * N);
    const zPos = 500 - z;
    el.style.transform = 'translateZ(' + zPos + 'px) rotate(' + (i * 9 + p * 90) + 'deg)';
    el.style.opacity = Math.max(0, Math.min(1, (z / 300), (GAP * N - z) / 1200)).toFixed(2);
  });
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>

More 3d effects