Scramble decode

Headings arrive as random characters and decode into words from left to right.

How it works

When a heading enters the viewport, a requestAnimationFrame loop rebuilds its text every frame: characters before a moving cursor are final, the rest are random symbols. The cursor advances with time, so longer headings take a little longer. Leaving the viewport resets them.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scramble decode</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: #050a05; color: #39ff88; }
.list { padding: 40vh 8vw; display: grid; gap: 40vh; }
.list h2 { font: 700 clamp(1.8rem, 6vw, 4rem)/1.1 ui-monospace, Menlo, monospace; margin: 0; text-shadow: 0 0 18px rgba(57,255,136,.45); }
</style>
</head>
<body>
<main class="list">
  <h2 data-text="Decrypting the page">Decrypting the page</h2>
  <h2 data-text="One glyph at a time">One glyph at a time</h2>
  <h2 data-text="Signal over noise">Signal over noise</h2>
  <h2 data-text="Access granted">Access granted</h2>
</main>
<script>
const GLYPHS = '!<>-_/[]{}=+*^?#ABCDEFXYZ0123456789';
function decode(el) {
  const target = el.dataset.text;
  const start = performance.now();
  cancelAnimationFrame(el._raf);
  const tick = now => {
    const cursor = Math.floor((now - start) / 45);
    let out = '';
    for (let i = 0; i < target.length; i++) {
      if (i < cursor || target[i] === ' ') out += target[i];
      else out += GLYPHS[Math.floor(Math.random() * GLYPHS.length)];
    }
    el.textContent = out;
    if (cursor < target.length) el._raf = requestAnimationFrame(tick);
  };
  el._raf = requestAnimationFrame(tick);
}
const io = new IntersectionObserver(entries => entries.forEach(e => {
  if (e.isIntersecting) decode(e.target);
  else { cancelAnimationFrame(e.target._raf); e.target.textContent = e.target.dataset.text.replace(/\S/g, '#'); }
}), { threshold: 0.6 });
document.querySelectorAll('[data-text]').forEach(el => io.observe(el));
</script>
</body>
</html>

More text effects