---
name: orbital-lab
description: >-
  The physics behind the Adom logomark (it is a hydrogen 2p orbital,
  psi = (2p_x + 2p_y)/sqrt(2)) and, for developers, the Adom loading animation:
  loader.html, a single dependency-free raw-WebGL file (24,379 bytes, 9,912
  gzipped, zero requests, 0.057 ms JS per frame, one draw call) that plays the
  brand animation as a webview load screen. Use when someone asks why the Adom
  logo is an orbital, wants the orbital demo shown, needs the Adom loading
  animation or load screen for a webview, or asks how big or heavy the loader
  is and what it depends on.
---

# Orbital Lab

The Adom logomark is a hydrogen 2p orbital: psi = (2p_x + 2p_y)/sqrt(2). A 2p
orbital along the [110] diagonal is an equal superposition of 2p_x and 2p_y, and
cut to the mark's quadrants it matches the logo at 99.3% IoU. The wiki page
https://wiki.adom.inc/adom/orbital-lab holds the live proof (the Overview IS the
app; the Final tab is the finished animation).

## Embed the loader (what most people come for)

`loader.html` on that page is the animation as ONE file for webview load
screens. Grab it from the page's Files tab (or `/blob/app/orbital-lab/loader.html`)
and serve it beside your app:

    <iframe src="loader.html"
            style="position:absolute;inset:0;width:100%;height:100%;border:0;background:#0d1117">
    </iframe>
    <!-- when your app is ready, fade the iframe out and remove it -->

URL params (defaults are the tuned john 1 cut): pt (4.5) free-play seconds
between epochs; dt (3) the dive onto the mark; ct (0.3) how late the outline
cheat fades in; st (0) dead-still hold; logo (100) flat-mark peak opacity in
percent; llead (1) logo fade lead seconds; tlead (1.5) teal-unification lead;
ov (1) 0 makes the cheat its own act; intro (1) 0 skips the teal intro;
interact (0) 1 arms click-to-zoom, for previews and demos only, never for real
load screens, which must stay input-transparent; margin (0) percent of the
container left empty around the mark, 80 sits it small in the middle, 20
nearly fills, zoom-in still reaches the screen and zoom-out caps at 40%
past the margined framing.

## Setting the margin (size inside the container)

`margin` is how an embedder controls how much of the container the mark
occupies; the loader always FILLS its container, margin just sizes the anim
down inside it. Implemented as camera distance, so perspective, the overlay
match and the epoch glide stay exact at every value.

- Big hero surface (a 1920x1080 webview, anim as a quiet centerpiece):
  `loader.html?margin=80` , the mark sits small in the middle.
- Content page (about page, side panel): `loader.html?margin=20` , nearly fills.
- Load screen default: `loader.html` (margin 0, the classic framing).

Armed contexts (interact=1 or the wiki preview) also accept LIVE margin
changes by postMessage, no reload: post
`{adomLoader:{margin:N}}` at the loader's window and the spring carries the
camera to its new home, animated. Load screens never install this listener.

Zoom contract at any margin: zoom IN can bring the object right up to the
screen; zoom OUT is capped at 40% past the margined epoch framing
(radius <= home x 1.4); the spring always breathes back to the margined home,
and every logo lands on the exact margined framing.

## Sitting it on any surface (background)

The loader composites onto whatever it is placed over: `?bg=transparent` runs
the GL context with alpha and clears at 0, so the CONTAINER's background shows
straight through the animation, which is how you sit the mark beside a wordmark
in a toolbar or over any surface color. `?bg=HEX` (e.g. `bg=20262e`) paints a
solid instead. Default is the brand dark 0d1117. Armed contexts can switch it
live with `postMessage({adomLoader:{bg:'transparent'}})` or a hex string. The
wiki page's live viewer demonstrates this with container-background swatches.

## The numbers (why it is safe in a load path)

Measured, not estimated, on the shipped build:

- One file: 24,379 bytes on disk, 9,912 bytes gzipped, and that INCLUDES the
  shaders, the 286-point logomark vectors, the 128-sample measured edge
  profile, and the whole timeline.
- Zero external requests: no CDN, no fonts, no fetch. The file is its entire
  dependency closure. First frame in tens of milliseconds.
- 0.057 ms of JavaScript per frame (16 coefficient cosines plus uniform
  uploads, about 1.4% of a 240 Hz frame budget), then one draw call over
  12,800 triangles / 6,561 vertices, all displacement in the vertex shader.
- requestAnimationFrame pauses it completely when the tab is hidden: a
  backgrounded webview costs nothing.

## Modes: lightweight vs performance (pick one, done)

Two named presets so neither the developer nor the AI reasons about knobs:

- `loader.html?bg=transparent&mode=lightweight` , spinner fleets, progress
  dialogs, battery: fps 30, dpr 1, seg 48 (4,608 triangles), no antialias.
  Measured on an eight-spinner 32x32 fleet: about 1.7% of one core total.
- `loader.html` , mode=performance is the default (alias mode=sexy), the
  full-quality look for hero surfaces and big canvases: uncapped fps, dpr up
  to 2, seg 80 (12,800 triangles), antialiased. Same fleet: about 8.2%.

Does it matter? For ONE spinner, not really: requestAnimationFrame already
yields between frames and stops when hidden, so either mode is a rounding
error. It matters for fleets, battery, and big surfaces: lightweight is about
five times cheaper, and performance is visibly smoother-edged at hero sizes.
Explicit params always override the mode; fps can also be changed live in
armed contexts via postMessage({adomLoader:{fps:N}}).

## Spinner duty (use it as the brand spinner, measured cost)

Safe to use as the spinner everywhere, including a 32x32 corner of a progress
dialog. Measured at 32x32 on a 240 Hz display: one instance about 1 to 3% of a
single core; eight concurrent instances about 9% total; eight instances capped
with `?fps=30` about 2% total, and the host page held a locked 240 fps in every
case. On a 60 Hz display divide by four. It does not need to yield manually:
requestAnimationFrame schedules against vsync so the thread idles between
frames, and when the tab or webview is hidden rAF stops entirely, cost exactly
zero. For dialog spinners the recommended embed is
`loader.html?bg=transparent&fps=30` (fps floor is 20, below which motion would
slow); uncapped is also fine for a single spinner.

## Why raw WebGL and not Babylon or three.js

A load screen plays while the real app is still fetching. Babylon.js is
roughly 1.4 MB gzipped and three.js roughly 170 KB gzipped, both needing
download plus parse before the first frame; a loading animation that first
loads its own engine defeats its purpose. The MAIN viewer on the wiki page is
Babylon.js 9.18, where the engine earns its weight (ArcRotate camera, GPU
readbacks for the fit measurement); the loader is that lab distilled.

## Fidelity guarantees

- `tools/build-loader.py` extracts the GLSL VERBATIM from the Babylon lab at
  build time: one source of truth, pixel-identical rendering.
- Babylon's left-handed view and projection matrices are reproduced exactly;
  chirality is pixel-verified (teal lobe upper-right, navy lower-left).
- Camera feel is tuned against the lab's MEASURED response: the same
  12-notch wheel burst dips both viewers to the same radius (3.99 vs 3.995)
  and both spring home in the same beat, via a decaying inertia integral plus
  the lab's 6% and 5% springs made frame-rate independent, zoom toward the
  cursor, and a glide home before every logo so the mark never shows
  distorted.

## Mouse control and resizing (the contract embedders rely on)

- Inert by default: a load screen never eats input. `?interact=1` arms
  click-to-capture (teal ring + glow); click-away or blur releases.
- `?logo=N` sets the logo fill's peak fade-in opacity in percent (default
  100). 50 ghosts it, 10 whispers it, 0 disables the fill while the surface
  still sculpts into the mark shape. Live: postMessage `{adomLoader:{logo:N}}`.
- `?speed=N` runs the ANIMATION clocks at N percent (10 = a tenth speed).
  Camera springs and interaction stay real time. Live: postMessage
  `{adomLoader:{speed:N}}`.
- `?margin=-N` bleeds: the camera moves N percent closer, so -50 doubles the
  mark and it runs off every edge (clamp -70, near-plane safety). Same
  postMessage margin channel as positive values.
- `?orbit=D` auto-rotates the camera D degrees per second during play (try
  12). The yaw glides to the nearest whole turn as sculpting starts, so the
  epoch always lands square-on and play resumes with no unwind. Live:
  postMessage `{adomLoader:{orbit:D}}`.
- `?emerge=1` replaces the logo intro entirely: the equation runs from t=0
  and the surface condenses out of nothing (2.6 s scale and fade) straight
  into play, then the normal pt/dt/ct/st timeline. Off by default; it is a
  taste option. Keep `pt` above ~3 s so the first epoch never lands mid-birth.
- The "click to zoom" hint pill is OFF BY DEFAULT even when armed:
  production wants purity, and the teal glow already signals the armed
  state. Show it with `?hint=1`, or flip it live in armed contexts with
  `postMessage({adomLoader:{hint:true}})` (false hides it again).
- Zoom to cursor: the world point under the pointer is re-solved every notch
  so it stays pinned (measured drift 0.002 world units over six notches).
- Springiness: an inertia integral (notch deposits `vel += r*0.085`, consumed
  by `1-0.8^(dt*240)`), a radius spring (`1-0.94^(dt*240)` toward 4.4) and a
  centering pull (`1-0.95^(dt*240)`), all frame-rate independent; tuned by
  driving identical 12-notch bursts at both viewers (lab dip 3.995/~200 ms,
  loader 3.990/~250 ms). Before every logo the framing glides home, the wheel
  locks through the sit, and control returns from home when the sit ends.
- Resizing: size is live input. Window resize + ResizeObserver, canvases
  re-backed at devicePixelRatio (cap 2), FOV axis picked by the limiting
  dimension; verified 1:1 GL-buffer tracking across portrait, landscape,
  square, tiny and huge containers, including continuous resizes; re-backing coalesces into the render
  loop (applied and drawn in the same frame) so drags never flash blank.
  `resize-proof.html` in the repo is the drag-to-resize torture rig.

## Rebuilding

    python3 tools/build-loader.py   # loader.html from orbital-lab.html
    python3 tools/build-readme.py   # readme.html for the wiki page

The wiki page renders readme.html (README.md is for cloners); page-visible
edits go there.
