Cyber Ambient Overlay

Seen on ChatGPT · credit OpenAI / ChatGPT
Open standalone ↗

How it works

The real /cyber header background is the #cyber-ambient-overlay-canvas 2D <canvas> from cdn/assets/cyber-g632ua04.js (the De() component) — not a particle network. The field is laid out on a fixed 13px lattice. A pool of glyphs (count clamp(cols*rows/72, 56, 180)) is scattered across the grid, each drawn from the 19-character set ` .:-=+*#%@<>/\[]{}01 using the 13px ui-monospace` font.

The motion comes from three invisible circular ripple wavessmall, medium, and large — that endlessly expand from points in the top 82% of the canvas and respawn when they reach their target radius. Each wave has its own speed (small 0.34–0.46, medium 0.28–0.38, large 0.22–0.32 px/frame), band thickness (16/22/30 +random), baseStrength (0.6/0.82/1.0) and a targetRadius scaled to a fraction of the canvas diagonal (0.4/0.62/0.85 ×). A wave's strength decays as baseStrength·(1−r/target)², so each ripple fades as it grows. New waves are spawned at least min(W,H)·0.28 away from the previous one.

Each wave carries three concentric gaussian rings (the crest plus two trailing rings offset by 13·3.2 and 13·6.2 px). For every glyph these rings produce two things: a radial displacement that physically shoves the character outward from the wave centre ((d·1.1 + f·0.78 + p·0.48)·13·0.7·strength), and an intensity that drives brightness. Intensity is modulated by sin(time/950) and a vertical fade clamp(1 − y/(H·1.06), 0.22, 1) so the top of the field stays brightest. Where intensity is high the glyph is recolored in full-saturation rainbow HSL (hsla(hue,100%,78%,…) in dark mode, hue from hueOffset + time·0.045 + g·90), gains a 4 + g·8 px blur shadow, and advances its character index. Glyphs also random-walk: a 10% per-frame chance to step one cell, and a 1.2% chance to teleport.

Underneath the glyphs, drawGlow() paints faint hsla(hue,46%,60%,~0.06) squares pulsing along the same ring crests, and a dim rgb(108,110,138) grid is stroked at 6% alpha. Each frame begins by filling a translucent rgba(33,33,33,0.18) rectangle (the #212121 dark surface color) which produces the fade-trail rather than clearing the canvas. The loop is throttled to ~24 fps (1000/24 ms gate), the device pixel ratio is clamped to 1, the canvas opacity is 0.72, and a linear-gradient mask dissolves the field toward the bottom — all matching the captured values. Dark-theme constants are hard-coded here since the standalone has no ChatGPT theme tokens to sample.

Code — copy & paste this whole file

<!doctype html>
<!--
  Cyber Ambient Overlay  —  faithful port of ChatGPT's /cyber page header
  ─────────────────────────────────────────────────────────────────────────────
  Source: https://chatgpt.com/cyber  →  canvas#cyber-ambient-overlay-canvas
  Logic distilled from chatgpt.com/cdn/assets/cyber-g632ua04.js (the `De()`
  React component). This is a 2D <canvas> "matrix field" — NOT a particle network.

  Three invisible circular RIPPLE waves (small / medium / large) endlessly grow
  outward from the top of the canvas and respawn. A grid of monospace glyphs
  (from the set ".:-=+*#%@<>/\[]{}01") is scattered on a 13px lattice. Each wave
  carries three concentric gaussian rings that (a) push nearby glyphs radially
  outward and (b) light them up in rainbow HSL, advancing the glyph character and
  adding a blur shadow. Faint colored squares pulse along the ring crests and a
  dim grid sits underneath. Each frame paints a translucent surface-colored rect
  for the fade-trail. Capped at ~24 fps.

  Completely standalone — no CDN, no web fonts, no external assets.
-->
<html lang="en">
<head>
  <link rel="icon" href="data:,">
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Cyber Ambient Overlay</title>
  <!-- a14y:begin head — machine-readable metadata; safe to delete when copying -->
  <meta name="description" content="A matrix field of monospace glyphs that ripple and rainbow as invisible circular waves wash outward across the header.">
  <link rel="canonical" href="https://fx.statico.io/effects/cyber-canvas.html">
  <link rel="alternate" type="text/markdown" href="https://fx.statico.io/effects/cyber-canvas.md" title="Cyber Ambient Overlay (Markdown)">
  <meta property="og:type" content="website">
  <meta property="og:site_name" content="cool web fx">
  <meta property="og:title" content="Cyber Ambient Overlay">
  <meta property="og:description" content="A matrix field of monospace glyphs that ripple and rainbow as invisible circular waves wash outward across the header.">
  <meta property="og:url" content="https://fx.statico.io/effects/cyber-canvas.html">
  <meta name="twitter:card" content="summary">
  <meta name="twitter:title" content="Cyber Ambient Overlay">
  <meta name="twitter:description" content="A matrix field of monospace glyphs that ripple and rainbow as invisible circular waves wash outward across the header.">
  <script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"SoftwareSourceCode","@id":"https://fx.statico.io/effects/cyber-canvas.html#effect","name":"Cyber Ambient Overlay","url":"https://fx.statico.io/effects/cyber-canvas.html","abstract":"A matrix field of monospace glyphs that ripple and rainbow as invisible circular waves wash outward across the header.","description":"A matrix field of monospace glyphs that ripple and rainbow as invisible circular waves wash outward across the header.","programmingLanguage":"HTML","codeSampleType":"full solution","runtimePlatform":"Web browser","codeRepository":"https://github.com/statico/cool-web-fx","license":"https://unlicense.org/","isBasedOn":"https://chatgpt.com/cyber","mainEntityOfPage":"https://fx.statico.io/demos/cyber-canvas","dateModified":"2026-07-27T17:24:00-07:00","inLanguage":"en"},{"@type":"BreadcrumbList","@id":"https://fx.statico.io/effects/cyber-canvas.html#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://fx.statico.io/"},{"@type":"ListItem","position":2,"name":"Cyber Ambient Overlay","item":"https://fx.statico.io/demos/cyber-canvas"},{"@type":"ListItem","position":3,"name":"Standalone source","item":"https://fx.statico.io/effects/cyber-canvas.html"}]}]}</script>
  <style>.a14y-doc{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}</style>
  <!-- a14y:end head -->
  <style>
    *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

    html, body {
      width: 100%;
      height: 100%;
      overflow: hidden;
      background: #212121;            /* ChatGPT dark --token-main-surface-primary */
    }

    /* Container matches the real page: positioned wrapper with a soft bottom
       mask so the field dissolves toward the footer, exactly as on /cyber. */
    #wrap {
      position: absolute;
      inset: 0;
      overflow: hidden;
      background-color: #212121;
      pointer-events: none;
      -webkit-mask-image: linear-gradient(to bottom,
        rgba(0,0,0,1) 0%, rgba(0,0,0,0.98) 70%, rgba(0,0,0,0.75) 88%, rgba(0,0,0,0) 100%);
              mask-image: linear-gradient(to bottom,
        rgba(0,0,0,1) 0%, rgba(0,0,0,0.98) 70%, rgba(0,0,0,0.75) 88%, rgba(0,0,0,0) 100%);
    }

    #cyber-ambient-overlay-canvas {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      opacity: 0.72;                  /* real value for dark theme */
    }
  </style>
</head>
<body>
  <!-- a14y:begin body — visually hidden description; safe to delete when copying -->
  <div class="a14y-doc">
    <h1>Cyber Ambient Overlay</h1>
    <p>A matrix field of monospace glyphs that ripple and rainbow as invisible circular waves wash outward across the header.</p>
    <h2>How it works</h2>
    <p>The real /cyber header background is the #cyber-ambient-overlay-canvas 2D &lt;canvas&gt; from cdn/assets/cyber-g632ua04.js (the De() component) — not a particle network. The field is laid out on a fixed 13px lattice. A pool of glyphs (count clamp(cols*rows/72, 56, 180)) is scattered across the grid, each drawn from the 19-character set ` .:-=+*#%@&lt;&gt;/\[]{}01  using the 13px ui-monospace font.</p>
    <p>The motion comes from three invisible circular ripple waves — small, medium, and large — that endlessly expand from points in the top 82% of the canvas and respawn when they reach their target radius. Each wave has its own speed (small 0.34–0.46, medium 0.28–0.38, large 0.22–0.32 px/frame), band thickness (16/22/30 +random), baseStrength (0.6/0.82/1.0) and a targetRadius scaled to a fraction of the canvas diagonal (0.4/0.62/0.85 ×). A wave's strength decays as baseStrength·(1−r/target)², so each ripple fades as it grows. New waves are spawned at least min(W,H)·0.28 away from the previous one.</p>
    <p>Each wave carries three concentric gaussian rings (the crest plus two trailing rings offset by 13·3.2 and 13·6.2 px). For every glyph these rings produce two things: a radial displacement that physically shoves the character outward from the wave centre ((d·1.1 + f·0.78 + p·0.48)·13·0.7·strength), and an intensity that drives brightness. Intensity is modulated by sin(time/950) and a vertical fade clamp(1 − y/(H·1.06), 0.22, 1) so the top of the field stays brightest. Where intensity is high the glyph is recolored in full-saturation rainbow HSL (hsla(hue,100%,78%,…) in dark mode, hue from hueOffset + time·0.045 + g·90), gains a 4 + g·8 px blur shadow, and advances its character index. Glyphs also random-walk: a 10% per-frame chance to step one cell, and a 1.2% chance to teleport.</p>
    <p>Underneath the glyphs, drawGlow() paints faint hsla(hue,46%,60%,~0.06) squares pulsing along the same ring crests, and a dim rgb(108,110,138) grid is stroked at 6% alpha. Each frame begins by filling a translucent rgba(33,33,33,0.18) rectangle (the #212121 dark surface color) which produces the fade-trail rather than clearing the canvas. The loop is throttled to ~24 fps (1000/24 ms gate), the device pixel ratio is clamped to 1, the canvas opacity is 0.72, and a linear-gradient` mask dissolves the field toward the bottom — all matching the captured values. Dark-theme constants are hard-coded here since the standalone has no ChatGPT theme tokens to sample.</p>
    <h2>How to use this file</h2>
    <p>This is a complete, self-contained HTML document — copy it verbatim rather than reconstructing it. Markdown version: <a href="https://fx.statico.io/effects/cyber-canvas.md">https://fx.statico.io/effects/cyber-canvas.md</a>. Full writeup and live preview: <a href="https://fx.statico.io/demos/cyber-canvas">https://fx.statico.io/demos/cyber-canvas</a>.</p>
    <h2>Source and credit</h2>
    <p>Effect originally seen on ChatGPT (https://chatgpt.com/cyber) — credit to OpenAI / ChatGPT. This is an independent recreation built from scratch for educational use; no proprietary source code was copied. Released under the Unlicense.</p>
    <h2>More</h2>
    <p>Terms used above are defined in the <a href="https://fx.statico.io/glossary">glossary</a>. See also the <a href="https://fx.statico.io/sitemap.md">site map</a>, <a href="https://fx.statico.io/AGENTS.md">AGENTS.md</a> and <a href="https://fx.statico.io/llms.txt">llms.txt</a>.</p>
  </div>
  <!-- a14y:end body -->
  <div id="wrap">
    <canvas id="cyber-ambient-overlay-canvas"></canvas>
  </div>

  <script>
  /* ====================================================================
     Cyber Ambient Overlay — animation engine
     Constants preserved verbatim from the real `De()` source.
  ==================================================================== */
  (function () {
    'use strict';

    /* ── Fixed constants from the real implementation ─────────────────── */
    const CELL   = 13;                          // lattice / font size in px
    const GLYPHS = '.:-=+*#%@<>/\\[]{}01';       // 19-character set
    const NG     = GLYPHS.length;               // 19
    const FRAME_MS = 1000 / 24;                 // ~24 fps cap (41.666…ms)

    // Dark-theme palette (the page picks these when the surface is dark).
    const SURFACE   = { r: 33, g: 33, b: 33 };  // #212121
    const FADE      = `rgba(${SURFACE.r}, ${SURFACE.g}, ${SURFACE.b}, 0.18)`;
    const GRID_RGB  = 'rgb(108, 110, 138)';
    const GRID_A    = 0.06;
    const DARK      = true;

    const wrap   = document.getElementById('wrap');
    const canvas = document.getElementById('cyber-ambient-overlay-canvas');
    const ctx    = canvas.getContext('2d', { alpha: true });

    // Device-pixel-ratio is clamped to 1 in the original (perf choice).
    const DPR = Math.min(window.devicePixelRatio || 1, 1);

    let W = 0, H = 0;        // CSS pixel width / height
    let cols = 0, rows = 0;  // lattice dimensions
    let last = 0, raf = 0;
    let running = !document.hidden;

    let glyphs = [];         // scattered matrix characters
    let waves  = [];         // 3 expanding ripple waves

    const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));

    /* ── Pick a spawn centre at least minDist from the previous wave ──── */
    function pushAway(prev, band) {
      const cx   = (band + 0.5) / 3 * W;        // column centre for this band
      const span = W / 3;
      const minD = Math.min(W, H) * 0.28;
      let pick = { x: Math.random() * W, y: Math.random() * H * 0.82 };
      for (let i = 0; i < 10; i++) {
        const cand = {
          x: clamp(cx + (Math.random() - 0.5) * span * 0.9, 0, W),
          y: Math.random() * H * 0.82,
        };
        if (Math.hypot(cand.x - prev.x, cand.y - prev.y) >= minD) return cand;
        pick = cand;
      }
      return pick;
    }

    /* ── Create a wave object. size ∈ {small,medium,large} ────────────── */
    function makeWave(size, band, prev) {
      const cx   = (band + 0.5) / 3 * W;
      const span = W / 3;
      const home = {
        x: clamp(cx + (Math.random() - 0.5) * span * 0.9, 0, W),
        y: Math.random() * H * 0.82,
      };
      const c = prev ? pushAway(prev, band) : home;
      const diag = Math.hypot(W, H);

      if (size === 'small') {
        return { x: c.x, y: c.y, radius: 0,
          speed: 0.34 + Math.random() * 0.12, band: 16 + Math.random() * 6,
          baseStrength: 0.6, strength: 0.6,
          targetRadius: diag * (0.40 + Math.random() * 0.18) };
      }
      if (size === 'medium') {
        return { x: c.x, y: c.y, radius: 0,
          speed: 0.28 + Math.random() * 0.10, band: 22 + Math.random() * 8,
          baseStrength: 0.82, strength: 0.82,
          targetRadius: diag * (0.62 + Math.random() * 0.18) };
      }
      return { x: c.x, y: c.y, radius: 0,
        speed: 0.22 + Math.random() * 0.10, band: 30 + Math.random() * 10,
        baseStrength: 1, strength: 1,
        targetRadius: diag * (0.85 + Math.random() * 0.20) };
    }

    /* ── Re-seed glyphs + waves (called on every resize) ──────────────── */
    function seed() {
      const n = clamp(Math.floor(cols * rows / 72), 56, 180);
      glyphs = Array.from({ length: n }, () => ({
        col: Math.floor(Math.random() * cols),
        row: Math.floor(Math.random() * rows),
        glyphOffset: Math.floor(Math.random() * NG),
        hueOffset: Math.random() * 360,
        drift: Math.random() * Math.PI * 2,
      }));
      const a = makeWave('small', 0);
      const b = makeWave('medium', 1, { x: a.x, y: a.y });
      const c = makeWave('large', 2, { x: b.x, y: b.y });
      waves = [a, b, c];
    }

    /* ── Resize / setup ───────────────────────────────────────────────── */
    function resize() {
      const r = wrap.getBoundingClientRect();
      W = Math.max(1, r.width);
      H = Math.max(1, r.height);
      cols = Math.max(1, Math.floor(W / CELL));
      rows = Math.max(1, Math.floor(H / CELL));
      canvas.width  = Math.floor(W * DPR);
      canvas.height = Math.floor(H * DPR);
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
      ctx.font = `${CELL}px ui-monospace, SFMono-Regular, Menlo, monospace`;
      ctx.textBaseline = 'top';
      ctx.textAlign = 'center';
      seed();
    }

    /* ── Faint background grid ────────────────────────────────────────── */
    function drawGrid() {
      ctx.strokeStyle = GRID_RGB;
      ctx.globalAlpha = GRID_A;
      ctx.lineWidth = 1;
      ctx.beginPath();
      for (let x = 0; x <= W; x += CELL) { ctx.moveTo(x, 0); ctx.lineTo(x, H); }
      for (let y = 0; y <= H; y += CELL) { ctx.moveTo(0, y); ctx.lineTo(W, y); }
      ctx.stroke();
      ctx.globalAlpha = 1;
    }

    /* ── Soft colored squares pulsing along the ring crests ───────────── */
    function drawGlow() {
      const ec = Math.floor(W / CELL);
      const er = Math.floor(H / CELL);
      for (let n = 0; n < er; n++) {
        for (let t = 0; t < ec; t++) {
          const cx = t * CELL + CELL * 0.5;
          const cy = n * CELL + CELL * 0.5;
          let i = 0;
          for (const w of waves) {
            const dist = Math.hypot(cx - w.x, cy - w.y);
            const a = Math.exp(-Math.abs(dist - w.radius) / (w.band * 1.05));
            const o = Math.exp(-Math.abs(dist - (w.radius - CELL * 3.4)) / (w.band * 1.2));
            const s = Math.exp(-Math.abs(dist - (w.radius - CELL * 6.8)) / (w.band * 1.35));
            i += (a + o * 0.62 + s * 0.38) * w.strength;
          }
          i = clamp(i, 0, 1);
          if (i < 0.06) continue;
          const hue = (Math.hypot(cx - waves[0].x, cy - waves[0].y) * 0.14 + waves[0].radius * 0.9) % 360;
          const alpha = i * (DARK ? 0.06 : 0.08);
          ctx.fillStyle = `hsla(${hue}, 46%, ${DARK ? 60 : 52}%, ${alpha})`;
          ctx.fillRect(t * CELL + 1, n * CELL + 1, CELL - 2, CELL - 2);
        }
      }
    }

    /* ── Advance waves + draw the displaced, glowing glyphs ───────────── */
    function drawGlyphs(time) {
      const tmod = Math.sin(time / 950);

      // Update + respawn waves.
      for (let i = 0; i < waves.length; i++) {
        const w = waves[i];
        w.radius += w.speed;
        const n = clamp(w.radius / w.targetRadius, 0, 1);
        w.strength = w.baseStrength * (1 - n) * (1 - n);
        if (w.radius >= w.targetRadius) {
          const size = i === 0 ? 'small' : i === 1 ? 'medium' : 'large';
          waves[i] = makeWave(size, i, { x: w.x, y: w.y });
        }
      }

      for (const gl of glyphs) {
        gl.drift += 0.08;
        if (Math.random() < 0.1) {
          gl.col = (gl.col + (Math.random() < 0.5 ? -1 : 1) + cols) % cols;
          gl.row = (gl.row + (Math.random() < 0.5 ? -1 : 1) + rows) % rows;
        }
        if (Math.random() < 0.012) {
          gl.col = Math.floor(Math.random() * cols);
          gl.row = Math.floor(Math.random() * rows);
        }

        const gx = gl.col * CELL + CELL * 0.5;
        const gy = gl.row * CELL;          // note: NOT vertically centred
        let intensity = 0, fx = 0, fy = 0, dist = 0;

        for (const w of waves) {
          const ddx = gx - w.x, ddy = gy - w.y;
          dist = Math.hypot(ddx, ddy);
          const ux = dist > 1e-4 ? ddx / dist : 0;
          const uy = dist > 1e-4 ? ddy / dist : 0;
          const d = Math.exp(-Math.abs(dist - w.radius) / w.band);
          const f = Math.exp(-Math.abs(dist - (w.radius - CELL * 3.2)) / (w.band * 1.25));
          const p = Math.exp(-Math.abs(dist - (w.radius - CELL * 6.2)) / (w.band * 1.4));
          const m = dist * 0.065 - w.radius * 0.09;
          const h = d + f * 0.82 + p * 0.62;
          const g = Math.sin(m) * 0.1 * h;
          intensity += Math.max(0, h + g) * w.strength;
          const disp = (d * 1.1 + f * 0.78 + p * 0.48) * CELL * 0.7 * w.strength;
          fx += ux * disp;
          fy += uy * disp;
        }

        const px = gx + fx;
        const py = gy + fy;
        const vfade = clamp(1 - py / (H * 1.06), 0.22, 1);
        const g = clamp(intensity * (0.82 + 0.12 * tmod), 0, 1) * vfade;
        if (g < 0.006) continue;

        const ci = ((Math.floor(gl.glyphOffset + time / 220 + gl.drift * 4 +
                     dist * 0.045 + waves[2].radius * 0.04 + g * NG) % NG) + NG) % NG;
        const ch  = GLYPHS[ci];
        const hue = (gl.hueOffset + time * 0.045 + g * 90) % 360;

        ctx.fillStyle   = `hsla(${hue}, 100%, ${DARK ? 78 : 46}%, ${clamp(0.05 + g * 0.72, 0, 0.86)})`;
        ctx.shadowBlur  = 4 + g * 8;
        ctx.shadowColor = `hsla(${hue}, 100%, ${DARK ? 72 : 46}%, ${0.2 + g * 0.24})`;
        ctx.fillText(ch, px, py);
      }

      ctx.shadowBlur = 0;
      ctx.shadowColor = 'transparent';
      ctx.globalAlpha = 1;
    }

    /* ── Frame loop (24 fps cap, with translucent fade-trail) ─────────── */
    function frame(time) {
      if (!running) { raf = 0; return; }
      if (time - last < FRAME_MS) { raf = requestAnimationFrame(frame); return; }
      last = time;

      ctx.globalCompositeOperation = 'source-over';
      ctx.fillStyle = FADE;
      ctx.fillRect(0, 0, W, H);

      drawGlow();
      drawGrid();
      drawGlyphs(time);

      raf = requestAnimationFrame(frame);
    }

    /* ── Pause when the tab is hidden ─────────────────────────────────── */
    function onVisibility() {
      const vis = !document.hidden;
      if (vis === running) return;
      running = vis;
      if (!running) { cancelAnimationFrame(raf); raf = 0; return; }
      last = 0;
      raf = requestAnimationFrame(frame);
    }

    resize();
    if (running) raf = requestAnimationFrame(frame);

    let rt = 0;
    window.addEventListener('resize', () => {
      clearTimeout(rt);
      rt = setTimeout(resize, 120);
    });
    document.addEventListener('visibilitychange', onVisibility);
  })();
  </script>
</body>
</html>

Source & credit

Effect seen on https://chatgpt.com/cyber — credit to OpenAI / ChatGPT.

This is an independent recreation of the effect built from scratch for educational use. No proprietary source code was copied.