Every real spherical harmonic through l = 3. The superposition keeps evolving; each slider is how much that state is allowed to join in. Numbers on the right are the live coefficient right now.
This IS loader.html, inlined, running the john fav cut: the final output, before any of the explanation. Click it once and your wheel zooms toward the cursor with the lab's exact springy feel; click anywhere else to give the wheel back to the page. Drag the teal corner or use the presets, the loader fills whatever you give it; the margin buttons retarget the framing live and the spring carries the camera to its new home. Everything below explains how to ship it.
loader.html on this page is the animation above distilled to ONE dependency-free file: raw WebGL, no Babylon, no CDN, no fonts, no fetch, about 25 KB. It boots instantly, which is the point: it plays while your webview's real content is still loading. Under 10 KB gzipped, zero requests, one draw call over 12,800 vertex-shader-displaced triangles, and about 0.06 ms of JS per frame, so the CPU stays asleep. The wheel is tuned against the lab's measured response (same burst, same dip, same spring home), zooming toward the cursor with the springy feel that always breathes the framing back to center. Defaults are the tuned numbers; every knob is a URL parameter, and a fresh random state is rolled at every epoch so it never repeats.
A load screen exists to be on screen while your real app is still fetching. An animation that first downloads and parses a 3D engine defeats its own purpose, so the loader ships as ONE hand-rolled WebGL file and nothing else. The main viewer at the top of this page IS Babylon.js 9.18, where a full engine earns its weight (ArcRotate camera, GPU pixel readbacks for the fit measurement, tooling); the loader is that lab distilled.
| approach | payload, gzipped | requests | first frame |
|---|---|---|---|
| Babylon.js + app | ~1.4 MB engine + your page | CDN + page | after download and parse |
| three.js + app | ~170 KB engine + your page | CDN + page | after download and parse |
| loader.html | 9,912 bytes, everything included | zero | tens of milliseconds |
Measured on this build, not estimated: 24,379 bytes on disk and 9,912 bytes gzipped, including the shaders, the 286-point logomark vectors, the 128-sample measured edge profile, and the whole timeline. Runtime: 0.057 ms of JavaScript per frame (16 coefficient cosines and uniform uploads, about 1.4% of a 240 Hz frame budget), then one draw call over 12,800 triangles / 6,561 vertices with all displacement in the vertex shader. requestAnimationFrame pauses it completely when the tab is hidden, so a backgrounded webview costs nothing.
The loader is not a re-implementation of the look. tools/build-loader.py
extracts the GLSL verbatim from the Babylon lab at build time (one source of
truth), reproduces Babylon's left-handed view and projection matrices exactly, and the
chirality is pixel-verified against the lab: teal lobe upper-right, navy lower-left.
Same equation, same cheat, same profile data, pixel-identical rendering. The camera feel
is tuned against the lab's measured response, not copied constants: 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 Babylon-style decaying inertia integral, the lab's
6% radius and 5% centering springs at its 240 fps cadence made frame-rate independent,
zoom toward the cursor, and a glide home before every logo so the mark always lands
undistorted.
Arming. A load screen must never eat input, so the loader is inert by default:
clicks and wheel pass straight through to your page. ?interact=1 arms it
(the preview above is armed): one click captures the wheel, a teal ring with a glow
says so, and clicking anywhere else or leaving the frame releases it.
Zoom to cursor. Every notch records the cursor in normalized device coordinates.
The world point under it is solved from the camera, w = n·r/m + pan,
the radius step is applied, and the pan is re-solved so that exact world point stays
pinned under the cursor: pan' = w − n·r'/m. Measured drift of
the pinned point across a six-notch corner zoom: 0.002 world units.
The springiness. Three exponential systems, all frame-rate independent via
pow(k, dt·240) so a 60 Hz laptop feels identical to a 240 Hz monitor:
| system | rule, per frame | feel |
|---|---|---|
| inertia integral | each notch deposits vel += r·0.085;
consumed by step = vel·(1−0.8^(dt·240)) |
notches stack and glide instead of stepping |
| radius spring | r += (4.4−r)·(1−0.94^(dt·240)) |
zoom always breathes back home |
| centering pull | pan −= pan·(1−0.95^(dt·240)) |
the mark drifts back to center |
Tuned by measurement, not by copying constants. The obvious approach, lifting
Babylon's wheelDeltaPercentage, nets to zero against the spring. So both
viewers were driven with the identical 12-notch wheel burst in instrumented pup windows
and the loader was tuned until the response curves matched: the lab dips to radius
3.995 and springs home in about 200 ms; the loader dips to 3.990 and springs home in
about 250 ms, one sample bucket apart.
The epoch override. Whatever you have zoomed or panned, a glide window of
max(0.6s, llead+0.2s, min(1.2s, dt)) eases the framing to the calibrated
home pose, arriving exactly at epoch-start, the wheel is locked through the sit so the
mark always lands undistorted, and home becomes the new state afterwards: the wheel
frees the instant the sit ends and play resumes from the calibrated framing.
Webviews get resized constantly, so the loader treats size as live input: it listens to
window resize AND a ResizeObserver on the document, re-backs both canvases at
devicePixelRatio (capped 2), and picks its FOV axis by the LIMITING dimension, so
portrait, landscape, square, tiny or huge, the mark keeps its fraction of the container
with no letterboxing and no stretching. Proven across 300x620, 1100x300, 420x420,
180x140, 1300x700 and 900x560: the GL buffer tracked the container 1:1 at every step,
including mid-animation continuous resizes, and re-backing is coalesced into the render loop (events only mark dirty, the new size is applied and DRAWN in the same frame), so a continuous drag repaints clean every frame with no blank flashes. The repo ships
resize-proof.html, a drag-to-resize torture rig around the real loader,
so you can prove it in your own container shape.
<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 -->
Grab loader.html from this page's Files tab (or clone the repo) and serve it beside your app. It sizes to the limiting axis of any container, portrait or landscape.
| param | default | meaning |
|---|---|---|
| pt | 4.5 | playground seconds of free evolution between epochs |
| dt | 3 | seconds to steer onto the mark, and back out |
| ct | 0.3 | seconds before the epoch the outline cheat fades in |
| st | 0 | seconds the mark is held dead still |
| logo | 100 | peak opacity of the flat teal mark, in percent |
| llead | 1 | seconds the logo fade starts before the sit and ends after it |
| tlead | 1.5 | seconds before the epoch the dark lobe fades to teal |
| ov | 1 | 0 makes the cheat its own act after the steer |
| intro | 1 | 0 skips the teal logo intro |
| interact | 0 | 1 arms click-to-zoom (for previews and demos, never load screens) |
| margin | 0 | percent of the container left empty around the mark: 80 sits it small in the middle of a big window, 20 nearly fills. Zoom-in still brings it to the screen; zoom-out caps at 40% past the margined framing |
These are the settings the loop was actually tuned to, saved live while dialing it in. All three ship in the Final tab's snapshot pulldown (marked *), and each maps to loader URL params. What the knobs mean: pt is free play between epochs, dt is how long the dive onto the mark takes, ct is how late the outline cheat is held back (smaller = the sculpting is nearly invisible), st is dead-still hold time, logo is the flat mark's peak opacity, llead starts the logo fade that many seconds before the epoch and ends it that many after, tlead turns the dark lobe teal that many seconds early so the mark reads as one colour by arrival.
| cut | numbers | why it feels the way it does |
|---|---|---|
| john fav (the preview below) |
pt 5 · dt 0.7 · ct 0.2 · st 1 logo 100% · llead 0.6 · tlead 0.2 |
The keeper. A 0.7s dive into a real 1s sit, and the teal unifies only 0.2s
before arrival, so the dark lobe survives almost to the mark and the colour
snap lands WITH the logo. Loader:
?pt=5&dt=0.7&ct=0.2&st=1&llead=0.6&tlead=0.2 |
| john 1 (the default) |
pt 4.5 · dt 3 · ct 0.3 · st 0 logo 100% · llead 1 · tlead 1.5 |
The stately cut. A long 3s dinner call lets you watch the physics converge; the cheat waits until the last 0.3s so you never catch it working; there is NO sit, the whole logo moment lives on its 1s lead, swelling to full teal exactly across the epoch and letting go. Loader: no params needed. |
| snap 2 | pt 5 · dt 1 · ct 0.2 · st 0 logo 100% · llead 0.3 · tlead 1.5 |
The middle cut. A 1s dive and a tight 0.3s logo flash: punchy, still readable.
Loader: ?pt=5&dt=1&ct=0.2&llead=0.3 |
| snap 3 | pt 5 · dt 0.7 · ct 0.2 · st 0 logo 80% · llead 0.5 · tlead 1.5 |
The heartbeat cut. A 0.7s dive with the logo capped at 80% so the 3D still shows
through the flash: the mark reads as a pulse, not a poster.
Loader: ?pt=5&dt=0.7&ct=0.2&logo=80&llead=0.5 |