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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
#!/usr/bin/env python3
"""
Generate loader.html: the directed loop as a standalone webview LOAD animation.
Constraints that shape it, in order:
1. ZERO network dependencies. No Babylon, no CDN, no fonts, no fetch. The
whole point is to be on screen while something ELSE is loading, so it
cannot itself wait on a 4 MB engine. Raw WebGL1, one file.
2. Identical physics and identical look. The GLSL is EXTRACTED VERBATIM from
orbital-lab.html at build time (single source of truth), the edge profile
and logomark paths are inlined, and the camera reproduces Babylon's
LEFT-HANDED view/projection exactly: this codebase already burned seven
attempts on a mirrored mark, so chirality is not left to chance.
3. John's tuned numbers as defaults (the "john 1" snapshot), overridable by
URL params: ?pt=4.5&dt=3&ct=0.3&st=0&logo=100&llead=1&tlead=1.5&ov=1&intro=1&interact=0&emerge=0&speed=100&orbit=0&margin=0..-80 for bleed
Run: python3 tools/build-loader.py
"""
import json, pathlib, re, sys
ROOT = pathlib.Path(__file__).resolve().parent.parent
lab = (ROOT / 'orbital-lab.html').read_text()
def extract(name):
m = re.search(r"ShadersStore\['" + name + r"'\] = `(.*?)`;", lab, re.S)
if not m: sys.exit('build failed: shader %s not found in orbital-lab.html' % name)
return m.group(1).strip()
vs = extract('labVertexShader').replace('worldViewProjection', 'uMVP')
# Strict GLSL ES 1.00 (raw WebGL1) has no min(int,int); Babylon's WebGL2
# context accepted it. Same maths, float-side.
vs = vs.replace(
""" float u = (clamp(cda, -PI4, PI4) + PI4) / (2.0 * PI4) * 127.0;
int i = int(floor(u));
float f = u - float(i);
int j = min(i + 1, 127);""",
""" 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);""")
if 'min(i + 1, 127)' in vs: sys.exit('build failed: ES 1.00 int-min transform missed')
fs = extract('labFragmentShader')
m = re.search(r"mat\.setFloats\('uLogoR', \[([^\]]+)\]\);", lab)
if not m: sys.exit('build failed: uLogoR table not found')
logoR = '[' + m.group(1).strip() + ']'
logomark = json.dumps(json.loads((ROOT / 'assets' / 'logomark.json').read_text()))
page = r'''<!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:__BG__}
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');
var FPSCAP=Math.max(0,P('fps',LIGHT?30:0)); 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=__VS__;
var FS=__FS__;
var LOGO_R=__LOGO_R__;
var PATHS=__PATHS__;
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='__BG__'; 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 setMVP(w,h){
var f=1/Math.tan(FOV/2), a=w/h, zf=ZF/(ZF-ZN);
var m0=(a>=1)? f/a : f, m5=(a>=1)? f : f*a;
// column-major proj(LH) x view(translate +R on z)
gl.uniformMatrix4fv(U.uMVP,false,new Float32Array([
m0,0,0,0, 0,m5,0,0, 0,0,zf,1, 0,0,-ZN*zf+zf*R- R*0 + (R*zf - ZN*zf)-(R*zf-ZN*zf) + (R*zf) - (ZN*ZF/(ZF-ZN)),0]));
}
/* The m[14] arithmetic above must be exact, so compute it plainly instead: */
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){
if(FPSCAP>0){
if(now-lastDraw < 1000/FPSCAP - 1){ requestAnimationFrame(frame); return; }
lastDraw=now;
}
applySize();
var dt=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);
}
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>
'''
page = (page
.replace('__VS__', '`' + vs + '`')
.replace('__FS__', '`' + fs + '`')
.replace('__LOGO_R__', logoR)
.replace('__PATHS__', logomark)
.replace('__BG__', '#0d1117'))
# drop the abandoned first setMVP draft entirely (kept the plain one)
page = re.sub(r"function setMVP\(w,h\)\{.*?\n\}\n/\* The m\[14\] arithmetic[^\n]*\n", '', page, flags=re.S)
if re.search(r'[–—]', page): sys.exit('build failed: em or en dash in loader')
out = ROOT / 'loader.html'
out.write_text(page)
print('wrote loader.html %s bytes' % f'{out.stat().st_size:,}')
print(' external requests: 0')