app
Orbital Lab
Public Made by Adomby adom
The Adom logomark is a hydrogen 2p orbital: psi = (2p_x + 2p_y)/sqrt(2). A live Babylon.js lab that proves it (all 16 real spherical harmonics, the alignment cheat, and the directed loop), plus loader.html: the same animation as one dependency-free WebGL file for webview load screens.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Adom loader</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='5' fill='%2300b8b0'/%3E%3C/svg%3E">
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#0d1117}
canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
</style>
</head>
<body>
<canvas id="gl"></canvas>
<canvas id="ov"></canvas>
<script>
/* The Adom logomark is a hydrogen 2p orbital: psi = (2p_x + 2p_y)/sqrt(2).
This file is the wiki page adom/orbital-lab distilled to a loader. */
'use strict';
var Q=new URLSearchParams(location.search);
function P(k,d){
var o=window.__ADOM_LOADER_PARAMS||{};
if(k in o) return o[k];
var v=parseFloat(Q.get(k));return isFinite(v)?v:d;
}
var PT=P('pt',4.5), DT=P('dt',3), CT=P('ct',0.3), ST=P('st',0);
var LOGO_PK=Math.max(0,Math.min(1,P('logo',100)/100));
var LLEAD=Math.abs(P('llead',1)), TLEAD=Math.abs(P('tlead',1.5));
var EMERGE=String(P('emerge',0))==='1';
/* speed: percent playback rate for the ANIMATION clocks (timeline, evolution,
birth). 100 is real time; 10 is a tenth speed. Camera springs and user
interaction stay real-time so the feel never goes gluey. */
var SPEED=Math.max(0.05,Math.min(3,Math.abs(P('speed',100))/100));
/* orbit: automatic camera yaw during PLAY, in degrees per second (0 = off,
try 12). The angle glides to the nearest whole turn as sculpting begins,
so every epoch still lands square-on for the logo overlay, and play
resumes from the equivalent orientation with no unwinding sweep. */
var ORBIT=(P('orbit',0)||0)*Math.PI/180;
var OV=Q.get('ov')!=='0', DO_INTRO=Q.get('intro')!=='0'&&!EMERGE;
/* margin: percent of the container left EMPTY around the mark. 0 is the
classic framing; 80 sits the anim small in the middle of a big window.
Implemented as camera distance, so perspective, the overlay match and the
epoch glide all stay exact at any margin. */
/* margin accepts NEGATIVE values for full bleed: -N moves the camera N%
closer, so -50 doubles the mark and it runs off every edge. Clamped at
-70 so the surface never crosses the near plane. */
var MARGIN=Math.max(-70,Math.min(90,P('margin',0)));
function homeFor(m){ return m<0 ? R*(1+m/100) : R/(1-m/100); }
/* MODES bundle the quality knobs so nobody has to reason about them:
mode=lightweight fps 30, dpr 1, seg 48, no antialias: spinner fleets,
dialogs, battery. Looks fine at small sizes.
mode=performance (the default, alias mode=sexy) uncapped, dpr up to 2,
seg 80, antialias: hero surfaces, big canvases.
Explicit params always override the mode. rAF still yields between frames
and stops when hidden in every mode. seg and aa are boot-time; fps can be
changed live by postMessage in armed contexts. */
var MODE=String((window.__ADOM_LOADER_PARAMS&&window.__ADOM_LOADER_PARAMS.mode)||Q.get('mode')||'').toLowerCase();
if(MODE==='sexy')MODE='performance';
var LIGHT=(MODE==='lightweight'||MODE==='light');
/* SAFEGUARD (2026-08-04, upstreamed from Hydrogen Desktop): default to a BOUNDED 60fps even
in performance mode instead of uncapped. An uncapped WebGL surface on a slower/integrated
GPU pegs the GPU and starves the embedding app's UI thread (frozen nav) and delivers frame
BURSTS that read as "crazy speed". A hero that genuinely wants uncapped opts out: ?fps=0. */
var FPSCAP=Math.max(0,P('fps',LIGHT?30:60)); if(FPSCAP>0&&FPSCAP<20)FPSCAP=20;
var DPRCAP=Math.max(1,Math.min(3,P('dpr',LIGHT?1:2)));
var SEGQ=Math.round(Math.max(24,Math.min(128,P('seg',LIGHT?48:80))));
var AAON=P('aa',LIGHT?0:1)>=1;
var lastDraw=0;
/* Background contract: bg=HEX paints a solid, bg=transparent leaves the GL
clear at alpha 0 so the CONTAINER's background shows straight through, and
the mark composites onto whatever surface it sits on (a toolbar, a hero,
anything). Changeable live by postMessage in armed contexts. */
var BG=[0.051,0.067,0.090], TRANSP=false;
function applyBg(raw){
raw=String(raw||'').toLowerCase().replace('#','');
TRANSP=(raw==='transparent');
var ok=/^[0-9a-f]{6}$/.test(raw);
BG=ok?[parseInt(raw.slice(0,2),16)/255,parseInt(raw.slice(2,4),16)/255,parseInt(raw.slice(4,6),16)/255]
:[0.051,0.067,0.090];
var css=TRANSP?'transparent':(ok?('#'+raw):'#0d1117');
document.documentElement.style.background=css;
document.body.style.background=css;
}
applyBg((window.__ADOM_LOADER_PARAMS&&window.__ADOM_LOADER_PARAMS.bg)||Q.get('bg'));
var VS=`precision highp float;
attribute vec3 position;
uniform mat4 uMVP;
uniform float c[16];
uniform float uScale;
varying float vPsi;
varying vec3 vN;
float psiOf(vec3 d){
float x=d.x, y=d.y, z=d.z;
float s = 0.0;
s += c[0] * 0.2820948;
s += c[1] * 0.4886025 * y;
s += c[2] * 0.4886025 * z;
s += c[3] * 0.4886025 * x;
s += c[4] * 1.0925484 * x * y;
s += c[5] * 1.0925484 * y * z;
s += c[6] * 0.3153916 * (3.0*z*z - 1.0);
s += c[7] * 1.0925484 * x * z;
s += c[8] * 0.5462742 * (x*x - y*y);
s += c[9] * 0.5900436 * y * (3.0*x*x - y*y);
s += c[10] * 2.8906114 * x * y * z;
s += c[11] * 0.4570458 * y * (5.0*z*z - 1.0);
s += c[12] * 0.3731763 * z * (5.0*z*z - 3.0);
s += c[13] * 0.4570458 * x * (5.0*z*z - 1.0);
s += c[14] * 1.4453057 * z * (x*x - y*y);
s += c[15] * 0.5900436 * x * (x*x - 3.0*y*y);
return s;
}
uniform float uFlat, uRot, uPlump, uGap, uEdge, uCorner;
uniform float uLogoR[128]; // the mark's measured edge, 0..90 degrees
/* The morph target for ANY direction.
Honest note on what works and what does not.
Sliding only the RADIUS toward the mark's edge leaves out-of-quadrant
vertices with a target of zero, so they pile at the node: that was the bead
and the creases.
Moving the DIRECTION too seemed like the fix, but both variants failed for
the same underlying reason: this sphere's parameterisation covers the full
180-degree lobe, and the mark's leaf is 90 degrees. Clamping stacks half the
vertices on the boundary planes (shredding). Remapping 2:1 spreads them, but
then every vertex including those at the node gets pushed out to the leaf
edge, so the nodal pinch vanishes and the two lobes fuse into a tube.
What actually measures best is the simplest thing: keep the physics surface
exactly as the wavefunction gives it, and cut it to the mark's quadrants.
That scored 95.9% IoU at 0.8% spill and renders clean, because it never
moves a vertex anywhere the surface was not already. It IS a clip, and that
is the honest description of the licence in the mark. */
vec3 shapedPos(vec3 dir){
float pp = psiOf(dir);
vec3 physics = dir * abs(pp);
if (uFlat <= 0.0001) return physics;
float PI4 = 0.7853981634;
float axis = radians(uRot);
vec2 e1 = vec2(cos(axis - PI4), sin(axis - PI4));
vec2 e2 = vec2(cos(axis + PI4), sin(axis + PI4));
float s = (pp >= 0.0) ? 1.0 : -1.0;
float lobe = (pp >= 0.0) ? axis : axis + 3.14159265;
/* A hard half-plane cut leaves a sharp corner where the flat edge meets the
outer curve; the mark has a real radius there. min(d,0) rounded is
0.5*(d - sqrt(d*d + rc*rc)), which eases into the cut over a band of
width rc instead of hitting it as a crease. */
vec2 q = physics.xy;
float rc = uCorner;
float d1 = dot(q, e1) * s;
float p1 = (rc > 1e-5) ? 0.5 * (d1 - sqrt(d1*d1 + rc*rc)) : min(d1, 0.0);
q -= e1 * (p1 * s);
float d2 = dot(q, e2) * s;
float p2 = (rc > 1e-5) ? 0.5 * (d2 - sqrt(d2*d2 + rc*rc)) : min(d2, 0.0);
q -= e2 * (p2 * s);
vec3 clipped = vec3(mix(physics.xy, q, uFlat), physics.z);
/* OUTER BOUNDARY correction.
The clip fixes the inner corner but leaves the outer edge as the plain
circle the physics gives: it overshoots the mark in some directions and
falls short in others. So for each direction, compare the orbital's own
in-plane radius against the mark's measured edge at that same angle and
scale by the ratio. Points outside the mark are pulled in, points inside
are pushed out, and the silhouette lands exactly on the vectors.
This scales the in-plane component only, so the solid stays coherent
out of plane rather than collapsing. */
if (uEdge > 0.0001) {
vec2 cq = clipped.xy;
float cl = length(cq);
if (cl > 1e-5) {
float cang = atan(cq.y, cq.x);
float cda = atan(sin(cang - lobe), cos(cang - lobe));
if (abs(cda) <= PI4 + 0.001) {
float u = (clamp(cda, -PI4, PI4) + PI4) / (2.0 * PI4) * 127.0;
float fi = floor(min(u, 126.0));
float f = u - fi;
int i = int(fi);
int j = int(fi + 1.0);
float rEdge = mix(uLogoR[i], uLogoR[j], f);
// the orbital's own equatorial radius in this same direction
vec3 eqd = vec3(cos(cang), sin(cang), 0.0);
float rPsi = abs(psiOf(eqd));
if (rPsi > 1e-5) {
float k = rEdge / rPsi;
clipped.xy = cq * mix(1.0, k, uEdge);
}
}
}
}
return clipped;
}
void main(void){
vec3 d = normalize(position);
float p = psiOf(d);
vPsi = p;
vec3 pos = shapedPos(d) * uScale;
pos.xy += normalize(vec2(cos(radians(uRot)), sin(radians(uRot))))
* ((p >= 0.0) ? 1.0 : -1.0) * uGap * uScale * uFlat;
/* Normals are differenced from the SHAPED radius, not the raw wavefunction.
Using the raw psi here left the lighting describing a surface that no
longer existed once the cheat moved it, which showed up as a dark crater
across the transformed lobe. */
vec3 t1 = normalize(abs(d.z) < 0.9 ? cross(d, vec3(0.,0.,1.)) : cross(d, vec3(1.,0.,0.)));
vec3 t2 = cross(d, t1);
float hh = 0.03;
vec3 a = normalize(d + t1*hh), b = normalize(d + t2*hh);
vec3 pa = shapedPos(a) * uScale;
vec3 pb = shapedPos(b) * uScale;
vec3 nn = cross(pa - pos, pb - pos);
vN = normalize(dot(nn, d) < 0.0 ? -nn : nn);
gl_Position = uMVP * vec4(pos, 1.0);
}`;
var FS=`precision highp float;
varying float vPsi;
varying vec3 vN;
uniform vec3 cPos, cNeg;
void main(void){
vec3 base = vPsi >= 0.0 ? cPos : cNeg;
vec3 n = normalize(vN);
float lam = 0.42 + 0.58*max(0.0, dot(n, normalize(vec3(-0.4,0.85,-0.55))));
float rim = pow(1.0 - abs(n.z), 2.5)*0.32;
gl_FragColor = vec4(base*lam + vec3(0.0,0.72,0.68)*rim, 1.0);
}`;
var LOGO_R=[0.29682, 0.31691, 0.33085, 0.34219, 0.3519, 0.36055, 0.36839, 0.37555, 0.38216, 0.3883, 0.39398, 0.39936, 0.40453, 0.40917, 0.41376, 0.41798, 0.422, 0.42584, 0.4294, 0.43293, 0.43608, 0.43927, 0.44211, 0.445, 0.44757, 0.45018, 0.45253, 0.45491, 0.45702, 0.45917, 0.46107, 0.46303, 0.46475, 0.46655, 0.46809, 0.46969, 0.4711, 0.47252, 0.47382, 0.47507, 0.47627, 0.47735, 0.47847, 0.47938, 0.48038, 0.48119, 0.48205, 0.48281, 0.4835, 0.48421, 0.48477, 0.48541, 0.48586, 0.48634, 0.48677, 0.48712, 0.48754, 0.48774, 0.48802, 0.4882, 0.48834, 0.48853, 0.48852, 0.48859, 0.48859, 0.48852, 0.48853, 0.48834, 0.4882, 0.48802, 0.48774, 0.48754, 0.48712, 0.48677, 0.48634, 0.48586, 0.48541, 0.48477, 0.48421, 0.4835, 0.48281, 0.48205, 0.48119, 0.48038, 0.47938, 0.47847, 0.47735, 0.47627, 0.47507, 0.47382, 0.47252, 0.4711, 0.46969, 0.46807, 0.46651, 0.46473, 0.46303, 0.46107, 0.45918, 0.45702, 0.45491, 0.45253, 0.45018, 0.44757, 0.445, 0.44211, 0.43927, 0.43608, 0.43293, 0.4294, 0.42584, 0.422, 0.41798, 0.41376, 0.40917, 0.40453, 0.3993, 0.39391, 0.38829, 0.38214, 0.37552, 0.36839, 0.36056, 0.3519, 0.34209, 0.33073, 0.31683, 0.2966];
var PATHS=[[[0.8046, 0.8046], [0.7789, 0.8291], [0.7525, 0.852], [0.7254, 0.8733], [0.6977, 0.8929], [0.6695, 0.911], [0.6408, 0.9274], [0.6118, 0.9422], [0.5825, 0.9553], [0.5531, 0.9668], [0.5235, 0.9766], [0.4939, 0.9847], [0.4644, 0.9911], [0.435, 0.9957], [0.4059, 0.9987], [0.3771, 1.0], [0.3486, 0.9995], [0.3206, 0.9972], [0.2932, 0.9932], [0.2664, 0.9874], [0.2403, 0.9799], [0.215, 0.9705], [0.1906, 0.9594], [0.1672, 0.9464], [0.1448, 0.9316], [0.1236, 0.9149], [0.1035, 0.8965], [0.0958, 0.8885], [0.0884, 0.8803], [0.0813, 0.8719], [0.0745, 0.8633], [0.0679, 0.8545], [0.0617, 0.8456], [0.0558, 0.8364], [0.0502, 0.8271], [0.0449, 0.8176], [0.0399, 0.8079], [0.0352, 0.7981], [0.0308, 0.7882], [0.0267, 0.7781], [0.0229, 0.7678], [0.0194, 0.7574], [0.0162, 0.7469], [0.0132, 0.7363], [0.0106, 0.7255], [0.0083, 0.7146], [0.0062, 0.7036], [0.0045, 0.6926], [0.003, 0.6814], [0.0018, 0.6701], [0.0009, 0.6587], [0.0003, 0.6473], [0.0, 0.6358], [0.0, 0.6357], [0.0, 0.6356], [0.0, 0.6354], [0.0, 0.6353], [0.0, 0.6352], [0.0, 0.6351], [0.0, 0.635], [0.0, 0.6349], [0.0, 0.6348], [0.0, 0.6346], [0.0, 0.0135], [0.0001, 0.0127], [0.0001, 0.0118], [0.0003, 0.011], [0.0004, 0.0102], [0.0007, 0.0094], [0.0009, 0.0086], [0.0013, 0.0079], [0.0016, 0.0071], [0.002, 0.0064], [0.0025, 0.0058], [0.003, 0.0051], [0.0035, 0.0045], [0.004, 0.0039], [0.0046, 0.0034], [0.0052, 0.0028], [0.0059, 0.0024], [0.0065, 0.0019], [0.0073, 0.0015], [0.008, 0.0012], [0.0087, 0.0008], [0.0095, 0.0006], [0.0103, 0.0003], [0.0111, 0.0002], [0.0119, 0.0], [0.0128, -0.0001], [0.0137, -0.0001], [0.6281, -0.0001], [0.64, 0.0], [0.6517, 0.0005], [0.6635, 0.0012], [0.6751, 0.0022], [0.6866, 0.0036], [0.698, 0.0052], [0.7093, 0.0072], [0.7206, 0.0094], [0.7316, 0.012], [0.7426, 0.0148], [0.7534, 0.018], [0.7641, 0.0215], [0.7747, 0.0253], [0.7851, 0.0295], [0.7954, 0.0339], [0.8055, 0.0386], [0.8154, 0.0437], [0.8252, 0.0491], [0.8348, 0.0548], [0.8442, 0.0608], [0.8534, 0.0671], [0.8624, 0.0737], [0.8712, 0.0807], [0.8799, 0.088], [0.8883, 0.0956], [0.8965, 0.1035], [0.915, 0.1235], [0.9316, 0.1448], [0.9464, 0.1672], [0.9594, 0.1906], [0.9705, 0.215], [0.9799, 0.2403], [0.9874, 0.2664], [0.9932, 0.2932], [0.9972, 0.3206], [0.9995, 0.3486], [1.0, 0.377], [0.9987, 0.4059], [0.9958, 0.435], [0.9911, 0.4644], [0.9847, 0.4939], [0.9766, 0.5235], [0.9668, 0.5531], [0.9553, 0.5825], [0.9422, 0.6118], [0.9274, 0.6408], [0.911, 0.6695], [0.8929, 0.6977], [0.8733, 0.7254], [0.852, 0.7525], [0.8291, 0.7789], [0.8046, 0.8046]], [[-0.8046, -0.8047], [-0.7789, -0.8292], [-0.7525, -0.8521], [-0.7254, -0.8734], [-0.6977, -0.893], [-0.6695, -0.9111], [-0.6408, -0.9275], [-0.6118, -0.9423], [-0.5825, -0.9554], [-0.5531, -0.9669], [-0.5235, -0.9767], [-0.4939, -0.9847], [-0.4644, -0.9912], [-0.435, -0.9958], [-0.4059, -0.9988], [-0.3771, -1.0001], [-0.3486, -0.9996], [-0.3206, -0.9973], [-0.2932, -0.9933], [-0.2664, -0.9875], [-0.2403, -0.98], [-0.215, -0.9706], [-0.1906, -0.9595], [-0.1672, -0.9465], [-0.1448, -0.9317], [-0.1236, -0.915], [-0.1035, -0.8965], [-0.0958, -0.8886], [-0.0884, -0.8804], [-0.0813, -0.872], [-0.0745, -0.8634], [-0.0679, -0.8546], [-0.0617, -0.8456], [-0.0558, -0.8365], [-0.0502, -0.8272], [-0.0449, -0.8177], [-0.0399, -0.808], [-0.0352, -0.7982], [-0.0308, -0.7883], [-0.0267, -0.7782], [-0.0229, -0.7679], [-0.0194, -0.7575], [-0.0162, -0.747], [-0.0132, -0.7364], [-0.0106, -0.7256], [-0.0083, -0.7147], [-0.0062, -0.7037], [-0.0045, -0.6927], [-0.003, -0.6815], [-0.0018, -0.6702], [-0.0009, -0.6588], [-0.0003, -0.6474], [-0.0, -0.6359], [-0.0, -0.6357], [-0.0, -0.6356], [-0.0, -0.6355], [-0.0, -0.6354], [-0.0, -0.6353], [-0.0, -0.6352], [-0.0, -0.6351], [-0.0, -0.635], [-0.0, -0.6349], [-0.0, -0.6347], [-0.0, -0.0136], [-0.0001, -0.0128], [-0.0001, -0.0119], [-0.0003, -0.0111], [-0.0004, -0.0103], [-0.0007, -0.0095], [-0.0009, -0.0087], [-0.0013, -0.008], [-0.0016, -0.0072], [-0.002, -0.0065], [-0.0025, -0.0059], [-0.003, -0.0052], [-0.0035, -0.0046], [-0.004, -0.004], [-0.0046, -0.0034], [-0.0052, -0.0029], [-0.0059, -0.0025], [-0.0065, -0.002], [-0.0073, -0.0016], [-0.008, -0.0012], [-0.0087, -0.0009], [-0.0095, -0.0007], [-0.0103, -0.0004], [-0.0111, -0.0002], [-0.0119, -0.0001], [-0.0128, -0.0], [-0.0137, -0.0], [-0.6281, -0.0], [-0.6399, -0.0001], [-0.6517, -0.0006], [-0.6634, -0.0013], [-0.6751, -0.0023], [-0.6866, -0.0037], [-0.698, -0.0053], [-0.7093, -0.0072], [-0.7205, -0.0095], [-0.7316, -0.0121], [-0.7426, -0.0149], [-0.7534, -0.0181], [-0.7641, -0.0216], [-0.7747, -0.0254], [-0.7851, -0.0295], [-0.7954, -0.034], [-0.8055, -0.0387], [-0.8154, -0.0438], [-0.8252, -0.0492], [-0.8347, -0.0549], [-0.8442, -0.0609], [-0.8534, -0.0672], [-0.8624, -0.0738], [-0.8712, -0.0808], [-0.8798, -0.0881], [-0.8883, -0.0957], [-0.8965, -0.1036], [-0.9149, -0.1236], [-0.9316, -0.1449], [-0.9464, -0.1673], [-0.9593, -0.1907], [-0.9705, -0.2151], [-0.9799, -0.2404], [-0.9874, -0.2665], [-0.9932, -0.2933], [-0.9972, -0.3207], [-0.9995, -0.3487], [-1.0, -0.3771], [-0.9987, -0.406], [-0.9957, -0.4351], [-0.991, -0.4645], [-0.9846, -0.494], [-0.9765, -0.5236], [-0.9668, -0.5532], [-0.9553, -0.5826], [-0.9422, -0.6119], [-0.9274, -0.6409], [-0.911, -0.6695], [-0.8929, -0.6978], [-0.8732, -0.7255], [-0.852, -0.7526], [-0.8291, -0.779], [-0.8046, -0.8047]]];
var glopt={antialias:AAON,alpha:true,premultipliedAlpha:true};
var glc=document.getElementById('gl'), gl=glc.getContext('webgl2',glopt)||glc.getContext('webgl',glopt);
var ovc=document.getElementById('ov'), ox=ovc.getContext('2d');
if(!gl){ document.body.style.background='#0d1117'; throw new Error('no webgl'); }
function sh(type,src){var s=gl.createShader(type);gl.shaderSource(s,src);gl.compileShader(s);
if(!gl.getShaderParameter(s,gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));return s;}
var prog=gl.createProgram();
gl.attachShader(prog,sh(gl.VERTEX_SHADER,VS));
gl.attachShader(prog,sh(gl.FRAGMENT_SHADER,FS));
gl.linkProgram(prog);
if(!gl.getProgramParameter(prog,gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog));
gl.useProgram(prog);
var U={}; ['uMVP','c','uScale','uFlat','uRot','uPlump','uGap','uEdge','uCorner','uLogoR','cPos','cNeg']
.forEach(function(n){U[n]=gl.getUniformLocation(prog,n);});
/* unit sphere, displaced entirely in the vertex shader */
var SEG=SEGQ, verts=[], idx=[];
for(var i=0;i<=SEG;i++){var th=i/SEG*Math.PI;
for(var j=0;j<=SEG;j++){var ph=j/SEG*2*Math.PI;
verts.push(Math.sin(th)*Math.cos(ph), Math.cos(th), Math.sin(th)*Math.sin(ph));}}
for(var r=0;r<SEG;r++)for(var q2=0;q2<SEG;q2++){
var a=r*(SEG+1)+q2,b=a+SEG+1;
idx.push(a,b,a+1,b,b+1,a+1);}
var vb=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,vb);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(verts),gl.STATIC_DRAW);
var ib=gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,ib);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,new Uint16Array(idx),gl.STATIC_DRAW);
var loc=gl.getAttribLocation(prog,'position');
gl.enableVertexAttribArray(loc);gl.vertexAttribPointer(loc,3,gl.FLOAT,false,0,0);
gl.enable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE);
gl.uniform1fv(U.uLogoR,new Float32Array(LOGO_R));
gl.uniform1f(U.uPlump,0);gl.uniform1f(U.uGap,0);gl.uniform1f(U.uCorner,0);gl.uniform1f(U.uRot,45);
var C_POS=[0x19/255,0xc8/255,0xbd/255], C_NEG=[0x12/255,0x3a/255,0x63/255];
gl.uniform3fv(U.cPos,C_POS);
/* Babylon LEFT-HANDED camera, reproduced exactly: eye (0,0,-4.4) looking +Z,
fov 0.8 fixed on the LIMITING axis so the mark keeps its fraction of any
canvas shape (the lab's fitFraming rule). */
var R=4.4, HOME=homeFor(MARGIN), CAMR=HOME, PANX=0, PANY=0, FOV=0.8, ZN=0.1, ZF=Math.max(100,HOME*3);
/* Babylon's ACTUAL zoom model, tuned against the lab's measured curve: each
notch deposits a decaying velocity integral (inertia 0.9) that is consumed
frame by frame straight into the radius, while the lab's springs pull
radius and pan home every frame. NOTCH below is the one tuning constant,
calibrated so a 12-notch burst matches the lab: dip to ~4.0, home ~200ms
after release. */
var NOTCH=0.085, velR=0, lastNX=0, lastNY=0;
function proj(w,h){
var f=1/Math.tan(FOV/2), a=w/h;
return (a>=1)? {m0:f/a, m5:f} : {m0:f, m5:f*a};
}
function setMVP2(w,h,r,px,py,s){
r=(r===undefined)?CAMR:r; px=px||0; py=py||0; s=(s===undefined)?1:s;
var m=proj(w,h);
var A=ZF/(ZF-ZN), B=-ZN*ZF/(ZF-ZN);
// view: yaw orbA about Y, model scale s (emerge birth), pan, then
// z' = z + r (eye at -r on z, LH). c=1,sn=0 reduces to the plain matrix.
var c=Math.cos(orbA), sn=Math.sin(orbA);
gl.uniformMatrix4fv(U.uMVP,false,new Float32Array([
m.m0*s*c,0,-A*s*sn,-s*sn,
0,m.m5*s,0,0,
m.m0*s*sn,0,A*s*c,s*c,
-m.m0*px,-m.m5*py,A*r+B,r
]));
}
var W=0,H=0,DPR=1,needSize=true;
/* Resize events only mark dirty. Re-backing a canvas CLEARS it, and doing
that on every event mid-drag leaves blank frames until the next rAF: the
jerk. Instead the size is applied at the TOP of the frame and the frame
draws immediately at the new size, so a continuous drag repaints clean
every single frame with at most one reallocation per frame. */
function resize(){ needSize=true; }
function applySize(){
var w=glc.clientWidth, h=glc.clientHeight, d=Math.min(DPRCAP,window.devicePixelRatio||1);
if(!needSize && w===W && h===H && d===DPR) return;
needSize=false;
if(w===W && h===H && d===DPR) return;
W=w; H=h; DPR=d;
glc.width=w*d; glc.height=h*d;
ovc.width=w*d; ovc.height=h*d;
gl.viewport(0,0,glc.width,glc.height);
}
window.addEventListener('resize',resize);
/* Containers resize without warning in webviews; the window resize event
covers iframe viewport changes, and the ResizeObserver is the belt for
any host that reflows the document instead. */
if(window.ResizeObserver) new ResizeObserver(resize).observe(document.documentElement);
resize();
/* ---- the directed loop, distilled ---- */
var L=[0,1,1,1,2,2,2,2,2,3,3,3,3,3,3,3];
var E=L.map(function(l){return -13.6/((l+1)*(l+1));});
var LOGO=new Array(16).fill(0); LOGO[1]=0.7071; LOGO[3]=0.7071;
function smooth(x){x=Math.max(0,Math.min(1,x));return x*x*(3-2*x);}
function lerp(a,b,t){return a+(b-a)*t;}
var gains=null;
function roll(){gains=new Array(16).fill(0);
var n=2+Math.floor(Math.random()*4);
for(var j=0;j<n;j++)gains[(Math.random()*16)|0]=0.35+Math.random()*0.65;}
roll();
var eS=PT+DT+(OV?0:CT), eE=eS+ST, CYC=eE+(OV?0:CT)+DT;
var INTRO={dark:0.9,logoIn:1.4,hold:0.5,reveal:1.2};
var inIntro=DO_INTRO, introT=0, t=(DO_INTRO||EMERGE)?0:eE, tEvo=0, rolled=false, last=performance.now();
/* emerge=1 replaces the logo intro: the equation runs from t=0 and the
surface condenses out of nothing (scale+fade over BIRTH_S seconds) into
plain play, then the normal pt/dt/ct/st timeline takes over. Keep pt
comfortably above BIRTH_S so the first epoch never lands mid-birth. */
var BIRTH_S=2.6, birth=EMERGE?0:1, birthT=0;
var orbA=0; // auto-orbit yaw, radians
function frame(now){
/* STOP WHEN HIDDEN (the header comment promised this; it was never implemented). An
off-screen orbital must not keep burning GPU. Reschedule without drawing or advancing
time; keep `last` current so returning to visible never computes a jumped dt. */
if(document.hidden){ last=now; requestAnimationFrame(frame); return; }
if(FPSCAP>0){
if(now-lastDraw < 1000/FPSCAP - 1){ requestAnimationFrame(frame); return; }
lastDraw=now;
}
applySize();
/* Clamp dt to [0, 0.05]: never negative (a backwards clock across resume/DST would run
the sim in reverse) and never a big jump (a stalled frame can't lurch). */
var dt=Math.max(0,Math.min(0.05,(now-last)/1000)); last=now;
var adt=dt*SPEED; tEvo+=adt;
if(birth<1){birthT+=adt;birth=smooth(Math.min(1,birthT/BIRTH_S));
glc.style.opacity=birth>=1?'':String(0.04+0.96*birth);}
var k,cheat,fill,teal,pinned=false,drawMesh=true;
if(inIntro){
introT+=adt;
var e1=INTRO.dark,e2=e1+INTRO.logoIn,e3=e2+INTRO.hold,e4=e3+INTRO.reveal,x=introT;
k=1;cheat=1;teal=1;pinned=true;drawMesh=x>=e2;
fill = x<e1?0 : x<e2?smooth((x-e1)/(e2-e1)) : x<e3?1 : 1-smooth((x-e3)/(e4-e3));
if(x>=e4){inIntro=false;t=eE;roll();}
}else{
t=(t+adt)%CYC;
var ctw=OV?Math.min(CT,DT):CT;
if(t<PT){k=0;}
else if(t<PT+DT){k=smooth((t-PT)/DT);}
else if(t<eS){k=1;}
else if(t<eE){k=1;pinned=true;}
else if(!OV&&t<eE+CT){k=1;}
else{var o=OV?t-eE:t-eE-CT;k=1-smooth(Math.min(1,o/DT));}
cheat = t<eS ? (t<=eS-ctw?0:smooth(1-(eS-t)/ctw))
: t<=eE ? 1 : (t>=eE+ctw?0:smooth(1-(t-eE)/ctw));
if(t>=eS&&!rolled){roll();rolled=true;} if(t<eS)rolled=false;
var mid=(eS+eE)/2, a2=mid-Math.max(0.001,(eE-eS)/2+LLEAD), b2=mid+Math.max(0.001,(eE-eS)/2+LLEAD);
fill=(LOGO_PK<=0||t<=a2||t>=b2||(b2-a2)<0.02)?0
:(t<mid?smooth((t-a2)/(mid-a2)):smooth((b2-t)/(b2-mid)))*LOGO_PK;
teal = t<eS ? (TLEAD<=0.01?0:(t<=eS-TLEAD?0:smooth(1-(eS-t)/TLEAD)))
: t<=eE ? 1 : (TLEAD<=0.01?0:(t>=eE+TLEAD?0:smooth(1-(t-eE)/TLEAD)));
}
/* Auto-orbit: yaw advances only in pure play; the moment sculpting starts
the angle is pulled to the nearest whole turn, so the epoch pose is the
calibrated square-on view and nothing ever has to spin backwards. */
if(ORBIT!==0){
var kk=inIntro?1:k;
if(kk<=0){ orbA+=ORBIT*adt; }
else{
var whole=Math.round(orbA/(2*Math.PI))*(2*Math.PI);
orbA=orbA+(whole-orbA)*smooth(Math.min(1,kk));
}
}
/* The camera comes HOME for the logo. Whatever the user zoomed or panned,
the framing glides back to the exact calibrated pose before epoch-start,
holds through the epoch (the overlay match depends on it), and hands the
wheel back afterwards from the home pose. */
var effR=CAMR, effPX=PANX, effPY=PANY, camK=0;
if(!inIntro){
/* Glide home for the logo, then home IS the new state: no gliding back
to the old zoom, the wheel simply frees again the moment the sit ends
and play resumes from the calibrated framing. */
var RET=Math.max(0.6, LLEAD+0.2, Math.min(1.2,DT));
camK = (t>eS-RET&&t<eS) ? smooth((t-(eS-RET))/RET) : 0;
if(t>=eS&&t<=eE+0.001){ CAMR=HOME; PANX=0; PANY=0; velR=0; camK=1; }
// consume the wheel's decaying integral, zooming toward the cursor
if(Math.abs(velR)>0.0001){
var stepK=1-Math.pow(0.8, dt*240);
var step=velR*stepK; velR-=step;
var m2=proj(W,H);
var wx=lastNX*CAMR/m2.m0+PANX, wy=lastNY*CAMR/m2.m5+PANY;
/* zoom in to the screen freely; zoom out capped 40% past the epoch framing */
var r2=Math.max(2.4,Math.min(HOME*1.4,CAMR-step));
PANX=wx-lastNX*r2/m2.m0; PANY=wy-lastNY*r2/m2.m5;
CAMR=r2;
}
/* The lab's springs, verbatim: radius 6% and pan 5% per frame at the
lab's 240fps cadence, frame-rate independent. */
var spr=1-Math.pow(0.94, dt*240), sprT=1-Math.pow(0.95, dt*240);
CAMR+=(HOME-CAMR)*spr; PANX-=PANX*sprT; PANY-=PANY*sprT;
window.__lock = (t>eS-RET && t<=eE);
effR=CAMR+(HOME-CAMR)*camK; effPX=PANX*(1-camK); effPY=PANY*(1-camK);
} else { effR=HOME; effPX=0; effPY=0; }
setMVP2(W,H,effR,effPX,effPY,birth);
window.__cam={r:+effR.toFixed(3),x:+effPX.toFixed(3),y:+effPY.toFixed(3),k:+camK.toFixed(2)};
var c=new Array(16), nrm=0, i;
if(pinned){ for(i=0;i<16;i++)c[i]=LOGO[i]; }
else{
for(i=0;i<16;i++){
var env=0.55+0.45*Math.cos(tEvo*(0.13+i*0.017)+i*1.7);
var live=gains[i]*env*Math.cos(E[i]*tEvo*0.5);
c[i]=live*(1-k)+LOGO[i]*k;
}
}
for(i=0;i<16;i++)nrm+=c[i]*c[i]; nrm=Math.sqrt(nrm)||1;
for(i=0;i<16;i++)c[i]/=nrm;
if(TRANSP) gl.clearColor(0,0,0,0); else gl.clearColor(BG[0],BG[1],BG[2],1);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
if(drawMesh){
gl.uniform1fv(U.c,new Float32Array(c));
gl.uniform1f(U.uScale,2.5*(0.9+0.25*k));
gl.uniform1f(U.uFlat,cheat);gl.uniform1f(U.uEdge,cheat);
gl.uniform3f(U.cNeg,lerp(C_NEG[0],C_POS[0],teal),lerp(C_NEG[1],C_POS[1],teal),lerp(C_NEG[2],C_POS[2],teal));
gl.drawElements(gl.TRIANGLES,idx.length,gl.UNSIGNED_SHORT,0);
}
ox.clearRect(0,0,ovc.width,ovc.height);
if(fill>0.004){
var mm=proj(W,H), ppw=mm.m0*ovc.width/2/(window.__cam?window.__cam.r:CAMR);
var er=window.__cam?window.__cam.r:CAMR;
var s=Math.min(ovc.width,ovc.height)*0.34*(R/er);
ox.save();ox.translate(ovc.width/2-(window.__cam?window.__cam.x:0)*ppw,
ovc.height/2+(window.__cam?window.__cam.y:0)*ppw);
ox.globalAlpha=fill;ox.fillStyle='#00b8b0';
for(var p2=0;p2<PATHS.length;p2++){var pts=PATHS[p2];ox.beginPath();
for(var j2=0;j2<pts.length;j2++){var X=pts[j2][0]*s,Y=-pts[j2][1]*s;
j2?ox.lineTo(X,Y):ox.moveTo(X,Y);}
ox.closePath();ox.fill();}
ox.restore();
}
requestAnimationFrame(frame);
}
/* EXACTLY ONE loop per document: if this script ever evaluates twice, a second concurrent
frame() loop would advance the sim clock in parallel (2x/3x "crazy speed"). */
if(!window.__ADOM_ORBITAL_RUNNING){
window.__ADOM_ORBITAL_RUNNING=true;
requestAnimationFrame(frame);
}
/* Click-to-zoom, OFF unless armed: real load screens must never eat input.
Armed via ?interact=1 (or window.__ADOM_LOADER_INTERACT, which the wiki
readme's inline preview sets). Click the canvas to capture the wheel for
zoom; click is released on window blur (clicking outside the iframe). */
if (Q.get('interact')==='1' || window.__ADOM_LOADER_INTERACT){
var live=false;
/* The hint pill is OFF by default even when armed: production wants purity.
?hint=1 shows it, and armed contexts can flip it live by postMessage. The
teal edge glow always communicates the armed state either way. */
var HINTON = P('hint',0)>=1;
var edge=document.createElement('div');
edge.style.cssText='position:fixed;inset:0;pointer-events:none;opacity:0;'+
'transition:opacity .18s ease;border:1px solid #00b8b0;'+
'box-shadow:inset 0 0 26px rgba(0,184,176,.16), inset 0 0 0 1px rgba(0,184,176,.35);';
document.body.appendChild(edge);
var hint=document.createElement('div');
hint.style.cssText='position:absolute;left:50%;bottom:16px;transform:translateX(-50%);'+
'padding:5px 12px;border-radius:999px;background:rgba(13,17,23,.78);'+
'border:1px solid #30363d;font:10px monospace;letter-spacing:.05em;'+
'color:#8b949e;pointer-events:none;opacity:0;transition:opacity .2s';
hint.textContent='click to zoom';
document.body.appendChild(hint);
function hintShow(o){ hint.style.opacity=(HINTON&&o)?'1':'0'; }
document.body.style.cursor='pointer';
document.addEventListener('mouseenter',function(){ if(!live) hintShow(true); });
document.addEventListener('mouseleave',function(){ hintShow(false); });
document.addEventListener('pointerdown',function(){
live=true; edge.style.opacity='1'; document.body.style.cursor='default';
hint.textContent='scroll to zoom'; hint.style.color='#00b8b0'; hintShow(true);
setTimeout(function(){ hintShow(false); },1400);
});
/* Armed contexts accept live control: {adomLoader:{margin:N}} retargets the
framing and the spring CARRIES the camera to its new home, animated. Real
load screens never install this listener. */
window.addEventListener('message',function(e){
var d=e.data&&e.data.adomLoader; if(!d) return;
if(typeof d.hint!=='undefined'){ HINTON=!!d.hint; if(!HINTON) hint.style.opacity='0'; }
if(typeof d.bg==='string'){ applyBg(d.bg); }
if(typeof d.fps==='number'){ FPSCAP=Math.max(0,d.fps); if(FPSCAP>0&&FPSCAP<20)FPSCAP=20; }
if(typeof d.margin==='number'){
MARGIN=Math.max(-70,Math.min(90,d.margin));
HOME=homeFor(MARGIN);
ZF=Math.max(100,HOME*3);
}
if(typeof d.speed==='number'){ SPEED=Math.max(0.05,Math.min(3,Math.abs(d.speed)/100)); }
if(typeof d.orbit==='number'){ ORBIT=d.orbit*Math.PI/180; }
if(typeof d.logo==='number'){ LOGO_PK=Math.max(0,Math.min(1,d.logo/100)); }
});
window.addEventListener('blur',function(){ live=false; edge.style.opacity='0'; document.body.style.cursor='pointer';
hint.textContent='click to zoom'; hint.style.color='#8b949e'; });
document.addEventListener('wheel',function(e){
if(!live) return;
e.preventDefault();
if(window.__lock) return; // the epoch owns the framing
lastNX=2*e.clientX/W-1; lastNY=1-2*e.clientY/H;
velR+=CAMR*NOTCH*(-e.deltaY/100); // wheel up = zoom in = radius shrinks
velR=Math.max(-HOME*0.75,Math.min(HOME*0.75,velR));
},{passive:false});
}
</script>
</body>
</html>