← Back to Fathom

[ Parable build guide ]

How Fathom was built

A public aquarium, rendered without a single photograph. The memorable moments are a raw-WebGL caustics shader rippling biolum light behind the hero, and a scroll-driven depth-column navigator that dives you from sunlit kelp to the crushing abyss — the light dimming and the pressure climbing on a live gauge as you go.

static · no build stepraw WebGL causticsSpectral / Inter / Space Monolight + darkreduced-motion aware

The idea

An aquarium's whole pitch is immersion — you walk downward, out of the daylight, into places that shouldn't be survivable. A page of stock reef photos can't carry that. So the site does two things a photo can't: it makes the water move (caustics), and it makes you descend (the depth column). The voice stays warm and plain-spoken — wonder up front, stewardship right behind it — because that's how good science communicators actually talk.

The stack

Deliberately plain. One index.html, one styles.css, one main.js. No framework, no bundler, no runtime scripts — even the caustics are hand-rolled WebGL, so there's nothing to build and it ships straight to GitHub Pages. The one external request is the webfonts, pulled from the Google Fonts CDN; block it and the type degrades cleanly to the Georgia / system-ui / ui-monospace fallback stacks. Type is Spectral (a literary serif with a marine, cartographic feel), Inter for body, and Space Mono for every instrument reading — depth, pressure, temperature, ticket totals.

Signature one — the caustics shader

Caustics are the net of bright, wobbling light the surface casts onto everything below. We fake them with a classic trick: run a point through a short feedback loop of sines and cosines, take the reciprocal length each pass, and the accumulation snaps into thin, animated light filaments. Raise it to a high power and only the brightest lines survive — the caustic net. A vertical smoothstep fades the light out toward the deep, and the whole thing is tinted biolum-cyan over abyss.

// inside the fragment shader — the caustic feedback loop
vec2 q = mod(p * TAU * 2.15, TAU) - 250.0;
vec2 i = q; float c = 1.0; float inten = 0.0045;
for (int n = 0; n < 5; n++) {
  float tt = t * (1.0 - (3.5 / float(n + 1)));
  i = q + vec2(cos(tt - i.x) + sin(tt + i.y), sin(tt - i.y) + cos(tt + i.x));
  c += 1.0 / length(vec2(q.x / (sin(i.x + tt) / inten),
                         q.y / (cos(i.y + tt) / inten)));
}
c = 1.17 - pow(c / 5.0, 1.4);
float glow = pow(abs(c), 7.0);          // keep only the bright filaments

It draws on a single fullscreen triangle, caps device-pixel-ratio at 1.5, runs at ~30fps, and a u_dark uniform brightens the water in light mode. If the WebGL context fails, a CSS gradient stands in and you'd never know.

Signature two — the depth column

The zones are just stacked bands that get darker as they go down. The intelligence is a sticky gauge: on scroll it finds which zone the viewport's midline is crossing, reads that zone's real depth range off data-top/data-bot, and interpolates an exact depth — then derives pressure, temperature and remaining sunlight from physics-ish curves.

const frac  = clamp((mid - r.top) / r.height, 0, 1);
const depth = Math.round(top + (bot - top) * frac);
const pressure = 1 + depth / 10;              // ~1 atm per 10 m
const temp     = Math.max(2, 19 - depth * 0.014);
const light    = 100 * Math.exp(-depth / 150); // gone by ~1000 m

That's why the "Sunlight" readout hits 0% right as you reach the twilight band — the numbers are tied to the same depth the band is illustrating, not faked on a timer.

Details that matter

The ticket checkout is a demo — it computes a live total, validates and confirms in-place, but charges nothing and books nothing. Wire it to a real checkout (Stripe, a timed-entry ticketing API) before taking payments.

Ship it on GitHub Pages

Nothing to build. Push the folder and point Pages at the root.

gh repo create bswxyz/fathom --public --source . --push
gh api --method POST /repos/bswxyz/fathom/pages -f 'source[branch]=main' -f 'source[path]=/'

Relative paths and a .nojekyll file mean it serves from the project subpath without a single config change. That's the whole point of the static build.

← Back to Fathom