SIGNAL LOST
— THE FIELD GUIDE

How to build a haunted broadcast: one fullscreen WebGL noise shader, a five-layer CSS CRT overlay stack, a pixel-level hidden-message tuner, a channel guide of impossible programs, four redacted archive fragments, and a decoder that only unlocks at 87.30 MHz. Everything is procedural — no images, no video, no AI-generated assets.

01CONCEPT

The site pretends to be a CRT television receiving a transmission that should not exist. Instead of decorating a normal landing page with "glitch effects", every element is diegetic — part of the fiction. The sticky navigation is a PROG channel selector (eight channels, each an anchor), the OSD corners hold a channel indicator and a PAL timecode counter, sections are channels, and scrolling between them fires a tracking error, as if the tuner slipped. Even the footer is an end-of-tape label: station of record, channel index, licence expired 1992, carrier current.

The art direction is precise decay: one phosphor green (#5dffa8) on a blue-black tube (#04080a), a single alert red and a single warning amber. Nothing else. Restraint is what separates "eerie broadcast" from "Halloween template".

TokenHexRole
bg#04080atube glass — never pure black; depth comes from the shader behind it
deep#0a1512panel fills
phosphor#5dffa8the one voice of the machine — titles, OSD, traces
bright#d8ffe9body text, near-white with green memory
dim#3e6b57secondary text, borders
alert#ff4d3dREC dot, tape-damage line — used twice on the whole site
amber#ffc857corrupted / recovered fragments

Type: VT323 (a genuine DEC VT320 terminal digitization) for everything the "television" says — huge display sizes at line-height: .86 — and IBM Plex Mono for everything a human wrote. Two fonts, two narrators.

02TECH STACK

RAW WEBGL 1.0 CANVAS 2D ×2 GSAP 3.12.5 SCROLLTRIGGER WEB AUDIO ZERO IMAGES

03THE STATIC SHADER

The entire background is one shader on a single fullscreen triangle. Three ideas do all the work: a hash-based snow (two octaves of white noise at different scales), a slow vertical interference drift, and a band-displacement glitch — the screen is cut into 36 horizontal bands and each band gets a random X offset scaled by a u_glitch uniform that JavaScript spikes on channel changes and random "tracking errors", then decays by ×0.9 per frame.

// fragment shader — the whole trick
float hash(vec2 p){
  p = fract(p * vec2(123.34, 456.21));
  p += dot(p, p + 45.32);
  return fract(p.x * p.y);
}

// tracking error: slice into bands, shear them while u_glitch > 0
float band  = floor(uv.y * 36.0);
float shift = (hash(vec2(band, floor(t*14.0))) - 0.5) * u_glitch * 0.18;
uv.x += shift;

// two octaves of snow + drifting interference wave
float snow  = hash(uv*u_res*0.5  + vec2(t*97.0,  t*31.0)) * 0.72
            + hash(uv*u_res*0.23 + vec2(t*13.0, -t*57.0)) * 0.28;
float drift = sin(uv.y*7.0 - t*0.35) * 0.5 + 0.5;
float v     = snow * (0.055 + drift*0.03 + u_glitch*0.35);

vec3 col = vec3(0.016, 0.031, 0.039) + vec3(0.36, 1.0, 0.66) * v;

And the JavaScript side of the handshake:

// spiked by the channel spy, decays every frame
glitchAmt *= 0.9;
if (Math.random() < 0.004) glitchAmt = Math.min(1, glitchAmt + 0.55);
gl.uniform1f(uGlitch, glitchAmt);

Performance notes: the canvas renders at half resolution — it is noise, nobody can tell, and it halves fragment work four-fold. devicePixelRatio is capped at 2, context loss is handled (webglcontextlost / restored), and if WebGL is missing entirely the canvas falls back to a flat tube color.

04THE CRT OVERLAY STACK

Five fixed, pointer-transparent layers sit on top of everything and sell the tube:

  1. Scanlines — a 4px repeating-linear-gradient multiplied onto the page.
  2. Aperture grille — 1px vertical R/G/B stripes at 2.8% opacity. Invisible until you look; missed instantly when removed.
  3. Tracking bar — a soft bright band that sweeps down the screen every 9 s on a cubic-bezier(.7,0,.3,1) curve.
  4. Vignette — two stacked radial gradients: dark corners plus a faint phosphor bloom in the center.
  5. Flicker — a white layer keyed with steps(1) to blink at 1.5–3% opacity twice per 4.2 s cycle. Sub-perceptual, but the page feels powered.
/* the layer nobody notices and everybody feels */
.flicker{ background:#eafff3; opacity:0; animation:flick 4.2s steps(1) infinite }
@keyframes flick{
  0%,93%,100%{opacity:0} 94%{opacity:.015}
  95%{opacity:0} 97%{opacity:.03} 97.6%{opacity:0}
}

The big headline uses the classic dual-pseudo-element glitch — ::before in red, ::after in cyan, both clipped with animated clip-path insets so the tear only fires for ~8% of each 3.8 s cycle. Idle most of the time, alive forever.

05THE HIDDEN-MESSAGE TUNER

Channel 04 is the heart of the site. A 232×124 ImageData buffer is filled with random snow every frame (upscaled with image-rendering: pixelated for honest chunky pixels). A second, offscreen canvas holds the message rendered once in VT323 — its alpha channel becomes a mask. Where the mask is on, the phosphor text is mixed in proportionally to signal²; where it is off, pure noise. Dragging horizontally moves a virtual tuning knob; the sweet spot sits at 87.30 MHz.

// signal strength: 1 at the sweet spot, 0 far away
var sig = Math.max(0, 1 - Math.abs(tune - TARGET) * 7);
var noiseAmp = 1 - sig*sig * 0.85;

for (var i = 0, p = 0; i < W*H; i++, p += 4) {
  var n = Math.random();
  var v = n * noiseAmp * 110 + 8;            // green-tinted snow
  var r = v*0.35, g = v*0.95, b = v*0.6;
  if (mask[p+3] > 128) {                     // hidden carrier
    var e = sig*sig * (0.75 + 0.25*n);
    r += 40*e; g += 235*e; b += 140*e;
  }
  px[p]=r; px[p+1]=g; px[p+2]=b; px[p+3]=255;
}

Two care details: an idle autopilot drifts the knob toward the sweet spot after 4 seconds untouched, so every visitor eventually sees the message even if they never interact; and under prefers-reduced-motion the tuner starts locked with the message fully visible.

06THE OSCILLOSCOPE

The scope trace is two summed sines plus per-sample jitter, redrawn at 30 fps. The CRT phosphor persistence comes from never clearing the canvas — each frame paints a translucent background rectangle, so old traces decay exponentially:

// afterglow: translucent wipe instead of clearRect
ctx.fillStyle = 'rgba(1,4,3,0.28)';
ctx.fillRect(0, 0, W, H);
// ...then stroke the new trace with a glow
ctx.shadowColor = 'rgba(93,255,168,0.8)';  ctx.shadowBlur = 12;
y = sin(x·f1·2π + 2.1t)·0.32 + sin(x·f2·2π − 3.4t)·0.11 + jitter

The cursor bends f1 and f2, eased with a simple bend += (goal − bend) × 0.08 — the trace feels like it resists you, the way real instruments do.

07THE FICTION LAYER

Three sections exist purely to deepen the story, and each one carries its own mechanic:

CH 03 — the channel guide

A broadcast schedule of impossible programs — WEATHER FOR THE INTERIOR, STATIC HOUR, an AFTERNOON FILM with a 34-year runtime. Three entries carry an unstable class whose program titles flicker on offset steps(1) keyframes, one row is a red [SIGNAL INTRUSION], and JavaScript reads new Date().getHours() against each row's data-h attribute to pin a blinking ON AIR badge on whatever is "broadcasting" at the visitor's local time. The page is different at 3 AM. That is the point.

CH 06 — recovered logs

Four paper fragments (maintenance log, viewer letter, internal memo, final shift log) with inked-out redactions. Each redaction is a run of block glyphs whose true text lives in a data-plain attribute; hover or focus scrambles the blocks into amber plaintext through the shared scramble() routine plus a 4-step jitter keyframe — the ink glitches off, it doesn't fade. On touch devices redactions resolve themselves when scrolled fully into view.

CH 05 — the decoder chain

The deep payoff for the tuner. CH 05 initially shows four lines of leetspeak-garbled ciphertext under an ■ ENCRYPTED badge and an unknown key. When the CH 04 tuner holds a lock at 87.30 MHz for ~1.1 s, it dispatches a carrier-lock CustomEvent; the decoder catches it, spikes the global glitch bus, scrambles the badge to ▲ DECODED, fills in the key, and demodulates the four lines one by one. Two systems, one event — the site rewards the person who tuned the radio with a message that was sitting in plain sight all along.

// CH 04 — holding the lock hands the key to CH 05
if (locked) {
  lockHold += 1/30;
  if (lockHold > 1.1 && !lockSent) {
    lockSent = true;
    window.dispatchEvent(new CustomEvent('carrier-lock'));
  }
}

08HIDDEN MESSAGES

Buried transmissions, for the patient:

WhereCipherPlaintext
CH 04 — tune to 87.30 MHzsignal maskWE NEVER STOPPED BROADCASTING
CH 05 — hold the 87.30 lock ~1 sgarbled carrierYOU FOUND THE FREQUENCY… WE LEFT A LIGHT ON
CH 02 — hover the corrupted log lineblock glyphsthey can hear us through the aerial
CH 06 — hover the four redactionsblock glyphsthe carrier is still on. nothing is powering it…
footer — hex stringASCII hexWE ARE STILL HERE
index.html — first HTML commentASCII hexWE ARE STILL HERE

09REPLICATE IT

  1. Build the shader first. A fullscreen triangle, the hash() above, and a u_glitch uniform. If the background is right, the site is already 60% haunted.
  2. Add the overlay stack in this order: scanlines → grille → tracking bar → vignette → flicker. Tune each until it is barely visible; the sum is the effect.
  3. Make the chrome diegetic. Channel indicator, timecode (25 fps PAL: frames = ⌊(s % 1) × 25⌋), REC dot. Fixed corners, VT323, phosphor text-shadow.
  4. Wire the glitch bus. One global glitchAmt variable that anything can spike — channel changes, boot, random timers — and the shader consumes. Systems that share one nervous system feel alive.
  5. Hide something. The tuner mask technique works for any message. Give visitors a reason to tell someone else what they found.
  6. Extend the fiction, don't decorate it. A schedule of impossible programs, redacted paperwork, a decoder that waits for the tuner — every new section should add a mechanic and a story beat. If a section could exist on a normal website, rewrite it until it couldn't.
  7. Respect prefers-reduced-motion: freeze the shader time, disable glitch keyframes, start the tuner locked. The mood must survive with the motion off.

AI assets

None. Every pixel on the site is generated at runtime by the code above — the honest way to fake a broken machine.


◀ RETURN TO TRANSMISSION

TX 87.3 MHz · FIELD GUIDE · 57 45 20 41 52 45 20 53 54 49 4C 4C 20 48 45 52 45