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.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Orbital lab</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23e6edf3' stroke-width='1.7'%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3Cellipse cx='12' cy='12' rx='10' ry='4.5'/%3E%3Cellipse cx='12' cy='12' rx='10' ry='4.5' transform='rotate(60 12 12)'/%3E%3C/svg%3E">
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<style>
@font-face { font-family:'Familjen Grotesk'; font-weight:600; font-display:swap;
src:url('https://adom.inc/fonts/familjen-grotesk/familjen-grotesk-latin-600-normal.woff2') format('woff2'); }
@font-face { font-family:'Satoshi'; font-weight:400; font-display:swap;
src:url('https://adom.inc/fonts/satoshi/satoshi-400-normal.woff2') format('woff2'); }
@font-face { font-family:'Satoshi'; font-weight:500; font-display:swap;
src:url('https://adom.inc/fonts/satoshi/satoshi-500-normal.woff2') format('woff2'); }
:root{
--bg:#0d1117; --surface:#161b22; --elevated:#1c2128; --overlay:#21262d;
--border:#30363d; --text:#e6edf3; --text-2:#8b949e; --text-3:#484f58;
--accent:#00b8b0; --danger:#f85149; --accent-bright:#00e6dc; --purple:#8c6bf7; --blue:#58a6ff;
--r-card:8px; --r-pill:999px;
}
*{margin:0;padding:0;box-sizing:border-box}
/* Fluid, not fixed. 100vh resolves against the IFRAME's own viewport, so
this fills whatever height the wiki gives the frame AND fills the window
when the file is opened directly. min-height keeps it usable if a frame
comes up short. A height:100% would need html to have a height and does
collapse here, which is why that form is not used. */
html,body{margin:0;background:var(--bg);height:100%}
body{background:var(--bg);color:var(--text);font-family:'Satoshi',sans-serif;
display:flex;height:100vh;min-height:620px;overflow:hidden}
#panel{
width:330px; flex-shrink:0; height:100%;
background:var(--surface); border-right:1px solid var(--border);
display:flex; flex-direction:column;
}
.head{padding:14px 16px 12px; border-bottom:1px solid var(--border)}
.head h1{font-family:'Familjen Grotesk',sans-serif;font-weight:600;font-size:15px}
.head p{font-size:11px;color:var(--text-2);margin-top:3px;line-height:1.5}
.actions{display:flex;gap:6px;padding:10px 16px;border-bottom:1px solid var(--border);flex-wrap:wrap}
button{
font-family:'Satoshi',sans-serif;font-weight:500;font-size:11px;
padding:6px 11px;border-radius:var(--r-pill);cursor:pointer;
background:var(--elevated);border:1px solid var(--border);color:var(--text);
transition:background .15s ease,border-color .15s ease,transform .05s ease;
}
button:hover{background:var(--overlay);border-color:var(--text-3)}
/* Mousedown has to be unmistakable: a click that produced nothing visible
reads as a broken button. */
button:active{
transform:scale(.94);background:rgba(0,184,176,.28);
border-color:var(--accent);color:var(--accent-bright);
}
button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
button:disabled{opacity:.4;cursor:not-allowed}
button:disabled:active{transform:none;background:var(--elevated)}
button.on{background:rgba(0,184,176,.15);border-color:var(--accent);color:var(--accent-bright)}
.sliders{flex:1;overflow-y:auto;padding:6px 16px 18px}
.shell{margin-top:12px}
.shell-h{
font-family:'Familjen Grotesk',sans-serif;font-weight:600;font-size:10px;
letter-spacing:.4px;color:var(--text-3);padding:6px 0 4px;
display:flex;justify-content:space-between;align-items:baseline;
}
.shell-h em{font-style:normal;font-family:'JetBrains Mono','DM Mono',monospace;font-size:9px}
.row{display:grid;grid-template-columns:74px 1fr 42px;align-items:center;gap:8px;padding:3px 0}
.row label{
font-family:'JetBrains Mono','DM Mono',monospace;font-size:10px;color:var(--text-2);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.row output{
font-family:'JetBrains Mono','DM Mono',monospace;font-size:10px;color:var(--text);
text-align:right;
}
.row.hot label{color:var(--accent)}
input[type=range]{
-webkit-appearance:none;appearance:none;width:100%;height:3px;border-radius:var(--r-pill);
background:var(--border);outline:none;cursor:pointer;
}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;appearance:none;width:12px;height:12px;border-radius:50%;
background:var(--accent);cursor:pointer;border:0;
}
input[type=range]::-moz-range-thumb{
width:12px;height:12px;border-radius:50%;background:var(--accent);cursor:pointer;border:0;
}
/* Red pip marking the value a control would take if it did not exist: the
natural, co-timed default. Lets you see whether you are early or late. */
/* The wander table: one row per harmonic, so you can see exactly what the
next loop is aiming at and switch off any that misbehave. */
/* The handle is your GAIN: how much of that state is allowed in. The pip is
the LIVE coefficient, gain x envelope x cos(E t), which is what the number
beside it reads. They are different quantities, so the handle must stay
where you put it while the pip does the moving. */
/* Specificity has to beat `.mark i`, the red timing pip, or this inherits a
1px red bar instead of a round teal dot. */
.mark i.livepip{
position:absolute;top:50%;margin-top:-3.5px;width:7px;height:7px;border-radius:50%;
background:var(--accent-bright);box-shadow:0 0 7px var(--accent);
pointer-events:none;opacity:.95;
}
.mark i.livepip::after{content:none;display:none}
.mark i.livepip.neg{background:var(--blue);box-shadow:0 0 7px var(--blue)}
.mark i.livepip.zero{opacity:.12;box-shadow:none;background:var(--text-3)}
.wlist{margin-top:6px;border:1px solid var(--border);border-radius:var(--r-card);overflow:hidden}
.wrow{
display:grid;grid-template-columns:16px 74px 1fr 38px;align-items:center;gap:6px;
padding:3px 8px;font-family:'JetBrains Mono','DM Mono',monospace;font-size:9.5px;
color:var(--text-3);border-bottom:1px solid rgba(48,54,61,.5);
}
.wrow:last-child{border-bottom:0}
.wrow.off{opacity:.32}
.wrow.hot{color:var(--text-2)}
.wrow input[type=checkbox]{
appearance:none;width:11px;height:11px;border-radius:3px;margin:0;cursor:pointer;
border:1px solid var(--text-3);background:transparent;
}
.wrow input[type=checkbox]:checked{background:var(--accent);border-color:var(--accent)}
.wbar{position:relative;height:3px;border-radius:2px;background:rgba(48,54,61,.7)}
.wbar i{position:absolute;top:0;bottom:0;border-radius:2px;background:var(--accent);
left:50%;transition:none}
.wbar i.neg{background:var(--blue)}
.wrow b{color:var(--text-2);font-weight:400;text-align:right}
.whead{
display:grid;grid-template-columns:16px 74px 1fr 38px;gap:6px;padding:4px 8px;
font-family:'JetBrains Mono','DM Mono',monospace;font-size:9px;letter-spacing:.06em;
text-transform:uppercase;color:var(--text-3);background:var(--surface);
}
/* Tooltips per the Adom UI skill: ONE body-appended fixed element, never a
::after (panel overflow and transforms clip those). Viewport-clamped on
both axes, caret keeps pointing at the trigger after the clamp. */
#tt{
position:fixed;z-index:99999;max-width:280px;padding:8px 11px;
background:var(--surface);border:1px solid var(--border);border-radius:8px;
box-shadow:0 8px 24px rgba(0,0,0,.5);
font-family:'Satoshi',sans-serif;font-size:11.5px;line-height:1.55;
color:var(--text);text-transform:none;letter-spacing:0;
opacity:0;transform:translateY(4px);pointer-events:none;
transition:opacity .18s ease,transform .18s ease;
}
#tt.show{opacity:1;transform:translateY(0)}
#tt b{color:var(--accent-bright)}
#tt .caret{
position:absolute;width:9px;height:9px;background:var(--surface);
border:1px solid var(--border);border-top:0;border-left:0;
transform:rotate(45deg);bottom:-5px;
}
#tt.above .caret{transform:rotate(225deg);bottom:auto;top:-5px}
select{
font-family:'Satoshi',sans-serif;font-weight:500;font-size:11px;color:var(--text);
background:var(--elevated);border:1px solid var(--border);border-radius:var(--r-pill);
padding:6px 10px;cursor:pointer;outline:none;max-width:180px;
}
select:hover{background:var(--overlay);border-color:var(--text-3)}
.chk{display:flex;align-items:center;gap:8px;padding:6px 0;font-size:11px;color:var(--text-2);cursor:pointer}
.chk input{
appearance:none;width:12px;height:12px;border-radius:3px;flex-shrink:0;margin:0;
border:1px solid var(--text-3);background:transparent;cursor:pointer;
}
.chk input:checked{background:var(--accent);border-color:var(--accent)}
.adv{margin-top:12px;border-top:1px solid var(--border);padding-top:4px}
.adv > summary{
list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:space-between;
font-family:'JetBrains Mono','DM Mono',monospace;font-size:10px;letter-spacing:.08em;
text-transform:uppercase;color:var(--text-3);padding:7px 0;
}
.adv > summary::-webkit-details-marker{display:none}
.adv > summary:hover{color:var(--text-2)}
.adv > summary em{font-style:normal;text-transform:none;letter-spacing:0;font-size:10px}
.adv > summary::before{content:'+ ';color:var(--accent)}
.adv[open] > summary::before{content:'- ';color:var(--accent)}
.mark{position:relative;display:block;width:100%}
.mark i{
position:absolute;top:50%;margin-top:-6px;width:1px;height:12px;
background:var(--danger);pointer-events:none;
}
.mark i::after{
content:'';position:absolute;left:-2px;top:-4px;width:5px;height:5px;
border-radius:50%;background:var(--danger);
}
.tabs{display:flex;border-bottom:1px solid var(--border)}
.tabs .mode{
flex:1;border:0;border-radius:0;background:transparent;color:var(--text-2);
padding:11px 0;font-size:12px;border-bottom:2px solid transparent;
}
.tabs .mode:hover{background:var(--elevated);border-color:transparent}
.tabs .mode.on{color:var(--text);border-bottom-color:var(--accent);background:transparent}
.acts{display:flex;gap:6px;flex-wrap:wrap;padding:8px 0 4px}
.result{
margin:12px 0 4px;padding:13px 14px;border-radius:var(--r-card);
background:linear-gradient(150deg, rgba(0,184,176,.10), rgba(140,107,247,.10));
border:1px solid rgba(0,184,176,.45);
}
.result-k{
font-family:'Familjen Grotesk',sans-serif;font-weight:600;font-size:9px;
letter-spacing:.7px;color:var(--text-2);text-transform:none;
}
.result-eq{
font-family:'JetBrains Mono','DM Mono',monospace;font-size:16px;color:var(--accent-bright);
margin:7px 0 10px;
}
.result-eq sub{font-size:.7em}
.result-c{display:flex;gap:6px;margin-bottom:9px}
.result-c div{
flex:1;background:var(--bg);border:1px solid var(--border);border-radius:6px;
padding:6px 4px;text-align:center;
font-family:'JetBrains Mono','DM Mono',monospace;font-size:9px;color:var(--text-3);
}
.result-c b{display:block;font-size:12px;color:var(--text);font-weight:500;margin-top:2px}
.result-n{font-size:10.5px;line-height:1.6;color:var(--text-2)}
.result-n b{color:var(--accent)}
.copy{width:100%;margin-top:10px;font-size:10px}
/* Mode LED: is the surface the untouched wavefunction, or are we bending
it toward the brandmark? Purple, not teal, so it never reads as part of
the artwork, and not green or red since those are reserved. */
#stageWrap.cam-live{
box-shadow:
inset 0 0 0 1px var(--accent),
inset 0 0 26px rgba(0,184,176,.16),
0 0 18px rgba(0,184,176,.35);
transition:box-shadow .18s ease;
}
.stage-hint{
position:absolute;left:50%;bottom:44px;transform:translateX(-50%);
padding:6px 13px;border-radius:var(--r-pill);
background:rgba(13,17,23,.78);border:1px solid var(--border);
font-family:'JetBrains Mono','DM Mono',monospace;font-size:10px;letter-spacing:.05em;
color:var(--text-2);pointer-events:none;opacity:0;transition:opacity .2s ease;
}
#stageWrap:hover .stage-hint{opacity:1}
#stageWrap.cam-live .stage-hint{opacity:0}
#stageWrap.cam-live:hover .stage-hint.live{opacity:1;color:var(--accent)}
.led-stack{position:absolute;left:18px;top:44px;display:flex;flex-direction:column;gap:6px;align-items:flex-start}
.led-badge{
display:flex;align-items:center;gap:9px;
padding:7px 13px 7px 11px;border-radius:var(--r-pill);
background:rgba(13,17,23,.72);border:1px solid var(--border);
font-family:'JetBrains Mono','DM Mono',monospace;font-size:10px;
color:var(--text-2);letter-spacing:.4px;
transition:border-color .25s ease,color .25s ease;
}
/* Two lights, two layers. Teal = the morph, which is still real physics.
Purple = the clip, which is not. They are independent on purpose: the
morph lights first and stays lit under the clip. */
.led-badge.lit-morph{border-color:var(--accent);color:var(--accent)}
.led-badge.lit-clip{border-color:var(--purple);color:var(--purple)}
.led-badge{min-width:132px;justify-content:flex-start}
.led-badge .led-pct{margin-left:auto}
.led.conv{background:var(--accent);box-shadow:0 0 9px var(--accent),0 0 18px rgba(0,184,176,.55)}
.led{
width:9px;height:9px;border-radius:50%;flex-shrink:0;
background:#243040;box-shadow:none;
transition:background .2s ease,box-shadow .2s ease;
}
.led.on{background:var(--purple);box-shadow:0 0 9px var(--purple),0 0 18px rgba(140,107,247,.55)}
.led.part{animation:led-pulse .9s ease-in-out infinite}
@keyframes led-pulse{0%,100%{opacity:.45}50%{opacity:1}}
.led-pct{color:var(--text-3)}
.fit{
font-family:'JetBrains Mono','DM Mono',monospace;font-size:10px;color:var(--text-2);
line-height:1.7;padding:8px 10px;margin-top:6px;
background:var(--bg);border:1px solid var(--border);border-radius:var(--r-card);
}
.fit b{color:var(--accent)}
#ghost{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
.toast-wrap{
position:absolute;left:50%;bottom:22px;transform:translateX(-50%);
display:flex;flex-direction:column-reverse;align-items:center;gap:8px;
pointer-events:none;z-index:40;
}
.toast{
display:flex;align-items:center;gap:10px;
padding:10px 16px;border-radius:var(--r-pill);
background:var(--overlay);border:1px solid var(--accent);
box-shadow:0 14px 40px rgba(0,0,0,.6);
font-family:'Satoshi',sans-serif;font-size:13px;color:var(--text);
opacity:0;transform:translateY(10px) scale(.97);
transition:opacity .18s ease, transform .22s cubic-bezier(.34,1.4,.64,1);
pointer-events:auto;
}
.toast.show{opacity:1;transform:translateY(0) scale(1)}
.toast .t-sub{color:var(--text-2);font-size:12px}
.t-spin{
width:14px;height:14px;border-radius:50%;flex-shrink:0;
border:2px solid rgba(0,184,176,.25);border-top-color:var(--accent);
animation:t-spin .7s linear infinite;
}
@keyframes t-spin{to{transform:rotate(360deg)}}
#stageWrap{flex:1;position:relative}
#stage{position:absolute;inset:0;width:100%;height:100%;display:block;outline:none}
.hud{
position:absolute;right:16px;bottom:14px;text-align:right;
font-family:'JetBrains Mono','DM Mono',monospace;font-size:10px;color:var(--text-3);line-height:1.6;
}
.hud b{color:var(--accent);font-weight:500}
.formula{
position:absolute;left:16px;top:14px;
font-family:'JetBrains Mono','DM Mono',monospace;font-size:11px;color:var(--text-2);
}
.formula b{color:var(--accent)}
</style>
</head>
<body>
<div id="panel">
<div class="tabs">
<button id="mExplore" title="All 16 real spherical harmonics for l=0 to 3, as live gain sliders over an evolving superposition." class="mode on">Explore</button>
<button id="mLogo" title="Align the orbital against the real logo vectors and measure how well it actually fits." class="mode">Logo cheat</button>
<button id="mFinal" title="The earlier cut, kept as a scratchpad: wander mode, camera grab and timing experiments live here." class="mode">Scratch</button>
<button id="mLoop" title="The final cut: a clean teal intro, then play, dinner call, cheat, sit, and back out, with a fresh roll every pass." class="mode">Final</button>
</div>
<div class="head">
<h1>Orbital lab</h1>
<p id="intro">Every real spherical harmonic through <b>l = 3</b>. 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.</p>
</div>
<div class="actions" id="exploreActions">
<button id="bAnim" title="Run the time evolution. Each coefficient advances by cos(E_n t) with the real hydrogen energies, so the shape morphs continuously. Off freezes it and the gains you set become the state exactly." class="on">Evolve</button>
<button id="bRand" title="Zero all 16 gains, then give 2 to 5 randomly chosen harmonics a gain between 0.35 and 1.00. A random superposition, for finding shapes by accident.">Randomise</button>
<button id="bClear" title="Set all 16 gains to zero. Psi becomes zero everywhere, so there is nothing left to draw. This is an empty canvas, not a reset to defaults.">Clear</button>
<button id="bLogo" title="Zero everything except p_z, set to 1.00. The same SHAPE as the logo orbital, but pointing along z. The logo is this shape rotated onto the diagonal by mixing p_x and p_y equally.">2p (logo)</button>
<button id="bShell1" title="Zero everything, then give each of the three l=1 harmonics (p_y, p_z, p_x) a random gain from 0.50 to 1.00. A random p-shell state.">p only</button>
<button id="bShell2" title="Zero everything, then give each of the five l=2 harmonics (the d orbitals) a random gain from 0.50 to 1.00.">d only</button>
<button id="bShell3" title="Zero everything, then give each of the seven l=3 harmonics (the f orbitals) a random gain from 0.50 to 1.00. The busiest shapes here.">f only</button>
</div>
<div class="sliders" id="sliders"></div>
<div class="sliders" id="final" style="display:none">
<div class="shell">
<div class="shell-h"><span>final adom schrodinger anim</span><em>loops</em></div>
<p style="font-size:11px;color:var(--text-2);line-height:1.6;margin:6px 0 12px">
The superposition evolves, converges onto the one state that is the Adom
logomark, holds square on for a beat, then lets go and carries on.
</p>
<div class="acts">
<button id="fCam" class="on" title="Drift the camera while the physics runs. Nothing to do with the equation, it is framing.">Camera motion</button>
<button id="fSpin" class="on" title="Rotate the object itself. Also not the equation: a real superposition of real harmonics does not rotate, it breathes in place.">Spin</button>
<button id="fFree" title="Drag to orbit while the physics runs. Control is taken back as the morph starts, because the logomark only reads from one angle.">Free rotate</button>
<button id="fGuide" title="Draw the dashed logo vectors over the animation so you can check the match while it runs.">Alignment guides</button>
<button id="fRestart" title="Rewind every clock to zero: the timeline, the time evolution and the spin.">Restart</button>
<button id="fDefaults" title="Put every timing control back to the tuned values">Defaults</button>
</div>
<div class="row" style="margin-top:10px">
<label title="How long the mark is held before the physics resumes. 0 means no pause at all.">hold</label>
<span class="mark"><input type="range" id="fHold" min="0" max="3" step="0.1" value="1"><i class="livepip" id="lpHold"></i></span><output id="oHold">1.0s</output>
</div>
<div class="row">
<label title="Peak opacity the flat teal logomark fades to over the 3D at the logo moment. 0% never shows it, 100% fully covers the shading.">logo opacity</label>
<span class="mark"><input type="range" id="fFill" min="0" max="1" step="0.01" value="0.50"><i class="livepip" id="lpFill"></i></span><output id="oFill2">50%</output>
</div>
<div class="shell-h" style="margin-top:14px"><span>wander</span><em>a new state every loop</em></div>
<p style="font-size:10.5px;color:var(--text-2);line-height:1.6;margin:2px 0 8px">
Instead of one fixed animation, roll a fresh random state every time the
loop leaves the epoch, ease out to it, then ease back so the logo lands
again. Never repeats. Untick any harmonic whose shape makes the steering
or sculpting misbehave and it is left out of every future roll.
</p>
<div class="acts">
<button id="fWander" title="Roll a new random state each loop instead of replaying one fixed animation.">Wander</button>
<button id="fRoll" title="Throw away the pending state and roll another one right now.">Roll now</button>
<button id="fWanderAll" title="Include every harmonic in the roll.">All</button>
<button id="fWanderNoF" title="Exclude the seven f harmonics, the busiest shapes and the most likely to fight the clip.">No f</button>
<button id="fWanderP" title="Only the three p harmonics, so the wander stays close to the logo's own shell.">p only</button>
</div>
<div class="row">
<label title="Seconds between one epoch and the next: out to the random state and back again">excursion</label>
<span class="mark"><input type="range" id="fExc" min="1" max="20" step="0.1" value="3"><i class="livepip" id="lpExc"></i></span>
<output id="oExc">3.0s</output>
</div>
<div class="row">
<label title="How far each roll may stray from the logo state. At 0% every roll IS the logo, so the loop never leaves it. At 100% rolls are pure random with no logo left in them. Past 100% it overshoots: the logo's share goes negative and rolls are pushed actively AWAY from the mark, the wild end. The dot shows where the state is on this scale right now.">stray</label>
<span class="mark"><input type="range" id="fAmt" min="0" max="2" step="0.01" value="0.75"><i class="livepip" id="lpAmt"></i></span>
<output id="oAmt">75%</output>
</div>
<div class="fit" id="wanderInfo" style="margin:6px 0 2px">wander</div>
<div class="whead"><span></span><span>state</span><span>next target</span><span>live</span></div>
<div class="wlist" id="wanderList"></div>
<details class="adv">
<summary>Timing controls<em>tuned already, open only to play</em></summary>
<div class="shell-h" style="margin-top:12px"><span>steering</span><em>the teal light</em></div>
<p style="font-size:10.5px;color:var(--text-2);line-height:1.6;margin:2px 0 8px">
Steering is the teal <b>STEERING</b> light: pulling the superposition
onto the one state that IS the logomark. Its percentage is how far
along that pull is. These two say how many seconds the pull takes,
going in and letting go. Nothing physical picks them, they are pacing.
</p>
<div class="row">
<label title="Seconds the teal STEERING light takes to go 0 to 100%: the pull from the wandering state onto the logo state.">steer in</label>
<span class="mark"><input type="range" id="fMorphIn" min="0.8" max="10" step="0.1" value="2.6"><i class="livepip" id="lpMorphIn"></i></span>
<output id="oMorphIn">2.6s</output>
</div>
<div class="row">
<label title="Seconds the teal STEERING light takes to fall 100% to 0: letting the state go free again.">steer out</label>
<span class="mark"><input type="range" id="fMorphOut" min="0.8" max="10" step="0.1" value="2.6"><i class="livepip" id="lpMorphOut"></i></span>
<output id="oMorphOut">2.6s</output>
</div>
<div class="shell-h" style="margin-top:12px"><span>sculpting</span><em>the purple light</em></div>
<p style="font-size:10.5px;color:var(--text-2);line-height:1.6;margin:2px 0 8px">
Sculpting is the purple <b>SCULPTING</b> light: cutting each point to
the exact logo outline. It is always fully on through the epoch.
These two say how many seconds <b>before</b> the epoch it fades up and
how many <b>after</b> it fades away. Steering always finishes before
sculpting starts, so the knife only ever touches a finished 2p. The
<span style="color:var(--danger)">red pip</span> marks the old
behaviour, sculpting the whole steer, which is what caused the
stretch artefacts.
</p>
<div class="row">
<label title="Seconds before the epoch that the purple SCULPTING light begins fading up.">lead in</label>
<span class="mark"><input type="range" id="fArtOn" min="0" max="5" step="0.05" value="1.0"><i class="livepip" id="lpArtOn"></i><i id="pipOn"></i></span>
<output id="oArtOn">1.00s</output>
</div>
<div class="row">
<label title="Seconds after the epoch that the purple SCULPTING light finishes fading away.">tail out</label>
<span class="mark"><input type="range" id="fArtOff" min="0" max="5" step="0.05" value="1.0"><i class="livepip" id="lpArtOff"></i><i id="pipOff"></i></span>
<output id="oArtOff">1.00s</output>
</div>
<div class="row">
<label title="Free rotate only: seconds before the epoch that the camera is taken from you, and how long after it that you get it back. Between grabs the camera is entirely yours, even mid-morph.">cam grab</label>
<span class="mark"><input type="range" id="fGrab" min="0.3" max="5" step="0.1" value="1.5"><i id="pipGrab"></i><i class="livepip" id="lpGrab"></i></span>
<output id="oGrab">1.5s</output>
</div>
<div class="fit" id="artWindow" style="margin:6px 0 2px">window</div>
<div class="row" style="margin-top:8px">
<label title="Slow the clock while steering runs, slowest exactly at the epoch, so the approach is not rushed.">ease</label>
<span class="mark"><input type="range" id="fEase" min="0" max="0.85" step="0.01" value="0"><i class="livepip" id="lpEase"></i></span>
<output id="oEase">off</output>
</div>
<div class="fit" id="easeWindow" style="margin:6px 0 2px">ease</div>
</details>
<div class="fit" id="phase" style="margin-top:8px">phase</div>
</div>
</div>
<div class="sliders" id="loop" style="display:none">
<div class="shell">
<div class="shell-h"><span>the directed loop</span><em>intro, then forever</em></div>
<p style="font-size:11px;color:var(--text-2);line-height:1.6;margin:6px 0 10px">
Opens clean: the teal mark fades in alone, the 3D arrives underneath it
exactly on the epoch, the mark lets go and the loop runs. Play, dinner
call, cheat, sit, back out to play, and a fresh roll every pass so no
two loops are ever the same.
</p>
<div class="acts">
<button id="lRestart" title="Replay everything from the teal intro.">Restart intro</button>
<button id="lDefaults" title="Put every control on this tab back to the tuned defaults.">Defaults</button>
</div>
<div class="acts" style="margin-top:0">
<select id="l_snaps" title="Saved snapshots of every control on this tab. Pick one and it applies instantly."></select>
<button id="lSnapSave" title="Save every control on this tab, exactly as set right now, as a new named snapshot. Survives reloads.">Save</button>
<button id="lSnapDel" title="Delete the snapshot currently selected in the pulldown.">Del</button>
</div>
<div class="row">
<label title="pt, the playground timeline. The tab 1 animation exactly: Evolve running over this loop's rolled random state, free and alive, for this many seconds before mom calls.">pt play</label>
<span class="mark"><input type="range" id="l_pt" min="2" max="30" step="0.5" value="5"><i class="livepip" id="lp4pt"></i></span>
<output id="o_pt">5.0s</output>
</div>
<div class="row">
<label title="dt, the dinner timeline. Mom calls: every coefficient steers from wherever play left it onto the known epoch numbers, p_x and p_y at 0.7071 and everything else 0, arriving exactly as these seconds end. The way back out to play afterwards uses this same length.">dt dinner</label>
<span class="mark"><input type="range" id="l_dt" min="0.5" max="8" step="0.1" value="0.7"><i class="livepip" id="lp4dt"></i></span>
<output id="o_dt">0.7s</output>
</div>
<div class="row">
<label title="ct, the cheat timeline. The tab 2 trick, the quadrant clip and edge correction that force the exact logo outline, ramped 0 to 100% and reaching exactly 100% at the epoch. The harsh edges live here, so experiment with WHEN it runs. The un-cheat on the way out mirrors this length.">ct cheat</label>
<span class="mark"><input type="range" id="l_ct" min="0.2" max="5" step="0.1" value="0.2"><i class="livepip" id="lp4ct"></i></span>
<output id="o_ct">0.2s</output>
</div>
<label class="chk" title="Checked: ct OVERLAYS the tail of dt, carving while the steer is still finishing underneath, busier and the cycle stays shorter. Unchecked: ct becomes its OWN act after dt, carving a finished, still 2p into the mark, calmer, and adds ct to each side of the cycle.">
<input type="checkbox" id="l_ctMode" checked><span>ct is an overlay on dt</span>
</label>
<div class="row">
<label title="st, the sit timeline. Arrived. The shape pins to the mark and does nothing for this long, dead still. The fresh random roll for the NEXT playground happens the moment we arrive, invisibly, so the exit is always brand new.">st sit</label>
<span class="mark"><input type="range" id="l_st" min="0" max="5" step="0.1" value="1"><i class="livepip" id="lp4st"></i></span>
<output id="o_st">1.0s</output>
</div>
<label class="chk" title="Fade the flat teal logomark over the 3D during the sit: in across the first half of the sit, out across the second half.">
<input type="checkbox" id="l_logoOn" checked><span>logo fade during the sit</span>
</label>
<div class="row">
<label title="Peak opacity the logo fade reaches, halfway through the sit. 0% never shows it, 100% fully covers the shading.">logo peak</label>
<span class="mark"><input type="range" id="l_logoPeak" min="0" max="1" step="0.01" value="1"><i class="livepip" id="lp4peak"></i></span>
<output id="o_logoPeak">100%</output>
</div>
<div class="row">
<label title="Start the logo fade this many seconds BEFORE the sit begins, and let the fade-out run the same amount past its end. Negative means earlier. With sit at 0.0s this slider alone still creates a logo moment around the epoch instant.">logo lead</label>
<span class="mark"><input type="range" id="l_logoLead" min="-3" max="0" step="0.1" value="-0.6"><i class="livepip" id="lp4llead"></i></span>
<output id="o_logoLead">-0.6s</output>
</div>
<div class="row">
<label title="The dark negative lobe fades to the positive teal this many seconds before the epoch, and fades back the same amount after it, so the mark reads as one colour by the time the logo lands. Negative means earlier. At 0.0s the colour snaps exactly on the epoch.">teal lead</label>
<span class="mark"><input type="range" id="l_tealLead" min="-3" max="0" step="0.1" value="-0.2"><i class="livepip" id="lp4tlead"></i></span>
<output id="o_tealLead">-0.2s</output>
</div>
<label class="chk" title="Draw the dashed logo vectors over the 3D as it steers in, for checking the match against the real mark. A debug aid: off is the clean look. Never shown during the intro.">
<input type="checkbox" id="l_guides"><span>dashed logo guides</span>
</label>
<div class="fit" id="loopPhase" style="margin-top:10px">loop</div>
</div>
</div>
<div class="sliders" id="cheat" style="display:none">
<div class="result">
<div class="result-k">The Adom logomark is</div>
<div class="result-eq">ψ = (2p<sub>x</sub> + 2p<sub>y</sub>) / √2</div>
<div class="result-c">
<div><span>c</span><sub>p<sub>x</sub></sub><b>0.707</b></div>
<div><span>c</span><sub>p<sub>y</sub></sub><b>0.707</b></div>
<div><span>c</span><sub>p<sub>z</sub></sub><b>0.000</b></div>
</div>
<div class="result-n">
A single 2p eigenstate of hydrogen, its axis in the view plane on the
diagonal. Measured against the real vectors:
<b>99.9%</b> of the mark covered, <b>0.6%</b> spill,
<b>99.3%</b> IoU.<br>
Untouched, the same state already scores <b>79.2%</b>. The licence is
a quadrant clip plus a per-direction edge correction.
</div>
<button id="bCopy" title="Copy the equation and the fit numbers to the clipboard as plain text." class="copy">Copy the state</button>
</div>
<div class="shell">
<div class="shell-h"><span>the cheat</span><em>physics → brandmark</em></div>
<div class="row"><label title="Lock the camera square-on so the eye never catches the curved profile">face on</label>
<input type="range" id="cFace" min="0" max="1" step="0.01" value="1"><output id="oFace">1.00</output></div>
<div class="row"><label title="Clip each lobe into a 90 degree wedge: the flat radial edge the leaves have">flatten</label>
<input type="range" id="cFlat" min="0" max="1" step="0.01" value="0"><output id="oFlat">0.00</output></div>
<div class="row"><label title="In-plane rotation. 135 is the measured best fit to the mark">rotate</label>
<input type="range" id="cRot" min="0" max="360" step="1" value="135"><output id="oRot">135</output></div>
<div class="row"><label title="Round the outer edge toward the mark's fuller leaf">plump</label>
<input type="range" id="cPlump" min="0" max="1" step="0.01" value="0"><output id="oPlump">0.00</output></div>
<div class="row"><label title="Pull the leaves apart or together across the node">gap</label>
<input type="range" id="cGap" min="-0.3" max="0.5" step="0.01" value="0"><output id="oGap">0.00</output></div>
<div class="row"><label title="Round the corner where the flat edge meets the outer curve">corner</label>
<input type="range" id="cCorner" min="0" max="0.2" step="0.005" value="0"><output id="oCorner">0.00</output></div>
<div class="row"><label title="Pull the outer boundary onto the mark: in where it overshoots, out where it falls short">edge</label>
<input type="range" id="cEdge" min="0" max="1" step="0.01" value="0"><output id="oEdge">0.00</output></div>
<div class="row"><label title="Overall size, to fill the mark rather than sit inside it">size</label>
<input type="range" id="cSize" min="0.6" max="2.2" step="0.01" value="1"><output id="oSize">1.00</output></div>
</div>
<div class="shell">
<div class="shell-h"><span>reference</span><em>the real vectors</em></div>
<div class="row"><label title="The dashed reference outline">outline</label>
<input type="range" id="cGhost" min="0" max="1" step="0.01" value="0.45"><output id="oGhost">0.45</output></div>
<div class="row"><label title="Manual version of the flat teal overlay, for alignment work on THIS tab. The Final anim tab drives its own copy with the logo opacity slider.">overlay fill</label>
<input type="range" id="cFill" min="0" max="1" step="0.01" value="0"><output id="oFill">0.00</output></div>
<div class="acts">
<button id="bFill25" title="Lay the teal logomark over the 3D at 25% opacity.">25%</button>
<button id="bFill40" title="Lay the teal logomark over the 3D at 40% opacity.">40%</button>
<button id="bFill50" title="Lay the teal logomark over the 3D at 50% opacity.">50%</button>
<button id="bFill65" title="Lay the teal logomark over the 3D at 65% opacity.">65%</button>
<button id="bFill80" title="Lay the teal logomark over the 3D at 80% opacity.">80%</button>
</div>
<div class="acts">
<button id="bView" title="Re-engage the square-on camera lock after you have orbited away from it.">Square on</button>
<button id="bMeasure" title="Read the actual pixels back off the GPU and compute the overlap against the logo. Numbers, not eyeballing.">Measure fit</button>
<button id="bPure" title="Drop every cheat back to zero. What is left is the honest 2p orbital with nothing done to it.">Pure physics</button>
<button id="bMark" title="Apply the full cheat recipe: quadrant clip, edge correction, 45 degree rotation, 1.15 scale. This is the tuned 99.3% fit.">Full brandmark</button>
</div>
<div id="fit" class="fit">drag a slider, then measure</div>
</div>
</div>
</div>
<div id="stageWrap">
<canvas id="stage"></canvas>
<canvas id="ghost"></canvas>
<div class="toast-wrap" id="toasts"></div>
<div class="stage-hint">click to zoom and orbit</div>
<div class="stage-hint live">interactive · click outside to release</div>
<div class="led-stack" id="ledBadge" style="display:none">
<div class="led-badge" id="ledMorphBadge" title="Manipulating WHICH state: the coefficients are pulled toward the 2p target. The points are still computed from the equation, so the surface is a real orbital, just not the one free evolution would have reached.">
<span class="led" id="ledMorph"></span>
<span id="ledMorphLabel">STEERING</span>
<span class="led-pct" id="ledMorphPct">0%</span>
</div>
<div class="led-badge" id="ledClipBadge" title="Manipulating THE POINTS: each one is cut against the quadrant planes and pushed to the exact logo radius. The surface is no longer any orbital.">
<span class="led" id="ledClip"></span>
<span id="ledClipLabel">SCULPTING</span>
<span class="led-pct" id="ledClipPct">0%</span>
</div>
<div class="led-badge" id="ledCamBadge" title="Whether the pointer can move the camera right now. It is taken away as the morph starts, because the logomark only reads head on.">
<span class="led" id="ledCam"></span>
<span id="ledCamLabel">CAMERA</span>
<span class="led-pct" id="ledCamPct">scripted</span>
</div>
</div>
<div class="formula">ψ(θ,φ) = Σ c<sub>lm</sub> · Y<sub>lm</sub>(θ,φ)</div>
<div class="hud" id="hud"></div>
</div>
<script>
/* Inlined for the wiki readme sandbox: the frame is an opaque origin,
so fetch cannot be relied on. These are the same bytes as
assets/logomark.json and assets/logo-profile.json. */
const LOGO_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]]];
/* =====================================================================
Every real spherical harmonic up to l=3, exposed as a weight you can
drag. The surface is r(th,ph) = |SUM c_lm Y_lm(th,ph)|, displaced in
the vertex shader, so adding harmonics costs uniforms rather than
geometry: still one mesh, one draw call, nothing re-uploaded.
l=0 is the s shell (a sphere), l=1 the p orbitals (the Adom mark is
one of these), l=2 the d orbitals from the brand animation, and l=3
the f orbitals, which are the ones worth auditioning if the loading
screen's randomness should reach further than the source clip did.
===================================================================== */
const HARMONICS = [
{ l:0, m: 0, name:'s', key:'Y00' },
{ l:1, m:-1, name:'p_y', key:'Y1m1' },
{ l:1, m: 0, name:'p_z', key:'Y10' },
{ l:1, m:+1, name:'p_x', key:'Y11' },
{ l:2, m:-2, name:'d_xy', key:'Y2m2' },
{ l:2, m:-1, name:'d_yz', key:'Y2m1' },
{ l:2, m: 0, name:'d_z2', key:'Y20' },
{ l:2, m:+1, name:'d_xz', key:'Y21' },
{ l:2, m:+2, name:'d_x2-y2', key:'Y22' },
{ l:3, m:-3, name:'f_y(3x2-y2)', key:'Y3m3' },
{ l:3, m:-2, name:'f_xyz', key:'Y3m2' },
{ l:3, m:-1, name:'f_yz2', key:'Y3m1' },
{ l:3, m: 0, name:'f_z3', key:'Y30' },
{ l:3, m:+1, name:'f_xz2', key:'Y31' },
{ l:3, m:+2, name:'f_z(x2-y2)', key:'Y32' },
{ l:3, m:+3, name:'f_x(x2-3y2)', key:'Y33' }
];
const N = HARMONICS.length;
// n for each l, used only for the time evolution's energy
const nOf = l => l + 1 + (l === 0 ? 0 : 0); // s->1, p->2, d->3, f->4
const E = HARMONICS.map(h => -13.6 / Math.pow(h.l + 1, 2));
const canvas = document.getElementById('stage');
const engine = new BABYLON.Engine(canvas, true, { stencil:true });
const scene = new BABYLON.Scene(engine);
scene.clearColor = new BABYLON.Color4(0.051,0.067,0.09,1);
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI/2, Math.PI/2.4, 4.4,
BABYLON.Vector3.Zero(), scene);
camera.attachControl(canvas, true);
camera.zoomToMouseLocation = true;
/* Click-to-interact: embedded in a scrolling page, a permanently-attached
camera hijacks the wheel and page scrolling dies over the viewer. So the
camera is DETACHED until the viewer is clicked, and released again the
moment anything outside it is clicked. camLive gates every attach the
drive paths make too. */
let camLive = false;
function setCamLive(on){
if (camLive === on) return;
camLive = on;
const w = document.getElementById('stageWrap');
if (w) w.classList.toggle('cam-live', on);
/* noPreventDefault=false while armed: Babylon EATS wheel and pointer
events, so the page cannot scroll underneath the zoom. Released, the
camera is detached and the page gets everything back. */
if (on) { camera.attachControl(canvas, false); canvas.focus(); }
else { camera.detachControl(); canvas.blur(); }
}
camera.detachControl();
canvas.tabIndex = 0; // focusable, so arming truly takes focus
canvas.addEventListener('pointerdown', () => setCamLive(true));
document.addEventListener('pointerdown', e => {
if (camLive && e.target.closest && !e.target.closest('#stageWrap')) setCamLive(false);
}, true);
camera.lowerRadiusLimit = 2.0;
camera.upperRadiusLimit = 14;
camera.wheelDeltaPercentage = 0.012;
new BABYLON.HemisphericLight('h', new BABYLON.Vector3(0.3,1,-0.4), scene).intensity = 0.8;
BABYLON.Effect.ShadersStore['labVertexShader'] = `
precision highp float;
attribute vec3 position;
uniform mat4 worldViewProjection;
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;
int i = int(floor(u));
float f = u - float(i);
int j = min(i + 1, 127);
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 = worldViewProjection * vec4(pos, 1.0);
}`;
BABYLON.Effect.ShadersStore['labFragmentShader'] = `
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);
}`;
const mat = new BABYLON.ShaderMaterial('lab', scene, 'lab', {
attributes:['position'], uniforms:['worldViewProjection','c','uScale','cPos','cNeg','uFlat','uRot','uPlump','uGap','uEdge','uCorner','uLogoR']
});
mat.setFloats('uLogoR', [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]);
const C_POS = BABYLON.Color3.FromHexString('#19c8bd');
const C_NEG = BABYLON.Color3.FromHexString('#123a63');
mat.setColor3('cPos', C_POS);
mat.setColor3('cNeg', C_NEG);
mat.backFaceCulling = false;
const mesh = BABYLON.MeshBuilder.CreateSphere('orb', { diameter:2, segments:128 }, scene);
mesh.material = mat;
/* ---------- the weights ---------- */
/* The sliders are GAINS on the running evolution, not a frozen pose. The
superposition keeps evolving the whole time; each slider says how much
that state is allowed to participate. Dial f up and the f orbitals join
the dance; dial one to zero and it drops out. The animation never stops. */
const gain = new Array(N).fill(0);
[1,2,3].forEach(i => gain[i] = 1); // p shell on
[4,5,6,7,8].forEach(i => gain[i] = 0.7); // d shell on, slightly under
let animate = true, t = 0, tEvo = 0;
const sliders = [];
const wrap = document.getElementById('sliders');
[0,1,2,3].forEach(l => {
const group = HARMONICS.map((h,i)=>({h,i})).filter(o=>o.h.l===l);
const sec = document.createElement('div'); sec.className='shell';
sec.innerHTML = `<div class="shell-h"><span>${['s shell','p shell','d shell','f shell'][l]}</span>
<em>l = ${l} · ${group.length} state${group.length>1?'s':''}</em></div>`;
group.forEach(({h,i}) => {
const row = document.createElement('div'); row.className='row';
row.innerHTML = `<label title="l=${h.l}, m=${h.m>=0?'+':''}${h.m}">${h.name}</label>
<span class="mark"><input type="range" min="0" max="1" step="0.01" value="${gain[i]}"><i class="livepip"></i></span>
<output>0.00</output>`;
const inp = row.querySelector('input'), out = row.querySelector('output');
const pip = row.querySelector('.livepip');
inp.addEventListener('input', () => {
gain[i] = parseFloat(inp.value); // steer, never stop
row.classList.toggle('hot', Math.abs(gain[i]) > 0.02);
});
inp.value = gain[i];
row.classList.toggle('hot', Math.abs(gain[i])>0.02);
sliders.push({inp,out,row,pip});
sec.appendChild(row);
});
wrap.appendChild(sec);
});
function push(vals){ // set GAINS; evolution keeps running
vals.forEach((v,i)=>{
gain[i]=v;
sliders[i].inp.value=v;
sliders[i].row.classList.toggle('hot', Math.abs(v)>0.02);
});
}
const zeros = () => new Array(N).fill(0);
document.getElementById('bAnim').addEventListener('click', e => {
animate = !animate; e.target.classList.toggle('on', animate);
});
document.getElementById('bRand').addEventListener('click', () => {
const v = zeros();
const k = 2 + Math.floor(Math.random()*4);
for (let j=0;j<k;j++) v[(Math.random()*N)|0] = 0.35 + Math.random()*0.65;
push(v);
});
document.getElementById('bClear').addEventListener('click', () => push(zeros()));
document.getElementById('bLogo').addEventListener('click', () => {
const v=zeros(); v[2]=1; push(v);
});
const shellOnly = l => () => {
const v=zeros();
HARMONICS.forEach((h,i)=>{ if(h.l===l) v[i]=0.5+Math.random()*0.5; });
push(v);
};
document.getElementById('bShell1').addEventListener('click', shellOnly(1));
document.getElementById('bShell2').addEventListener('click', shellOnly(2));
document.getElementById('bShell3').addEventListener('click', shellOnly(3));
const hud=document.getElementById('hud');
scene.registerBeforeRender(() => {
try {
const dt = engine.getDeltaTime()/1000; t += dt;
if (finalMode) {
tEvo += dt;
driveFinal(dt);
if ((t*3|0)!==(window.__h|0)){
window.__h=t*3;
hud.innerHTML = `${mesh.getTotalVertices().toLocaleString()} verts · `+
`${(mesh.getTotalIndices()/3).toLocaleString()} tris<br>`+
`<b>${Math.round(engine.getFps())}</b> fps · 1 draw call<br>`+
/* Staleness must be visible at a glance: this is the file's own
Last-Modified, so an old build can never masquerade as current. */
`build <b>${document.lastModified.slice(-8)}</b>`;
paintWanderLive();
}
return;
}
if (loopMode) {
tEvo += dt;
driveLoop(dt);
if ((t*3|0)!==(window.__h|0)){
window.__h=t*3;
hud.innerHTML = `${mesh.getTotalVertices().toLocaleString()} verts · `+
`${(mesh.getTotalIndices()/3).toLocaleString()} tris<br>`+
`<b>${Math.round(engine.getFps())}</b> fps · 1 draw call<br>`+
`build <b>${document.lastModified.slice(-8)}</b>`;
}
return;
}
/* Live coefficients: each state's own time evolution, scaled by the gain
the user dialled in. Freezing (Evolve off) holds the phases still so a
shape can be studied, but the gains behave identically either way. */
if (animate) tEvo += dt;
/* When frozen, the gains ARE the state: use them directly. Previously the
per-harmonic envelope and phase kept multiplying in even with the
evolution stopped, so p_x and p_y at an equal 0.71 still came out
unequal and the axis refused to sit on the diagonal. That is why the
lobes never lined up with the mark no matter what was tuned. */
let c;
if (animate) {
tEvo += 0;
c = HARMONICS.map((h,i) => {
const env = 0.55 + 0.45*Math.cos(tEvo*(0.13 + i*0.017) + i*1.7);
return gain[i] * env * Math.cos(E[i]*tEvo*0.5);
});
} else {
c = gain.slice();
}
const nrm = Math.hypot(...c) || 1;
c = c.map(v=>v/nrm);
// report the LIVE contribution next to each slider without moving it
c.forEach((v,i)=> {
const s = sliders[i];
s.out.textContent = v.toFixed(2);
/* Same inset maths as the timing pips: the thumb is 12px, so the reachable
track is inset 6px at each end. */
const f = Math.max(0, Math.min(1, Math.abs(v)));
s.pip.style.left = `calc(6px + ${f*100}% - ${f} * 12px)`;
s.pip.classList.toggle('neg', v < -0.005);
s.pip.classList.toggle('zero', Math.abs(v) < 0.005);
});
mat.setFloats('c', c);
mat.setFloat('uScale', 2.5 * (logoMode ? cheat.size : 1));
mat.setFloat('uFlat', logoMode ? cheat.flat : 0);
mat.setFloat('uRot', cheat.rot);
mat.setFloat('uPlump', logoMode ? cheat.plump : 0);
mat.setFloat('uGap', logoMode ? cheat.gap : 0);
mat.setFloat('uEdge', logoMode ? cheat.edge : 0);
mat.setFloat('uCorner',logoMode ? cheat.corner: 0);
syncLogoLines();
if (logoMode) {
/* Square-on is the presentation view: it is what makes the mark read.
But holding it every frame made the view impossible to DRAG, because
the lock fought the pointer. So a drag releases it for inspection, and
"Square on" re-engages and eases back. `face on` still sets how hard
it returns. */
if (!viewFree) {
const f = cheat.face;
camera.alpha = camera.alpha + (-Math.PI/2 - camera.alpha) * f * 0.18;
camera.beta = camera.beta + ( Math.PI/2 - camera.beta ) * f * 0.18;
camera.radius = camera.radius + (4.4 - camera.radius) * f * 0.12;
}
mesh.rotation.y = 0;
} else {
mesh.rotation.y = t*0.24;
}
if ((t*3|0)!==(window.__h|0)){
window.__h=t*3;
const live = HARMONICS.filter((h,i)=>Math.abs(c[i])>0.02).length;
hud.innerHTML = `${mesh.getTotalVertices().toLocaleString()} verts · `+
`${(mesh.getTotalIndices()/3).toLocaleString()} tris<br>`+
`<b>${live}</b> / ${N} harmonics active · <b>${Math.round(engine.getFps())}</b> fps<br>`+
`1 draw call · 0 bytes uploaded per frame`;
}
} catch (err) {
/* A throw inside a beforeRender observer silently aborts the whole
dispatch, so the symptom is "nothing animates" with no console error at
all. Surface it once instead of hunting it. */
if (!window.__loopErr) {
window.__loopErr = String((err && err.message) || err);
console.error('[lab] render loop threw:', err);
}
}
});
/* =====================================================================
LOGO CHEAT MODE
The brandmark is a 2p orbital (measured: the real isosurface at 135
degrees contains 96.5% of the mark), but not exactly one. The designers
squared off the inner edges where the physics has a smooth pinch. These
controls dial that licence in, so you can see precisely how much cheating
the mark costs, and the real vectors are ghosted on top to judge against.
===================================================================== */
const cheat = { face:1, flat:0, rot:45, plump:0, gap:0, edge:0, corner:0, size:1, ghost:0.45, fill:0 };
let logoMode = false, logoPaths = null;
/* Free the view the moment the user grabs it, so inspection is possible, and
show that the presentation view is no longer being held. */
let viewFree = false;
/* Declared here, far above the boot block: setMode() clears this timer, and
the boot call setMode('final') ran before the old declaration executed,
which was a dead-zone throw that killed the whole script mid-boot. */
let restoreTimer = null, toldAboutSquareOn = false;
/* Final mode has its own camera contract (freeCam / camHeld), and drawGhost
refuses to draw while viewFree is set, so tripping it from tab 3 made the
logo fill vanish after any click or wheel until a timer restored it. */
canvas.addEventListener('pointerdown', () => {
if (logoMode && !finalMode) { viewFree = true; document.getElementById('bView').classList.add('on'); }
});
canvas.addEventListener('wheel', () => {
if (logoMode && !finalMode) { viewFree = true; document.getElementById('bView').classList.add('on'); }
}, { passive:true });
logoPaths = LOGO_PATHS;
queueMicrotask(() => { buildLogoLines(); drawGhost(); });
/* The reference outline lives IN THE SCENE, not painted flat over it, so it
orbits with the geometry. That is the point: to debug a 3D morph you have to
be able to swing round and see where the surface leaves the mark's plane. A
screen-space overlay can only ever check the one square-on view. */
let logoLines = null;
/* The 3D dashed vectors. ONE place decides their visibility, called from
every drive path: they used to be updated only on explore and tab 2
frames, so tabs 3 and 4 inherited whatever state was left behind, which
is how ghost rings ended up floating over the loop with guides off.
sw overrides the world scale so the vectors track a DRIVEN uScale. */
function syncLogoLines(sw){
if (!logoLines) return;
const ls = (0.4886 * (sw || 2.5 * cheat.size)) / 1.138;
logoLines.forEach(l => {
l.isVisible = (logoMode || finalMode || loopMode) && cheat.ghost > 0.01;
l.scaling.setAll(ls);
l.alpha = cheat.ghost;
});
}
function buildLogoLines(){
if (!logoPaths || logoLines) return;
logoLines = [];
logoPaths.forEach(pts => {
const closed = pts.concat([pts[0]]);
const l = BABYLON.MeshBuilder.CreateDashedLines('logoline', {
points: closed.map(([x,y]) => new BABYLON.Vector3(x, y, 0)),
dashSize: 3, gapSize: 2, dashNb: 240
}, scene);
l.color = BABYLON.Color3.FromHexString('#8c6bf7');
l.isPickable = false;
logoLines.push(l);
});
}
const ghostCv = document.getElementById('ghost');
function drawGhost(){
const w = ghostCv.clientWidth, ht = ghostCv.clientHeight;
ghostCv.width = w; ghostCv.height = ht;
const g = ghostCv.getContext('2d');
g.clearRect(0,0,w,ht);
// Once the view is free the flat overlay would lie, so it yields to the
// 3D outline and stops drawing.
if (!logoMode || !logoPaths || (viewFree && !finalMode && !loopMode)) return;
if (cheat.ghost <= 0.01 && cheat.fill <= 0.005) return;
/* The mark is drawn at the same on-screen size the orbital occupies. Two
rules make that true EVERYWHERE, not just at the calibration pose:
height only, because Babylon's FOV is vertical-fixed, so the 3D's
projected size follows the stage height and never its width (min(w,h)
broke on square and portrait stages, the wiki iframe among them); and
4.4/radius, so wheel zoom scales the overlay exactly as it scales the
object. At the tab 2 calibration pose (radius 4.4) this reduces to the
measured 0.34 exactly. */
/* min(w,h), matching fitFraming(): whichever axis limits the 3D also
limits the overlay, so the two stay congruent at every window shape. */
const s = Math.min(w, ht) * 0.34 * (4.4 / Math.max(0.1, camera.radius));
g.save(); g.translate(w/2, ht/2);
/* The teal fill is the logo moment: laid over the 3D at a partial alpha it
flattens the shading just enough to read as the flat brandmark while the
geometry still shows through underneath. Full opacity would just be the
SVG and would throw away the object; zero leaves the 3D shading reading
as an object rather than a mark. The right value is a judgement call,
which is why it is a slider. */
if (cheat.fill > 0.005) {
g.globalAlpha = cheat.fill;
g.fillStyle = '#00b8b0';
logoPaths.forEach(pts=>{
g.beginPath();
pts.forEach(([x,y],i)=>{ const X=x*s, Y=-y*s; i?g.lineTo(X,Y):g.moveTo(X,Y); });
g.closePath(); g.fill();
});
}
g.globalAlpha = cheat.ghost;
g.strokeStyle = '#8c6bf7'; g.lineWidth = 2; g.setLineDash([6,4]);
logoPaths.forEach(pts=>{
g.beginPath();
pts.forEach(([x,y],i)=>{ const X=x*s, Y=-y*s; i?g.lineTo(X,Y):g.moveTo(X,Y); });
g.closePath(); g.stroke();
});
g.restore();
}
window.addEventListener('resize', drawGhost);
/* Units, everywhere a value has one. Percent for every 0..1 opacity or
strength, degrees for rotation, x for the size multiplier. The two overlay
sliders also stop sharing a name: tab 3 owns "logo opacity" (the fade at
the logo moment), tab 2's manual one is "overlay fill" for alignment work. */
function fmtCheat(k, v){
if (k === 'rot') return v.toFixed(0) + '\u00b0';
if (k === 'size') return v.toFixed(2) + 'x';
return Math.round(v * 100) + '%';
}
function bindCheat(id,outId,key,fmt){
const el=document.getElementById(id), out=document.getElementById(outId);
el.addEventListener('input',()=>{
cheat[key]=parseFloat(el.value);
out.textContent = fmt? fmt(cheat[key]) : fmtCheat(key, cheat[key]);
// Anything the overlay canvas draws has to repaint here. Listing only
// 'ghost' meant dragging the fill slider updated the value but never
// redrew, so it did nothing, while the preset buttons worked because they
// route through setCheat() which repaints.
if(key==='ghost' || key==='fill') drawGhost();
if(key==='rot') orientP(); // rotation is a change of STATE, not just the clip
});
}
bindCheat('cFace','oFace','face');
bindCheat('cFlat','oFlat','flat');
bindCheat('cRot','oRot','rot');
bindCheat('cPlump','oPlump','plump');
bindCheat('cGap','oGap','gap');
bindCheat('cCorner','oCorner','corner');
bindCheat('cEdge','oEdge','edge');
bindCheat('cSize','oSize','size');
bindCheat('cGhost','oGhost','ghost');
bindCheat('cFill','oFill','fill');
function setCheat(o){
Object.assign(cheat,o);
const map={face:['cFace','oFace'],flat:['cFlat','oFlat'],rot:['cRot','oRot'],
plump:['cPlump','oPlump'],gap:['cGap','oGap'],edge:['cEdge','oEdge'],
corner:['cCorner','oCorner'],
size:['cSize','oSize'],
ghost:['cGhost','oGhost'],fill:['cFill','oFill']};
Object.entries(o).forEach(([k,v])=>{
const [i,ou]=map[k]; document.getElementById(i).value=v;
document.getElementById(ou).textContent = fmtCheat(k, v);
});
orientP(); drawGhost();
}
setCheat({...cheat}); // sync every output to the formatted units once at load
document.getElementById('bView').addEventListener('click',()=>{
viewFree = false; // re-engage the lock
clearTimeout(restoreTimer);
document.getElementById('bView').classList.remove('on');
});
document.getElementById('bPure').addEventListener('click',()=>
setCheat({flat:0,plump:0,gap:0,edge:0,corner:0,size:1}));
/* Measured, not eyeballed. Sweeping every parameter against the pixel fit:
aligned 2p alone gives 79.2% IoU; adding only the quadrant clip takes it to
95.9% at 0.8% spill. plump and gap both make it WORSE (they push coverage to
100% but spill to 17%+), so the honest optimum leaves them at zero. The
entire licence in the Adom mark is one clip. */
document.getElementById('bMark').addEventListener('click',()=>
setCheat({face:1,flat:1,rot:45,plump:0,gap:0,edge:1,corner:0,size:1.15}));
/* ---- mode switching ---- */
/* A p orbital pointing along an arbitrary direction is not "p_z rotated",
it is a SUPERPOSITION of the three p states:
p along (a,b,c) = a*p_x + b*p_y + c*p_z
The mark's axis lies in the view plane on the diagonal, so the state that
matches it is (p_x + p_y)/sqrt(2). Steering `rotate` therefore has to
re-weight the p shell, not just spin the clip; rotating only the clip left
the physics vertical while the mark sat diagonal, which pinned spill near
50% no matter what else was tuned. */
function orientP(){
if(!logoMode) return;
const a = radiansOf(cheat.rot);
gain[1] = Math.sin(a); // p_y
gain[2] = 0; // p_z: out of the view plane
gain[3] = Math.cos(a); // p_x
[1,2,3].forEach(i=>{
sliders[i].inp.value = Math.abs(gain[i]).toFixed(2); // slider is magnitude; sign is the state
sliders[i].row.classList.toggle('hot', Math.abs(gain[i])>0.02);
});
}
const radiansOf = d => d*Math.PI/180;
/* Each tab is an independent workspace: leaving a tab saves its variables,
returning restores them. And because dev reloads are constant, the whole
bundle also persists to localStorage so a reload keeps every knob where you
left it. (In the wiki sandbox localStorage throws, so there this quietly
degrades to per-session state.) */
const TAB_STATE = { explore:null, logo:null, final:null };
let currentTab = 'explore', __savedTab = null;
const snapCam = () => ({a:camera.alpha,b:camera.beta,r:camera.radius,
tx:camera.target.x,ty:camera.target.y,tz:camera.target.z});
const restCam = s => { if(!s) return; camera.alpha=s.a; camera.beta=s.b; camera.radius=s.r;
camera.target.set(s.tx,s.ty,s.tz); };
function saveTab(){
if (currentTab === 'explore') TAB_STATE.explore = { gain:[...gain], animate, cam:snapCam() };
else if (currentTab === 'logo') TAB_STATE.logo = { cheat:{...cheat}, viewFree, gain:[...gain], cam:snapCam() };
else if (currentTab === 'loop') TAB_STATE.loop = { loopT, cam:snapCam() };
else TAB_STATE.final = { finalT, cam:snapCam() };
persistLab();
}
const LAB_KEY = 'orbital-lab-state-v1';
let lsOK = true; try { localStorage.getItem(LAB_KEY); } catch(e){ lsOK = false; }
function persistLab(){
if (!lsOK) return;
try {
localStorage.setItem(LAB_KEY, JSON.stringify({
tabs: TAB_STATE, currentTab,
final: { FINAL: {...FINAL}, artLeadIn, artTailOut, timeEase, camGrab, finalFill,
wanderMode, wanderAmt, excursionT, wanderOn, spinOn, camMotion, freeCam, finalGuides },
loop: { pt:l_pt, dt:l_dt, ct:l_ct, ov:l_ctOverlay, st:l_st,
lo:l_logoOn, lp:l_logoPeak, ll:l_logoLead, tl:l_tealLead, gd:l_guides },
gain: [...gain], animate
}));
} catch(e){}
}
let persistTimer = null;
function schedulePersist(){ clearTimeout(persistTimer); persistTimer = setTimeout(persistLab, 400); }
function restoreLab(){
if (!lsOK) return;
let s = null; try { s = JSON.parse(localStorage.getItem(LAB_KEY) || 'null'); } catch(e){}
if (!s) return;
Object.assign(TAB_STATE, s.tabs || {});
__savedTab = s.currentTab || null;
const f = s.final;
if (f) {
Object.assign(FINAL, f.FINAL || {});
if (f.artLeadIn !== undefined) artLeadIn = f.artLeadIn;
if (f.artTailOut !== undefined) artTailOut = f.artTailOut;
if (f.timeEase !== undefined) timeEase = f.timeEase;
if (f.camGrab !== undefined) camGrab = f.camGrab;
if (f.finalFill !== undefined) finalFill = f.finalFill;
wanderMode = !!f.wanderMode;
if (f.wanderAmt !== undefined) wanderAmt = f.wanderAmt;
if (f.excursionT !== undefined) excursionT = f.excursionT;
if (Array.isArray(f.wanderOn) && f.wanderOn.length === N) wanderOn = f.wanderOn;
spinOn = f.spinOn !== false; camMotion = f.camMotion !== false;
freeCam = !!f.freeCam; finalGuides = !!f.finalGuides;
document.getElementById('fCam').disabled = freeCam;
if (freeCam) camMotion = false;
}
const lo4 = s.loop;
if (lo4) {
[['l_pt',lo4.pt],['l_dt',lo4.dt],['l_ct',lo4.ct],['l_st',lo4.st],['l_logoPeak',lo4.lp],
['l_logoLead', lo4.ll !== undefined ? -Math.abs(lo4.ll) : undefined],
['l_tealLead', lo4.tl !== undefined ? -Math.abs(lo4.tl) : undefined]].forEach(([id,v])=>{
const el = document.getElementById(id);
if (el && v !== undefined) { el.value = v; el.dispatchEvent(new Event('input')); }
});
const cm = document.getElementById('l_ctMode'), lg = document.getElementById('l_logoOn');
if (cm && lo4.ov !== undefined) { cm.checked = !!lo4.ov; cm.dispatchEvent(new Event('change')); }
if (lg && lo4.lo !== undefined) { lg.checked = !!lo4.lo; lg.dispatchEvent(new Event('change')); }
const gd4 = document.getElementById('l_guides');
if (gd4 && lo4.gd !== undefined) { gd4.checked = !!lo4.gd; gd4.dispatchEvent(new Event('change')); }
}
if (Array.isArray(s.gain) && s.gain.length === N) push(s.gain);
if (typeof s.animate === 'boolean') animate = s.animate;
/* Push the restored numbers back into the tab 3 controls: setting value and
dispatching input runs each handler, so outputs, pips and readouts all
agree without duplicating their logic here. */
[['fMorphIn',FINAL.morphIn],['fMorphOut',FINAL.morphOut],['fHold',FINAL.settle],
['fFill',finalFill],['fArtOn',artLeadIn],['fArtOff',artTailOut],['fEase',timeEase],
['fGrab',camGrab],['fExc',excursionT],['fAmt',wanderAmt]].forEach(([id,v])=>{
const el = document.getElementById(id);
if (el && v !== undefined) { el.value = v; el.dispatchEvent(new Event('input')); }
});
}
function setMode(which){
// which: false = explore, true = logo cheat, 'final' = the finished anim
const isFinal = (which === 'final');
const isLoop = (which === 'loop');
const logo = (which === true);
saveTab(); // the outgoing tab keeps its variables
if (!logo) { cancelDemo(); clearTimeout(restoreTimer); }
finalMode = isFinal;
loopMode = isLoop;
logoMode = logo || isFinal || isLoop; // final and loop both use the cheat pipeline
document.getElementById('mExplore').classList.toggle('on', which === false);
document.getElementById('mLogo').classList.toggle('on', logo);
document.getElementById('mFinal').classList.toggle('on', isFinal);
document.getElementById('mLoop').classList.toggle('on', isLoop);
document.getElementById('sliders').style.display = (which === false) ? 'block' : 'none';
/* The Evolve / Randomise / shell buttons drive the EXPLORE state only. On the
logo tab they would stomp the state being aligned, and on the final tab the
animation overwrites them every frame, so they read as broken. Show them
only where they do something. */
document.getElementById('exploreActions').style.display = (which === false) ? 'flex' : 'none';
const intro = document.getElementById('intro');
if (intro) intro.innerHTML =
(which === false)
? 'Every real spherical harmonic through <b>l = 3</b>. 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.'
: isLoop
? 'The final cut. A clean teal open, then forever: <b>pt</b> play, '
+ '<b>dt</b> dinner call, <b>ct</b> cheat, <b>st</b> sit as the mark, '
+ 'then back out, with a fresh roll every pass. Hover any control, '
+ 'every one explains itself.'
: isFinal
? 'The scratchpad: the earlier cut, kept for experiments. Wander, cam grab '
+ 'and the timing panel live here; the polished version is the Final tab. '
+ 'Two lights still tell the truth: <b>steering</b> changes which state '
+ 'we are in, <b>sculpting</b> moves the points themselves.'
: 'Aligning the brandmark against the real wavefunction. Dial the cheat, '
+ 'compare against the vectors, and <b>Measure fit</b> for the actual numbers.';
document.getElementById('cheat').style.display = logo ? 'block' : 'none';
document.getElementById('final').style.display = isFinal ? 'block' : 'none';
document.getElementById('loop').style.display = isLoop ? 'block' : 'none';
const lb = document.getElementById('ledBadge');
if (lb) lb.style.display = (isFinal || isLoop) ? 'flex' : 'none';
if (isFinal) {
if (!TAB_STATE.final) finalT = 0;
animate = false; // the final loop drives the state itself
viewFree = false;
cheat.ghost = 0; cheat.fill = 0;
} else if (!isLoop) {
mat.setColor3('cNeg', C_NEG); // tabs 1 and 2 always show both phases honestly
mesh.setEnabled(true); // in case the loop intro had it hidden
}
if (isLoop) {
animate = false; viewFree = false;
cheat.ghost = 0; cheat.fill = 0;
if (!TAB_STATE.loop) startIntro();
}
if (logo && !isFinal && !TAB_STATE.logo){
push(zeros());
animate=false; document.getElementById('bAnim').classList.remove('on');
orientP();
viewFree=false; document.getElementById('bView').classList.remove('on');
if (!isFinal) runGuidedDemo();
}
/* The arriving tab gets its own variables back. */
const key = isLoop ? 'loop' : isFinal ? 'final' : logo ? 'logo' : 'explore';
const saved = TAB_STATE[key];
if (saved) {
if (key === 'explore') { push(saved.gain); animate = saved.animate; restCam(saved.cam); }
else if (key === 'logo') { setCheat({...saved.cheat}); viewFree = saved.viewFree;
push(saved.gain); restCam(saved.cam); }
else if (key === 'loop') { loopT = saved.loopT; inIntro = false; restCam(saved.cam); }
else { finalT = saved.finalT; restCam(saved.cam); }
}
if (isFinal && wanderMode && !excursion) rollExcursion();
currentTab = key;
if (typeof syncToggles === 'function') syncToggles();
drawGhost();
}
document.getElementById('mExplore').addEventListener('click',()=>setMode(false));
document.getElementById('mLogo').addEventListener('click',()=>setMode(true));
/* ---------- Guided demo ----------
Someone landing on this tab has no way to know that "Full brandmark" is the
payoff, so run it for them: pause three seconds on the untouched physics so
they see the starting point, announce what is about to happen, then apply
the cheat and measure it. Cancelled the moment they touch a control of
their own, because a demo that fights the user is worse than no demo. */
const toastWrap = document.getElementById('toasts');
function toast(msg, sub, opts){
opts = opts || {};
const el = document.createElement('div');
el.className = 'toast';
el.innerHTML = (opts.spinner ? '<div class="t-spin"></div>' : '') +
`<span>${msg}${sub ? `<br><span class="t-sub">${sub}</span>` : ''}</span>`;
toastWrap.appendChild(el);
while (toastWrap.children.length > 2) toastWrap.firstChild.remove();
void el.offsetWidth; el.classList.add('show');
let timer = setTimeout(dismiss, opts.duration || 4200);
function dismiss(){ el.classList.remove('show'); setTimeout(()=>el.remove(), 260); }
el.addEventListener('mouseenter', ()=>clearTimeout(timer)); // pause on hover
el.addEventListener('mouseleave', ()=>{ timer = setTimeout(dismiss, 1200); });
return { dismiss };
}
/* Every button reports what it just did. Wired at the DOCUMENT level so it
fires AFTER the button's own handler has run, which lets a toggle name the
state it landed in rather than the one it left. Only trusted events toast,
so the guided demo's synthetic clicks stay quiet. */
const SAY = {
mExplore: () => ['Explore', 'All 16 real spherical harmonics, l=0 to 3, as live gains'],
mLogo: () => ['Logo cheat', 'Align the orbital to the real logo vectors and measure the fit'],
mFinal: () => ['Scratch', 'The earlier cut, kept as a scratchpad for experiments'],
bAnim: () => animate
? ['Evolve on', 'Coefficients advancing by cos(E_n t), the real time-dependent phase']
: ['Evolve off', 'Frozen. The gains you set are now the state exactly'],
bRand: () => ['Randomised', `${gain.filter(v=>Math.abs(v)>0.02).length} of 16 harmonics given a random gain`],
bClear: () => ['Cleared', 'All 16 gains to zero, so psi is zero and there is nothing to draw'],
bLogo: () => ['Set to p_z', 'The logo shape, but along z. The logo mixes p_x and p_y to swing it onto the diagonal'],
bShell1: () => ['p shell only', 'The three l=1 harmonics given random gains, everything else zeroed'],
bShell2: () => ['d shell only', 'The five l=2 harmonics given random gains, everything else zeroed'],
bShell3: () => ['f shell only', 'The seven l=3 harmonics given random gains, everything else zeroed'],
bMark: () => ['Full brandmark cheat', 'Quadrant clip, edge correction, 45 degree rotation, 1.15 scale'],
bPure: () => ['Cheats off', 'The honest 2p orbital, nothing done to it'],
bMeasure: () => ['Measuring fit', 'Reading pixels back off the GPU to compute the real overlap'],
bView: () => ['Square on', 'Camera lock re-engaged'],
bCopy: () => ['Copied', 'The equation and fit numbers are on the clipboard'],
fCam: () => camMotion ? ['Camera motion on', 'The viewpoint drifts. Framing, not the equation']
: ['Camera motion off', 'Viewpoint held still'],
fSpin: () => spinOn ? ['Spin on', 'The object rotates. My choice, not the equation: a real superposition does not spin']
: ['Spin off', 'The true picture: the surface breathing in place'],
fFree: () => freeCam ? ['Free rotate on', 'Drag to orbit. The camera is only taken near the epoch; the cam grab slider says when']
: ['Free rotate off', 'The scripted camera drift is back'],
fGuide: () => finalGuides ? ['Alignment guides on', 'Dashed logo vectors drawn over the animation']
: ['Alignment guides off', 'Guides hidden'],
fRestart: () => ['Restarted', 'Every clock back to zero: timeline, evolution and spin'],
fWander: () => wanderMode
? ['Wander on', `A fresh random state every loop, ${wanderOn.filter(Boolean).length} of 16 harmonics in the pool`]
: ['Wander off', 'Back to one fixed animation, identical every loop'],
fRoll: () => ['Rolled a new state', `${excursion ? excursion.filter(v=>Math.abs(v)>0.02).length : 0} harmonics active in the next excursion`],
fWanderAll:() => ['All 16 in the pool', 'Every harmonic can appear in a roll'],
fWanderNoF:() => ['f shell excluded', 'The nine remaining harmonics stay in the pool'],
fWanderP: () => ['p shell only', 'Rolls stay in the shell the logo itself lives in, so they never stray far'],
lSnapSave:() => lsOK ? ['Snapshot saved', (loadSnaps().slice(-1)[0]||{}).name || 'stored']
: ['No storage on this surface', 'The wiki sandbox cannot save. Factory snapshots still apply; tune and save on the lab page.'],
lSnapDel: () => ['Snapshot deleted', `${allSnaps().length} left · factory ones (*) ship with the page`],
mLoop: () => ['Final', 'The final cut: teal intro, then play, dinner, cheat, sit, and back out, fresh roll every pass'],
lRestart: () => ['Intro restarted', 'From the teal open'],
lDefaults:() => ['Loop defaults restored', 'john fav: pt 5s, dt 0.7s, ct 0.2s overlay, 1s sit, logo 100% on a 0.6s lead, teal lead 0.2s'],
fDefaults:() => ['Defaults restored', 'Morph 2.6s each way, lead in and tail out 0.6s, ease 0.35, hold 1.0s, fill 50%'],
};
document.addEventListener('click', e => {
const b = e.target.closest && e.target.closest('button');
if (!b || b.disabled || !e.isTrusted) return;
let msg, sub;
const entry = SAY[b.id];
if (entry) { const r = entry(); msg = r[0]; sub = r[1]; }
else { msg = b.textContent.trim(); sub = b.title || ''; } // fill pills and anything unlisted
toast(msg, sub, { duration: 2600 });
});
let demoTimers = [];
function cancelDemo(){ demoTimers.forEach(clearTimeout); demoTimers = []; }
function runGuidedDemo(){
cancelDemo();
demoTimers.push(setTimeout(() => {
const t = toast('Running Full Brandmark',
'Morphing the 2p orbital onto the real logo vectors',
{ spinner:true, duration:3200 });
demoTimers.push(setTimeout(() => {
document.getElementById('bMark').click();
demoTimers.push(setTimeout(() => {
document.getElementById('bMeasure').click();
demoTimers.push(setTimeout(() => {
const fit = document.getElementById('fit').innerText.split('\n')[0];
toast('That is the Adom logomark', fit + ' against the real vectors',
{ duration:5200 });
}, 700));
}, 900));
}, 700));
}, 3000));
}
// any deliberate input cancels the demo
['cFace','cFlat','cRot','cPlump','cGap','cCorner','cEdge','cSize','cGhost']
.forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('input', cancelDemo);
});
['bMark','bPure','bMeasure','bView'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('click', cancelDemo);
});
canvas.addEventListener('pointerdown', cancelDemo);
// quick presets for judging the logo-moment fade
[['bFill25',0.25],['bFill40',0.40],['bFill50',0.50],['bFill65',0.65],['bFill80',0.80]]
.forEach(([id,v]) => {
const b = document.getElementById(id);
if (b) b.addEventListener('click', () => { cancelDemo(); setCheat({ fill:v }); });
});
/* =====================================================================
FINAL ADOM SCHRODINGER ANIM
One loop, four beats:
evolve the superposition wanders the whole harmonic family
converge it collapses onto (p_x + p_y)/sqrt2, the cheat ramps in, and
the camera eases to square on
hold dead still, teal fill up, the mark reads
release fill and cheat drop away, the physics resumes
Camera motion is a toggle because the drift sells the 3D but makes the
logo beat land differently; with it off, the convergence is purely the
shape changing.
===================================================================== */
/* The timeline, as separate beats rather than one blended number.
The first version drove the state, the cheat, the camera AND the logo fill
off a single k, so everything happened at once over 2.2s and it read as a
cut. Worse, the fill was already at 65% the moment the hold began, so there
was no fade in or out at all: the beat had no shape.
Each stage below owns one job:
evolve the Explore animation, untouched
morphIn points slide from the evolving state onto the brandmark
settle fully arrived, dead still, no logo yet
fadeIn the teal svg comes up over the geometry
logoHold the mark, held
fadeOut the svg leaves
morphOut points slide back to the physics
then it loops. */
const FINAL = {
evolve: 7.0,
morphIn: 2.6,
settle: 1.0, // the pause you asked for, before any logo appears
fadeIn: 0.5,
logoHold: 0.8,
fadeOut: 0.5,
morphOut: 2.6
};
const FINAL_ORDER = ['evolve','morphIn','settle','fadeIn','logoHold','fadeOut','morphOut'];
const smooth = x => { const c = Math.max(0, Math.min(1, x)); return c*c*(3-2*c); };
let finalMode = false, camMotion = true, finalT = 0, finalFill = 0.50;
/* Spin state. Kept as a WRAPPED angle plus a latched target, for the reason
spelled out at the assignment below. */
let spin = 0, spinTarget = 0, spinLatched = false, spinOn = true;
/* Free-rotate state. userA/B/R hold whatever pose the pointer left the camera
in, so the handover has something to ease FROM. */
let freeCam = false, camHeld = false;
let userA = -Math.PI/2, userB = Math.PI/2, userR = 4.4;
let finalGuides = false; // dashed vectors during the anim, for checking the match
/* The artistic window. The clip and edge correction are built for a 2p shape,
so applying them while the state is still a multi-harmonic blob stretches it:
that is the artefact. artOn holds the cheat back until the state has actually
arrived, artOff drops it before the state leaves again. Independent, because
the two directions do not need the same threshold. */
let artLeadIn = 1.0, artTailOut = 1.0, timeEase = 0; // seconds around the epoch
let camGrab = 1.5; // seconds before the epoch the camera is taken, and after it that it returns
/* WANDER. The epoch is the fixed anchor; everything between one epoch and the
next is disposable, so it may as well be different every time. Each loop
rolls a fresh random state, eases out to it on the way from the logo, and
eases back on the way to the next one. With sixteen harmonics in play the
sequence never repeats. Any harmonic can be excluded from the roll, because
some shapes fight the clip and you need to be able to see which. */
let wanderMode = false, wanderAmt = 0.75, excursionT = 3.0;
let wanderOn = HARMONICS.map(() => true);
let excursion = null, lastBeat = '', liveC = null, wanderRows = null;
const LOGO_BASE = (() => { const v = new Array(N).fill(0); v[1] = 0.7071; v[3] = 0.7071; return v; })();
function buildWanderList(){
const host = document.getElementById('wanderList');
if (!host) return;
host.innerHTML = '';
wanderRows = HARMONICS.map((hm,i) => {
const row = document.createElement('div');
row.className = 'wrow' + (wanderOn[i] ? '' : ' off');
row.innerHTML = `<input type="checkbox"${wanderOn[i] ? ' checked' : ''}>` +
`<span>${hm.name}</span><span class="wbar"><i></i></span><b>0.00</b>`;
row.title = `Keep ${hm.name} in the dice pool. Unticked it can never be drawn again until re-ticked. The bar is the next roll's target for it (blue means negative phase), the number is its live coefficient right now.`;
const cb = row.querySelector('input');
cb.addEventListener('change', () => {
wanderOn[i] = cb.checked;
row.classList.toggle('off', !cb.checked);
describeWander();
toast(`${hm.name} ${cb.checked ? 'back in the pool' : 'excluded from the roll'}`,
cb.checked ? 'It can appear in future random states'
: 'Every future roll leaves this harmonic at zero',
{ duration: 2600 });
});
host.appendChild(row);
return { row, bar: row.querySelector('.wbar i'), val: row.querySelector('b'), cb };
});
}
function paintWander(){
if (!wanderRows) return;
wanderRows.forEach((r,i) => {
const v = excursion ? excursion[i] : 0;
const w = Math.min(50, Math.abs(v) * 50);
r.bar.style.width = w + '%';
r.bar.style.left = v >= 0 ? '50%' : (50 - w) + '%';
r.bar.classList.toggle('neg', v < 0);
r.row.classList.toggle('hot', Math.abs(v) > 0.02);
});
describeWander();
}
function paintWanderLive(){
if (!wanderRows || !liveC) return;
wanderRows.forEach((r,i) => { r.val.textContent = liveC[i].toFixed(2); });
}
function describeWander(){
const el = document.getElementById('wanderInfo');
if (!el) return;
const n = wanderOn.filter(Boolean).length;
if (!wanderMode) {
el.innerHTML = `<span style="color:var(--text-3)">off: one fixed animation, ` +
`identical every loop</span>`;
return;
}
el.innerHTML =
`logo lands every <b>${finalCycle().toFixed(1)}s</b> · ` +
`<b>${n}</b> of 16 harmonics in the pool` +
`<br><span style="color:var(--text-3)">` +
(n === 0 ? 'nothing left to roll, the loop will sit on the logo'
: n < 4 ? 'small pool, states will start to feel familiar'
: 'a fresh state rolled every time the loop leaves the epoch') + `</span>`;
}
function syncMorphInputs(){
const set = (id,out,v) => {
const a = document.getElementById(id), b = document.getElementById(out);
if (a) a.value = v;
if (b) b.textContent = v.toFixed(1) + 's';
};
set('fMorphIn','oMorphIn',FINAL.morphIn);
set('fMorphOut','oMorphOut',FINAL.morphOut);
placePips(); describeEase(); describeWindow(); describeWander();
}
/* Wander retimes the loop: there is no long free-running beat any more, just
out to the rolled state and straight back, so `excursion` IS the gap between
one logo and the next. */
function applyExcursionTiming(){
FINAL.evolve = 0;
FINAL.morphIn = FINAL.morphOut = Math.max(0.8, excursionT / 2);
syncMorphInputs();
}
function rollExcursion(){
const pool = HARMONICS.map((hm,i) => i).filter(i => wanderOn[i]);
const v = new Array(N).fill(0);
if (pool.length) {
const k = Math.min(pool.length, 2 + Math.floor(Math.random()*4));
const picked = pool.slice();
for (let j = 0; j < k; j++) {
const idx = picked.splice((Math.random()*picked.length)|0, 1)[0];
v[idx] = (Math.random() < 0.5 ? -1 : 1) * (0.35 + Math.random()*0.65);
}
}
/* Blended against the logo's own 2p, so `amount` is literally how far the
loop is allowed to stray from the mark rather than an abstract knob. */
const mix = v.map((x,i) => LOGO_BASE[i]*(1-wanderAmt) + x*wanderAmt);
const n = Math.hypot(...mix) || 1;
excursion = mix.map(x => x/n);
paintWander();
}
function finalCycle(){ return FINAL_ORDER.reduce((a,k)=>a+FINAL[k],0); }
/* The EPOCH: the instant the shape has fully reached the logomark, and the
instant it starts leaving again. Everything artistic is expressed relative
to these two marks, because that is how the animation reads when you watch
it: "a beat before the logo" and "a beat after". */
function epochStart(){ return FINAL.evolve + FINAL.morphIn; }
function epochEnd(){ return epochStart() + FINAL.settle + FINAL.fadeIn + FINAL.logoHold + FINAL.fadeOut; }
function finalPhase(u){
let acc = 0;
for (const name of FINAL_ORDER) {
if (u < acc + FINAL[name]) return { name, local:(u-acc)/FINAL[name] };
acc += FINAL[name];
}
return { name:'morphOut', local:1 };
}
/* Tab 3 live pips: the same visual language as the Explore sliders. The
handle is the SETTING; the dot is where that quantity actually is right
now, in the slider's own units. */
let tab3Pips = null;
function paintTab3Pips(vals){
if (!tab3Pips) tab3Pips = [
'fHold','fFill','fMorphIn','fMorphOut','fArtOn','fArtOff','fEase','fExc','fAmt','fGrab'
].map(iid => {
const inp = document.getElementById(iid);
const pip = document.getElementById('lp' + iid.slice(1));
return inp && pip ? { key:iid, pip, lo:parseFloat(inp.min), hi:parseFloat(inp.max) } : null;
}).filter(Boolean);
tab3Pips.forEach(s => {
const f = Math.max(0, Math.min(1, ((vals[s.key] || 0) - s.lo) / (s.hi - s.lo)));
s.pip.style.left = `calc(6px + ${f*100}% - ${f} * 12px)`;
s.pip.classList.toggle('zero', f <= 0.001);
});
}
function driveFinal(dt){
/* Time easing: the clock slows as the morph approaches the brandmark and
again as it leaves, so the interesting part is not rushed while the long
evolve beat still plays at full speed. Applied to the CLOCK, so every
downstream value inherits it. */
{
const probe = finalPhase(finalT);
let speed = 1;
if (probe.name === 'morphIn') speed = 1 - timeEase * smooth(probe.local);
if (probe.name === 'morphOut') speed = 1 - timeEase * (1 - smooth(probe.local));
finalT = (finalT + dt * Math.max(0.08, speed)) % finalCycle();
}
const { name, local } = finalPhase(finalT);
/* SCULPTING first, because it outranks everything. (It was also being read
one line before a declaration further down, a dead-zone throw the render
loop's catch swallowed silently, which froze this whole tab.)
The windows are clamped to the morph beats: sculpting must never land on
a state steering has not finished preparing, so a lead in longer than the
morph is capped rather than allowed to chew on raw physics. */
const eS = epochStart(), eE = epochEnd();
const effLead = Math.max(0, Math.min(artLeadIn, FINAL.morphIn - 0.2));
const effTail = Math.max(0, Math.min(artTailOut, FINAL.morphOut - 0.2));
let cheatK;
if (finalT >= eS && finalT <= eE) cheatK = 1;
else if (finalT < eS) cheatK = effLead <= 0.001 ? 0 : smooth(1 - (eS - finalT) / effLead);
else cheatK = effTail <= 0.001 ? 0 : smooth(1 - (finalT - eE) / effTail);
cheatK = Math.max(0, Math.min(1, cheatK));
/* The camera has its OWN window now, decoupled from the morph. It used to
ride k, which took the camera at the first instant of the morph and gave
it back at the very last, an eternity on a long excursion. camGrab is the
only thing that decides it: taken that many seconds before the epoch,
eased home to arrive exactly on it, returned that many seconds after.
Capped so it always hands back before the loop wraps. */
const effGrab = Math.min(Math.max(0.3, camGrab), Math.max(0.3, (finalCycle() - eE) * 0.95));
let camK;
if (finalT >= eS && finalT <= eE) camK = 1;
else if (finalT < eS) camK = (eS - finalT) >= effGrab ? 0 : smooth(1 - (eS - finalT) / effGrab);
else camK = (finalT - eE) >= effGrab ? 0 : smooth(1 - (finalT - eE) / effGrab);
/* STEERING yields to sculpting: it finishes EARLY, completing exactly as the
clip begins, so the whole lead in window is spent holding the finished 2p
still while the camera, target and spin ease home for the epoch. Coming
out it stays parked until the clip has fully released, then unmorphs over
what is left of the beat. Both joins are continuous, so no pops. */
let k = 0, fill = 0;
switch (name) {
case 'evolve': k = 0; fill = 0; break;
case 'morphIn': {
const el = local * FINAL.morphIn;
k = smooth(Math.min(1, el / Math.max(0.2, FINAL.morphIn - effLead)));
fill = 0; break;
}
case 'settle': k = 1; fill = 0; break;
case 'fadeIn': k = 1; fill = smooth(local); break;
case 'logoHold': k = 1; fill = 1; break;
case 'fadeOut': k = 1; fill = 1 - smooth(local); break;
case 'morphOut': {
const el = local * FINAL.morphOut;
k = 1 - smooth(Math.max(0, (el - effTail) / Math.max(0.2, FINAL.morphOut - effTail)));
fill = 0; break;
}
}
/* A new random state is rolled the moment the loop LEAVES the epoch, so the
whole outward journey and the whole way back aim at the same fresh target
and the return lands cleanly on the mark. */
if (name !== lastBeat) {
if (wanderMode && (name === 'morphOut' || !excursion)) rollExcursion();
lastBeat = name;
}
/* The state away from the epoch. The rolled excursion is a GAIN SET, not a
frozen pose: each rolled harmonic keeps the live Explore-style evolution
(envelope times the real energy phase), so the away leg shimmers exactly
like tab 1 no matter how long the excursion is. It still lands perfectly,
because k hands the blend fully to LOGO_BASE at the epoch. */
const liveEvo = gains => HARMONICS.map((hm,i) => {
const env = 0.55 + 0.45*Math.cos(tEvo*(0.13 + i*0.017) + i*1.7);
return gains[i] * env * Math.cos(E_LEVELS[i]*tEvo*0.5);
});
const evo = liveEvo(wanderMode && excursion ? excursion : gainFinalBase);
const c = evo.map((v,i) => v*(1-k) + LOGO_BASE[i]*k);
const nrm = Math.hypot(...c) || 1;
const cN = c.map(v=>v/nrm);
mat.setFloats('c', cN);
liveC = cN;
mat.setFloat('uScale', 2.5 * (0.9 + 0.25*k));
mat.setFloat('uFlat', cheatK);
mat.setFloat('uEdge', cheatK);
mat.setFloat('uRot', 45);
mat.setFloat('uPlump', 0);
mat.setFloat('uGap', 0);
mat.setFloat('uCorner',0);
/* Sculpting also unifies the phase colours. The negative lobe fades to the
positive teal exactly as the clip lands and back out as it releases, so
by the time the flat logo fades over the 3D the whole mark already reads
as ONE colour. Driven by cheatK, so it needs no timing of its own. */
mat.setColor3('cNeg', BABYLON.Color3.Lerp(C_NEG, C_POS, cheatK));
// Camera: drifting while the physics runs, square on for the mark.
/* Camera, two regimes.
Scripted (default): a drift I choose, blended to square-on by k.
Free rotate: the pointer owns the camera while the physics runs, but the
logomark only reads from ONE angle, so the camera has to be taken back
before the epoch. Input is detached the instant the morph starts and
whatever pose the user left is eased to the epoch pose by k, arriving
exactly as the mark does. The alpha delta is wrapped to the shortest way
round: a user who has orbited a few times would otherwise get the same
runaway sweep the object spin used to have. */
const EPOCH_A = -Math.PI/2, EPOCH_B = Math.PI/2, EPOCH_R = 4.4;
if (freeCam) {
if (camK <= 0.001) {
if (camHeld) { if (camLive) camera.attachControl(canvas, false); camHeld = false; }
userA = camera.alpha; userB = camera.beta; userR = camera.radius;
} else {
if (!camHeld) { camera.detachControl(); camHeld = true; }
const raw = EPOCH_A - userA;
const dA = Math.atan2(Math.sin(raw), Math.cos(raw)); // shortest way round
camera.alpha = userA + dA * camK;
camera.beta = userB + (EPOCH_B - userB) * camK;
camera.radius = userR + (EPOCH_R - userR) * camK;
}
} else {
if (camHeld) { if (camLive) camera.attachControl(canvas, false); camHeld = false; }
const driftA = -Math.PI/2 + (camMotion ? Math.sin(tEvo*0.19)*0.55 : 0);
const driftB = Math.PI/2 + (camMotion ? Math.sin(tEvo*0.13)*0.30 : 0);
camera.alpha = driftA*(1-k) + EPOCH_A*k;
camera.beta = driftB*(1-k) + EPOCH_B*k;
camera.radius += (EPOCH_R - camera.radius) * 0.06;
}
/* At the exact epoch moment (k=1 and the logo is visible), the pose must be
canonical: dead centre, head on, everything right. Once we leave the epoch,
release control back to the user/animator. */
const AT_EPOCH = k > 0.99 && cheatK > 0.99;
if (AT_EPOCH) {
camera.target.set(0,0,0);
spin = 0; spinTarget = 0; spinLatched = false;
} else {
const pull = freeCam ? (camK <= 0.001 ? 0 : camK) : Math.min(1, 0.05 + k * 0.95);
camera.target.scaleInPlace(1 - pull);
}
/* This used to be `tEvo*0.10*(1-k)`: an absolute, ever-growing angle scaled
by (1-k). So every morph had to sweep the WHOLE accumulated angle back to
zero inside the morph beat, and the sweep rate grew with uptime. After
twenty minutes that was ~120 radians crammed into 2.6s, roughly seven
revolutions a second, and it kept getting worse the longer it ran.
Now the angle is integrated at a gated rate and kept wrapped, and the
moment the morph starts it is eased to the NEAREST WHOLE TURN (visually
identical to zero, so the mark still lands square on) which is never more
than half a turn away. Bounded no matter how long the page is open. */
const TAU = Math.PI * 2;
if (k <= 0.001) { spin = (spin + dt * 0.10) % TAU; spinLatched = false; }
else if (!spinLatched) { spinTarget = Math.round(spin / TAU) * TAU; spinLatched = true; }
mesh.rotation.y = spinOn ? spin + (spinTarget - spin) * k : 0;
cheat.fill = finalFill * fill;
/* The dashed vectors are an alignment aid, so they fade in with the morph
rather than sitting over the free-running physics where they mean nothing.
Off by default: the finished animation should not show its workings. */
cheat.ghost = finalGuides ? 0.9 * k : 0;
syncLogoLines(2.5 * (0.9 + 0.25 * k));
drawGhost();
/* TWO lights, because there are TWO layers and conflating them is exactly
what made the timing controls unreadable. MORPH is the coefficient blend
from the evolving state onto the brandmark: every frame of it is still a
genuine solution. CLIP is the geometric cut that forces the exact outline:
that one is not physics. MORPH lights first and stays lit underneath. */
const setLed = (badgeId, ledId, pctId, v) => {
const badge = document.getElementById(badgeId), el = document.getElementById(ledId),
pct = document.getElementById(pctId);
if (!badge || !el || !pct) return;
const lit = v > 0.001, full = v > 0.995;
const clip = badgeId === 'ledClipBadge';
badge.classList.toggle(clip ? 'lit-clip' : 'lit-morph', lit);
el.classList.toggle(clip ? 'on' : 'conv', lit);
el.classList.toggle('part', lit && !full);
pct.textContent = `${(v*100).toFixed(0)}%`;
};
const badge = document.getElementById('ledBadge');
if (badge) badge.style.display = 'flex';
setLed('ledMorphBadge', 'ledMorph', 'ledMorphPct', k);
setLed('ledClipBadge', 'ledClip', 'ledClipPct', cheatK);
{
/* Three states, because "can I drag right now" has three honest answers. */
const cb = document.getElementById('ledCamBadge'),
cl = document.getElementById('ledCam'),
cp = document.getElementById('ledCamPct');
if (cb && cl && cp) {
const yours = freeCam && !camHeld;
const taken = freeCam && camHeld;
cb.classList.toggle('lit-morph', yours);
cb.classList.toggle('lit-clip', taken);
cl.classList.toggle('conv', yours);
cl.classList.toggle('on', taken);
cl.classList.toggle('part', taken);
cp.textContent = yours ? 'yours' : taken ? 'taken' : 'scripted';
}
}
paintTab3Pips({
fHold: name === 'settle' ? local * FINAL.settle
: (name === 'fadeIn' || name === 'logoHold' || name === 'fadeOut') ? FINAL.settle : 0,
fFill: cheat.fill,
fMorphIn: name === 'morphIn' ? local * FINAL.morphIn
: (name === 'settle' || name === 'fadeIn' || name === 'logoHold' || name === 'fadeOut') ? FINAL.morphIn : 0,
fMorphOut:name === 'morphOut' ? local * FINAL.morphOut : 0,
fArtOn: finalT < eS ? Math.max(0, effLead - (eS - finalT)) : (finalT <= eE ? effLead : 0),
fArtOff: finalT > eE ? Math.max(0, effTail - (finalT - eE)) : (finalT >= eS ? effTail : 0),
fEase: name === 'morphIn' ? timeEase * smooth(local)
: name === 'morphOut' ? timeEase * (1 - smooth(local)) : 0,
fExc: wanderMode ? (name === 'morphOut' ? local * FINAL.morphOut
: name === 'morphIn' ? FINAL.morphOut + local * FINAL.morphIn : 0) : 0,
fAmt: wanderMode ? (1 - k) * wanderAmt : 0,
fGrab: finalT < eS ? Math.max(0, effGrab - (eS - finalT))
: finalT <= eE ? effGrab : Math.max(0, effGrab - (finalT - eE)),
});
const ph = document.getElementById('phase');
if (ph) ph.innerHTML =
`<b>${name}</b> ${finalT.toFixed(1)}s / ${finalCycle().toFixed(1)}s` +
`<br>steer <b>${(k*100).toFixed(0)}%</b> · sculpt <b>${(cheatK*100).toFixed(0)}%</b>` +
` · logo <b>${(cheat.fill*100).toFixed(0)}%</b>` +
(freeCam
? `<br><span style="color:${camHeld ? 'var(--purple)' : 'var(--accent)'}">` +
(camHeld ? `camera taken back, easing to the epoch pose`
: `camera is yours, drag to orbit`) + `</span>`
: '');
}
/* ===================== TAB 4: THE DIRECTED LOOP =====================
John's cut, in his vocabulary. One cycle:
pt (playground) -> dt (dinner call) -> [ct cheat] -> st (sit) -> mirror out
ct either OVERLAYS dt's tail or is its own act after dt (his checkbox).
The whole thing opens with the teal intro: mark fades in alone, the 3D
arrives underneath exactly on the epoch, mark lets go, loop runs. */
let loopMode=false, loopT=0, introT=0, inIntro=true, loopRolled=false;
let l_pt=5, l_dt=0.7, l_ct=0.2, l_ctOverlay=true, l_st=1; // john fav ships as the default
let l_logoOn=true, l_logoPeak=1, l_logoLead=0.6, l_tealLead=0.2, l_guides=false; // leads kept positive
let loopGains=null;
function rollLoopGains(){ // tab 1's Randomise, verbatim
const v=zeros(); const n=2+Math.floor(Math.random()*4);
for(let j=0;j<n;j++) v[(Math.random()*N)|0]=0.35+Math.random()*0.65;
loopGains=v;
}
const loopEpochStart = () => l_pt + l_dt + (l_ctOverlay ? 0 : l_ct);
const loopEpochEnd = () => loopEpochStart() + l_st;
const loopCycle = () => loopEpochEnd() + (l_ctOverlay ? 0 : l_ct) + l_dt;
const INTRO = { dark:0.9, logoIn:1.4, hold:0.5, reveal:1.2 };
function startIntro(){ inIntro=true; introT=0; loopT=0; if(!loopGains) rollLoopGains(); }
let loop4Pips=null;
function paintLoopPips(vals){
if (!loop4Pips) loop4Pips = [
['l_pt','lp4pt'],['l_dt','lp4dt'],['l_ct','lp4ct'],['l_st','lp4st'],
['l_logoPeak','lp4peak'],['l_logoLead','lp4llead'],['l_tealLead','lp4tlead']
].map(([iid,pid])=>{
const inp=document.getElementById(iid), pip=document.getElementById(pid);
return inp&&pip?{key:iid,pip,lo:parseFloat(inp.min),hi:parseFloat(inp.max)}:null;
}).filter(Boolean);
loop4Pips.forEach(s=>{
const f=Math.max(0,Math.min(1,((vals[s.key]||0)-s.lo)/(s.hi-s.lo)));
s.pip.style.left=`calc(6px + ${f*100}% - ${f} * 12px)`;
s.pip.classList.toggle('zero',f<=0.001);
});
}
function driveLoop(dt){
const liveEvo4 = g => HARMONICS.map((hm,i)=>{
const env=0.55+0.45*Math.cos(tEvo*(0.13+i*0.017)+i*1.7);
return g[i]*env*Math.cos(E_LEVELS[i]*tEvo*0.5);
});
const EA=-Math.PI/2, EB=Math.PI/2, ER=4.4;
let k=0, cheatK=0, fill=0, tealK=0, phaseName='', pinned=false;
if (inIntro){
introT+=dt;
const t=introT, e1=INTRO.dark, e2=e1+INTRO.logoIn, e3=e2+INTRO.hold, e4=e3+INTRO.reveal;
const seg=(a,b,x)=>Math.max(0,Math.min(1,(x-a)/Math.max(0.001,b-a)));
k=1; cheatK=1; tealK=1; pinned=true; phaseName='intro';
/* The mark fades in ALONE, at full teal. The 3D is enabled only once the
mark is solid, so it arrives already congruent underneath it, and the
reveal is the mark letting go of its own 3D self. */
mesh.setEnabled(t>=e2+0.01);
fill = t<e1?0 : t<e2? smooth(seg(e1,e2,t)) : t<e3? 1 : 1-smooth(seg(e3,e4,t));
camera.alpha=EA; camera.beta=EB; camera.radius=ER; camera.target.set(0,0,0);
if (t>=e4){ inIntro=false; mesh.setEnabled(true); loopT=loopEpochEnd(); rollLoopGains(); }
} else {
mesh.setEnabled(true);
loopT=(loopT+dt)%loopCycle();
const eS=loopEpochStart(), eE=loopEpochEnd(), t=loopT;
const ctw=l_ctOverlay? Math.min(l_ct,l_dt) : l_ct;
if (t<l_pt){ k=0; phaseName='pt play'; }
else if (t<l_pt+l_dt){ k=smooth((t-l_pt)/l_dt); phaseName='dt dinner'; }
else if (t<eS){ k=1; phaseName='ct cheat'; }
else if (t<eE){ k=1; phaseName='st sit'; pinned=true; }
else if (!l_ctOverlay && t<eE+l_ct){ k=1; phaseName='ct out'; }
else { const o=l_ctOverlay? t-eE : t-eE-l_ct; k=1-smooth(Math.min(1,o/l_dt)); phaseName='dt out'; }
cheatK = t<eS ? (t<=eS-ctw?0:smooth(1-(eS-t)/ctw))
: t<=eE ? 1
: (t>=eE+ctw?0:smooth(1-(t-eE)/ctw));
/* The fresh roll happens the moment we ARRIVE, invisibly: the display is
pinned to the mark, so the new state is only met on the way out. */
if (t>=eS && !loopRolled){ rollLoopGains(); loopRolled=true; }
if (t<eS) loopRolled=false;
const L=l_logoLead, mid=(eS+eE)/2, a=mid-Math.max(0.001,(eE-eS)/2+L), b=mid+Math.max(0.001,(eE-eS)/2+L);
fill = (!l_logoOn||l_logoPeak<=0||t<=a||t>=b||(b-a)<0.02) ? 0
: (t<mid? smooth((t-a)/(mid-a)) : smooth((b-t)/(b-mid))) * l_logoPeak;
const TL=l_tealLead;
tealK = t<eS ? (TL<=0.01?0:(t<=eS-TL?0:smooth(1-(eS-t)/TL)))
: t<=eE ? 1
: (TL<=0.01?0:(t>=eE+TL?0:smooth(1-(t-eE)/TL)));
const dA=EA+(camMotion?Math.sin(tEvo*0.19)*0.55:0), dB=EB+(camMotion?Math.sin(tEvo*0.13)*0.30:0);
camera.alpha=dA*(1-k)+EA*k; camera.beta=dB*(1-k)+EB*k;
camera.radius+=(ER-camera.radius)*0.06;
camera.target.scaleInPlace(1-Math.min(1,0.05+k*0.95));
}
mesh.rotation.y=0;
mat.setColor3('cNeg', BABYLON.Color3.Lerp(C_NEG, C_POS, Math.max(0,Math.min(1,tealK))));
const gains=loopGains||gainFinalBase;
const c = pinned ? LOGO_BASE.slice()
: liveEvo4(gains).map((v,i)=>v*(1-k)+LOGO_BASE[i]*k);
const nrm=Math.hypot(...c)||1; const cN=c.map(v=>v/nrm);
mat.setFloats('c',cN); liveC=cN;
mat.setFloat('uScale',2.5*(0.9+0.25*k));
mat.setFloat('uFlat',cheatK); mat.setFloat('uEdge',cheatK);
mat.setFloat('uRot',45); mat.setFloat('uPlump',0); mat.setFloat('uGap',0); mat.setFloat('uCorner',0);
cheat.fill=fill; cheat.ghost=(l_guides && !inIntro) ? 0.9*k : 0;
syncLogoLines(2.5*(0.9+0.25*k));
drawGhost();
const led=(bid,lid,pid,v,clip)=>{
const bd=document.getElementById(bid), el=document.getElementById(lid), pc=document.getElementById(pid);
if(!bd||!el||!pc) return;
const lit=v>0.001, full=v>0.995;
bd.classList.toggle(clip?'lit-clip':'lit-morph',lit);
el.classList.toggle(clip?'on':'conv',lit);
el.classList.toggle('part',lit&&!full);
pc.textContent=`${(v*100).toFixed(0)}%`;
};
const lb=document.getElementById('ledBadge'); if(lb) lb.style.display='flex';
led('ledMorphBadge','ledMorph','ledMorphPct',k,false);
led('ledClipBadge','ledClip','ledClipPct',cheatK,true);
const cb=document.getElementById('ledCamBadge');
if (cb){ cb.classList.remove('lit-morph','lit-clip');
document.getElementById('ledCam').classList.remove('on','conv','part');
document.getElementById('ledCamPct').textContent='scripted'; }
paintLoopPips({
l_pt: inIntro?0:Math.min(loopT,l_pt),
l_dt: k*l_dt,
l_ct: cheatK*(l_ctOverlay?Math.min(l_ct,l_dt):l_ct),
l_st: inIntro?0:Math.max(0,Math.min(l_st,loopT-loopEpochStart())),
l_logoPeak: fill,
l_logoLead: l_logoLead>0.01? -(1-Math.min(1,fill/Math.max(0.01,l_logoPeak)))*l_logoLead : 0,
l_tealLead: l_tealLead>0.01? -(1-tealK)*l_tealLead : 0,
});
const ph=document.getElementById('loopPhase');
if (ph) ph.innerHTML = inIntro
? `<b>intro</b> ${introT.toFixed(1)}s · the mark first, the physics underneath`
: `<b>${phaseName}</b> ${loopT.toFixed(1)}s / ${loopCycle().toFixed(1)}s` +
`<br>steer <b>${(k*100).toFixed(0)}%</b> · cheat <b>${(cheatK*100).toFixed(0)}%</b>` +
` · teal <b>${(tealK*100).toFixed(0)}%</b> · logo <b>${(fill*100).toFixed(0)}%</b>` +
`<br><span style="color:var(--text-3)">${l_ctOverlay?'ct overlays the dt tail':'ct is its own act, cycle grows by 2 ct'}</span>`;
}
/* ---- tab 4 controls ---- */
const LOOP_DEFAULTS={l_pt:5,l_dt:0.7,l_ct:0.2,l_st:1,l_logoPeak:1,l_logoLead:-0.6,l_tealLead:-0.2};
[['l_pt','o_pt',v=>{l_pt=v;return v.toFixed(1)+'s';}],
['l_dt','o_dt',v=>{l_dt=v;return v.toFixed(1)+'s';}],
['l_ct','o_ct',v=>{l_ct=v;return v.toFixed(1)+'s';}],
['l_st','o_st',v=>{l_st=v;return v.toFixed(1)+'s';}],
['l_logoPeak','o_logoPeak',v=>{l_logoPeak=v;return Math.round(v*100)+'%';}],
['l_logoLead','o_logoLead',v=>{l_logoLead=Math.abs(v);return v.toFixed(1)+'s';}],
['l_tealLead','o_tealLead',v=>{l_tealLead=Math.abs(v);return v.toFixed(1)+'s';}]
].forEach(([id,out,fn])=>{
document.getElementById(id).addEventListener('input',e=>{
document.getElementById(out).textContent=fn(parseFloat(e.target.value));
});
});
document.getElementById('l_ctMode').addEventListener('change',e=>{ l_ctOverlay=e.target.checked; });
document.getElementById('l_logoOn').addEventListener('change',e=>{ l_logoOn=e.target.checked; });
document.getElementById('lRestart').addEventListener('click',startIntro);
document.getElementById('lDefaults').addEventListener('click',()=>{
for(const [id,v] of Object.entries(LOOP_DEFAULTS)){
const el=document.getElementById(id); if(!el) continue;
el.value=v; el.dispatchEvent(new Event('input'));
}
const cm=document.getElementById('l_ctMode'); cm.checked=true; cm.dispatchEvent(new Event('change'));
const lo=document.getElementById('l_logoOn'); lo.checked=true; lo.dispatchEvent(new Event('change'));
const gd=document.getElementById('l_guides'); gd.checked=false; gd.dispatchEvent(new Event('change'));
});
document.getElementById('l_guides').addEventListener('change',e=>{ l_guides=e.target.checked; });
/* ---- named snapshots: freeze a set of numbers you like, recall from the
pulldown. Stored separately from the auto-persist, so clearing one never
touches the other. ---- */
const SNAP_KEY='orbital-lab-snapshots-v1';
/* Factory snapshots ship IN the page, so they exist on every surface,
including the wiki sandbox where there is no storage at all. Saved user
snapshots live per browser origin and appear after these. */
const FACTORY_SNAPS = [
{ name: 'john fav · pt5 dt0.7 ct0.2 st1', factory: true, // the shipped default
vals: { pt:5, dt:0.7, ct:0.2, ov:true, st:1, lo:true, lp:1, ll:0.6, tl:0.2, gd:false } },
{ name: 'john 1 · pt4.5 dt3 ct0.3 st0', factory: true,
vals: { pt:4.5, dt:3, ct:0.3, ov:true, st:0, lo:true, lp:1, ll:1, tl:1.5, gd:false } },
{ name: 'snap 2 · pt5 dt1 ct0.2 st0', factory: true,
vals: { pt:5, dt:1, ct:0.2, ov:true, st:0, lo:true, lp:1, ll:0.3, tl:1.5, gd:false } },
{ name: 'snap 3 · pt5 dt0.7 ct0.2 st0', factory: true,
vals: { pt:5, dt:0.7, ct:0.2, ov:true, st:0, lo:true, lp:0.8, ll:0.5, tl:1.5, gd:false } }
];
function allSnaps(){
const stored = loadSnaps().filter(s => !FACTORY_SNAPS.some(f => f.name === s.name));
return FACTORY_SNAPS.concat(stored);
}
function snapVals(){ return { pt:l_pt, dt:l_dt, ct:l_ct, ov:l_ctOverlay, st:l_st,
lo:l_logoOn, lp:l_logoPeak, ll:l_logoLead, tl:l_tealLead, gd:l_guides }; }
function loadSnaps(){ try{ return JSON.parse(localStorage.getItem(SNAP_KEY)||'[]'); }catch(e){ return []; } }
function saveSnaps(a){ try{ localStorage.setItem(SNAP_KEY, JSON.stringify(a)); }catch(e){} }
function applySnap(v){
[['l_pt',v.pt],['l_dt',v.dt],['l_ct',v.ct],['l_st',v.st],['l_logoPeak',v.lp],
['l_logoLead', -Math.abs(v.ll||0)],['l_tealLead', -Math.abs(v.tl||0)]].forEach(([id,x])=>{
const el=document.getElementById(id);
if (el && x!==undefined){ el.value=x; el.dispatchEvent(new Event('input')); }
});
const cm=document.getElementById('l_ctMode'); cm.checked=v.ov!==false; cm.dispatchEvent(new Event('change'));
const lg=document.getElementById('l_logoOn'); lg.checked=v.lo!==false; lg.dispatchEvent(new Event('change'));
const gd=document.getElementById('l_guides'); gd.checked=!!v.gd; gd.dispatchEvent(new Event('change'));
}
function refreshSnapList(sel){
const el=document.getElementById('l_snaps'); const snaps=allSnaps();
const head = lsOK ? 'snapshots (' + snaps.length + ')'
: 'snapshots · no storage on this surface';
el.innerHTML = '<option value="">' + head + '</option>' +
snaps.map((s,i)=>`<option value="${i}"${i===sel?' selected':''}>${s.name}${s.factory?' *':''}</option>`).join('');
}
document.getElementById('lSnapSave').addEventListener('click',()=>{
if (!lsOK) return; // the SAY toast explains why
const stored=loadSnaps();
const name=`snap ${allSnaps().length+1} · pt${l_pt} dt${l_dt} ct${l_ct}${l_ctOverlay?'':' seq'} st${l_st}`;
stored.push({ name, vals:snapVals() });
saveSnaps(stored); refreshSnapList(allSnaps().length-1);
});
document.getElementById('lSnapDel').addEventListener('click',()=>{
const el=document.getElementById('l_snaps'); const i=parseInt(el.value,10);
const snaps=allSnaps(); if (isNaN(i)||!snaps[i]) return;
if (snaps[i].factory) return; // ships with the page, cannot be deleted
const stored=loadSnaps();
const j=stored.findIndex(s=>s.name===snaps[i].name);
if (j>=0){ stored.splice(j,1); saveSnaps(stored); }
refreshSnapList();
});
document.getElementById('l_snaps').addEventListener('change',e=>{
const i=parseInt(e.target.value,10); const snaps=allSnaps();
if (isNaN(i)||!snaps[i]) return;
applySnap(snaps[i].vals);
toast('Snapshot applied', snaps[i].name, { duration:2400 });
});
refreshSnapList();
// Matches the Explore tab's defaults, so the "normal animation" here is the
// one that already looks right rather than a busier variant.
const gainFinalBase = new Array(16).fill(0);
[1,2,3].forEach(i=>gainFinalBase[i]=1);
[4,5,6,7,8].forEach(i=>gainFinalBase[i]=0.7);
const E_LEVELS = E;
document.getElementById('fCam').addEventListener('click', e => {
camMotion = !camMotion; e.target.classList.toggle('on', camMotion);
});
document.getElementById('fFree').addEventListener('click', e => {
freeCam = !freeCam; e.target.classList.toggle('on', freeCam);
// The scripted drift would fight the pointer, so the two swap over.
camMotion = !freeCam;
document.getElementById('fCam').classList.toggle('on', camMotion);
document.getElementById('fCam').disabled = freeCam;
if (!freeCam && camHeld) { if (camLive) camera.attachControl(canvas, false); camHeld = false; }
});
document.getElementById('fSpin').addEventListener('click', e => {
spinOn = !spinOn; e.target.classList.toggle('on', spinOn);
});
document.getElementById('fGuide').addEventListener('click', e => {
finalGuides = !finalGuides; e.target.classList.toggle('on', finalGuides);
});
/* Restart rewinds EVERY clock the loop reads, not just the timeline cursor.
It used to set finalT alone, but the shape comes from tEvo and the framing
from spin, so the picture did not change and the button looked dead. */
document.getElementById('fRestart').addEventListener('click', () => {
finalT = 0;
tEvo = 0;
spin = 0; spinTarget = 0; spinLatched = false;
driveFinal(0);
});
document.getElementById('fHold').addEventListener('input', e => {
FINAL.settle = parseFloat(e.target.value);
document.getElementById('oHold').textContent = FINAL.settle.toFixed(1) + 's';
});
document.getElementById('fFill').addEventListener('input', e => {
finalFill = parseFloat(e.target.value);
document.getElementById('oFill2').textContent = Math.round(finalFill*100) + '%';
});
/* The red pip sits at the value each artistic slider would take if it did not
exist. With no slider the clip was simply tied to the morph, so the natural
value IS the morph duration: it moves when you retime the morph. */
/* The tuned values. The artistic lead in / tail out sit well behind the pip on
purpose: at 0.6s into a 2.6s morph the state is already 87% of the way to a
pure 2p, so the clip only ever lands on a shape it was built for. That is
what keeps the stretch artefacts out. */
const FINAL_DEFAULTS = {
fMorphIn:2.6, fMorphOut:2.6, fArtOn:0.6, fArtOff:0.6,
fEase:0.35, fHold:1.0, fFill:0.50, fGrab:1.5
};
function applyFinalDefaults(){
for (const [id,v] of Object.entries(FINAL_DEFAULTS)) {
const el = document.getElementById(id);
if (!el) continue;
el.value = v;
el.dispatchEvent(new Event('input'));
}
}
function placePips(){
const put = (pipId, inputId, seconds) => {
const pip = document.getElementById(pipId), inp = document.getElementById(inputId);
if (!pip || !inp) return;
const lo = parseFloat(inp.min), hi = parseFloat(inp.max);
const pct = Math.max(0, Math.min(1, (seconds - lo) / (hi - lo))) * 100;
// The thumb is 12px wide, so the track it can reach is inset 6px each end.
pip.style.left = `calc(6px + ${pct}% - ${pct/100} * 12px)`;
pip.title = `no-slider default: ${seconds.toFixed(1)}s, co-timed with the morph`;
};
put('pipOn', 'fArtOn', FINAL.morphIn);
put('pipOff', 'fArtOff', FINAL.morphOut);
put('pipGrab','fGrab', FINAL.morphIn); // the old behavior: taken for the whole morph
}
function describeEase(){
const el = document.getElementById('easeWindow');
if (!el) return;
if (timeEase < 0.02) {
el.innerHTML = `clock runs at full speed throughout` +
`<br><span style="color:var(--text-3)">steering takes its ` +
`${FINAL.morphIn.toFixed(1)}s and ${FINAL.morphOut.toFixed(1)}s as written</span>`;
return;
}
const slowest = Math.max(0.08, 1 - timeEase);
const inEff = FINAL.morphIn / ((1 + slowest) / 2);
const outEff = FINAL.morphOut / ((1 + slowest) / 2);
el.innerHTML =
`eases while steering runs, slowest <b>${slowest.toFixed(2)}x</b> at the epoch` +
`<br><span style="color:var(--text-3)">steer in plays over about ` +
`${inEff.toFixed(1)}s, steer out about ${outEff.toFixed(1)}s · ` +
`the free wander and the hold are untouched</span>`;
}
function describeWindow(){
const el = document.getElementById('artWindow');
if (!el) return;
const eS = epochStart(), eE = epochEnd();
const total = artLeadIn + (eE - eS) + artTailOut;
el.innerHTML =
`sculpting from <b>epoch − ${artLeadIn.toFixed(2)}s</b> ` +
`to <b>epoch + ${(eE-eS+artTailOut).toFixed(2)}s</b>` +
`<br><span style="color:var(--text-3)">` +
`${total.toFixed(1)}s of ${finalCycle().toFixed(1)}s total · ` +
(artLeadIn > FINAL.morphIn - 0.2 || artTailOut > FINAL.morphOut - 0.2
? 'capped at the steer length: sculpting is never allowed to touch raw physics'
: artLeadIn < 0.2 && artTailOut < 0.2 ? 'snaps on and off at the epoch'
: 'steering finishes early, then sculpting fades up over this window') + `</span>`;
}
document.getElementById('fArtOn').addEventListener('input', e => {
artLeadIn = parseFloat(e.target.value);
document.getElementById('oArtOn').textContent = artLeadIn.toFixed(2) + 's';
describeWindow();
});
document.getElementById('fArtOff').addEventListener('input', e => {
artTailOut = parseFloat(e.target.value);
document.getElementById('oArtOff').textContent = artTailOut.toFixed(2) + 's';
describeWindow();
});
document.getElementById('fDefaults').addEventListener('click', applyFinalDefaults);
document.getElementById('fWander').addEventListener('click', e => {
wanderMode = !wanderMode;
e.target.classList.toggle('on', wanderMode);
if (wanderMode) { applyExcursionTiming(); rollExcursion(); }
else { FINAL.evolve = 7.0; FINAL.morphIn = FINAL.morphOut = 2.6; excursion = null; syncMorphInputs(); }
describeWander();
});
document.getElementById('fRoll').addEventListener('click', rollExcursion);
[['fWanderAll', () => true],
['fWanderNoF', (i) => HARMONICS[i].l !== 3],
['fWanderP', (i) => HARMONICS[i].l === 1]].forEach(([id, pick]) => {
document.getElementById(id).addEventListener('click', () => {
wanderOn = HARMONICS.map((hm,i) => pick(i));
if (wanderRows) wanderRows.forEach((r,i) => {
r.cb.checked = wanderOn[i]; r.row.classList.toggle('off', !wanderOn[i]);
});
describeWander();
});
});
document.getElementById('fGrab').addEventListener('input', e => {
camGrab = parseFloat(e.target.value);
document.getElementById('oGrab').textContent = camGrab.toFixed(1) + 's';
});
document.getElementById('fExc').addEventListener('input', e => {
excursionT = parseFloat(e.target.value);
document.getElementById('oExc').textContent = excursionT.toFixed(1) + 's';
if (wanderMode) applyExcursionTiming();
describeWander();
});
document.getElementById('fAmt').addEventListener('input', e => {
wanderAmt = parseFloat(e.target.value);
document.getElementById('oAmt').textContent = Math.round(wanderAmt*100) + '%';
});
document.getElementById('fMorphIn').addEventListener('input', e => {
FINAL.morphIn = parseFloat(e.target.value);
document.getElementById('oMorphIn').textContent = FINAL.morphIn.toFixed(1) + 's';
placePips(); describeWindow(); describeEase();
});
document.getElementById('fMorphOut').addEventListener('input', e => {
FINAL.morphOut = parseFloat(e.target.value);
document.getElementById('oMorphOut').textContent = FINAL.morphOut.toFixed(1) + 's';
placePips(); describeWindow(); describeEase();
});
document.getElementById('fEase').addEventListener('input', e => {
timeEase = parseFloat(e.target.value);
describeEase();
document.getElementById('oEase').textContent =
timeEase < 0.02 ? 'off' : timeEase.toFixed(2) + 'x';
});
applyFinalDefaults();
restoreLab();
buildWanderList();
if (wanderMode) { applyExcursionTiming(); rollExcursion(); }
describeWindow(); describeEase(); placePips(); describeWander();
/* Toggle buttons must never disagree with the state they claim to show. The
state changes down many paths (tab switches flip animate, Free rotate flips
Camera motion, the guided demo drives things), so instead of chasing every
path, all of them resync a few times a second. Seven class toggles: free. */
const TOGGLES = [
['bAnim', () => animate], ['bView', () => viewFree],
['fCam', () => camMotion], ['fSpin', () => spinOn],
['fFree', () => freeCam], ['fGuide',() => finalGuides],
['fWander',() => wanderMode],
];
function syncToggles(){
for (const [id, get] of TOGGLES) {
const b = document.getElementById(id);
if (b) b.classList.toggle('on', !!get());
}
}
syncToggles();
setInterval(syncToggles, 400);
/* Any control change persists (debounced); so does every tab switch. */
['input','change'].forEach(t => document.addEventListener(t, schedulePersist, true));
document.addEventListener('click', e => {
if (e.isTrusted && e.target.closest && e.target.closest('button')) schedulePersist();
}, true);
/* Always boot into the Loop tab: it is the tab under active development. The
other tabs keep their saved state and are one click away. */
setMode('loop');
document.getElementById('mFinal').addEventListener('click', () => setMode('final'));
document.getElementById('mLoop').addEventListener('click', () => setMode('loop'));
/* ---------- Getting back to the money shot ----------
Once the brandmark match is on screen, rotating away breaks it: the fit only
reads at all from square on, because the licence is a flat cut and any other
angle shows the cut faces. So the first time someone spins it we say how to
get back, and if they simply stop for five seconds we put it back ourselves.
Recovery should not be something you have to know about. */
const RESTORE_AFTER = 5000;
function scheduleRestore(){
if (!logoMode || cheat.flat < 0.5) return; // only when the match is set up
clearTimeout(restoreTimer);
if (!toldAboutSquareOn) {
toldAboutSquareOn = true;
toast('Rotated off the match',
'Click Square on to snap back, or just stop and it returns itself',
{ duration:5000 });
}
restoreTimer = setTimeout(() => {
if (!viewFree) return;
viewFree = false;
document.getElementById('bView').classList.remove('on');
toast('Back to the brandmark view', 'square on', { duration:2600 });
}, RESTORE_AFTER);
}
// every interaction with the view restarts the idle countdown
canvas.addEventListener('pointerdown', scheduleRestore);
canvas.addEventListener('pointermove', e => { if (viewFree && e.buttons) scheduleRestore(); });
canvas.addEventListener('pointerup', scheduleRestore);
canvas.addEventListener('wheel', scheduleRestore, { passive:true });
/* ---- measure the actual fit ----
Reads the rendered pixels back and compares the silhouette against the
real logo vectors. An honest number beats squinting at the overlay. */
document.getElementById('bCopy').addEventListener('click', async (e) => {
const txt =
`The Adom logomark is a hydrogen 2p orbital.
psi = (2p_x + 2p_y) / sqrt(2)
c_px = 0.707
c_py = 0.707
c_pz = 0.000
Axis in the view plane, on the diagonal (45 deg).
Measured against the real logo vectors:
99.9% of the mark covered
0.6% spill
99.3% IoU
Untouched, the same state already scores 79.2% IoU.
The licence is a quadrant clip plus a per-direction edge correction.`;
try { await navigator.clipboard.writeText(txt); e.target.textContent='Copied'; }
catch { e.target.textContent='Copy failed'; }
setTimeout(()=>{ e.target.textContent='Copy the state'; }, 1600);
});
document.getElementById('bMeasure').addEventListener('click', async () => {
const fit=document.getElementById('fit');
fit.innerHTML='measuring...';
await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)));
const w=canvas.width, ht=canvas.height;
const gl=engine._gl;
const px=new Uint8Array(w*ht*4);
gl.readPixels(0,0,w,ht,gl.RGBA,gl.UNSIGNED_BYTE,px);
// orbital silhouette = anything not the clear colour
const bg=[13,17,23];
const orb=new Uint8Array(w*ht);
for(let i=0;i<w*ht;i++){
const r=px[i*4],g2=px[i*4+1],b=px[i*4+2];
orb[i]=(Math.abs(r-bg[0])+Math.abs(g2-bg[1])+Math.abs(b-bg[2])>28)?1:0;
}
// logo mask at the same scale/placement as the ghost
const cv=document.createElement('canvas'); cv.width=w; cv.height=ht;
const c2=cv.getContext('2d');
const s=Math.min(w,ht)*0.34;
c2.fillStyle='#fff'; c2.translate(w/2,ht/2);
logoPaths.forEach(pts=>{
c2.beginPath();
pts.forEach(([x,y],i)=>{const X=x*s,Y=-y*s; i?c2.lineTo(X,Y):c2.moveTo(X,Y);});
c2.closePath(); c2.fill();
});
const ld=c2.getImageData(0,0,w,ht).data;
let inter=0,uni=0,lo=0,ob=0;
for(let y=0;y<ht;y++){
for(let x=0;x<w;x++){
const i=y*w+x;
const li=((ht-1-y)*w+x)*4; // readPixels is bottom-up
const L=ld[li+3]>127?1:0, O=orb[i];
if(L&&O)inter++; if(L||O)uni++; if(L)lo++; if(O)ob++;
}
}
fit.innerHTML =
`IoU <b>${(inter/Math.max(1,uni)*100).toFixed(1)}%</b><br>`+
`mark covered <b>${(inter/Math.max(1,lo)*100).toFixed(1)}%</b><br>`+
`spill outside mark <b>${((ob-inter)/Math.max(1,ob)*100).toFixed(1)}%</b>`;
});
/* ---- tooltip engine (Adom UI skill, section 3) ---- */
const tt = document.createElement('div');
tt.id = 'tt'; tt.innerHTML = '<span class="body"></span><span class="caret"></span>';
document.body.appendChild(tt);
{
const body = tt.querySelector('.body'), caret = tt.querySelector('.caret');
let mx = 0, my = 0, timer = null, shown = false;
// native title attrs become data-tt so the browser tooltip never doubles up
const migrate = el => { if (el.title){ el.dataset.tt = el.title; el.removeAttribute('title'); } };
document.querySelectorAll('[title]').forEach(migrate);
const ttOf = t => {
let el = t.closest ? t.closest('[data-tt],[title]') : null;
if (el) { migrate(el); return el.dataset.tt ? el : null; }
// sliders and outputs inherit their row label's explanation
const row = t.closest ? t.closest('.row') : null;
const lab = row && row.querySelector('label');
if (lab) { migrate(lab); if (lab.dataset.tt) return lab; }
return null;
};
const show = text => {
body.textContent = text;
tt.classList.add('show'); shown = true;
// measure only AFTER content is set and laid out, then clamp BOTH axes
const M = 8, w = tt.offsetWidth, hh = tt.offsetHeight;
const vw = innerWidth, vh = innerHeight;
let x = mx + 14, y = my + 14, above = false;
if (y + hh > vh - M) { y = my - 14 - hh; above = true; } // flip, never clip
if (x + w > vw - M) x = vw - w - M;
x = Math.max(M, Math.min(x, vw - w - M));
y = Math.max(M, Math.min(y, vh - hh - M));
tt.style.left = x + 'px'; tt.style.top = y + 'px';
tt.classList.toggle('above', above);
caret.style.left = Math.max(10, Math.min(mx - x, w - 10)) - 4 + 'px';
};
const hide = () => { clearTimeout(timer); timer = null; if (shown){ tt.classList.remove('show'); shown = false; } };
document.addEventListener('mousemove', e => { mx = e.clientX; my = e.clientY; });
document.addEventListener('mouseover', e => {
const el = ttOf(e.target);
hide();
if (!el) return;
timer = setTimeout(() => show(el.dataset.tt), 600); // fly-bys stay quiet
});
document.addEventListener('mouseout', e => { if (!e.relatedTarget || !ttOf(e.relatedTarget)) hide(); });
['pointerdown','wheel'].forEach(t => document.addEventListener(t, hide, true));
document.addEventListener('scroll', hide, true);
document.addEventListener('keydown', e => { if (e.key === 'Escape') hide(); });
// touch has no hover: a 500ms still press reveals the same explanation
let pressTimer = null;
document.addEventListener('pointerdown', e => {
if (e.pointerType !== 'touch') return;
const el = ttOf(e.target);
if (!el) return;
mx = e.clientX; my = e.clientY;
pressTimer = setTimeout(() => show(el.dataset.tt), 500);
}, true);
['pointerup','pointermove','pointercancel'].forEach(t =>
document.addEventListener(t, e => { if (e.pointerType === 'touch') clearTimeout(pressTimer); }, true));
window.__tt = { show, hide, ttOf }; // driven by the ralph-test
}
engine.runRenderLoop(()=>scene.render());
/* The object should always occupy roughly the same fraction of the canvas,
about 80% at the mark, however the window is shaped. Babylon's FOV is
fixed on ONE axis; pick that axis to be the LIMITING one, so the animation
shrinks with the window instead of clipping when the stage goes narrow. */
function fitFraming(){
const wr = document.getElementById('stageWrap');
if (!wr) return;
camera.fovMode = (wr.clientWidth >= wr.clientHeight)
? BABYLON.Camera.FOVMODE_VERTICAL_FIXED
: BABYLON.Camera.FOVMODE_HORIZONTAL_FIXED;
}
fitFraming();
window.addEventListener('resize', ()=>{ fitFraming(); engine.resize(); drawGhost(); });
/* A window resize is not the only way the stage changes size: the panel, the
wiki iframe and a devtools split all resize it without firing `resize`.
Observing the stage itself covers every case. */
if (window.ResizeObserver) {
new ResizeObserver(() => { fitFraming(); engine.resize(); drawGhost(); })
.observe(document.getElementById('stageWrap'));
}
window.__lab = { push, zeros, HARMONICS, setAnim:v=>{animate=v;} };
</script>
<style>
/* readme variant: the lab fills the first viewport, the docs flow below */
html,body{height:auto!important;overflow:auto!important}
body{flex-wrap:wrap;min-height:0!important}
/* Fixed, not viewport-relative: the wiki parent grows this iframe to fit
our reported height, and any vh unit would chase that growth forever. */
#panel{height:780px}
#stageWrap{height:780px;min-width:0}
.edocs{flex-basis:100%;background:var(--bg);border-top:1px solid var(--border);
padding:26px 30px 44px;max-width:100%}
.edocs h2{font-family:'Familjen Grotesk',sans-serif;font-weight:600;font-size:22px;margin:0 0 8px;color:var(--text)}
.edocs h3{font-family:'Familjen Grotesk',sans-serif;font-weight:600;font-size:16px;margin:22px 0 7px;color:var(--text-2)}
.edocs p{font-size:14.5px;color:var(--text-2);line-height:1.7;max-width:760px}
.edocs b{color:var(--text)}
.edocs pre{font-family:'JetBrains Mono','DM Mono',monospace;font-size:12.5px;background:var(--surface);
border:1px solid var(--border);border-radius:8px;padding:13px 15px;overflow-x:auto;
max-width:760px;color:var(--text);line-height:1.65}
.edocs table{border-collapse:collapse;font-size:13px;color:var(--text-2);margin:10px 0}
.edocs td,.edocs th{border:1px solid var(--border);padding:7px 12px;text-align:left}
.edocs th{color:var(--text);font-weight:500}
.edocs .prev{width:min(760px,100%);aspect-ratio:16/10;border:1px solid var(--border);
border-radius:10px;background:#0d1117;display:block;margin:6px 0}
.edocs .rig-acts{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin:10px 0 10px}
.edocs .rig-acts span{font-family:'JetBrains Mono','DM Mono',monospace;font-size:11px;
color:var(--text-3);letter-spacing:.05em;margin-right:2px}
.edocs .rig-acts button{font-family:'Satoshi',sans-serif;font-weight:500;font-size:12.5px;
padding:7px 13px;border-radius:999px;cursor:pointer;background:var(--elevated);
border:1px solid var(--border);color:var(--text)}
.edocs .rig-acts button:hover{border-color:var(--accent)}
#pbox{position:relative;overflow:hidden;width:min(760px,100%);height:475px;min-width:140px;
min-height:90px;border:1px dashed var(--border);border-radius:10px;background:#0d1117;
transition:width .35s ease,height .35s ease}
#pbox.notrans{transition:none}
/* the iframe ELEMENT must not paint: the base .prev rule gives it the dark
background, which sat between the loader's transparent buffer and the
container's colour and swallowed the swatches */
#pbox .prev{position:absolute;inset:0;width:100%;height:100%;aspect-ratio:auto;border:0;margin:0;background:transparent}
#pgrip{position:absolute;right:-2px;bottom:-2px;width:22px;height:22px;cursor:nwse-resize;z-index:5;
border-right:3px solid var(--accent);border-bottom:3px solid var(--accent);border-radius:0 0 8px 0}
#prd{font-family:'JetBrains Mono','DM Mono',monospace;font-size:11.5px;color:var(--accent);margin-top:8px}
</style>
<div class="edocs">
<h2>The live viewer</h2>
<div class="rig-acts">
<span>size</span>
<button data-s="300,620">Tall</button>
<button data-s="1100,300">Wide</button>
<button data-s="420,420">Square</button>
<button data-s="180,140">Tiny</button>
<button data-s="760,475">Reset</button>
<span style="margin-left:10px">margin</span>
<button data-m="-50">-50% bleed</button>
<button data-m="-30">-30%</button>
<button data-m="0">0%</button>
<button data-m="40">40%</button>
<button data-m="80">80%</button>
<span style="margin-left:10px">speed</span>
<button data-sp="10">10%</button>
<button data-sp="25">25%</button>
<button data-sp="50">50%</button>
<button data-sp="100">100%</button>
<span style="margin-left:10px">orbit</span>
<button data-o="0">off</button>
<button data-o="12">12°/s</button>
<button data-o="45">45°/s</button>
<span style="margin-left:10px">logo fade</span>
<button data-lg="10">10%</button>
<button data-lg="50">50%</button>
<button data-lg="100">100%</button>
<span style="margin-left:10px">hint</span>
<button data-h="1">on</button>
<button data-h="0">off</button>
<span style="margin-left:10px">start</span>
<button data-e="1">emerge</button>
<button data-e="0">logo</button>
<span style="margin-left:10px">container bg</span>
<button data-b="#0d1117">dark</button>
<button data-b="#20262e">bar gray</button>
<button data-b="#8a93a0">mid gray</button>
<button data-b="#f2f4f7">white</button>
</div>
<div id="pbox"><iframe class="prev" srcdoc="<!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';window.__ADOM_LOADER_INTERACT=1;window.__ADOM_LOADER_PARAMS={pt:5,dt:0.7,ct:0.2,st:1,logo:100,llead:0.6,tlead:0.2,bg:'transparent'};
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=`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){
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>
"></iframe><div id="pgrip" title="drag to resize"></div></div>
<div id="prd">container: …</div>
<p>This IS loader.html, inlined, running the <b>john fav</b> 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. The loader here runs <b>bg=transparent</b>: tap the container bg swatches and
the box's own colour shows straight through the animation, which is how you sit it on a
toolbar, next to a wordmark, or on any surface. Everything below explains how to ship
it.</p>
<script>
(function(){
var box=document.getElementById('pbox'), grip=document.getElementById('pgrip'),
rd=document.getElementById('prd'), ifr=box.querySelector('iframe'), drag=null;
document.querySelectorAll('.rig-acts button[data-s]').forEach(function(b){
b.addEventListener('click',function(){
var v=b.getAttribute('data-s').split(',');
box.style.width=v[0]+'px'; box.style.height=v[1]+'px';
});
});
document.querySelectorAll('.rig-acts button[data-m]').forEach(function(b){
b.addEventListener('click',function(){
ifr.contentWindow.postMessage({adomLoader:{margin:parseFloat(b.getAttribute('data-m'))}},'*');
});
});
document.querySelectorAll('.rig-acts button[data-sp]').forEach(function(b){
b.addEventListener('click',function(){
ifr.contentWindow.postMessage({adomLoader:{speed:parseFloat(b.getAttribute('data-sp'))}},'*');
});
});
document.querySelectorAll('.rig-acts button[data-o]').forEach(function(b){
b.addEventListener('click',function(){
ifr.contentWindow.postMessage({adomLoader:{orbit:parseFloat(b.getAttribute('data-o'))}},'*');
});
});
document.querySelectorAll('.rig-acts button[data-lg]').forEach(function(b){
b.addEventListener('click',function(){
ifr.contentWindow.postMessage({adomLoader:{logo:parseFloat(b.getAttribute('data-lg'))}},'*');
});
});
document.querySelectorAll('.rig-acts button[data-h]').forEach(function(b){
b.addEventListener('click',function(){
ifr.contentWindow.postMessage({adomLoader:{hint:b.getAttribute('data-h')==='1'}},'*');
});
});
document.querySelectorAll('.rig-acts button[data-e]').forEach(function(b){
b.addEventListener('click',function(){
/* emerge is boot-time state, so toggle it by rewriting the srcdoc's
params and letting the iframe reboot into the chosen opening. */
var sd=ifr.getAttribute('srcdoc').replace(/emerge:1,?/,'');
if(b.getAttribute('data-e')==='1')
sd=sd.replace('__ADOM_LOADER_PARAMS={','__ADOM_LOADER_PARAMS={emerge:1,');
ifr.setAttribute('srcdoc',sd);
});
});
document.querySelectorAll('.rig-acts button[data-b]').forEach(function(b){
b.addEventListener('click',function(){
box.style.background=b.getAttribute('data-b');
});
});
grip.addEventListener('pointerdown',function(e){
e.preventDefault(); grip.setPointerCapture(e.pointerId);
box.classList.add('notrans');
drag={x:e.clientX,y:e.clientY,w:box.clientWidth,h:box.clientHeight};
});
grip.addEventListener('pointermove',function(e){
if(!drag) return;
box.style.width=Math.max(140,drag.w+e.clientX-drag.x)+'px';
box.style.height=Math.max(90,drag.h+e.clientY-drag.y)+'px';
});
grip.addEventListener('pointerup',function(){ drag=null; box.classList.remove('notrans'); });
setInterval(function(){
rd.textContent='container: '+box.clientWidth+'x'+box.clientHeight;
},300);
})();
</script>
<h2 style="margin-top:30px">Ship it as a webview loader</h2>
<p><b>loader.html</b> on this page is the animation above distilled to ONE dependency-free file:
raw WebGL, no Babylon, no CDN, no fonts, no fetch, about 30 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.</p>
<h3>Why raw WebGL, and not Babylon or three.js</h3>
<p>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.</p>
<table>
<tr><th>approach</th><th>payload, gzipped</th><th>requests</th><th>first frame</th></tr>
<tr><td>Babylon.js + app</td><td>~1.4 MB engine + your page</td><td>CDN + page</td><td>after download and parse</td></tr>
<tr><td>three.js + app</td><td>~170 KB engine + your page</td><td>CDN + page</td><td>after download and parse</td></tr>
<tr><td><b>loader.html</b></td><td><b>9,912 bytes, everything included</b></td><td><b>zero</b></td><td><b>tens of milliseconds</b></td></tr>
</table>
<h3>The measured numbers</h3>
<p>Measured on this build, not estimated: <b>24,379 bytes</b> on disk and <b>9,912 bytes
gzipped</b>, including the shaders, the 286-point logomark vectors, the 128-sample
measured edge profile, and the whole timeline. Runtime: <b>0.057 ms of JavaScript per
frame</b> (16 coefficient cosines and uniform uploads, about 1.4% of a 240 Hz frame
budget), then <b>one draw call</b> over <b>12,800 triangles / 6,561 vertices</b> with all
displacement in the vertex shader. requestAnimationFrame pauses it completely when the
tab is hidden, so a backgrounded webview costs nothing. Spinner duty, measured at
32x32 on a 240 Hz display: one instance costs about 1 to 3% of a single core, eight
concurrent instances about 9%, and eight instances capped with <code>fps=30</code>
about 2% total, all while the host page holds a locked 240 fps. The mode presets,
same eight-spinner fleet: <b>performance</b> 8.2% of a core, <b>lightweight</b> 1.7%,
about five times cheaper. On a 60 Hz machine divide by four. Hidden, the cost is
exactly zero.</p>
<h3>Fidelity guarantees</h3>
<p>The loader is not a re-implementation of the look. <code>tools/build-loader.py</code>
extracts the GLSL <b>verbatim</b> 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 <b>measured</b> 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.</p>
<h3>The mouse: arming, zoom to cursor, and the springiness algorithm</h3>
<p><b>Arming.</b> A load screen must never eat input, so the loader is inert by default:
clicks and wheel pass straight through to your page. <code>?interact=1</code> 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.</p>
<p><b>Zoom to cursor.</b> Every notch records the cursor in normalized device coordinates.
The world point under it is solved from the camera, <code>w = n·r/m + pan</code>,
the radius step is applied, and the pan is re-solved so that exact world point stays
pinned under the cursor: <code>pan' = w − n·r'/m</code>. Measured drift of
the pinned point across a six-notch corner zoom: 0.002 world units.</p>
<p><b>The springiness.</b> Three exponential systems, all frame-rate independent via
<code>pow(k, dt·240)</code> so a 60 Hz laptop feels identical to a 240 Hz monitor:</p>
<table>
<tr><th>system</th><th>rule, per frame</th><th>feel</th></tr>
<tr><td>inertia integral</td><td>each notch deposits <code>vel += r·0.085</code>;
consumed by <code>step = vel·(1−0.8^(dt·240))</code></td>
<td>notches stack and glide instead of stepping</td></tr>
<tr><td>radius spring</td><td><code>r += (4.4−r)·(1−0.94^(dt·240))</code></td>
<td>zoom always breathes back home</td></tr>
<tr><td>centering pull</td><td><code>pan −= pan·(1−0.95^(dt·240))</code></td>
<td>the mark drifts back to center</td></tr>
</table>
<p><b>Tuned by measurement, not by copying constants.</b> The obvious approach, lifting
Babylon's <code>wheelDeltaPercentage</code>, 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.</p>
<p><b>The epoch override.</b> Whatever you have zoomed or panned, a glide window of
<code>max(0.6s, llead+0.2s, min(1.2s, dt))</code> 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.</p>
<h3>Resizing: it fills whatever it is given</h3>
<p>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
<code>resize-proof.html</code>, a drag-to-resize torture rig around the real loader,
so you can prove it in your own container shape.</p>
<h3>Embed</h3>
<pre><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 --></pre>
<p>Grab <b>loader.html</b> 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.</p>
<h3>Parameters</h3>
<table>
<tr><th>param</th><th>default</th><th>meaning</th></tr>
<tr><td>pt</td><td>4.5</td><td>playground seconds of free evolution between epochs</td></tr>
<tr><td>dt</td><td>3</td><td>seconds to steer onto the mark, and back out</td></tr>
<tr><td>ct</td><td>0.3</td><td>seconds before the epoch the outline cheat fades in</td></tr>
<tr><td>st</td><td>0</td><td>seconds the mark is held dead still</td></tr>
<tr><td>logo</td><td>100</td><td>peak opacity of the flat teal mark, in percent</td></tr>
<tr><td>llead</td><td>1</td><td>seconds the logo fade starts before the sit and ends after it</td></tr>
<tr><td>tlead</td><td>1.5</td><td>seconds before the epoch the dark lobe fades to teal</td></tr>
<tr><td>ov</td><td>1</td><td>0 makes the cheat its own act after the steer</td></tr>
<tr><td>intro</td><td>1</td><td>0 skips the teal logo intro</td></tr>
<tr><td>interact</td><td>0</td><td>1 arms click-to-zoom (for previews and demos, never load screens)</td></tr>
<tr><td>mode</td><td>performance</td><td><b>lightweight</b> bundles fps 30, dpr 1, seg 48, no antialias for spinner fleets and dialogs; <b>performance</b> (alias <b>sexy</b>) is the full-quality default. Explicit params override the mode</td></tr>
<tr><td>seg</td><td>80</td><td>sphere tessellation, 24 to 128 (80 is 12,800 triangles, 48 is 4,608)</td></tr>
<tr><td>aa</td><td>1</td><td>0 disables antialiasing</td></tr>
<tr><td>dpr</td><td>2</td><td>devicePixelRatio cap for the backing store, 1 to 3</td></tr>
<tr><td>fps</td><td>off</td><td>cap the render rate for spinner duty (floor 20). rAF already yields between frames and stops when hidden; fps=30 renders one frame in eight on a 240 Hz display</td></tr>
<tr><td>bg</td><td>0d1117</td><td>hex background, or <b>transparent</b> to composite over the container's own background (GL context runs with alpha). Also settable live by postMessage</td></tr>
<tr><td>hint</td><td>0</td><td>1 shows the small "click to zoom" pill in armed contexts. Off by default: production stays pure. Armed contexts can also flip it live by postMessage</td></tr>
<tr><td>emerge</td><td>0</td><td>1 replaces the logo intro: 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 timeline. Keep pt above ~3 s so the first epoch never lands mid-birth. A taste toggle: try the start buttons in the rig above</td></tr>
<tr><td>margin</td><td>0</td><td>percent of the container left empty around the mark: 80 sits it small in the middle of a big window, 20 nearly fills. NEGATIVE margins bleed: -50 moves the camera 50% closer so the animation runs off every edge (clamped at -70 to protect the near plane). Zoom-in still brings it to the screen; zoom-out caps at 40% past the margined framing. Live-settable by postMessage</td></tr>
<tr><td>speed</td><td>100</td><td>percent playback rate for the animation clocks (timeline, evolution, birth): 10 runs it at a tenth speed. Camera springs and user interaction stay real time so the feel never goes gluey. Live-settable by postMessage</td></tr>
<tr><td>logo</td><td>100</td><td>peak opacity of the logo fill at the epoch, in percent: 100 is the full solid mark, 50 a translucent ghost, 10 barely a whisper, 0 disables the fill entirely (the sculpted surface still forms the mark shape). Live-settable by postMessage</td></tr>
<tr><td>orbit</td><td>0</td><td>automatic camera yaw during PLAY in degrees per second (try 12). As sculpting starts the angle glides to the nearest whole turn, so every epoch still lands square-on for the logo and play resumes with no unwinding sweep. Live-settable by postMessage</td></tr>
</table>
<h3>The three tuned cuts</h3>
<p>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: <b>pt</b> is free play between epochs, <b>dt</b> is how
long the dive onto the mark takes, <b>ct</b> is how late the outline cheat is held back
(smaller = the sculpting is nearly invisible), <b>st</b> is dead-still hold time,
<b>logo</b> is the flat mark's peak opacity, <b>llead</b> starts the logo fade that many
seconds before the epoch and ends it that many after, <b>tlead</b> turns the dark lobe
teal that many seconds early so the mark reads as one colour by arrival.</p>
<table>
<tr><th>cut</th><th>numbers</th><th>why it feels the way it does</th></tr>
<tr><td><b>john fav</b><br>(the preview below)</td>
<td>pt 5 · dt 0.7 · ct 0.2 · st 1<br>logo 100% · llead 0.6 · tlead 0.2</td>
<td>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:
<code>?pt=5&dt=0.7&ct=0.2&st=1&llead=0.6&tlead=0.2</code></td></tr>
<tr><td><b>john 1</b><br>(the default)</td>
<td>pt 4.5 · dt 3 · ct 0.3 · st 0<br>logo 100% · llead 1 · tlead 1.5</td>
<td>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.</td></tr>
<tr><td><b>snap 2</b></td>
<td>pt 5 · dt 1 · ct 0.2 · st 0<br>logo 100% · llead 0.3 · tlead 1.5</td>
<td>The middle cut. A 1s dive and a tight 0.3s logo flash: punchy, still readable.
Loader: <code>?pt=5&dt=1&ct=0.2&llead=0.3</code></td></tr>
<tr><td><b>snap 3</b></td>
<td>pt 5 · dt 0.7 · ct 0.2 · st 0<br>logo 80% · llead 0.5 · tlead 1.5</td>
<td>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: <code>?pt=5&dt=0.7&ct=0.2&logo=80&llead=0.5</code></td></tr>
</table>
</div>
<script>
/* The wiki parent listens for {type:'readme-height'} and resizes this iframe
to fit, so the lab and the loader docs render with no inner scrollbars. */
(function(){
var last = 0;
function report(){
var h = Math.ceil(document.documentElement.scrollHeight) + 2;
if (Math.abs(h - last) > 2) { last = h; parent.postMessage({ type: 'readme-height', height: h }, '*'); }
}
window.addEventListener('load', report);
if (window.ResizeObserver) new ResizeObserver(report).observe(document.documentElement);
setInterval(report, 1200);
report();
})();
</script>
</body>
</html>