Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553855485558556855785588559856085618562856385648565856685678568856985708571857285738574857585768577857885798580858185828583858485858586858785888589859085918592859385948595859685978598859986008601860286038604860586068607860886098610861186128613861486158616861786188619862086218622862386248625862686278628862986308631863286338634863586368637863886398640864186428643864486458646864786488649865086518652865386548655865686578658865986608661866286638664866586668667866886698670867186728673867486758676867786788679868086818682868386848685868686878688868986908691869286938694869586968697869886998700870187028703870487058706870787088709871087118712871387148715871687178718871987208721872287238724872587268727872887298730873187328733873487358736873787388739874087418742874387448745874687478748874987508751875287538754875587568757875887598760876187628763876487658766876787688769877087718772877387748775877687778778877987808781878287838784878587868787878887898790879187928793879487958796879787988799880088018802880388048805880688078808880988108811881288138814881588168817881888198820882188228823882488258826882788288829883088318832883388348835883688378838883988408841884288438844884588468847884888498850885188528853885488558856885788588859886088618862886388648865886688678868886988708871887288738874887588768877887888798880888188828883888488858886888788888889889088918892889388948895889688978898889989008901890289038904890589068907890889098910891189128913891489158916891789188919892089218922892389248925892689278928892989308931893289338934893589368937893889398940894189428943894489458946894789488949895089518952895389548955895689578958895989608961896289638964896589668967896889698970897189728973897489758976897789788979898089818982898389848985898689878988898989908991899289938994899589968997899889999000900190029003900490059006900790089009901090119012901390149015901690179018901990209021902290239024902590269027902890299030903190329033903490359036903790389039904090419042904390449045904690479048904990509051905290539054905590569057905890599060906190629063906490659066906790689069907090719072907390749075907690779078907990809081908290839084908590869087908890899090909190929093909490959096909790989099910091019102910391049105910691079108910991109111911291139114911591169117911891199120912191229123912491259126912791289129913091319132913391349135913691379138913991409141914291439144914591469147914891499150915191529153915491559156915791589159916091619162916391649165916691679168916991709171917291739174917591769177917891799180918191829183918491859186918791889189919091919192919391949195919691979198919992009201920292039204920592069207920892099210921192129213921492159216921792189219922092219222922392249225922692279228922992309231923292339234923592369237923892399240924192429243924492459246924792489249925092519252925392549255925692579258925992609261926292639264926592669267926892699270927192729273927492759276927792789279928092819282928392849285928692879288928992909291929292939294929592969297929892999300930193029303930493059306930793089309931093119312931393149315931693179318931993209321932293239324932593269327932893299330933193329333933493359336933793389339934093419342934393449345934693479348934993509351935293539354935593569357935893599360936193629363936493659366936793689369937093719372937393749375937693779378937993809381938293839384938593869387938893899390939193929393939493959396939793989399940094019402940394049405940694079408940994109411941294139414941594169417941894199420942194229423942494259426942794289429943094319432943394349435943694379438943994409441944294439444944594469447944894499450945194529453945494559456945794589459946094619462946394649465946694679468946994709471947294739474947594769477947894799480948194829483948494859486948794889489949094919492949394949495949694979498949995009501950295039504950595069507950895099510951195129513951495159516951795189519952095219522952395249525952695279528952995309531953295339534953595369537953895399540954195429543954495459546954795489549955095519552955395549555955695579558955995609561956295639564956595669567956895699570957195729573957495759576957795789579958095819582958395849585958695879588958995909591959295939594959595969597959895999600960196029603960496059606960796089609961096119612961396149615961696179618961996209621962296239624962596269627962896299630963196329633963496359636963796389639964096419642964396449645964696479648964996509651965296539654965596569657965896599660966196629663966496659666966796689669967096719672967396749675967696779678967996809681968296839684968596869687968896899690969196929693969496959696969796989699970097019702970397049705970697079708970997109711971297139714971597169717971897199720972197229723972497259726972797289729973097319732973397349735973697379738973997409741974297439744974597469747974897499750975197529753975497559756975797589759976097619762976397649765976697679768976997709771977297739774977597769777977897799780978197829783978497859786978797889789979097919792979397949795979697979798979998009801980298039804980598069807980898099810981198129813981498159816981798189819982098219822982398249825982698279828982998309831983298339834983598369837983898399840984198429843984498459846984798489849985098519852985398549855985698579858985998609861986298639864986598669867986898699870987198729873987498759876987798789879988098819882988398849885988698879888988998909891989298939894989598969897989898999900990199029903990499059906990799089909991099119912991399149915991699179918991999209921992299239924992599269927992899299930993199329933993499359936993799389939994099419942994399449945994699479948994999509951995299539954995599569957995899599960996199629963996499659966996799689969997099719972997399749975997699779978997999809981998299839984998599869987998899899990999199929993999499959996999799989999100001000110002100031000410005100061000710008100091001010011100121001310014100151001610017100181001910020100211002210023100241002510026100271002810029100301003110032100331003410035100361003710038100391004010041100421004310044100451004610047100481004910050100511005210053100541005510056100571005810059100601006110062100631006410065100661006710068100691007010071100721007310074100751007610077100781007910080100811008210083100841008510086100871008810089100901009110092100931009410095100961009710098100991010010101101021010310104101051010610107101081010910110101111011210113101141011510116101171011810119101201012110122101231012410125101261012710128101291013010131101321013310134101351013610137101381013910140101411014210143101441014510146101471014810149101501015110152101531015410155101561015710158101591016010161101621016310164101651016610167101681016910170101711017210173101741017510176101771017810179101801018110182101831018410185101861018710188101891019010191101921019310194101951019610197101981019910200102011020210203102041020510206102071020810209102101021110212102131021410215102161021710218102191022010221102221022310224102251022610227102281022910230102311023210233102341023510236102371023810239102401024110242102431024410245102461024710248102491025010251102521025310254102551025610257102581025910260102611026210263102641026510266102671026810269102701027110272102731027410275102761027710278102791028010281102821028310284102851028610287102881028910290102911029210293102941029510296102971029810299103001030110302103031030410305103061030710308103091031010311103121031310314103151031610317103181031910320103211032210323103241032510326103271032810329103301033110332103331033410335103361033710338103391034010341103421034310344103451034610347103481034910350103511035210353103541035510356103571035810359103601036110362103631036410365103661036710368103691037010371103721037310374103751037610377103781037910380103811038210383103841038510386103871038810389103901039110392103931039410395103961039710398103991040010401104021040310404104051040610407104081040910410104111041210413104141041510416104171041810419104201042110422104231042410425104261042710428104291043010431104321043310434104351043610437104381043910440104411044210443104441044510446104471044810449104501045110452104531045410455104561045710458104591046010461104621046310464104651046610467104681046910470104711047210473104741047510476104771047810479104801048110482104831048410485104861048710488104891049010491104921049310494104951049610497104981049910500105011050210503105041050510506105071050810509105101051110512105131051410515105161051710518105191052010521105221052310524105251052610527105281052910530105311053210533105341053510536105371053810539105401054110542105431054410545105461054710548105491055010551105521055310554105551055610557105581055910560105611056210563105641056510566105671056810569105701057110572105731057410575105761057710578105791058010581105821058310584105851058610587105881058910590105911059210593105941059510596105971059810599106001060110602106031060410605106061060710608106091061010611106121061310614106151061610617106181061910620106211062210623106241062510626106271062810629106301063110632106331063410635106361063710638106391064010641106421064310644106451064610647106481064910650106511065210653106541065510656106571065810659106601066110662106631066410665106661066710668106691067010671106721067310674106751067610677106781067910680106811068210683106841068510686106871068810689106901069110692106931069410695106961069710698106991070010701107021070310704107051070610707107081070910710107111071210713107141071510716107171071810719107201072110722107231072410725107261072710728107291073010731107321073310734107351073610737107381073910740107411074210743107441074510746107471074810749107501075110752107531075410755107561075710758107591076010761107621076310764107651076610767107681076910770107711077210773107741077510776107771077810779107801078110782107831078410785107861078710788107891079010791107921079310794107951079610797107981079910800108011080210803108041080510806108071080810809108101081110812108131081410815108161081710818108191082010821108221082310824108251082610827108281082910830108311083210833108341083510836108371083810839108401084110842108431084410845108461084710848108491085010851108521085310854108551085610857108581085910860108611086210863108641086510866108671086810869108701087110872108731087410875108761087710878108791088010881108821088310884108851088610887108881088910890108911089210893108941089510896108971089810899109001090110902109031090410905109061090710908109091091010911109121091310914109151091610917109181091910920109211092210923109241092510926109271092810929109301093110932109331093410935109361093710938109391094010941109421094310944109451094610947109481094910950109511095210953109541095510956109571095810959109601096110962109631096410965109661096710968109691097010971109721097310974109751097610977109781097910980109811098210983109841098510986109871098810989109901099110992109931099410995109961099710998109991100011001110021100311004110051100611007110081100911010110111101211013110141101511016110171101811019110201102111022110231102411025110261102711028110291103011031110321103311034110351103611037110381103911040110411104211043110441104511046110471104811049110501105111052110531105411055110561105711058110591106011061110621106311064110651106611067110681106911070110711107211073110741107511076110771107811079110801108111082110831108411085110861108711088110891109011091110921109311094110951109611097110981109911100111011110211103111041110511106111071110811109111101111111112111131111411115111161111711118111191112011121111221112311124111251112611127111281112911130111311113211133111341113511136111371113811139111401114111142111431114411145111461114711148111491115011151111521115311154111551115611157111581115911160111611116211163111641116511166111671116811169111701117111172111731117411175111761117711178111791118011181111821118311184111851118611187111881118911190111911119211193111941119511196111971119811199112001120111202112031120411205112061120711208112091121011211112121121311214112151121611217112181121911220112211122211223112241122511226112271122811229112301123111232112331123411235112361123711238112391124011241112421124311244112451124611247112481124911250112511125211253112541125511256112571125811259112601126111262112631126411265112661126711268112691127011271112721127311274112751127611277112781127911280112811128211283112841128511286112871128811289112901129111292112931129411295112961129711298112991130011301113021130311304113051130611307113081130911310113111131211313113141131511316113171131811319113201132111322113231132411325113261132711328113291133011331113321133311334113351133611337113381133911340113411134211343113441134511346113471134811349113501135111352113531135411355113561135711358113591136011361113621136311364113651136611367113681136911370113711137211373113741137511376113771137811379113801138111382113831138411385113861138711388113891139011391113921139311394113951139611397113981139911400114011140211403114041140511406114071140811409114101141111412114131141411415114161141711418114191142011421114221142311424114251142611427114281142911430114311143211433114341143511436114371143811439114401144111442114431144411445114461144711448114491145011451114521145311454114551145611457114581145911460114611146211463114641146511466114671146811469114701147111472114731147411475114761147711478114791148011481114821148311484114851148611487114881148911490114911149211493114941149511496114971149811499115001150111502115031150411505115061150711508115091151011511115121151311514115151151611517115181151911520115211152211523115241152511526115271152811529115301153111532115331153411535115361153711538115391154011541115421154311544115451154611547115481154911550115511155211553115541155511556115571155811559115601156111562115631156411565115661156711568115691157011571115721157311574115751157611577115781157911580115811158211583115841158511586115871158811589115901159111592115931159411595115961159711598115991160011601116021160311604116051160611607116081160911610116111161211613116141161511616116171161811619116201162111622116231162411625116261162711628116291163011631116321163311634116351163611637116381163911640116411164211643116441164511646116471164811649116501165111652116531165411655116561165711658116591166011661116621166311664116651166611667116681166911670116711167211673116741167511676116771167811679116801168111682116831168411685116861168711688116891169011691116921169311694116951169611697116981169911700117011170211703117041170511706117071170811709117101171111712117131171411715117161171711718117191172011721117221172311724117251172611727117281172911730117311173211733117341173511736117371173811739117401174111742117431174411745117461174711748117491175011751117521175311754117551175611757117581175911760117611176211763117641176511766117671176811769117701177111772117731177411775117761177711778117791178011781117821178311784117851178611787117881178911790117911179211793117941179511796117971179811799118001180111802118031180411805118061180711808118091181011811118121181311814118151181611817118181181911820118211182211823118241182511826118271182811829118301183111832118331183411835118361183711838118391184011841118421184311844118451184611847118481184911850118511185211853118541185511856118571185811859118601186111862118631186411865118661186711868118691187011871118721187311874118751187611877118781187911880118811188211883118841188511886118871188811889118901189111892118931189411895118961189711898118991190011901119021190311904119051190611907119081190911910119111191211913119141191511916119171191811919119201192111922119231192411925119261192711928119291193011931119321193311934119351193611937119381193911940119411194211943119441194511946119471194811949119501195111952119531195411955119561195711958119591196011961119621196311964119651196611967119681196911970119711197211973119741197511976119771197811979119801198111982119831198411985119861198711988119891199011991119921199311994119951199611997119981199912000120011200212003120041200512006120071200812009120101201112012120131201412015120161201712018120191202012021120221202312024120251202612027120281202912030120311203212033120341203512036120371203812039120401204112042120431204412045120461204712048120491205012051120521205312054120551205612057120581205912060120611206212063120641206512066120671206812069120701207112072120731207412075120761207712078120791208012081120821208312084120851208612087120881208912090120911209212093120941209512096120971209812099121001210112102121031210412105121061210712108121091211012111121121211312114121151211612117121181211912120121211212212123121241212512126121271212812129121301213112132121331213412135121361213712138121391214012141121421214312144121451214612147121481214912150121511215212153121541215512156121571215812159121601216112162121631216412165121661216712168121691217012171121721217312174121751217612177121781217912180121811218212183121841218512186121871218812189121901219112192121931219412195121961219712198121991220012201122021220312204122051220612207122081220912210122111221212213122141221512216122171221812219122201222112222122231222412225122261222712228122291223012231122321223312234122351223612237122381223912240122411224212243122441224512246122471224812249122501225112252122531225412255122561225712258122591226012261122621226312264122651226612267122681226912270122711227212273122741227512276122771227812279122801228112282122831228412285122861228712288122891229012291122921229312294122951229612297122981229912300123011230212303123041230512306123071230812309123101231112312123131231412315123161231712318123191232012321123221232312324123251232612327123281232912330123311233212333123341233512336123371233812339123401234112342123431234412345123461234712348123491235012351123521235312354123551235612357123581235912360123611236212363123641236512366123671236812369123701237112372123731237412375123761237712378123791238012381123821238312384123851238612387123881238912390123911239212393123941239512396123971239812399124001240112402124031240412405124061240712408124091241012411124121241312414124151241612417124181241912420124211242212423124241242512426124271242812429124301243112432124331243412435124361243712438124391244012441124421244312444124451244612447124481244912450124511245212453124541245512456124571245812459124601246112462124631246412465124661246712468124691247012471124721247312474124751247612477124781247912480124811248212483124841248512486124871248812489124901249112492124931249412495124961249712498124991250012501125021250312504125051250612507125081250912510125111251212513125141251512516125171251812519125201252112522125231252412525125261252712528125291253012531125321253312534125351253612537125381253912540125411254212543125441254512546125471254812549125501255112552125531255412555125561255712558125591256012561125621256312564125651256612567125681256912570125711257212573125741257512576125771257812579125801258112582125831258412585125861258712588125891259012591125921259312594125951259612597125981259912600126011260212603126041260512606126071260812609126101261112612126131261412615126161261712618126191262012621126221262312624126251262612627126281262912630126311263212633126341263512636126371263812639126401264112642126431264412645126461264712648126491265012651126521265312654126551265612657126581265912660126611266212663126641266512666126671266812669126701267112672126731267412675126761267712678126791268012681126821268312684126851268612687126881268912690126911269212693126941269512696126971269812699127001270112702127031270412705127061270712708127091271012711127121271312714127151271612717127181271912720127211272212723127241272512726127271272812729127301273112732127331273412735127361273712738127391274012741127421274312744127451274612747127481274912750127511275212753127541275512756127571275812759127601276112762127631276412765127661276712768127691277012771127721277312774127751277612777127781277912780127811278212783127841278512786127871278812789127901279112792127931279412795127961279712798127991280012801128021280312804128051280612807128081280912810128111281212813128141281512816128171281812819128201282112822128231282412825128261282712828128291283012831128321283312834128351283612837128381283912840128411284212843128441284512846128471284812849128501285112852128531285412855128561285712858128591286012861128621286312864128651286612867128681286912870128711287212873128741287512876128771287812879128801288112882128831288412885128861288712888128891289012891128921289312894128951289612897128981289912900129011290212903129041290512906129071290812909129101291112912129131291412915129161291712918129191292012921129221292312924129251292612927129281292912930129311293212933129341293512936129371293812939129401294112942129431294412945129461294712948129491295012951129521295312954129551295612957129581295912960129611296212963129641296512966129671296812969129701297112972129731297412975129761297712978129791298012981129821298312984129851298612987129881298912990129911299212993129941299512996129971299812999130001300113002130031300413005130061300713008130091301013011130121301313014130151301613017130181301913020130211302213023130241302513026130271302813029130301303113032130331303413035130361303713038130391304013041130421304313044130451304613047130481304913050130511305213053130541305513056130571305813059130601306113062130631306413065130661306713068130691307013071130721307313074130751307613077130781307913080130811308213083130841308513086130871308813089130901309113092130931309413095130961309713098130991310013101131021310313104131051310613107131081310913110131111311213113131141311513116131171311813119131201312113122131231312413125131261312713128131291313013131131321313313134131351313613137131381313913140131411314213143131441314513146131471314813149131501315113152131531315413155131561315713158131591316013161131621316313164131651316613167131681316913170131711317213173131741317513176131771317813179131801318113182131831318413185131861318713188131891319013191131921319313194131951319613197131981319913200132011320213203132041320513206132071320813209132101321113212132131321413215132161321713218132191322013221132221322313224132251322613227132281322913230132311323213233132341323513236132371323813239132401324113242132431324413245132461324713248132491325013251132521325313254132551325613257132581325913260132611326213263132641326513266132671326813269132701327113272132731327413275132761327713278132791328013281132821328313284132851328613287132881328913290132911329213293132941329513296132971329813299133001330113302133031330413305133061330713308133091331013311133121331313314133151331613317133181331913320133211332213323133241332513326133271332813329133301333113332133331333413335133361333713338133391334013341133421334313344133451334613347133481334913350133511335213353133541335513356133571335813359133601336113362133631336413365133661336713368133691337013371133721337313374133751337613377133781337913380133811338213383133841338513386133871338813389133901339113392133931339413395133961339713398133991340013401134021340313404134051340613407134081340913410134111341213413134141341513416134171341813419134201342113422134231342413425134261342713428134291343013431134321343313434134351343613437134381343913440134411344213443134441344513446134471344813449134501345113452134531345413455134561345713458134591346013461134621346313464134651346613467134681346913470134711347213473134741347513476134771347813479134801348113482134831348413485134861348713488134891349013491134921349313494134951349613497134981349913500135011350213503135041350513506135071350813509135101351113512135131351413515135161351713518135191352013521135221352313524135251352613527135281352913530135311353213533135341353513536135371353813539135401354113542135431354413545135461354713548135491355013551135521355313554135551355613557135581355913560135611356213563135641356513566135671356813569135701357113572135731357413575135761357713578135791358013581135821358313584135851358613587135881358913590135911359213593135941359513596135971359813599136001360113602136031360413605136061360713608136091361013611136121361313614136151361613617136181361913620136211362213623136241362513626136271362813629136301363113632136331363413635136361363713638136391364013641136421364313644136451364613647136481364913650136511365213653136541365513656136571365813659136601366113662136631366413665136661366713668136691367013671136721367313674136751367613677136781367913680136811368213683136841368513686136871368813689136901369113692136931369413695136961369713698136991370013701137021370313704137051370613707137081370913710137111371213713137141371513716137171371813719137201372113722137231372413725137261372713728137291373013731137321373313734137351373613737137381373913740137411374213743137441374513746137471374813749137501375113752137531375413755137561375713758137591376013761137621376313764137651376613767137681376913770137711377213773137741377513776137771377813779137801378113782137831378413785137861378713788137891379013791137921379313794137951379613797137981379913800138011380213803138041380513806138071380813809138101381113812138131381413815138161381713818138191382013821138221382313824138251382613827138281382913830138311383213833138341383513836138371383813839138401384113842138431384413845138461384713848138491385013851138521385313854138551385613857138581385913860138611386213863138641386513866138671386813869138701387113872138731387413875138761387713878138791388013881138821388313884138851388613887138881388913890138911389213893138941389513896138971389813899139001390113902139031390413905139061390713908139091391013911139121391313914139151391613917139181391913920139211392213923139241392513926139271392813929139301393113932139331393413935139361393713938139391394013941139421394313944139451394613947139481394913950139511395213953139541395513956139571395813959139601396113962139631396413965139661396713968139691397013971139721397313974139751397613977139781397913980139811398213983139841398513986139871398813989139901399113992139931399413995139961399713998139991400014001140021400314004140051400614007140081400914010140111401214013140141401514016140171401814019140201402114022140231402414025140261402714028140291403014031140321403314034140351403614037140381403914040140411404214043140441404514046140471404814049140501405114052140531405414055140561405714058140591406014061140621406314064140651406614067140681406914070140711407214073140741407514076140771407814079140801408114082140831408414085140861408714088140891409014091140921409314094140951409614097140981409914100141011410214103141041410514106141071410814109141101411114112141131411414115141161411714118141191412014121141221412314124141251412614127141281412914130141311413214133141341413514136141371413814139141401414114142141431414414145141461414714148141491415014151141521415314154141551415614157141581415914160141611416214163141641416514166141671416814169141701417114172141731417414175141761417714178141791418014181141821418314184141851418614187141881418914190141911419214193141941419514196141971419814199142001420114202142031420414205142061420714208142091421014211142121421314214142151421614217142181421914220142211422214223142241422514226142271422814229142301423114232142331423414235142361423714238142391424014241142421424314244142451424614247142481424914250142511425214253142541425514256142571425814259142601426114262142631426414265142661426714268142691427014271142721427314274142751427614277142781427914280142811428214283142841428514286142871428814289142901429114292142931429414295142961429714298142991430014301143021430314304143051430614307143081430914310143111431214313143141431514316143171431814319143201432114322143231432414325143261432714328143291433014331143321433314334143351433614337143381433914340143411434214343143441434514346143471434814349143501435114352143531435414355143561435714358143591436014361143621436314364143651436614367143681436914370143711437214373143741437514376143771437814379143801438114382143831438414385143861438714388143891439014391143921439314394143951439614397143981439914400144011440214403144041440514406144071440814409144101441114412144131441414415144161441714418144191442014421144221442314424144251442614427144281442914430144311443214433144341443514436144371443814439144401444114442144431444414445144461444714448144491445014451144521445314454144551445614457144581445914460144611446214463144641446514466144671446814469144701447114472144731447414475144761447714478144791448014481144821448314484144851448614487144881448914490144911449214493144941449514496144971449814499145001450114502145031450414505145061450714508145091451014511145121451314514145151451614517145181451914520145211452214523145241452514526145271452814529145301453114532145331453414535145361453714538145391454014541145421454314544145451454614547145481454914550145511455214553145541455514556145571455814559145601456114562145631456414565145661456714568145691457014571145721457314574145751457614577145781457914580145811458214583145841458514586145871458814589145901459114592145931459414595145961459714598145991460014601146021460314604146051460614607146081460914610146111461214613146141461514616146171461814619146201462114622146231462414625146261462714628146291463014631146321463314634146351463614637146381463914640146411464214643146441464514646146471464814649146501465114652146531465414655146561465714658146591466014661146621466314664146651466614667146681466914670146711467214673146741467514676146771467814679146801468114682146831468414685146861468714688146891469014691146921469314694146951469614697146981469914700147011470214703147041470514706147071470814709147101471114712147131471414715147161471714718147191472014721147221472314724147251472614727147281472914730147311473214733147341473514736147371473814739147401474114742147431474414745147461474714748147491475014751147521475314754147551475614757147581475914760147611476214763147641476514766147671476814769147701477114772147731477414775147761477714778147791478014781147821478314784147851478614787147881478914790147911479214793147941479514796147971479814799148001480114802148031480414805148061480714808148091481014811148121481314814148151481614817148181481914820148211482214823148241482514826148271482814829148301483114832148331483414835148361483714838148391484014841148421484314844148451484614847148481484914850148511485214853148541485514856148571485814859148601486114862148631486414865148661486714868148691487014871148721487314874148751487614877148781487914880148811488214883148841488514886148871488814889148901489114892148931489414895148961489714898148991490014901149021490314904149051490614907149081490914910149111491214913149141491514916149171491814919149201492114922149231492414925149261492714928149291493014931149321493314934149351493614937149381493914940149411494214943149441494514946149471494814949149501495114952149531495414955149561495714958149591496014961149621496314964149651496614967149681496914970149711497214973149741497514976149771497814979149801498114982149831498414985149861498714988149891499014991149921499314994149951499614997149981499915000150011500215003150041500515006150071500815009150101501115012150131501415015150161501715018150191502015021150221502315024150251502615027150281502915030150311503215033150341503515036150371503815039150401504115042150431504415045150461504715048150491505015051150521505315054150551505615057150581505915060150611506215063150641506515066150671506815069150701507115072150731507415075150761507715078150791508015081150821508315084150851508615087150881508915090150911509215093150941509515096150971509815099151001510115102151031510415105151061510715108151091511015111151121511315114151151511615117151181511915120151211512215123151241512515126151271512815129151301513115132151331513415135151361513715138151391514015141151421514315144151451514615147151481514915150151511515215153151541515515156151571515815159151601516115162151631516415165151661516715168151691517015171151721517315174151751517615177151781517915180151811518215183151841518515186151871518815189151901519115192151931519415195151961519715198151991520015201152021520315204152051520615207152081520915210152111521215213152141521515216152171521815219152201522115222152231522415225152261522715228152291523015231152321523315234152351523615237152381523915240152411524215243152441524515246152471524815249152501525115252152531525415255152561525715258152591526015261152621526315264152651526615267152681526915270152711527215273152741527515276152771527815279152801528115282152831528415285152861528715288152891529015291152921529315294152951529615297152981529915300153011530215303153041530515306153071530815309153101531115312153131531415315153161531715318153191532015321153221532315324153251532615327153281532915330153311533215333153341533515336153371533815339153401534115342153431534415345153461534715348153491535015351153521535315354153551535615357153581535915360153611536215363153641536515366153671536815369153701537115372153731537415375153761537715378153791538015381153821538315384153851538615387153881538915390153911539215393153941539515396153971539815399154001540115402154031540415405154061540715408154091541015411154121541315414154151541615417154181541915420154211542215423154241542515426154271542815429154301543115432154331543415435154361543715438154391544015441154421544315444154451544615447154481544915450154511545215453154541545515456154571545815459154601546115462154631546415465154661546715468154691547015471154721547315474154751547615477154781547915480154811548215483154841548515486154871548815489154901549115492154931549415495154961549715498154991550015501155021550315504155051550615507155081550915510155111551215513155141551515516155171551815519155201552115522155231552415525155261552715528155291553015531155321553315534155351553615537155381553915540155411554215543155441554515546155471554815549155501555115552155531555415555155561555715558155591556015561155621556315564155651556615567155681556915570155711557215573155741557515576155771557815579155801558115582155831558415585155861558715588155891559015591155921559315594155951559615597155981559915600156011560215603156041560515606156071560815609156101561115612156131561415615156161561715618156191562015621156221562315624156251562615627156281562915630156311563215633156341563515636156371563815639156401564115642156431564415645156461564715648156491565015651156521565315654156551565615657156581565915660156611566215663156641566515666156671566815669156701567115672156731567415675156761567715678156791568015681156821568315684156851568615687156881568915690156911569215693156941569515696156971569815699157001570115702157031570415705157061570715708157091571015711157121571315714157151571615717157181571915720157211572215723157241572515726157271572815729157301573115732157331573415735157361573715738157391574015741157421574315744157451574615747157481574915750157511575215753157541575515756157571575815759157601576115762157631576415765157661576715768157691577015771157721577315774157751577615777157781577915780157811578215783157841578515786157871578815789157901579115792157931579415795157961579715798157991580015801158021580315804158051580615807158081580915810158111581215813158141581515816158171581815819158201582115822158231582415825158261582715828158291583015831158321583315834158351583615837158381583915840158411584215843158441584515846158471584815849158501585115852158531585415855158561585715858158591586015861158621586315864158651586615867158681586915870158711587215873158741587515876158771587815879158801588115882158831588415885158861588715888158891589015891158921589315894158951589615897158981589915900159011590215903159041590515906159071590815909159101591115912159131591415915159161591715918159191592015921159221592315924159251592615927159281592915930159311593215933159341593515936159371593815939159401594115942159431594415945159461594715948159491595015951159521595315954159551595615957159581595915960159611596215963159641596515966159671596815969159701597115972159731597415975159761597715978159791598015981159821598315984159851598615987159881598915990159911599215993159941599515996159971599815999160001600116002160031600416005160061600716008160091601016011160121601316014160151601616017160181601916020160211602216023160241602516026160271602816029160301603116032160331603416035160361603716038160391604016041160421604316044160451604616047160481604916050160511605216053160541605516056160571605816059160601606116062160631606416065160661606716068160691607016071160721607316074160751607616077160781607916080160811608216083160841608516086160871608816089160901609116092160931609416095160961609716098160991610016101161021610316104161051610616107161081610916110161111611216113161141611516116161171611816119161201612116122161231612416125161261612716128161291613016131161321613316134161351613616137161381613916140161411614216143161441614516146161471614816149161501615116152161531615416155161561615716158161591616016161161621616316164161651616616167161681616916170161711617216173161741617516176161771617816179161801618116182161831618416185161861618716188161891619016191161921619316194161951619616197161981619916200162011620216203162041620516206162071620816209162101621116212162131621416215162161621716218162191622016221162221622316224162251622616227162281622916230162311623216233162341623516236162371623816239162401624116242162431624416245162461624716248162491625016251162521625316254162551625616257162581625916260162611626216263162641626516266162671626816269162701627116272162731627416275162761627716278162791628016281162821628316284162851628616287162881628916290162911629216293162941629516296162971629816299163001630116302163031630416305163061630716308163091631016311163121631316314163151631616317163181631916320163211632216323163241632516326163271632816329163301633116332163331633416335163361633716338163391634016341163421634316344163451634616347163481634916350163511635216353163541635516356163571635816359163601636116362163631636416365163661636716368163691637016371163721637316374163751637616377163781637916380163811638216383163841638516386163871638816389163901639116392163931639416395163961639716398163991640016401164021640316404164051640616407164081640916410164111641216413164141641516416164171641816419164201642116422164231642416425164261642716428164291643016431164321643316434164351643616437164381643916440164411644216443164441644516446164471644816449164501645116452164531645416455164561645716458164591646016461164621646316464164651646616467164681646916470164711647216473164741647516476164771647816479164801648116482164831648416485164861648716488164891649016491164921649316494164951649616497164981649916500165011650216503165041650516506165071650816509165101651116512165131651416515165161651716518165191652016521165221652316524165251652616527165281652916530165311653216533165341653516536165371653816539165401654116542165431654416545165461654716548165491655016551165521655316554165551655616557165581655916560165611656216563165641656516566165671656816569165701657116572165731657416575165761657716578165791658016581165821658316584165851658616587165881658916590165911659216593165941659516596165971659816599166001660116602166031660416605166061660716608166091661016611166121661316614166151661616617166181661916620166211662216623166241662516626166271662816629166301663116632166331663416635166361663716638166391664016641166421664316644166451664616647166481664916650166511665216653166541665516656166571665816659166601666116662166631666416665166661666716668166691667016671166721667316674166751667616677166781667916680166811668216683166841668516686166871668816689166901669116692166931669416695166961669716698166991670016701167021670316704167051670616707167081670916710167111671216713167141671516716167171671816719167201672116722167231672416725167261672716728167291673016731167321673316734167351673616737167381673916740167411674216743167441674516746167471674816749167501675116752167531675416755167561675716758167591676016761167621676316764167651676616767167681676916770167711677216773167741677516776167771677816779167801678116782167831678416785167861678716788167891679016791167921679316794167951679616797167981679916800168011680216803168041680516806168071680816809168101681116812168131681416815168161681716818168191682016821168221682316824168251682616827168281682916830168311683216833168341683516836168371683816839168401684116842168431684416845168461684716848168491685016851168521685316854168551685616857168581685916860168611686216863168641686516866168671686816869168701687116872168731687416875168761687716878168791688016881168821688316884168851688616887168881688916890168911689216893168941689516896168971689816899169001690116902169031690416905169061690716908169091691016911169121691316914169151691616917169181691916920169211692216923169241692516926169271692816929169301693116932169331693416935169361693716938169391694016941169421694316944169451694616947169481694916950169511695216953169541695516956169571695816959169601696116962169631696416965169661696716968169691697016971169721697316974169751697616977169781697916980169811698216983169841698516986169871698816989169901699116992169931699416995169961699716998169991700017001170021700317004170051700617007170081700917010170111701217013170141701517016170171701817019170201702117022170231702417025170261702717028170291703017031170321703317034170351703617037170381703917040170411704217043170441704517046170471704817049170501705117052170531705417055170561705717058170591706017061170621706317064170651706617067170681706917070170711707217073170741707517076170771707817079170801708117082170831708417085170861708717088170891709017091170921709317094170951709617097170981709917100171011710217103171041710517106171071710817109171101711117112171131711417115171161711717118171191712017121171221712317124171251712617127171281712917130171311713217133171341713517136171371713817139171401714117142171431714417145171461714717148171491715017151171521715317154171551715617157171581715917160171611716217163171641716517166171671716817169171701717117172171731717417175171761717717178171791718017181171821718317184171851718617187171881718917190171911719217193171941719517196171971719817199172001720117202172031720417205172061720717208172091721017211172121721317214172151721617217172181721917220172211722217223172241722517226172271722817229172301723117232172331723417235172361723717238172391724017241172421724317244172451724617247172481724917250172511725217253172541725517256172571725817259172601726117262172631726417265172661726717268172691727017271172721727317274172751727617277172781727917280172811728217283172841728517286172871728817289172901729117292172931729417295172961729717298172991730017301173021730317304173051730617307173081730917310173111731217313173141731517316173171731817319173201732117322173231732417325173261732717328173291733017331173321733317334173351733617337173381733917340173411734217343173441734517346173471734817349173501735117352173531735417355173561735717358173591736017361173621736317364173651736617367173681736917370173711737217373173741737517376173771737817379173801738117382173831738417385173861738717388173891739017391173921739317394173951739617397173981739917400174011740217403174041740517406174071740817409174101741117412174131741417415174161741717418174191742017421174221742317424174251742617427174281742917430174311743217433174341743517436174371743817439174401744117442174431744417445174461744717448174491745017451174521745317454174551745617457174581745917460174611746217463174641746517466174671746817469174701747117472174731747417475174761747717478174791748017481174821748317484174851748617487174881748917490174911749217493174941749517496174971749817499175001750117502175031750417505175061750717508175091751017511175121751317514175151751617517175181751917520175211752217523175241752517526175271752817529175301753117532175331753417535175361753717538175391754017541175421754317544175451754617547175481754917550175511755217553175541755517556175571755817559175601756117562175631756417565175661756717568175691757017571175721757317574175751757617577175781757917580175811758217583175841758517586175871758817589175901759117592175931759417595175961759717598175991760017601176021760317604176051760617607176081760917610176111761217613176141761517616176171761817619176201762117622176231762417625176261762717628176291763017631import { createPreviewEvidence } from "./preview-evidence.mjs";import { createPlaybackClock } from "./playback-clock.mjs";// Manages a piece and the transitions between pieces like a// hypervisor or shell.
/* #region 🏁 todo#endregion */
// 📊 Worker timing telemetry - stores timing data to be sent to main threadconst diskTimings = { sessionStarted: null, preambleComplete: null, loadStarted: null, fetchComplete: null, compileComplete: null, bootStarted: null, bootComplete: null, firstPaint: null, firstRenderSent: null,};const diskTimingStart = performance.now();
// 🔬 Module load timingconst _importStart = performance.now();
import * as quat from "../dep/gl-matrix/quat.mjs";import * as mat3 from "../dep/gl-matrix/mat3.mjs";import * as mat4 from "../dep/gl-matrix/mat4.mjs";import * as vec2 from "../dep/gl-matrix/vec2.mjs";import * as vec3 from "../dep/gl-matrix/vec3.mjs";import * as vec4 from "../dep/gl-matrix/vec4.mjs";
import * as graph from "./graph.mjs";import { setAskTokenProvider } from "./ask.mjs";import { splitColorCodes, stripColorCodes as stripCodes, hasColorCodes, mapColorCodes,} from "./color-codes.mjs";
import * as num from "./num.mjs";import * as text from "./text.mjs";import * as geo from "./geo.mjs";import * as gizmo from "./gizmo.mjs";import * as ui from "./ui.mjs";import * as help from "./help.mjs";import * as platform from "./platform.mjs";
import { signed as shop } from "./shop.mjs";import { parse, metadata, inferTitleDesc, updateCode } from "./parse.mjs";import { Socket } from "./socket.mjs"; // TODO: Eventually expand to `net.Socket`import { Diagnostics } from "./diagnostics.mjs"; // Reports a piece's health to its author.import { Chat } from "./chat.mjs"; // TODO: Eventually expand to `net.Socket`import { notArray, defaultTemplateStringProcessor, uint8ArrayToBase64, base64ToUint8Array,} from "./helpers.mjs";import { createRestrictedApi, shouldRestrictPiece } from "./restricted-api.mjs";import { clearPermissions, resolvePermissionRequest } from "./piece-permissions.mjs";const { pow, abs, round, sin, random, min, max, floor, cos } = Math;const { keys } = Object;
import { nopaint_boot, nopaint_act, nopaint_is, nopaint_cancelStroke, nopaint_renderPerfHUD, nopaint_triggerBakeFlash } from "../systems/nopaint.mjs";import { getPreserveFadeAlpha, setPreserveFadeAlpha } from "./fade-state.mjs";import * as prompt from "../systems/prompt-system.mjs";import * as world from "../systems/world.mjs";import { headers } from "./headers.mjs";import { logs, log } from "./logs.mjs";import { soundWhitelist } from "./sound/sound-whitelist.mjs";
import { CamDoll } from "./cam-doll.mjs";
import { TextFields, TextInput, Typeface } from "../lib/type.mjs";
import * as lisp from "./kidlisp.mjs";import { isKidlispSource, fetchCachedCode, fetchKidlispMetadata, getCachedCode, initPersistentCache, getCachedCodeMultiLevel, saveCodeToAllCaches, enableKidlispConsole, enableKidlispTrace, disableKidlispTrace, clearExecutionTrace, postExecutionTrace } from "./kidlisp.mjs"; // Add lisp evaluator.import { makeP5WorkerModule } from "./p5-worker.mjs"; // .js pieces → real p5 in this worker (Option B).import * as l5 from "./l5.mjs";
import { qrcode as qr, ErrorCorrectLevel } from "../dep/@akamfoad/qr/qr.mjs";import { microtype, MatrixChunky8 } from "../disks/common/fonts.mjs";import { calculateVHSEffects, getLeaderPixelColor, blendColorWithVHS} from "../disks/common/tape-player.mjs";
const _importEnd = performance.now();// Only log if imports took more than 50ms (slow network or first load)if (_importEnd - _importStart > 50) { console.log(`📦 [DISK] Imports: ${(_importEnd - _importStart).toFixed(0)}ms`);}
function matrixDebugEnabled() { if (typeof window !== "undefined" && window?.acMatrixDebug) return true; if (typeof globalThis !== "undefined" && globalThis?.acMatrixDebug) return true; return false;}
function inkFloodLoggingEnabled() { if (typeof globalThis !== "undefined" && globalThis.AC_LOG_INK_COLORS) return true; if (typeof process !== "undefined" && process.env?.AC_LOG_INK_COLORS === "1") return true; return false;}
function inkFloodLogPrefix() { let label = null; if (typeof process !== "undefined" && process.env?.AC_LOG_INK_LABEL) { label = process.env.AC_LOG_INK_LABEL; } else if (typeof globalThis !== "undefined" && globalThis.AC_LOG_INK_LABEL) { label = globalThis.AC_LOG_INK_LABEL; } return label ? `[${label}] ` : "";}
function cloneColorForLog(color) { if (Array.isArray(color)) return Array.from(color); return color;}
function cloneArgsForLog(args) { return Array.from(args).map((arg) => { if (Array.isArray(arg)) return Array.from(arg); if (arg && typeof arg === "object") { try { return JSON.parse(JSON.stringify(arg)); } catch (err) { return String(arg); } } return arg; });}
if (typeof globalThis !== "undefined") { if (globalThis.acMatrixDebug === undefined) { globalThis.acMatrixDebug = true; } if (typeof window !== "undefined" && window.acMatrixDebug === undefined) { window.acMatrixDebug = globalThis.acMatrixDebug; }
// 🖼️ Expose image cache utilities for debugging if (typeof window !== "undefined") { window.acClearImageCache = () => { imageCache.clear(); console.log("🗑️ Image cache cleared"); }; window.acImageCacheStats = () => { console.log("🖼️ Image cache stats:", { memorySize: imageCache.memory.size, urls: Array.from(imageCache.memory.keys()) }); }; }}
// Fetch piece metadata (trust level, author, etc.) from APIasync function fetchPieceMetadata(pieceCode) { try { const { protocol, hostname } = getSafeUrlParts(); const apiUrl = `${protocol}//${hostname}/api/piece-metadata?code=${encodeURIComponent(pieceCode)}`;
const response = await fetch(apiUrl); if (!response.ok) { console.warn(`⚠️ Failed to fetch metadata for piece: ${pieceCode}`); return { code: pieceCode, trustLevel: "untrusted", anonymous: true }; }
const metadata = await response.json(); return metadata; } catch (error) { console.warn(`⚠️ Error fetching piece metadata:`, error); // Default to untrusted on error return { code: pieceCode, trustLevel: "untrusted", anonymous: true }; }}
// Helper function to safely check for sandboxed environments in both main thread and worker contextsfunction isSandboxed() { try { // Check for spider mode first if (typeof window !== "undefined" && window.acSPIDER) { return true; // Spider mode should behave like sandboxed for URL construction }
// In workers, window is undefined but we can still check self.origin or location if (typeof window !== "undefined") { return window.origin === "null"; } else if (typeof self !== "undefined" && self.origin) { return self.origin === "null"; } else if (typeof location !== "undefined" && location.origin) { return location.origin === "null"; } else { return false; // Default to not sandboxed if we can't determine } } catch (err) { return false; // Default to not sandboxed if there's an error }}
// Helper function to get safe protocol and hostname for URL constructionfunction getSafeUrlParts() { try { const sandboxed = isSandboxed();
if (sandboxed) { return { protocol: "https:", hostname: "aesthetic.computer", host: "aesthetic.computer" }; } else { // Try to get location info from various contexts let loc = null; if (typeof location !== "undefined") { loc = location; } else if (typeof self !== "undefined" && self.location) { loc = self.location; } else if (typeof window !== "undefined" && window.location) { loc = window.location; }
if (loc) { return { protocol: loc.protocol, hostname: loc.hostname || loc.host, // `host` keeps the port (localhost:8888) — hostname drops it, // which breaks root-relative preloads on port-carrying dev hosts. host: loc.host || loc.hostname }; } else { // Fallback if no location available return { protocol: "https:", hostname: "aesthetic.computer", host: "aesthetic.computer" }; } } } catch (err) { // Fallback to defaults if there's any error return { protocol: "https:", hostname: "aesthetic.computer", host: "aesthetic.computer" }; }}
const SAME_ORIGIN_BUILT_IN_PIECE_HOSTS = new Set([ "aesthetic.computer", "www.aesthetic.computer", "kidlisp.com", "www.kidlisp.com", "notepat.com", "www.notepat.com", "nopaint.art", "www.nopaint.art", "oskiewar.com", "www.oskiewar.com", "laklok.com", "www.laklok.com", "p5.aesthetic.computer", "sitemap.aesthetic.computer",]);
function isLocalDevelopmentHost(hostname) { return ( hostname === "localhost" || hostname === "127.0.0.1" || /^192\.168\./.test(hostname) || /^10\./.test(hostname) || /^172\.(1[6-9]|2[0-9]|3[01])\./.test(hostname) );}
function getBuiltInPieceBaseUrl() { const { protocol, hostname } = getSafeUrlParts();
if (typeof window !== "undefined" && window.acSPIDER) { return "https://aesthetic.computer"; }
if ( isLocalDevelopmentHost(hostname) && typeof location !== "undefined" && location.port ) { return `${protocol}//${hostname}:${location.port}`; }
if (SAME_ORIGIN_BUILT_IN_PIECE_HOSTS.has(hostname)) { return `${protocol}//${hostname}`; }
return "https://aesthetic.computer";}
let tf; // Active typeface global.
// Cache for loaded typefaces to avoid recreating themconst typefaceCache = new Map();
// 🔤 Dev helper: Clear all font caches (call from console: clearFontCaches())// This clears typeface instances, advance caches, and forces fonts.mjs to reloadasync function clearFontCaches() { console.log("🔤 Clearing font caches...");
// 1. Clear typeface instance cache const clearedTypefaces = typefaceCache.size; for (const [name, typeface] of typefaceCache) { // Clear the advance cache on each typeface if (typeface.advanceCache) { typeface.advanceCache.clear(); } } typefaceCache.clear();
// 2. Clear IndexedDB glyph cache try { const { clearGlyphCache } = await import("./type.mjs"); if (clearGlyphCache) { await clearGlyphCache(); console.log("🔤 IndexedDB glyph cache cleared"); } } catch (e) { console.log("🔤 IndexedDB cache clear skipped:", e.message); }
// 3. Force reimport of fonts.mjs by cache-busting try { const timestamp = Date.now(); const fontsUrl = `../disks/common/fonts.mjs?t=${timestamp}`; const fontsModule = await import(fontsUrl); console.log("🔤 fonts.mjs reloaded, MatrixChunky8 * advance:", fontsModule.MatrixChunky8?.advances?.['*']); } catch (e) { console.log("🔤 fonts.mjs reload skipped:", e.message); }
console.log(`🔤 Cleared ${clearedTypefaces} typeface(s). Refresh page or load a piece to see changes.`); return true;}
// Expose to window for dev console accessif (typeof window !== "undefined") { window.clearFontCaches = clearFontCaches;
// 🔤 Auto-clear font caches in dev mode on page load const isDevMode = location.host === "localhost:8888" || location.host === "aesthetic.local:8888" || location.host === "local.aesthetic.computer"; if (isDevMode) { // Clear IndexedDB glyph cache on dev mode page loads (async () => { try { const { clearGlyphCache } = await import("./type.mjs"); if (clearGlyphCache) { await clearGlyphCache(); console.log("🔤 Dev mode: IndexedDB glyph cache auto-cleared"); } } catch (e) { // Silently ignore if not available yet } })(); }}
const DEFAULT_TYPEFACE_BLOCK_WIDTH = 6;const DEFAULT_TYPEFACE_BLOCK_HEIGHT = 10;const HUD_LABEL_TEXT_MARGIN = 0;const KIDLISP_HUD_WRAP_CHARACTER_LIMIT = 28;const MIN_HUD_WRAP_WIDTH = 120;
// ✨ Corner label hover drift. Each character wanders on its own soft spring —// a slow sideways sway plus an upward hop — so the word reads as alive and// pressable while staying legible. Amplitudes are px, speeds are rad/ms// (periods of 1.5–3s), and the whole thing scales by an eased 0..1 amount so// it wakes up and settles instead of snapping. The hit box never moves.const HUD_DRIFT_X = 1.5;const HUD_DRIFT_Y = 2;const HUD_DRIFT_EASE_IN_MS = 220;const HUD_DRIFT_EASE_OUT_MS = 380;// Golden-ratio scattered phases, so neighbouring letters never move in step.const HUD_DRIFT_PHASES = Float32Array.from( { length: 64 }, (_, i) => ((i * 0.6180339887) % 1) * Math.PI * 2,);
function hudDriftEase(amount) { return amount * amount * (3 - 2 * amount); // smoothstep}
function hudDriftX(i, t, ease) { const p = HUD_DRIFT_PHASES[i & 63]; return ( HUD_DRIFT_X * ease * (0.65 * Math.sin(t * 0.0021 + p) + 0.35 * Math.sin(t * 0.0037 + p * 1.7)) );}
function hudDriftY(i, t, ease) { const p = HUD_DRIFT_PHASES[(i + 17) & 63]; // |sin| hops: sharp at the floor, soft at the top — the bounce. return -HUD_DRIFT_Y * ease * Math.abs(Math.sin(t * 0.0028 + p));}
// Resolve a `\code\` (or a shadow's "r,g,b") the way `write` does; `reset`// falls back to the label's base color.function hudCodeColor(code, base) { if (!code) return base; if (Array.isArray(code)) return code; const s = code.trim(); const lower = s.toLowerCase(); if (lower === "reset" || lower === "default" || lower === "base") return base; if (s.includes(",")) { const parts = s.split(",").map((n) => parseInt(n.trim(), 10) || 0); while (parts.length < 3) parts.push(0); return parts.slice(0, 4); } return graph.findColor(s) ?? base;}
function resolveTypefaceInstance(typefaceRef) { if (!typefaceRef) return undefined; if (typefaceRef instanceof Typeface) return typefaceRef; if (typeof typefaceRef === "string") { if (typefaceCache.has(typefaceRef)) { return typefaceCache.get(typefaceRef); } const instance = new Typeface(typefaceRef); typefaceCache.set(typefaceRef, instance); return instance; } if (typeof typefaceRef === "object" && typeof typefaceRef?.getGlyph === "function") { return typefaceRef; } return undefined;}
function ensureTypefaceLoaded(typeface) { if (!typeface || typeof typeface.load !== "function") return; const requiresLoad = typeface.data?.bdfFont || typeface.name === "unifont" || typeface.name === "MatrixChunky8";
if (!requiresLoad) return;
if (!typeface.__loadPromise) { typeface.__loadPromise = typeface.load($commonApi.net.preload, () => { if (typeof window !== "undefined" && window.$activePaintApi?.needsPaint) { window.$activePaintApi.needsPaint(); } }); }
return typeface.__loadPromise;}
function getTypefaceForMeasurement(typefaceName) { if (!typefaceName) return tf; const resolved = resolveTypefaceInstance(typefaceName); return resolved || tf;}
// Flag to skip $ replacement during shadow renderinglet _isRenderingShadow = false;
function writeHudLabelText( $, text, { x = 0, y = 0, typefaceName, preserveColors = true, bounds, wordWrap, } = {},) { if (text === undefined || text === null || text === "") return;
const content = preserveColors ? text : stripColorCodes(text); const effectiveBounds = typeof bounds === "number" && bounds > 0 ? bounds : undefined; const shouldWrap = wordWrap === undefined ? effectiveBounds !== undefined : wordWrap;
$.write( content, { x, y }, undefined, effectiveBounds, shouldWrap, typefaceName, );
return $;}
// Helper function to determine if a color is dark (needs light shadow) or light (needs dark shadow)function isColorDark(colorStr) {
let rgb;
// Handle RGB comma-separated format like "0,0,0" or "255,0,0" if (typeof colorStr === 'string' && colorStr.includes(',')) { const parts = colorStr.split(',').map(n => parseInt(n.trim(), 10)); rgb = parts; } // Handle named colors else if (typeof colorStr === 'string') { const lower = colorStr.toLowerCase(); const resolved = graph.findColor(lower); if (Array.isArray(resolved)) { rgb = resolved; } else { // For unknown colors, assume they're light return false; } } // Handle array format [r, g, b] else if (Array.isArray(colorStr)) { rgb = colorStr; }
if (!rgb || rgb.length < 3) return false;
// Calculate relative luminance using the formula from WCAG // https://www.w3.org/TR/WCAG20/#relativeluminancedef const [r, g, b] = rgb; const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
// If luminance is less than 0.5, it's a dark color return luminance < 0.5;}
// Helper function to get shadow color for a specific text colorconst BRIGHT_SHADOW_RGB = "220,220,220";const DARK_SHADOW_RGB = "30,20,50"; // Dark purple-blue shadow for light mode
function getShadowColorForText(colorStr) { if (!colorStr) return "64,64,64"; // Default to dark gray shadow
// Check if we're in light mode (need darker shadows for bright colors) const isLightMode = $commonApi && !$commonApi.dark;
let rgb; let normalizedCommand = null; if (typeof colorStr === "string") { normalizedCommand = colorStr.trim().toLowerCase(); // Treat complex command-based color strings (fade, scroll, random, etc.) as needing appropriate shadows if (/[:(]/.test(normalizedCommand) || normalizedCommand.includes("?")) { return isLightMode ? DARK_SHADOW_RGB : BRIGHT_SHADOW_RGB; } }
// Parse the color string to get RGB values if (typeof colorStr === 'string' && colorStr.includes(',')) { const parts = colorStr.split(',').map(n => parseInt(n.trim(), 10)); rgb = parts; } else if (typeof colorStr === 'string') { const lower = colorStr.toLowerCase(); const resolved = graph.findColor(lower); if (Array.isArray(resolved)) { rgb = resolved; } else { // Fallback to dark gray for unknown colors return "64,64,64"; } } else if (Array.isArray(colorStr)) { rgb = colorStr; }
if (!rgb || rgb.length < 3) return "64,64,64";
const [r, g, b] = rgb;
// Handle very dark colors (near black) before channel-specific logic if (r <= 24 && g <= 24 && b <= 24) { return BRIGHT_SHADOW_RGB; }
// Calculate luminance to check if color is bright const luminance = (0.299 * r + 0.587 * g + 0.114 * b);
// 🌞 LIGHT MODE: For bright colors on light background, use strong dark shadows if (isLightMode && luminance > 140) { // Use a consistent dark shadow for all bright colors in light mode return DARK_SHADOW_RGB; }
// 🎨 Special case: Detect RGB channel colors and return channel-tinted shadows // Red channel: R value, zero G and B if (r >= 0 && g === 0 && b === 0) { // Red channel shadow - darker red, stronger and more saturated const shadowR = Math.max(32, Math.round(r * 0.4)); // Keep at least 40% intensity return `${shadowR},0,0`; // Pure red shadow }
// Green channel: zero R, G value, zero B if (r === 0 && g >= 0 && b === 0) { // Green channel shadow - darker green const shadowG = Math.max(32, Math.round(g * 0.4)); return `0,${shadowG},0`; // Pure green shadow }
// Blue channel: zero R, G value (75% of B), B value (deepskyblue pattern: 0, 191, 255) // Detects pattern where g = b * 0.75 (within rounding tolerance) if (r === 0 && b >= 0 && g >= 0) { const expectedG = Math.round(b * 0.75); // Allow 1 pixel tolerance for rounding if (Math.abs(g - expectedG) <= 1) { // Blue/deepskyblue channel shadow - darker blue with cyan tint const shadowG = Math.max(24, Math.round(g * 0.4)); const shadowB = Math.max(32, Math.round(b * 0.4)); return `0,${shadowG},${shadowB}`; // Cyan-blue shadow } }
// Check if the color is dark - if so, lighten it for shadow if (isColorDark(colorStr)) { // Render near-white shadows under very dark text for readability if (r <= 24 && g <= 24 && b <= 24) { return "255,255,255"; }
// Lighten the color by mixing with white (stronger mix for dark colors) const factor = 0.85; // Mix 85% with white for a much lighter shadow const shadowR = Math.round(r + (255 - r) * factor); const shadowG = Math.round(g + (255 - g) * factor); const shadowB = Math.round(b + (255 - b) * factor); return `${shadowR},${shadowG},${shadowB}`; }
// For light colors, darken by mixing with dark gray const factor = 0.6; // Mix 60% darker for shadow const shadowR = Math.round(r * (1 - factor)); const shadowG = Math.round(g * (1 - factor)); const shadowB = Math.round(b * (1 - factor)); return `${shadowR},${shadowG},${shadowB}`;}
// Helper function to replace color codes in text with shadow colorsfunction replaceColorCodesWithShadows(text, defaultTextColor = "white") { if (!text || !textContainsColorCodes(text)) return text;
let currentTextColor = defaultTextColor;
// Rewrite every code, leave the text (and its escapes) alone. return mapColorCodes(text, (colorStr) => { if (!colorStr) return colorStr;
const normalized = colorStr.trim(); const lower = normalized.toLowerCase();
if (lower === "reset" || lower === "default" || lower === "base") { currentTextColor = defaultTextColor; } else { currentTextColor = normalized; }
// Get the appropriate shadow color for this text color. // mapColorCodes re-wraps this in the `\…\` delimiters itself. return getShadowColorForText(currentTextColor); });}
function drawHudLabelText( $, text, { x = 0, y = 0, typefaceName, textColor = "white", shadowColor, shadowOffsetX = 1, shadowOffsetY = 1, preserveColors = true, bounds, wordWrap, } = {},) { if (!text) return;
const containsColorCodes = textContainsColorCodes(text); const shouldPreserveColors = preserveColors || ((typefaceName === "MatrixChunky8" || typefaceName === "unifont") && containsColorCodes);
const effectiveBounds = typeof bounds === "number" && bounds > 0 ? bounds : undefined; const shouldWrap = wordWrap === undefined ? effectiveBounds !== undefined : wordWrap;
let effectiveShadowColor = shadowColor; if (!effectiveShadowColor && textColor) { effectiveShadowColor = getShadowColorForText(textColor); } if (!effectiveShadowColor) { effectiveShadowColor = "black"; }
const shouldRenderShadow = effectiveShadowColor && !( (typefaceName === "MatrixChunky8" || typefaceName === "unifont") && matrixDebugEnabled() );
if (shouldRenderShadow) { _isRenderingShadow = true; // If the text has color codes and we're preserving colors, use dynamic shadows if (shouldPreserveColors && containsColorCodes) { // Replace color codes with appropriate shadow colors const shadowText = replaceColorCodesWithShadows(text, textColor);
writeHudLabelText($, shadowText, { x: x + shadowOffsetX, y: y + shadowOffsetY, typefaceName, preserveColors: true, // Keep the shadow color codes bounds: effectiveBounds, wordWrap: shouldWrap, }); } else { // Original behavior: single shadow color for entire text $.ink(effectiveShadowColor); writeHudLabelText($, text, { x: x + shadowOffsetX, y: y + shadowOffsetY, typefaceName, preserveColors: false, bounds: effectiveBounds, wordWrap: shouldWrap, }); } _isRenderingShadow = false; }
if (textColor) { $.ink(textColor); }
const content = shouldPreserveColors ? text : stripColorCodes(text); $.write( content, { x, y }, undefined, effectiveBounds, shouldWrap, typefaceName, );}
// Hover twin of drawHudLabelText: one `write` per character so each can sit// on its own drift offset. Two passes — all shadows first, then all glyphs —// so no letter's shadow lands on a drifted neighbour. Color codes are walked// with splitColorCodes, and `\n` starts a new line; wrapped text doesn't come// here. `i` counts drawn characters and picks the phase.function drawHudLabelDrift( $, text, { x, y, typefaceName, typeface, textColor, lineStep, t, ease },) { if (!text) return; const parts = splitColorCodes(text); const perCodeShadows = parts.length > 1; // plain labels keep a flat black shadow const blockWidth = typeface?.blockWidth || DEFAULT_TYPEFACE_BLOCK_WIDTH; for (let pass = 0; pass < 2; pass++) { const shadow = pass === 0; let color = textColor; let cx = x, cy = y, i = 0; for (let p = 0; p < parts.length; p++) { if (p % 2 === 1) { color = hudCodeColor(parts[p], textColor); continue; } const run = parts[p]; for (let j = 0; j < run.length; j++) { const ch = run[j]; if (ch === "\n") { cx = x; cy += lineStep; continue; } if (ch !== " ") { const dx = Math.round(cx + hudDriftX(i, t, ease)); const dy = Math.round(cy + hudDriftY(i, t, ease)); if (shadow) { $.ink( perCodeShadows ? hudCodeColor(getShadowColorForText(color), "black") : "black", ); $.write(ch, { x: dx + 1, y: dy + 1 }, undefined, undefined, false, typefaceName); } else { $.ink(color); $.write(ch, { x: dx, y: dy }, undefined, undefined, false, typefaceName); } } cx += typeface?.getAdvance?.(ch) ?? blockWidth; i++; } } }}
export const noWorker = { onMessage: undefined, postMessage: undefined };
let ROOT_PIECE = "prompt"; // This gets set straight from the host html file for the ac.
let USER; // A holder for the logged in user. (Defined in `boot`)let sessionStarted = false; // A flag that waits to boot until a session was// found or not.
let LAN_HOST; // The IP address of the hosting machine on the local network.let SHARE_SUPPORTED; // Whether navigator.share is supported. (For `dl`)let PREVIEW_OR_ICON; // Whether we are in preview or icon mode. (From boot.)let VSCODE; // Whether we are running the vscode extesion or not. (From boot.)let TV_MODE = false; // Whether running in TV mode (disables touch/keyboard input)let DEVICE_MODE = false; // Whether running in device mode (device.kidlisp.com - skip HUD overlays)let SOLO_MODE = false; // Whether running in solo mode (prevents navigating away from piece)let HIGHLIGHT_MODE = false; // Whether HUD highlighting is enabledlet HIGHLIGHT_COLOR = "64,64,64"; // Default highlight color (gray)let PERF_MODE = false; // Whether to show KidLisp performance/FPS HUDlet AUTO_SCALE_MODE = false; // Whether to enable auto-density scaling to maintain target FPSlet SPOOF_AUDIO_MODE = false; // Whether to generate synthetic audio data (for headless captures)let NOGAP_MODE = false; // Whether gap is disabled (nogap URL param)let AUDIO_SAMPLE_RATE = 0;let loopPaused = false; // Whether the main loop is paused (sent from bios)let debug = false; // This can be overwritten on boot.let nopaintPerf = false; // Performance panel for nopaint system debugging (disabled by default)let visible = true; // Is aesthetic.computer visibly rendering or not?let pieceBackground = false; // Does the current piece want background sim ticks?// True screen buffer state — refreshed each frame after content.pixels transfer.// _mainScreenObject tracks the actual screen object so page() can detect when a brush// switches back to the main screen (vs a sub-buffer) by object identity.let _trueScreenPixels = null;let _trueScreenWidth = 0;let _trueScreenHeight = 0;let _mainScreenObject = null;
// 🎯 Global KidLisp Instance - Single source of truth for all KidLisp operationslet globalKidLispInstance = null;
// 🎨 Persistent first line color storage - survives instance resetslet persistentFirstLineColor = null;
// 🎨 Global function to store background color from KidLisp (worker-safe)function storePersistentFirstLineColor(color) { persistentFirstLineColor = color; // Also store in window if available if (typeof window !== "undefined" && window.setPersistentFirstLineColor) { window.setPersistentFirstLineColor(color); }}
// 🎨 Global function to get background color for reframe (worker-safe)function getPersistentFirstLineColor() {
return persistentFirstLineColor;}
// 🎮 Create gamepad helper API for easy access to per-gamepad eventsfunction createGamepadAPI(gamepadEvents) { if (!gamepadEvents) return [];
return gamepadEvents.map((events, index) => { if (!events || events.length === 0) return null;
return { index, events,
// Helper to check if button was pushed/released button(buttonIndex) { const pushEvent = events.find(e => e.button === buttonIndex && e.action === 'push' ); const releaseEvent = events.find(e => e.button === buttonIndex && e.action === 'release' ); return { pushed: !!pushEvent, released: !!releaseEvent, event: pushEvent || releaseEvent }; },
// Helper to get axis value (returns most recent value in frame) axis(axisIndex) { const axisEvent = events.findLast(e => e.axis === axisIndex); return axisEvent ? axisEvent.value : 0; },
// Check if this gamepad is connected (has events) connected() { return events && events.length > 0; },
// Get the gamepad device ID/name get id() { return events[0]?.gamepadId || null; } }; });}
// 🎨 Make storage function globally available for KidLispif (typeof globalThis !== "undefined") { globalThis.storePersistentFirstLineColor = storePersistentFirstLineColor; globalThis.getPersistentFirstLineColor = getPersistentFirstLineColor;}
// Global function to set persistent first line colorif (typeof window !== "undefined") { window.setPersistentFirstLineColor = function(color) { persistentFirstLineColor = color; };
// Global function to get persistent first line color window.getPersistentFirstLineColor = function() { return persistentFirstLineColor; };}
function resolveBackgroundFillSpec(colorLike) { if (colorLike === undefined || colorLike === null) return null;
let resolved; try { resolved = graph.findColor(colorLike); } catch (err) { return null; }
if (!Array.isArray(resolved) || resolved.length === 0) { return null; }
const first = resolved[0]; if (typeof first === "string" && first.startsWith("fade:")) { const fadeInfo = graph.parseFadeColor(resolved); if (fadeInfo) { return { type: "fade", fadeInfo, }; } return null; }
const clampChannel = (value, fallback = 0) => { if (typeof value !== "number" || !Number.isFinite(value)) return fallback; return Math.max(0, Math.min(255, Math.round(value))); };
const r = clampChannel(resolved[0], 0); const g = clampChannel(resolved[1], r); const b = clampChannel(resolved[2], r); const a = clampChannel(resolved[3], 255);
return { type: "solid", rgba: [r, g, b, a], };}
function computeFadePositionForPixel(x, y, width, height, fadeInfo) { if (!fadeInfo || !width || !height) return 0;
const maxX = Math.max(width - 1, 0); const maxY = Math.max(height - 1, 0); const direction = fadeInfo.direction;
if (typeof direction === "number" && Number.isFinite(direction)) { return graph.calculateAngleFadePosition( x, y, 0, 0, maxX, maxY, direction, ); }
const numeric = direction !== undefined ? parseFloat(direction) : NaN; if (!Number.isNaN(numeric) && Number.isFinite(numeric)) { return graph.calculateAngleFadePosition( x, y, 0, 0, maxX, maxY, numeric, ); }
const safeDiv = (value, denom) => (denom <= 0 ? 0 : value / denom);
switch (direction) { case "horizontal-reverse": return safeDiv(maxX - x, maxX); case "vertical": return safeDiv(y, maxY); case "vertical-reverse": return safeDiv(maxY - y, maxY); case "diagonal": { const dx = safeDiv(x, maxX); const dy = safeDiv(y, maxY); return (dx + dy) / 2; } case "diagonal-reverse": { const dx = safeDiv(maxX - x, maxX); const dy = safeDiv(maxY - y, maxY); return (dx + dy) / 2; } case "horizontal": default: return safeDiv(x, maxX); }}
function fillExpandedWithSolidPixels(screen, width, height, oldWidth, oldHeight, rgba) { const [r, g, b] = rgba;
const applyPixel = (x, y) => { const i = (y * width + x) * 4; screen.pixels[i] = r; screen.pixels[i + 1] = g; screen.pixels[i + 2] = b; screen.pixels[i + 3] = 255; };
if (width > oldWidth) { for (let y = 0; y < height; y++) { for (let x = oldWidth; x < width; x++) { applyPixel(x, y); } } }
if (height > oldHeight) { const bottomLimit = width > oldWidth ? oldWidth : width; for (let y = oldHeight; y < height; y++) { for (let x = 0; x < bottomLimit; x++) { applyPixel(x, y); } } }}
function fillExpandedWithFadePixels(screen, width, height, oldWidth, oldHeight, fadeInfo) { const applyPixel = (x, y) => { const t = computeFadePositionForPixel(x, y, width, height, fadeInfo); const [r = 0, g = 0, b = 0, a = 255] = graph.getLocalFadeColor(t, x, y, fadeInfo); const i = (y * width + x) * 4; screen.pixels[i] = Math.max(0, Math.min(255, Math.round(r))); screen.pixels[i + 1] = Math.max(0, Math.min(255, Math.round(g))); screen.pixels[i + 2] = Math.max(0, Math.min(255, Math.round(b))); screen.pixels[i + 3] = Math.max(0, Math.min(255, Math.round(a || 255))); };
if (width > oldWidth) { for (let y = 0; y < height; y++) { for (let x = oldWidth; x < width; x++) { applyPixel(x, y); } } }
if (height > oldHeight) { const bottomLimit = width > oldWidth ? oldWidth : width; for (let y = oldHeight; y < height; y++) { for (let x = 0; x < bottomLimit; x++) { applyPixel(x, y); } } }}
const projectionMode = location.search.indexOf("nolabel") > -1; // Skip loading noise.// 🐚 shellhtml mode — the runtime is hosted by an HTML shell (prompt.ac) that// owns the prompt + return-to-prompt chrome in crisp DOM above the pixel// buffer. The composited typable prompt and corner label stand down here.const shellHTMLMode = location.search.indexOf("shellhtml") > -1;
// Mirror the prompt TextInput out to the shell whenever it changes, so LLM// replies, command confirmations, and the thinking state render in DOM even// though the composited prompt never paints. Runs from the prompt system's// sim in place of prompt_sim.let shellLabelLast = null;function shellLabelSync() { const text = currentHUDTxt || ""; const plain = currentHUDPlainTxt || stripCodes(text) || ""; const color = Array.isArray(currentHUDTextColor) ? currentHUDTextColor.slice(0, 4) : null; const state = text + "\0" + plain + "\0" + (color ? color.join(",") : ""); if (state === shellLabelLast) return; shellLabelLast = state; send({ type: "hud:label:shell", content: { text, plain, color } });}
let shellPromptLast = null;function shellPromptSync($) { const input = $.system?.prompt?.input; if (!input) return; const thinking = !!$.system.prompt.thinking; const state = (input.text || "") + "\0" + thinking; if (state === shellPromptLast) return; shellPromptLast = state; send({ type: "prompt:text:shell", content: { text: input.text || "", thinking }, });}
import { setDebug } from "../disks/common/debug.mjs";import { customAlphabet } from "../dep/nanoid/nanoid.js";import { setPackMode, getPackMode, checkPackMode } from "./pack-mode.mjs";// import { update } from "./glaze.mjs";
// 🧾 Piece run telemetry → /api/piece-log (collection: piece-runs)// Captures console output during a piece's lifetime. Each piece load// gets a fresh pieceId; events are batched and flushed every 2s.const previewEvidence = createPreviewEvidence(message => send(message));// Whistlegraph drafts use local preview evidence. Do not upload their console// content or create IP/geography-linked public piece-run telemetry.const privateWhistlegraphPreview = new URLSearchParams(location.search).get("preview") === "walkieware";const pieceRuns = (() => { let current = null; const startPerf = performance.now(); const origConsole = { log: console.log.bind(console), warn: console.warn.bind(console), error: console.error.bind(console), info: console.info.bind(console), };
function serialize(v) { if (v === undefined) return "undefined"; if (v === null) return "null"; if (typeof v === "string") return v; if (typeof v === "number" || typeof v === "boolean") return String(v); if (v instanceof Error) return `${v.name}: ${v.message}\n${v.stack || ""}`; try { return JSON.stringify(v); } catch { return String(v); } }
function post(phase, pieceId, body = {}) { if (privateWhistlegraphPreview) return; try { fetch("/api/piece-log", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ pieceId, phase, ...body }), keepalive: true, }).catch(() => {}); } catch {} }
function patch(level) { console[level] = function (...args) { origConsole[level](...args); previewEvidence.record(level,args.map(serialize).join(" ")); if (!current) return; current.events.push({ level, at: Date.now(), elapsed: Math.round(performance.now() - current.startPerf), message: args.map(serialize).join(" "), }); if (current.events.length >= 25) current.flush(); else if (!current.flushTimer) current.flushTimer = setTimeout(current.flush, 2000); }; } ["log", "warn", "error", "info"].forEach(patch);
function complete(run, summary = {}) { if (!run) return; if (run.flushTimer) { clearTimeout(run.flushTimer); run.flushTimer = null; } if (run.events.length) { const events = run.events.splice(0, run.events.length); post("log", run.pieceId, { data: { events } }); } post("complete", run.pieceId, { data: { duration: Date.now() - run.startedAt, ...summary }, }); }
return { start({ slug, params, colon, host, user }) { if (privateWhistlegraphPreview) return null; const prev = current; const pieceId = (typeof crypto !== "undefined" && crypto.randomUUID?.()) || `${Date.now()}-${Math.random().toString(16).slice(2)}`; const run = { pieceId, events: [], startPerf: performance.now(), startedAt: Date.now(), flushTimer: null, flush: null, }; run.flush = () => { if (run.flushTimer) { clearTimeout(run.flushTimer); run.flushTimer = null; } if (!run.events.length) return; const events = run.events.splice(0, run.events.length); post("log", run.pieceId, { data: { events } }); }; current = run; post("start", pieceId, { meta: { slug, params, colon, host, user, bootId: typeof self !== "undefined" ? self.acBOOT_ID || null : null, startedAt: new Date().toISOString(), userAgent: typeof navigator !== "undefined" ? navigator.userAgent : null, }, }); // Sentinel event so we can tell console-capture works even if the // piece itself emits nothing. run.events.push({ level: "info", at: Date.now(), elapsed: 0, message: `▶ piece-run started: ${slug}`, }); run.flushTimer = setTimeout(run.flush, 2000); // Flush the prior run asynchronously so it doesn't block the new piece. if (prev) setTimeout(() => complete(prev), 0); return pieceId; }, error(err) { if (!current) return; post("error", current.pieceId, { data: { message: err?.message || String(err), stack: err?.stack || null }, }); }, flush() { current?.flush?.(); }, get pieceId() { return current?.pieceId || null; }, };})();
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";const nanoid = customAlphabet(alphabet, 4);
const defaults = { boot: ({ cursor, screen: { width, height }, resolution, api }) => { if (location.host.indexOf("botce") > -1) resolution(width, height, 0); if (platform.AestheticExtension) resolution(width, height, 0); cursor("native"); }, // aka Setup sim: () => false, // A framerate independent of rendering. paint: ({ noise16Aesthetic, noise16Sotce, slug, wipe, ink, write, screen, net }) => { // In PACK mode (exported bundles), skip noise16 — just show black if (typeof window !== "undefined" && window.acPACK_MODE) { wipe("black"); return; } // TODO: Make this a boot choice via the index.html file? if (!projectionMode) { if (slug?.indexOf("wipppps") > -1) { wipe("black"); } else if (slug?.indexOf("botce") > -1) { noise16Sotce(); } else { noise16Aesthetic(); if (net.motd) { ink(255, 255, 255, 200).write( net.motd, { center: "x", y: Math.floor(screen.height / 2) }, undefined, screen.width - 18, ); } if (net.loadFailureText) { ink("white").write( net.loadFailureText, { x: 6, y: 6 }, [64, 64], screen.width - 6, ); } } } }, beat: () => false, // Runs every bpm. act: () => false, // All user interaction. leave: () => false, // Before unload. receive: () => false, // Handle messages from BIOS (file drops, etc.) preview: ({ wipe, slug }) => { wipe(64).ink(255).write(slug, { center: "xy", size: 1 }); }, icon: ({ glaze, wipe, screen }) => { glaze({ on: false }); // console.log(screen.width, screen.height); wipe(70, 50, 100) .ink(200, 30, 100) .box(screen.width / 2, screen.height / 2, 48, 72, "*center"); },};
let loadAfterPreamble = null;let hotSwap = null;
// 🔎 NoPaint// Inheritable via `export const system = "nopaint"` from any piece.// Boilerplate for a distributed raster editor.const nopaint = { leave: function leave($) { const { store, system, page, screen, flatten } = $; if (NPnoOnLeave === false) { // ^ This is set when reloading a brush without storing changes. (`N` key)
if (system.nopaint.bakeOnLeave) { // Add bake commands. page(system.painting); //$activePaintApi = $; // In case of recursive paint functions like `write`. bake($); // page(screen); // TODO: This should work but it doesnt... // Seems to have something to do with // the $activePaintAPI being set // and it's specific to the `write` // implementation. flatten(); // Force-run the above painting commands. } // Bake any changes into the existing painting using the screen buffer. // Note... this may not include the full API that `paint` has... // Which could lead to inconsistencies in `bake` calls. 23.05.04.13.01
// const p = system.nopaint.translation; // Offset by the pan position. // page(system.painting).paste(screen, -p.x, -p.y); // TODO: Why the + 1 offset here... // painting.paint(); // TODO: Why is this here? // page(screen); //}
addUndoPainting(system.painting, $.slug);
// Idea: Check to see if anything actually got painted by doing a diff on // the pixels?
store["painting"] = { width: system.painting.width, height: system.painting.height, pixels: system.painting.pixels, }; //system.painting; // Remember the painting data.
// console.log("System painting:", system.painting);
store.persist("painting", "local:db");
system.nopaint.syncWip($).catch((error) => console.warn("Painting WIP:", error.message));
// 🎨 Broadcast painting update to other tabs $commonApi.broadcastPaintingUpdate("updated", { source: "leave", slug: $.slug });
// And its transform. store["painting:transform"] = { translation: system.nopaint.translation, zoom: system.nopaint.zoomLevel, }; store.persist("painting:transform", "local:db"); } else { // Restore a painting if `no`ing. const paintings = system.nopaint.undo.paintings; page(system.painting) .paste(paintings[paintings.length - 1]) .page(screen); }
NPnoOnLeave = false; }, // 🥞 Bake (to the painting) bake: function bake({ paste, system }) { paste(system.nopaint.buffer); },};
const undoPaintings = []; // Stores the last two paintings.let undoPosition = 0;
function addUndoPainting(painting, step = "unspecified") { if (!painting) return; // If there is no painting present, silently pass. const op = painting.pixels; const pixels = new Uint8ClampedArray(op.length); pixels.set(op);
if (undoPaintings.length > undoPosition + 1) { undoPaintings.length = undoPosition + 1; }
if (undoPaintings.length > 0) { const lastPainting = undoPaintings[undoPaintings.length - 1];
// Check for equality in the two states. // TODO: How long does this take? const eq = painting.width === lastPainting.width && painting.height === lastPainting.height && pixels.every((value, index) => value === lastPainting.pixels[index]);
if (eq) { // console.log("💩 The undo stack was not changed:", undoPaintings.length); return; } }
undoPaintings.push({ pixels, width: painting.width, height: painting.height, });
if ($commonApi.system.nopaint.recording) { $commonApi.system.nopaint.addToRecord({ label: step, painting: { pixels, width: painting.width, height: painting.height, }, }); }
undoPosition = undoPaintings.length - 1; if ($commonApi.system.nopaint.wipSync) { clearTimeout($commonApi.system.nopaint.wipTimer); const sync = $commonApi.system.nopaint.wipSync; $commonApi.system.nopaint.wipTimer = setTimeout(() => { if ($commonApi.system.nopaint.wipSync === sync) { $commonApi.system.nopaint.syncWip().catch((error) => console.warn("Painting WIP:", error.message)); } }, 120); }
// Note: This could be extended to increase the size of the // undo stack, and images could be diffed? 23.01.31.01.30 const maxUndoSteps = 32; if (undoPaintings.length > maxUndoSteps) undoPaintings.shift();
if (debug && logs.painting) console.log("💩 Added undo painting...", undoPaintings.length);}
let system = null; // Used to add built-in templated behaviors like `nopaint`.
// 🎞️ Piece-level FPS control (timing-based, doesn't affect global render loop)let pieceFPS = null; // Target FPS for the current piece (null = no limit)let lastPaintTime = 0; // Last time paint() was executedlet lastPaintOut = undefined; // Store last paintOut to preserve correct noPaint behaviorlet shouldSkipPaint = false; // Whether to skip this frame's paint calllet pieceFrameCount = 0; // Frame counter that only increments when piece actually paints
// 🎬 Piece Transition System// Two transition types available: "drip" (default, fast) and "bubble-wrap" (organic, slower)// Set to "none" to disable transitions entirely (fast cut)const TRANSITION_TYPE = "none"; // "none", "drip" or "bubble-wrap"
// 🧬 Piece Transition State: overlay pixels → fades to reveal piece underneath// Captures the OUTGOING piece's pixels when transitioning between pieceslet pieceTransition = { active: false, phase: "loading", // "loading" (slow, reach ~50%) or "revealing" (fast, finish) overlayPixels: null, // The captured frame that overlays on top (from outgoing piece) generation: 0, maxGenerations: 60, // Total frames for full transition width: 0, height: 0, // Drip-specific state (per-column y-offset) - BLINDS STYLE dripOffsets: null, // Int16Array of column y-offsets (positive = distance moved) dripSpeeds: null, // Float32Array of per-column speeds dripDirections: null, // Int8Array: 1 = down, -1 = up (alternating blinds) targetOffset: 0, // Target offset for loading phase (~50% of height) // Bubble-wrap-specific state blockData: null, // Per-block data for bubble-wrap blockSize: 8, blocksX: 0, blocksY: 0,};
// 🧬 Legacy alias for backwards compatibilityconst golTransition = pieceTransition;
// 🧬 Scale overlay pixels to match new resolution (nearest-neighbor)function scaleOverlayPixels(srcPixels, srcWidth, srcHeight, dstWidth, dstHeight) { const dstPixels = new Uint8ClampedArray(dstWidth * dstHeight * 4); const xRatio = srcWidth / dstWidth; const yRatio = srcHeight / dstHeight;
for (let y = 0; y < dstHeight; y++) { for (let x = 0; x < dstWidth; x++) { const srcX = Math.floor(x * xRatio); const srcY = Math.floor(y * yRatio); const srcIdx = (srcY * srcWidth + srcX) * 4; const dstIdx = (y * dstWidth + x) * 4;
dstPixels[dstIdx] = srcPixels[srcIdx]; dstPixels[dstIdx + 1] = srcPixels[srcIdx + 1]; dstPixels[dstIdx + 2] = srcPixels[srcIdx + 2]; dstPixels[dstIdx + 3] = srcPixels[srcIdx + 3]; } }
return dstPixels;}
// ═══════════════════════════════════════════════════════════════════════════// 🪟 BLINDS TRANSITION - Alternating up/down strips (FAST & PERFORMANT)// ═══════════════════════════════════════════════════════════════════════════
// Initialize blinds transition - alternating columns slide up/downfunction initDripTransition(width, height) { pieceTransition.dripOffsets = new Int16Array(width); pieceTransition.dripSpeeds = new Float32Array(width); pieceTransition.dripDirections = new Int8Array(width); pieceTransition.maxGenerations = 30; // Faster! pieceTransition.phase = "loading"; pieceTransition.targetOffset = Math.floor(height * 0.35); // ~35% during loading (less wait)
// Initialize per-column state - alternating directions like blinds for (let x = 0; x < width; x++) { pieceTransition.dripOffsets[x] = 0; // Start at 0 (overlay fully in place) // Alternate: even columns slide DOWN, odd columns slide UP pieceTransition.dripDirections[x] = (x % 2 === 0) ? 1 : -1; // Faster speed during loading phase pieceTransition.dripSpeeds[x] = 4 + Math.random() * 3; // 4-7 pixels per frame }}
// Signal that piece is loaded - switch to fast reveal phasefunction transitionPieceLoaded() { if (pieceTransition.active && pieceTransition.phase === "loading" && pieceTransition.dripSpeeds) { pieceTransition.phase = "revealing"; // Boost speeds for the reveal phase - FAST! for (let x = 0; x < pieceTransition.dripSpeeds.length; x++) { pieceTransition.dripSpeeds[x] = 10 + Math.random() * 8; // 10-18 pixels per frame } console.log("🎬 Transition: Piece loaded! Switching to reveal phase"); }}
// Step blinds transition - advance each column's offsetfunction dripStep() { pieceTransition.generation++; const { dripOffsets, dripSpeeds, dripDirections, height, phase, targetOffset } = pieceTransition;
let allDone = true;
for (let x = 0; x < dripOffsets.length; x++) { const currentOffset = dripOffsets[x];
if (phase === "loading") { // Loading phase: slowly move toward target (~45%) if (currentOffset < targetOffset) { dripOffsets[x] += dripSpeeds[x]; // Clamp to target during loading if (dripOffsets[x] > targetOffset) { dripOffsets[x] = targetOffset; } allDone = false; } } else { // Revealing phase: accelerate to completion if (currentOffset < height) { dripOffsets[x] += dripSpeeds[x]; // Add acceleration (gravity/momentum) dripSpeeds[x] += 0.3; allDone = false; } } }
// During loading phase, never report "done" - wait for reveal if (phase === "loading") { return true; // Keep running }
return !allDone && pieceTransition.generation < pieceTransition.maxGenerations;}
// Apply blinds overlay - alternating columns slide up/down to reveal new framefunction applyDripOverlay(screenPixels) { const { overlayPixels, dripOffsets, dripDirections, width, height } = pieceTransition; if (!overlayPixels || !screenPixels || !dripOffsets || !dripDirections) return;
for (let x = 0; x < width; x++) { const offset = Math.max(0, dripOffsets[x] | 0); const direction = dripDirections[x];
if (offset >= height) continue; // Column fully slid off
const visibleRows = height - offset;
if (direction > 0) { // SLIDE DOWN - overlay moves down, reveals new frame from TOP for (let y = 0; y < visibleRows; y++) { const srcY = y; // Read from top of overlay const dstY = y + offset; // Write shifted down const srcIdx = (srcY * width + x) * 4; const dstIdx = (dstY * width + x) * 4;
screenPixels[dstIdx] = overlayPixels[srcIdx]; screenPixels[dstIdx + 1] = overlayPixels[srcIdx + 1]; screenPixels[dstIdx + 2] = overlayPixels[srcIdx + 2]; screenPixels[dstIdx + 3] = overlayPixels[srcIdx + 3]; } } else { // SLIDE UP - overlay moves up, reveals new frame from BOTTOM for (let y = 0; y < visibleRows; y++) { const srcY = offset + y; // Read from bottom portion of overlay const dstY = y; // Write shifted up const srcIdx = (srcY * width + x) * 4; const dstIdx = (dstY * width + x) * 4;
screenPixels[dstIdx] = overlayPixels[srcIdx]; screenPixels[dstIdx + 1] = overlayPixels[srcIdx + 1]; screenPixels[dstIdx + 2] = overlayPixels[srcIdx + 2]; screenPixels[dstIdx + 3] = overlayPixels[srcIdx + 3]; } } }}
// ═══════════════════════════════════════════════════════════════════════════// 🫧 BUBBLE-WRAP TRANSITION - Organic morphing dissolve (DEPRECATED: slower)// ═══════════════════════════════════════════════════════════════════════════
// Initialize bubble-wrap transition - per-pixel dissolve with organic noisefunction initBubbleWrapCells(width, height) { pieceTransition.blockSize = 1; // Per-pixel pieceTransition.blocksX = width; pieceTransition.blocksY = height; pieceTransition.blockData = null; pieceTransition.maxGenerations = 30; // Slower for organic feel}
// Step bubble-wrap transitionfunction bubbleWrapStep(screenPixels) { pieceTransition.generation++; return pieceTransition.generation < pieceTransition.maxGenerations;}
// Apply bubble-wrap overlay - chunky biological smoothie morphfunction applyBubbleWrapOverlay(screenPixels) { const { overlayPixels, width, height, generation, maxGenerations } = pieceTransition; if (!overlayPixels || !screenPixels) return;
// Progress 0-1 const progress = generation / maxGenerations; // Eased for smooth start/end const eased = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
// Time offset for animated noise const t = generation * 0.1;
// Displacement peaks mid-transition - CHUNKY const dispPeak = Math.sin(progress * Math.PI); const maxDisp = dispPeak * 16;
// Block size for chunky feel const bs = 4;
for (let y = 0; y < height; y++) { const rowOff = y * width; // Quantize y for chunky blocks const qy = (y / bs | 0) * bs;
for (let x = 0; x < width; x++) { const i = (rowOff + x) * 4; // Quantize x for chunky blocks const qx = (x / bs | 0) * bs;
// Organic noise at block resolution for chunky dissolve const n1 = Math.sin(qx * 0.05 + t) * Math.cos(qy * 0.04 + t * 0.7); const n2 = Math.sin(qx * 0.09 + qy * 0.07 - t * 0.6) * 0.5; const n3 = Math.cos(qx * 0.02 - qy * 0.03 + t * 0.4) * 0.4; const noise = (n1 + n2 + n3) * 0.5 + 0.5;
// Per-block blend with organic variation const threshold = eased * 1.5 - 0.25; const edge = 0.3; let blend = (threshold - noise * 0.85) / edge; blend = blend < 0 ? 0 : blend > 1 ? 1 : blend;
// SMOOTHIE: chaotic displacement that samples from BOTH frames const swirl = Math.sin(qx * 0.06 + qy * 0.04 + t * 1.5) * maxDisp; const churn = Math.cos(qx * 0.05 - qy * 0.07 + t * 1.2) * maxDisp;
// Sample old frame with swirling displacement let ox = (x + swirl) | 0; let oy = (y + churn) | 0; ox = ox < 0 ? 0 : ox >= width ? width - 1 : ox; oy = oy < 0 ? 0 : oy >= height ? height - 1 : oy; const oi = (oy * width + ox) * 4;
// Sample new frame with OPPOSITE swirl - mixing! let nx = (x - churn * 0.7) | 0; let ny = (y + swirl * 0.6) | 0; nx = nx < 0 ? 0 : nx >= width ? width - 1 : nx; ny = ny < 0 ? 0 : ny >= height ? height - 1 : ny; const ni = (ny * width + nx) * 4;
// Get swirled colors from both const oldR = overlayPixels[oi]; const oldG = overlayPixels[oi + 1]; const oldB = overlayPixels[oi + 2]; const newR = screenPixels[ni]; const newG = screenPixels[ni + 1]; const newB = screenPixels[ni + 2];
// Chunky biological blend - smoothie mix! const inv = 1 - blend; let r = oldR * inv + newR * blend; let g = oldG * inv + newG * blend; let b = oldB * inv + newB * blend;
// Extra chaos: sometimes swap channels mid-transition if (dispPeak > 0.5 && ((qx + qy + generation) & 7) === 0) { const tmp = r; r = g; g = b; b = tmp; }
screenPixels[i] = r; screenPixels[i + 1] = g; screenPixels[i + 2] = b; } }}
// ═══════════════════════════════════════════════════════════════════════════// 🎬 UNIFIED TRANSITION API - Dispatches to drip, bubble-wrap, or none// ═══════════════════════════════════════════════════════════════════════════
// Initialize transition based on current typefunction initGOLCells(width, height) { if (TRANSITION_TYPE === "none") return; // No transition if (TRANSITION_TYPE === "drip") { initDripTransition(width, height); } else { initBubbleWrapCells(width, height); }}
// Step transitionfunction golStep(screenPixels) { if (TRANSITION_TYPE === "none") return false; // Immediately done if (TRANSITION_TYPE === "drip") { return dripStep(); } else { return bubbleWrapStep(screenPixels); }}
// Apply transition overlayfunction applyGOLOverlay(screenPixels) { if (TRANSITION_TYPE === "drip") { applyDripOverlay(screenPixels); } else { applyBubbleWrapOverlay(screenPixels); }}
let boot = defaults.boot;let sim = defaults.sim;let paint = defaults.paint;let beat = defaults.beat;let brush, lift, filter; // Only set in the `nopaint` system.let act = defaults.act;let leave = defaults.leave;let receive = defaults.receive; // Handle messages from BIOSlet preview = defaults.preview;let icon = defaults.icon;let bake; // Currently only used by the `nopaint` system.
let leaving = false; // Set to true on first piece.let leaveLoad; // A callback for loading the next disk after leaving.
let previewMode = false; // Detects ?preview on a piece and yields its// preview function if it exists.let firstPreviewOrIcon = true;let iconMode = false; // Detects ?icon on a piece and yields its// icon function if it exists.let previewOrIconMode;let hideLabel = false;// Whether this frame ever asked to be rid of the corner label. `hideLabel`// is recomputed from the incoming slug on every load, and a live `code`// reload carries no query at all — so a piece that arrived with `?nolabel`// grew its label back the moment it was edited, which is the one moment a// preview card is being watched. Suppression belongs to the view, not to the// piece: boot already treats `nolabel` that way, preserving it across every// navigation, and this worker serves exactly one view for its whole life.let hideLabelForFrame = false;let hideLabelViaTab = false; // Track if label is hidden via tab key toggle
// Helper function to toggle HUD visibility (used by Tab key and center tap)function toggleHUDVisibility(isDoubleTap = false, skipSound = false) { const currentTime = performance.now();
if (hudAnimationState.animating) { // Animation in progress: reverse direction and continue from current position const elapsed = currentTime - hudAnimationState.startTime; const progress = Math.min(elapsed / hudAnimationState.duration, 1.0);
// Flip the target state hudAnimationState.visible = !hudAnimationState.visible;
// Update the remembered state for when QR fullscreen is turned off if (hudAnimationState.qrFullscreen) { hudAnimationState.cornersVisibleBeforeFullscreen = hudAnimationState.visible; }
// Restart animation from current position by adjusting the start time // If we were 30% through a hide animation, start the show animation at 70% progress const remainingProgress = 1.0 - progress; hudAnimationState.startTime = currentTime - (remainingProgress * hudAnimationState.duration);
// Special double-tap: immediately show HUD if hiding if (isDoubleTap && !hudAnimationState.visible) { hudAnimationState.animating = false; hudAnimationState.visible = true; hudAnimationState.opacity = 1.0; hudAnimationState.slideOffset = { x: 0, y: 0 }; hudAnimationState.qrSlideOffset = { x: 0, y: 0 };
// Update remembered state if (hudAnimationState.qrFullscreen) { hudAnimationState.cornersVisibleBeforeFullscreen = true; } }
if (!skipSound) { $commonApi.sound.synth({ type: "sine", tone: hudAnimationState.visible ? 600 : 400, duration: 0.15, attack: 0.1, decay: 0.8, volume: 0.2, }); } } else { // No animation in progress: start new animation hudAnimationState.animating = true; hudAnimationState.startTime = currentTime; hudAnimationState.visible = !hudAnimationState.visible;
// Update the remembered state for when QR fullscreen is turned off if (hudAnimationState.qrFullscreen) { hudAnimationState.cornersVisibleBeforeFullscreen = hudAnimationState.visible; }
if (!skipSound) { $commonApi.sound.synth({ type: "sine", tone: hudAnimationState.visible ? 600 : 400, duration: 0.15, attack: 0.1, decay: 0.8, volume: 0.2, }); } }}
// Helper function to toggle QR fullscreen mode for KidLisp piecesfunction toggleQRFullscreen() { try { // Only allow QR fullscreen for KidLisp pieces that have QR codes const sourceCode = currentText || currentHUDTxt; console.log("🔍 [QR Toggle] sourceCode:", sourceCode, "currentPath:", currentPath);
// Use the centralized KidLisp detection from kidlisp.mjs const isInlineKidlispPiece = (currentPath && lisp.isKidlispSource(currentPath) && !currentPath.endsWith('.lisp')) || currentPath === "(...)" || (sourceCode && sourceCode.startsWith("$")) || (currentPath && currentPath.includes("/disks/$")) || (sourceCode && lisp.isKidlispSource(sourceCode));
console.log("🔍 [QR Toggle] isInlineKidlispPiece:", isInlineKidlispPiece);
if (!isInlineKidlispPiece) { console.log("⚠️ [QR Toggle] Not a KidLisp piece, skipping QR fullscreen toggle"); return; }
if (!hudAnimationState.qrFullscreen) { // Turning ON fullscreen QR console.log("✅ [QR Toggle] Enabling fullscreen mode");
// Remember current corner visibility state hudAnimationState.cornersVisibleBeforeFullscreen = hudAnimationState.visible;
// If corners are currently visible, animate them out for fullscreen QR if (hudAnimationState.visible) { // Check if animation is already in progress if (hudAnimationState.animating) { // Animation in progress: reverse it const currentTime = performance.now(); const elapsed = currentTime - hudAnimationState.startTime; const remaining = Math.max(0, hudAnimationState.duration - elapsed); hudAnimationState.startTime = currentTime - remaining; } else { // No animation in progress: start new animation hudAnimationState.animating = true; hudAnimationState.startTime = performance.now(); } hudAnimationState.visible = false; }
hudAnimationState.qrFullscreen = true;
// Remove corner QR hitbox when going fullscreen send({ type: "button:hitbox:remove", content: "qr-corner" });
} else { // Turning OFF fullscreen QR console.log("✅ [QR Toggle] Disabling fullscreen mode"); hudAnimationState.qrFullscreen = false;
// Remove fullscreen QR hitbox send({ type: "button:hitbox:remove", content: "qr-fullscreen" });
// Restore corner visibility to what it was before fullscreen QR was activated if (hudAnimationState.cornersVisibleBeforeFullscreen && !hudAnimationState.visible) { // Check if animation is already in progress if (hudAnimationState.animating) { // Animation in progress: reverse it const currentTime = performance.now(); const elapsed = currentTime - hudAnimationState.startTime; const remaining = Math.max(0, hudAnimationState.duration - elapsed); hudAnimationState.startTime = currentTime - remaining; } else { // No animation in progress: start new animation hudAnimationState.animating = true; hudAnimationState.startTime = performance.now(); } hudAnimationState.visible = true; } }
// Play sound feedback if ($commonApi?.sound?.synth) { $commonApi.sound.synth({ type: "sine", tone: hudAnimationState.qrFullscreen ? 500 : 250, duration: 0.12, attack: 0.1, decay: 0.9, volume: 0.25, }); } } catch (err) { console.error("❌ [QR Toggle] Error toggling QR fullscreen:", err); }}
// In PACK mode, show HUD for non-KidLisp pieces (JS bundles) but hide for KidLispconst _packModeHudVisible = getPackMode() ? (typeof window !== 'undefined' && window.acPACK_COLOPHON?.piece?.isKidLisp === false) : true;let hudAnimationState = { visible: _packModeHudVisible, animating: false, startTime: 0, duration: 500, // 500ms animation opacity: 1.0, slideOffset: { x: 0, y: 0 }, // HUD label offset (slides to top-left) qrSlideOffset: { x: 0, y: 0 }, // QR overlay offset (slides to bottom-right) labelWidth: 120, // HUD label width - updated when HUD is drawn labelHeight: 40, // HUD label height - updated when HUD is drawn qrSize: 80, // QR overlay size for bounding box animations lastTabTime: 0, // Track last tab press for double-tap detection qrFullscreen: false, // Track if QR code is in fullscreen mode cornersVisibleBeforeFullscreen: true // Remember corner state before fullscreen QR};
// Clock melody cached code for QR display (received from clock.mjs)let cachedClockCode = null;
let module, loadedModule; // Currently loaded piece module code with an extra reference for `hotSwap`.
// 🎨 Enhanced painting change detection with immediate pixel monitoringlet lastPaintingHash = null;let paintingChangeCheckInterval = null;let frameBasedMonitoring = false;
// Performance optimization: Use requestAnimationFrame for more efficient monitoringfunction enableFrameBasedMonitoring() { if (frameBasedMonitoring) return; frameBasedMonitoring = true;
let lastDimensions = null;
function checkPaintingChanges() { if ($commonApi.system?.painting && !$commonApi._processingBroadcast) { const currentHash = generatePaintingHash($commonApi.system.painting); const currentDimensions = { width: $commonApi.system.painting.width, height: $commonApi.system.painting.height };
// Add detailed logging for nopaint interference detection const isNopaintActive = $commonApi.system?.nopaint?.is?.("painting"); const nopaintBuffer = $commonApi.system?.nopaint?.buffer;
if (lastPaintingHash !== null && currentHash !== lastPaintingHash) { // Check if this is a dimension change (resize operation) const isDimensionChange = lastDimensions && (lastDimensions.width !== currentDimensions.width || lastDimensions.height !== currentDimensions.height);
// Skip broadcasting during active nopaint operations to prevent interference if (isNopaintActive) { console.log(`🚫 SKIPPING broadcast during nopaint operation to prevent live preview interference`); } else { // Immediate broadcast with appropriate source $commonApi.broadcastPaintingUpdateImmediate("frame_update", { source: isDimensionChange ? "resize" : "frame_monitor", hash: currentHash.substr(0,8) }); } }
lastPaintingHash = currentHash; lastDimensions = currentDimensions; }
if (frameBasedMonitoring) { requestAnimationFrame(checkPaintingChanges); } }
requestAnimationFrame(checkPaintingChanges);}
// 🎨 Broadcast throttling to prevent infinite loopslet lastBroadcastTime = 0;let broadcastThrottleDelay = 100; // Minimum ms between broadcastslet pieceMetadata = null; // Metadata for current piece (trustLevel, etc.)let currentPath, currentHost, currentSearch, currentColon, currentParams, currentHash, currentText, currentCode, currentHUDTxt, currentHUDPlainTxt, // Plain text version without color codes currentOriginalCodeId, // Track original $code identifier for sharing currentHUDTextColor, currentHUDStatusColor = "red", currentHUDButton, currentHUDButtonActive = false, // Global flag to block other button interactions when HUD is active currentHUDButtonDirectTouch = false, // Track if HUD button was directly tapped (not rolled over) currentHUDHovered = false, // Pointer is over the corner label (passive, no press) currentHUDHoverAmount = 0, // 0..1 — how far the hover drift has eased in currentHUDHoverClock = 0, // performance.now() of the last ease step currentHUDScrub = 0, currentHUDScrubReadyState = null, // null | "share" | "edit" currentHUDLabelFontName, currentHUDLabelBlockWidth = tf?.blockWidth ?? DEFAULT_TYPEFACE_BLOCK_WIDTH, currentHUDLabelBlockHeight = tf?.blockHeight ?? DEFAULT_TYPEFACE_BLOCK_HEIGHT, currentHUDShareWidth = (tf?.blockWidth ?? DEFAULT_TYPEFACE_BLOCK_WIDTH) * "share ".length, currentHUDEditWidth = tf?.blockWidth ?? DEFAULT_TYPEFACE_BLOCK_WIDTH, currentHUDLabelMeasuredWidth = 0, currentHUDTextHeight = 0, currentHUDTextBoxWidth = 0, currentHUDTextBoxHeight = 0, currentHUDLeftPad = 0, currentHUDOffset, currentHUDQR = null, // QR code URL to show to the left of label (set via hud.qr()) currentHUDQRCells = null, // Cached QR code cells currentHUDAuthor = null, // @handle of the author for KidLisp pieces currentHUDHits = null, // Hit count for KidLisp pieces currentHUDSuperscript = null, // Custom superscript text (e.g. ".com") shown after label in cyan forceTinyHudLabel = false, // Force MatrixChunky8 font for HUD label (set via hud.tinyLabel()) qrOverlayCache = new Map(), // Cache for QR overlays to prevent regeneration every frame hudLabelCache = null; // Cache for HUD label to prevent regeneration every frame
// 📦 Global update state — polled once per worker lifetime so a fresh build is// visible from any piece (not just `prompt`, which used to own polling and// aborted it on leave). Exposed via `$commonApi.system.update`.let globalUpdateReady = false;let globalVersionInfo = null; // { deployed, latest, status, behindBy, recentCommits }let globalRecentCommits = [];let updatePollStarted = false;let updatePollController = null;let updateBadgeBoxScreen = null; // {x, y, w, h} of the rendered badge for hitbox bookkeepinglet updateBadgeHitboxRegistered = false;let globalAutoReload = false;// Whether this frame wants updates taken rather than offered. Same shape,// and same reason, as `hideLabelForFrame`: it describes the view, it arrives// once in the URL, and a live reload carries no query to restate it with.let autoUpdateForFrame = false;
// 📱 LAN Dev mode identity (set by session server in dev mode)let devIdentity = null; // { name, host, hostIp, mode, connectionId }
// 📡 Remote log forwarding for LAN Dev modelet remoteLogQueue = [];let remoteLogSocket = null;const MAX_REMOTE_LOG_QUEUE = 50;
// 🩺 What this piece looks like from in here, for whoever is authoring it.// Silent until `channel()` is given a code channel — see `diagnostics.mjs`.const diagnostics = new Diagnostics({ send: (type, content) => socket?.send(type, content),});// The worker has no `window`, so this is the only listener that ever sees an// uncaught error thrown by a piece.diagnostics.watch(globalThis);
function setupRemoteLogging() { const originalConsole = { log: console.log.bind(console), warn: console.warn.bind(console), error: console.error.bind(console), };
function sendRemoteLog(level, args) { // 🩺 The author's copy. Independent of LAN dev mode, which is what makes // this work in production at all. diagnostics.note(level, serializeArgs(args)); if (!devIdentity || !remoteLogSocket?.connected) { // Queue logs until connected if (remoteLogQueue.length < MAX_REMOTE_LOG_QUEUE) { remoteLogQueue.push({ level, args: serializeArgs(args), time: Date.now() }); } return; }
try { const logData = { level, args: serializeArgs(args), deviceName: devIdentity.name, connectionId: devIdentity.connectionId, time: Date.now(), }; remoteLogSocket.send("dev:log", logData); } catch (e) { // Silently fail to avoid infinite loop } }
function serializeArgs(args) { return args.map(arg => { if (arg === null) return 'null'; if (arg === undefined) return 'undefined'; if (typeof arg === 'string') return arg; if (typeof arg === 'number' || typeof arg === 'boolean') return String(arg); if (arg instanceof Error) return `${arg.name}: ${arg.message}\n${arg.stack}`; try { return JSON.stringify(arg, null, 2); } catch (e) { return String(arg); } }); }
// Intercept console methods console.log = (...args) => { originalConsole.log(...args); sendRemoteLog('log', args); };
console.warn = (...args) => { originalConsole.warn(...args); sendRemoteLog('warn', args); };
console.error = (...args) => { originalConsole.error(...args); sendRemoteLog('error', args); };
// Capture unhandled errors (only in main thread with window) if (typeof window !== 'undefined') { window.addEventListener('error', (event) => { sendRemoteLog('error', [`Uncaught: ${event.message} at ${event.filename}:${event.lineno}:${event.colno}`]); });
window.addEventListener('unhandledrejection', (event) => { sendRemoteLog('error', [`Unhandled Promise: ${event.reason}`]); }); }}
function flushRemoteLogQueue() { if (!devIdentity || !remoteLogSocket?.connected) return;
while (remoteLogQueue.length > 0) { const log = remoteLogQueue.shift(); try { remoteLogSocket.send("dev:log", { level: log.level, args: log.args, deviceName: devIdentity.name, connectionId: devIdentity.connectionId, time: log.time, queued: true, }); } catch (e) { break; } }}
let lastMatrixChunkyHighlightLog = null;let lastMatrixChunkyWriteDiagnosticLog = null;let lastMatrixChunkyPixelLog = null;
// Helper functions to safely access window flags in both main thread and worker contextsfunction isQROverlayCacheDisabled() { try { return typeof window !== 'undefined' && window.acDISABLE_QR_OVERLAY_CACHE; } catch (e) { return false; // Default to enabled (caching on) }}
function isHUDLabelCacheDisabled() { try { return typeof window !== 'undefined' && window.acDISABLE_HUD_LABEL_CACHE; } catch (e) { return false; // Default to enabled if we can't check }}
// Make cache globally accessible for character loading systemif (typeof window !== 'undefined') { window.qrOverlayCache = qrOverlayCache;
// Clear caches if they are disabled from BIOS if (isQROverlayCacheDisabled()) { qrOverlayCache.clear(); // console.log("🚫 QR overlay cache disabled and cleared from BIOS"); }
// if (isHUDLabelCacheDisabled()) { // console.log("🚫 HUD label cache disabled from BIOS"); // }}//currentPromptButton;
// Color codes are scanned by lib/color-codes.mjs, which knows that `\\` is an// escaped backslash — the producers escape text they didn't author, so a user// who types `\red\` gets those characters instead of red text.
// Utility function to strip color codes from textfunction stripColorCodes(str) { return stripCodes(str);}
function textContainsColorCodes(str) { return hasColorCodes(str);}
function hasKidLispMarkers(text) { if (!text) return false; return ( text.includes("ink ") || text.includes("line ") || text.includes("box ") || text.includes("circle ") || text.includes("spin ") || text.includes("(") || textContainsColorCodes(text) || /\d+s\.\.\./.test(text) || /\?\s/.test(text) );}
function detectKidLispPiece({ currentPath, currentHUDTxt, currentText, cleanText }) { const sourceCode = currentText || currentHUDTxt;
return ( (currentPath && lisp.isKidlispSource(currentPath) && !currentPath.endsWith('.lisp')) || currentPath === "(...)" || (sourceCode && sourceCode.startsWith("$")) || (currentPath && (currentPath.includes("/disks/$") || currentPath.includes("$"))) || (sourceCode && lisp.isKidlispSource && lisp.isKidlispSource(sourceCode)) || hasKidLispMarkers(currentHUDTxt) || textContainsColorCodes(cleanText) || hasKidLispMarkers(sourceCode) );}
// Detect embedded/inline KidLisp specifically (kidlisp.com editor, PJ KidLisp)// Does NOT include .lisp file pieces - only inline/embedded codefunction detectEmbeddedKidLisp({ currentPath, currentHUDTxt, currentText }) { const sourceCode = currentText || currentHUDTxt; return ( currentPath === "(...)" || (sourceCode && sourceCode.startsWith("$")) || (currentPath && currentPath.includes("/disks/$")) || (currentPath && currentPath.includes("$") && !currentPath.endsWith('.lisp')) );}
function updateHUDStatus() { if (udp.connected && socket?.connected) { currentHUDStatusColor = "lime"; } else if (udp.connected || socket?.connected) { currentHUDStatusColor = "orange"; } else { currentHUDStatusColor = "red"; }}
let loading = false;let loadingStartTime = null; // Track when loading started for timeout safetyconst LOADING_TIMEOUT_MS = 10000; // 10 second max loading time
// Safety check for stuck loading statefunction checkLoadingTimeout() { if (loading && loadingStartTime && (Date.now() - loadingStartTime > LOADING_TIMEOUT_MS)) { console.warn("🔴 Loading timeout exceeded - forcing loading reset"); loading = false; loadingStartTime = null; return true; // Was stuck } return false;}
let reframe;let pendingPieceReload = null; // Queue for piece-reload messages before $commonApi.reload is ready
const sfxProgressReceivers = {}, sfxSampleReceivers = {}, sfxKillReceivers = {}, sfxDurationReceivers = {};const sfxDurations = {}; // Lengths BIOS reported when each sound finished decoding.let $sampleCount = 0n;
const signals = []; // Easy messages from embedded DOM content.const actAlerts = []; // Messages that get put into act and cleared after// every frame.let reframed = false;let formReframing = false; // Just for 3D camera updates.
let paintings = {}; // Cached bitmaps from a piece.
// Expose pending image count for oven/grabber to poll (available immediately)if (typeof window !== "undefined") { window.acPendingImages = () => Object.values(paintings).filter((v) => v === "fetching").length;}
// 🖼️ Image cache system for paste/stamp - persists across refreshes// Similar to $kidlisp code cache but for image dataconst imageCache = { // In-memory cache for fast access (RAM) memory: new Map(), // url -> ImageBitmap
// IndexedDB persistence reference (set during boot) store: null,
// Initialize with store reference for persistent caching init(storeRef) { this.store = storeRef; },
// Get from memory cache get(url) { return this.memory.get(url); },
// Check if image is cached in memory has(url) { return this.memory.has(url); },
// Save to both memory and persistent storage async set(url, imageBitmap) { // Save to memory this.memory.set(url, imageBitmap);
// Save to IndexedDB for persistence across refreshes if (this.store && imageBitmap) { try { let pixelData, imgWidth, imgHeight;
if (imageBitmap.pixels) { // Already a pixel buffer { width, height, pixels } imgWidth = imageBitmap.width; imgHeight = imageBitmap.height; pixelData = imageBitmap.pixels; } else { // ImageBitmap — convert to pixel data via canvas drawImage const canvas = typeof OffscreenCanvas !== 'undefined' ? new OffscreenCanvas(imageBitmap.width, imageBitmap.height) : document.createElement('canvas'); canvas.width = imageBitmap.width; canvas.height = imageBitmap.height; const ctx = canvas.getContext('2d'); ctx.drawImage(imageBitmap, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); imgWidth = imageData.width; imgHeight = imageData.height; pixelData = imageData.data; }
// Store the pixel data along with metadata this.store[`image-cache:${url}`] = { width: imgWidth, height: imgHeight, data: Array.from(pixelData), cached: Date.now(), version: 1 }; this.store.persist(`image-cache:${url}`, "local:db"); } catch (error) { console.warn(`Failed to save image to persistent cache: ${url}`, error); } } },
// Load from persistent storage (IndexedDB) async loadFromPersistent(url) { if (!this.store) return null;
try { const data = await this.store.retrieve(`image-cache:${url}`, "local:db"); if (data && data.data && data.width && data.height) { // Reconstruct ImageData from stored array const imageData = new ImageData( new Uint8ClampedArray(data.data), data.width, data.height );
// Convert ImageData back to ImageBitmap const imageBitmap = await createImageBitmap(imageData);
// Cache in memory for future access this.memory.set(url, imageBitmap);
return imageBitmap; } } catch (error) { console.warn(`Failed to load image from persistent cache: ${url}`, error); } return null; },
// Clear all caches clear() { this.memory.clear(); },
// Check if URL is from aesthetic.computer media backend (safe to cache) isCacheable(url) { if (!url) return false; const urlStr = url.toString().toLowerCase(); return ( urlStr.includes('aesthetic.computer') || urlStr.includes('art.aesthetic.computer') || urlStr.includes('/media/@') || // User paintings urlStr.match(/\/@[\w-]+\/\d+/) // @handle/timestamp format ); }};
let screen;let currentDisplay; // TODO: Remove this? 22.09.29.11.38
// 🖼️ HD layer — an OffscreenCanvas 2D surface at native device resolution,// composited by bios above the pixel canvas (below glaze / corner labels /// ui). Pieces acquire it with `hd()` inside `paint`; disk owns the buffer:// it (re)allocates on resize, pre-scales the context so logical screen// coordinates stay valid, ships an ImageBitmap to bios after each painted// frame, and clears the bios layer when the piece changes. The HD layer is// NOT captured in tapes — pieces should keep painting the pixel buffer// underneath as the recorded / fallback view.let hdCanvas = null; // OffscreenCanvas backing store.let hdContext = null; // Its 2d context, pre-scaled to device resolution.let hdPainted = false; // A piece drew this frame → send a bitmap in `render`.let hdVisible = false; // bios's layer is currently showing one of our bitmaps.let hdPixelRatio = 1; // devicePixelRatio from init-from-bios (reframed updates it).const HD_MAX_PIXELS = 4096 * 2304; // Backing-store ceiling (≈4K) for perf.
function hd() { if (!screen || typeof OffscreenCanvas === "undefined") return null; // Previews/icons capture the PIXEL buffer, so hand that mode back to the // piece's low-res path by refusing the layer — an hd piece treats a null // return as "paint the pixel buffer this frame instead". Tapes stay hd: // bios live-encodes the hd bitmaps into the recording (see bios hdTape*). if (PREVIEW_OR_ICON) return null; const sub = currentDisplay?.subdivisions || 2; const dpr = currentDisplay?.pixelRatio || hdPixelRatio; // Native device pixels per logical screen pixel, capped so giant displays // never allocate an unbounded backing store. const maxScale = Math.sqrt(HD_MAX_PIXELS / (screen.width * screen.height)); const scale = Math.max(1, Math.min(sub * dpr, maxScale)); const w = Math.round(screen.width * scale); const h = Math.round(screen.height * scale); if (!hdCanvas || hdCanvas.width !== w || hdCanvas.height !== h) { hdCanvas = new OffscreenCanvas(w, h); hdContext = hdCanvas.getContext("2d"); } hdContext.setTransform(scale, 0, 0, scale, 0, 0); hdPainted = true; return { ctx: hdContext, width: screen.width, height: screen.height, scale };}
// Tear down between pieces: drop the buffer and hide bios's layer right away// (out-of-band, so the old piece's last frame never floats over the next).function hdUnload() { if (!hdCanvas) return; hdCanvas = hdContext = null; hdPainted = false; hdVisible = false; send({ type: "hd:clear" });}
let cursorCode;let pieceHistoryIndex = -1; // Gets incremented to 0 when first piece loads.let paintCount = 0n;let simCount = 0n;let booted = false;// let initialSim = true;let noPaint = false;let labelBack = false;let hiccupTimeout; // Prevent multiple hiccups from being triggered at once.
// 🌐 Check if running on a branded .com domain (e.g. notepat.com) and redirect to aesthetic.computerfunction redirectIfBrandedDomain() { const { hostname } = getSafeUrlParts(); if (!hostname) return false; // Match notepat.com or www.notepat.com (add more branded domains here as needed) if (hostname === "notepat.com" || hostname === "www.notepat.com") { send({ type: "web", content: { url: "https://aesthetic.computer", blank: false } }); return true; } if (hostname === "laklok.com" || hostname === "www.laklok.com") { send({ type: "web", content: { url: "https://aesthetic.computer", blank: false } }); return true; } return false;}
let storeRetrievalResolutions = {}, storeDeletionResolutions = {};
// USB flash support (Electron only)let usbDeviceListResolution = null;let usbFlashResolution = null;
// There are two instances of Socket that run in parallel...let socket, socketStartDelay; // Socket server for each piece.
// ❤️🔥 TODO: Explose these somehow to the $commonApi.
// TODO: Extract `chat` into an external class.
const chatDebug = location.host === "local.aesthetic.computer" || location.host === "localhost:8888" || location.host === "aesthetic.local:8888";const chatClient = new Chat(chatDebug, send);
let udp = { send: (type, content) => { send({ type: "udp:send", content: { type, content } }); }, receive: ({ type, content }) => { // console.log("🩰 Received `piece` message from UDP:", type, content);
// 🧚 Ambient cursor (fairies) support. Disabled for KidLisp pieces. if (type === "fairy:point" /*&& socket?.id !== id*/ && visible) { const isKidlisp = detectKidLispPiece({ currentPath, currentHUDTxt, currentText }) || (currentPath && currentPath.endsWith('.lisp')); if (!isKidlisp) { fairies.push({ x: content.x, y: content.y }); } return; }
udpReceive?.(type, content); }, kill: (outageSeconds) => { udp.connected = false; send({ type: "udp:disconnect", content: { outageSeconds } }); }, connected: false, }, udpReceive = undefined;
let scream = null; // 😱 Allow priviledged users to send alerts to everyone.// (A great end<->end socket + redis test.)let screaming = false;let screamingTimer; // Keep track of scream duration.
const fairies = []; // Render cursor points of other active users,// dumped each frame.
let glazeEnabled = false; // Keep track of whether glaze is on or off.
// *** Dark Mode ***// (By @tarighian)// Pass `true` or `false` to override or `default` to the system setting.function darkMode(enabled) { if (enabled === "default") { darkMode($commonApi.dark || false); store.delete("dark-mode"); actAlerts.push($commonApi.dark ? "dark-mode" : "light-mode"); return $commonApi.dark; } else { // true or false store["dark-mode"] = enabled; store.persist("dark-mode"); $commonApi.dark = enabled; actAlerts.push($commonApi.dark ? "dark-mode" : "light-mode"); return enabled; }}
// *** Store ***// This object is used to store and retrieve data across disks// The `local` method encodes everything as a JSON string.
// Note: It strangely also contains an API which could be redefined// unintentionally. 22.10.15.01.57// 22.11.25.11.07: The methods should be refactored out of the storage object / not share keys.const store = { persist: function (key, method = "local") { // Send data over the thread through a key in this object. send({ type: "store:persist", content: { key, data: this[key], method, }, }); // TODO: Turn the existing key into a retrieval function / promise? }, retrieve: function (key, method = "local") { const promise = new Promise((resolve) => { storeRetrievalResolutions[key] = resolve; });
send({ type: "store:retrieve", content: { key, method } });
return promise; }, delete: function (key, method = "local") { // Remove the key from the ram store, no matter what the method. delete store[key];
const promise = new Promise((resolve) => { storeDeletionResolutions[key] = resolve; });
send({ type: "store:delete", content: { key, method, }, }); return promise; },};
// Promise based API calls (through `bios` and back)let fileImport;let serverUpload, serverUploadProgressReporter;let zipCreation;// Pending authorize() calls, oldest first. Bios answers them in order, so a// queue keeps two requests in flight (a piece's inbox fetch and its census,// say) from clobbering each other — with a single slot the first promise// never settled and an awaiting boot hung on the noise forever.let authorizationRequests = [];let fileOpenRequest;let fileEncodeRequest;let gpuResponse;let web3Response;let tezosConnectResponse;let tezosDisconnectResponse;let tezosAddressResponse;let tezosSignResponse;let tezosCallResponse;
// Otherlet activeVideo; // TODO: Eventually this can be a bank to store video textures.let videoDeviceCount = 0;let lastActiveVideo;let videoSwitching = false;let preloadPromises = {};let inFocus;let loadFailure;
// 🎄 Piece code cache for merry preloading// Maps piece slug -> { code, blobUrl, type: 'mjs' | 'lisp' }const pieceCodeCache = new Map();
// Duration tracking for playlist progress bar (similar to tape system)let durationStartTime = null; // When the piece started (null if no duration) - using performance.now()let durationTotal = null; // Total duration in seconds (null if no duration)let durationProgress = 0; // Current progress (0-1)let durationCompleted = false; // Whether duration has completedlet durationBlinkState = false; // For blinking the completed barlet pageLoadTime = performance.now(); // Time when the page first loaded - using performance.now()
// 1. ✔ API
// TODO: Eventually add a wiggle bank so all wiggles are indexed// and start at random angles.// let wiggler = 0;let wiggleAngle = 0;
// TODO; Change this to true and update all brushes.// let NPdontPaintOnLeave = false;let NPnoOnLeave = false;
// 🔴 Recorder (Singleton)class Recorder { printProgress = 0; presentProgress = 0; printing = false; // Set by a callback from `bios`. printed = false; // " recording = false; // " rollingCallback; recorded = false; // " presenting = false; // " playing = false; // " cutCallback; printCallback; framesCallback; loadCallback;
tapeTimerStart; tapeProgress = 0; tapeTimerDuration; tapeFrameMode = false; // Whether we're recording based on frames instead of time tapeFrameStart = 0; // Starting frame count tapeFrameTarget = 0; // Target number of frames to record
videoOnLeave = false;
constructor() {}
tapeTimerSet(durationOrFrames, time, isFrameMode = false) { if (isFrameMode) { // Frame-based recording this.tapeFrameMode = true; this.tapeFrameStart = Number($commonApi.paintCount || 0n); this.tapeFrameTarget = durationOrFrames; this.tapeTimerStart = null; this.tapeTimerDuration = null;
log.tape.log(`Starting frame-based recording: ${durationOrFrames} frames`);
// Set a longer failsafe for frame-based recording (frames could take a while) if (this.failsafeTimeout) { clearTimeout(this.failsafeTimeout); }
this.failsafeTimeout = setTimeout(() => { log.tape.warn("Frame-based failsafe triggered! Recording may have stalled."); this.tapeProgress = 0; this.tapeFrameMode = false; this.tapeFrameStart = 0; this.tapeFrameTarget = 0;
if (typeof this.cut === 'function') { this.cut(() => { $commonApi.jump("video"); }); } else { log.tape.warn("Cut function not available in failsafe"); $commonApi.jump("video"); } this.failsafeTimeout = null; }, Math.max(30000, durationOrFrames * 1000)); // At least 30 seconds, or 1 second per frame } else { // Time-based recording (existing behavior) this.tapeFrameMode = false; this.tapeTimerStart = time; this.tapeTimerDuration = durationOrFrames;
// Add a failsafe timer as backup in case tapeTimerStep stops being called if (this.failsafeTimeout) { clearTimeout(this.failsafeTimeout); }
// Set a failsafe that will trigger cut if the normal timer fails this.failsafeTimeout = setTimeout(() => { log.tape.warn("Failsafe timer triggered! Normal timer may have failed."); if (this.tapeTimerDuration && this.tapeTimerStart) { this.tapeProgress = 0; this.tapeTimerStart = null; this.tapeTimerDuration = null;
if (typeof this.cut === 'function') { this.cut(() => { $commonApi.jump("video"); }); } else { console.warn(`🎬 ⚠️ Cut function not available in failsafe, manual jump to video`); $commonApi.jump("video"); } } this.failsafeTimeout = null; }, (durationOrFrames + 1) * 1000); // Add 1 second buffer to the failsafe } }
tapeTimerStep({ needsPaint, sound: { time } }) { if (this.tapeFrameMode) { // Frame-based recording if (!this.tapeFrameTarget) return;
const currentFrame = Number($commonApi.paintCount || 0n); const framesPassed = currentFrame - this.tapeFrameStart;
this.tapeProgress = framesPassed / this.tapeFrameTarget; needsPaint();
// Check if we've reached or exceeded the target frame count if (framesPassed >= this.tapeFrameTarget) { // Clear failsafe since normal timer completed if (this.failsafeTimeout) { clearTimeout(this.failsafeTimeout); this.failsafeTimeout = null; }
this.tapeProgress = 0; this.tapeFrameMode = false; this.tapeFrameStart = 0; this.tapeFrameTarget = 0;
log.tape.success(`Frame-based recording complete: ${framesPassed} frames`);
// Add safety check for callback existence if (typeof this.cut === 'function') { this.cut(() => { $commonApi.jump("video"); }); } else { log.tape.warn("Cut function not available"); $commonApi.jump("video"); } } } else { // Time-based recording (existing behavior) if (!this.tapeTimerDuration) return;
// Enhanced timing with race condition protection this.tapeProgress = (time - this.tapeTimerStart) / this.tapeTimerDuration; needsPaint();
const secondsOver = this.tapeProgress * this.tapeTimerDuration - this.tapeTimerDuration;
// Run for an extra 150 milliseconds. if (this.tapeProgress >= 1 && secondsOver > 0.15) { // Clear failsafe since normal timer completed if (this.failsafeTimeout) { clearTimeout(this.failsafeTimeout); this.failsafeTimeout = null; }
this.tapeProgress = 0; this.tapeTimerStart = null; this.tapeTimerDuration = null;
// Add safety check for callback existence if (typeof this.cut === 'function') { this.cut(() => { $commonApi.jump("video"); }); } else { log.tape.warn("Cut function not available"); $commonApi.jump("video"); } } } }
slate() { send({ type: "recorder:slate" }); // Kill the MediaRecorder instance. // TODO: Should printing and playing also be set to false? //$commonApi.rec.printing = false; // " $commonApi.rec.recording = false; // Reset this singleton. $commonApi.rec.recorded = false; // $commonApi.rec.printed = false; // " $commonApi.rec.printProgress = 0; // " $commonApi.rec.cleanMode = false; // Reset clean mode flag this.tapeFrameMode = false; this.tapeFrameStart = 0; this.tapeFrameTarget = 0; }
rolling(opts, cb) { send({ type: "recorder:rolling", content: opts }); this.rollingCallback = cb; }
cut(cb) { $commonApi.rec.cutCallback = cb; this.tapeProgress = 0; this.tapeTimerStart = null; this.tapeTimerDuration = null; this.tapeFrameMode = false; this.tapeFrameStart = 0; this.tapeFrameTarget = 0; send({ type: "signal", content: "recorder:cut" }); }
print(cb) { $commonApi.rec.printCallback = cb; send({ type: "recorder:print" }); }
present(noplay = false) { this.presentProgress = 0; send({ type: "recorder:present", content: { noplay } }); }
unpresent() { send({ type: "recorder:unpresent" }); }
play() { send({ type: "recorder:present:play" }); }
pause() { send({ type: "recorder:present:pause" }); }
requestFrames(cb) { $commonApi.rec.framesCallback = cb; send({ type: "recorder:request-frames" }); }}
let cachedAPI; // 🪢 This is a bit hacky. 23.04.21.14.59
const hourGlasses = [];
async function uploadPainting(picture, progress, handle, filename) { if (typeof picture === "string") { return { url: picture }; // Assume a URL and just return it. } else { // Assume picture is a buffer with `{ pixels, width, height }` // Upload it to the temporary bucket. filename ||= `painting-${num.timestamp()}.png`;
try { const data = await $commonApi.upload( filename, { pixels: picture.pixels, width: picture.width, height: picture.height, }, (p) => { console.log("Painting upload progress:", p); progress?.(p); }, !handle ? "art" : undefined, // Store in temporary if no HANDLE. ); console.log("🪄 Painting uploaded:", data.slug, data.ext, data.url); return data; } catch (err) { console.error("🪄 Painting upload failed:", err); } }}
// Returns whether leaving a piece and also can overwrite the value.function isLeaving(set) { if (set === true || set === false) leaving = set; return leaving;}
let docs; // Memorized by `requestDocs`.
let baseTime = Date.now(); // Virtual clock baselet baseReal = Date.now(); // Real time at last baseTimelet clockFetching = false;let lastServerTime = undefined;let clockOffset = 0; // Smoothed offset from serverconst playbackClock = createPlaybackClock(() => baseTime + (Date.now() - baseReal));
// 🤖 Robo Class - For sending synthetic events through the act system// 🤖 Robo: synthetic pen/event dispatcher decoupled from the hardware pen.// Maintains its own virtual pen state so the real mouse/touch doesn't// interfere with robot-driven gestures.class Robo { constructor() { this.currentAPI = null; // Virtual pen state — independent of $api.pen (which tracks the real mouse/touch). this.pen = { x: 0, y: 0, px: 0, py: 0, pressure: 0.5, delta: { x: 0, y: 0 }, device: "robot" }; }
setAPI(api) { this.currentAPI = api; }
// Send a synthetic event. The event payload carries its own coordinates — // downstream handlers (nopaint updateBrush etc.) should prefer these over // $api.pen for events with device === "robot". sendEvent(eventType, coordinates = {}) { if (!this.currentAPI) { console.warn("🤖 Robo: No API context available"); return; }
const x = coordinates.x ?? 0; const y = coordinates.y ?? 0; const px = coordinates.px ?? this.pen.x ?? 0; const py = coordinates.py ?? this.pen.y ?? 0; const pressure = coordinates.pressure ?? 0.5;
// Update the robo's own virtual pen (NOT $api.pen). this.pen.px = this.pen.x; this.pen.py = this.pen.y; this.pen.x = x; this.pen.y = y; this.pen.pressure = pressure; this.pen.delta.x = x - px; this.pen.delta.y = y - py; this.pen.device = "robot";
const eventPayload = { device: "robot", type: eventType, x, y, px, py, pressure, delta: { x: x - px, y: y - py }, };
try { this.currentAPI.act(eventType, eventPayload); } catch (error) { console.error("🤖 Robo: Error dispatching event:", error); } }
touch(x, y) { this.sendEvent("touch:1", { x, y, pressure: 0.5 }); } draw(x, y, px, py) { this.sendEvent("draw:1", { x, y, px, py, pressure: 0.5 }); } lift(x, y) { this.sendEvent("lift:1", { x, y, pressure: 0.5 }); } act(eventType, coordinates) { this.sendEvent(eventType, coordinates); }}
// 🎹 Persistent DAW state - survives frame updates since $commonApi.sound gets recreated each frameconst persistentDawState = { bpm: null, playing: null, time: null, sampleRate: null,};
// 🎛️ Persistent spreadnob stateconst persistentSpreadnobState = { note: null, target: null, value: null, active: null, min: null, max: null,};
const $commonApi = { lisp, // A global reference to the `kidlisp` evalurator. undef: undefined, // A global api shorthand for undefined. hd, // 🖼️ Native-resolution Canvas2D layer (see the hd() definition above). clock: { offset: function () { if (clockFetching) return;
// Skip API calls in OBJKT mode or SPIDER mode if (getPackMode() || (typeof window !== 'undefined' && window.acSPIDER)) { clockFetching = false; return; }
clockFetching = true; const t0 = Date.now();
fetch("/api/clock") .then((response) => { if (!response.ok) { return response.text().then((err) => { clockFetching = false; throw new Error( `Failed to fetch offset: ${response.status} ${err}`, ); }); }
return response.text().then((serverTimeISO) => { const t1 = Date.now(); const serverTime = new Date(serverTimeISO).getTime();
// Assume serverTime is the midpoint of request const rtt = t1 - t0; const approxClientMidpoint = t0 + rtt / 2; const targetOffset = serverTime - approxClientMidpoint;
// Blend the clock offset gradually (e.g. 10% of the way each resync) const blendFactor = 0.25; clockOffset += (targetOffset - clockOffset) * blendFactor;
// Recompute base time to keep virtual time in sync baseTime = Date.now() + clockOffset; baseReal = Date.now();
// console.log('synced')
lastServerTime = serverTime; clockFetching = false; }); }) .catch((err) => { console.error("Clock:", err); clockFetching = false; }); },
resync: function () { $commonApi.clock.offset(); },
time: function () { return new Date(playbackClock.time()); }, },
// Enable Pointer Lock penLock: () => { send({ type: "pen:lock" }); }, // Send a message to BIOS (for effect control, etc.) send: (msg) => { send(msg); }, chat: chatClient.system, dark: undefined, // If we are in dark mode. theme: { light: { wipeBG: 150, wipeNum: 200, }, dark: { wipeBG: 32, wipeNum: 64, }, }, glaze: function (content) { if (glazeEnabled === content.on) return; // Prevent glaze from being fired twice... glazeEnabled = content.on; // Send glaze message immediately when turning ON (don't wait for paint cycle) // This ensures glaze activates even when returning from a non-glaze piece if (content.on) { send({ type: "glaze", content }); } else { glazeAfterReframe = { type: "glaze", content }; } },
// Toggle HUD visibility (same as Tab key functionality) // skipSound: true for KidLisp pieces to avoid beep/bop sounds on tap toggleHUD: function (isDoubleTap = false, skipSound = false) { toggleHUDVisibility(isDoubleTap, skipSound); },
jump: function jump(to, ahistorical = false, alias = false) { console.log("🧭 jump() called:", to, "SOLO_MODE:", SOLO_MODE, "leaving:", leaving, "loading:", loading);
if (SOLO_MODE) { console.log("🔒 Jump blocked: solo mode active"); return; }
if (leaving) { console.log("🚪🐴 Jump cancelled, already leaving..."); return; } const jumpOut = to.startsWith("out:") || (to.startsWith("http") && platform.Aesthetic);
if (shop.indexOf(to) > -1) to = "/" + to; // Jump out for shop products.
if ( ((to.startsWith("http") || to.startsWith("/")) && !to.endsWith(".mjs")) || jumpOut ) { to = to.replace("out:", ""); try { // url = new URL(to); console.log("🐎 Jumping to web URL:", to); $commonApi.net.web(to, jumpOut); return; } catch (e) { // Could not construct a valid url from the jump, so we will be // running a local aesthetic.computer piece. console.log("🐎 URL construction failed, treating as local piece:", e.message); return; } } else { leaving = true; graph.unmask(); // Clear any active mask when leaving a piece }
// Preserve resolution parameters across jumps (device mode, TV mode, etc.) // This ensures HUD visibility and other display settings persist when navigating { const params = []; if (DEVICE_MODE) params.push("device=true"); if (TV_MODE) params.push("tv=true"); if (SOLO_MODE) params.push("solo=true"); if (HIGHLIGHT_MODE) { if (HIGHLIGHT_COLOR && HIGHLIGHT_COLOR !== "64,64,64") { params.push(`highlight=${encodeURIComponent(HIGHLIGHT_COLOR)}`); } else { params.push("highlight=true"); } } if (PERF_MODE) params.push("perf=true"); if (AUTO_SCALE_MODE) params.push("autoScale=true"); if (hideLabel) params.push("nolabel=true"); if (NOGAP_MODE) params.push("nogap=true"); if (SPOOF_AUDIO_MODE) params.push("spoofaudio=true");
if (params.length > 0) { // Query params must land BEFORE any #fragment, otherwise they get // glued into the hash (e.g. `painting#qwfV8wDk?nogap=true`) and the // fragment-based code lookup — like painting short codes from `#eou` // — resolves the corrupted string instead of the real code. const hashIdx = to.indexOf("#"); const base = hashIdx >= 0 ? to.slice(0, hashIdx) : to; const frag = hashIdx >= 0 ? to.slice(hashIdx) : ""; const separator = base.includes("?") ? "&" : "?"; to = base + separator + params.join("&") + frag; console.log("🧭 Preserving resolution params:", to); } }
function loadLine() { load(parse(to), ahistorical, alias, false, callback); }
let callback; leaveLoad = () => { // Intercept returns to the prompt when taping from a piece directly. if ($commonApi.rec.videoOnLeave && to.split("~")[0] === "prompt") { to = "video"; $commonApi.rec.videoOnLeave = false; $commonApi.rec.cut(loadLine); } else { if (to.split("~")[0] === "prompt" && globalKidLispInstance?.clearBakedLayers) { globalKidLispInstance.clearBakedLayers(); graph.unmask(); // Clear mask when returning to prompt from KidLisp } loadLine(); // Or just load normally. } }; return (cb) => (callback = cb); },
// 🎄 Preload piece modules for merry pipelines (prevents network latency during fast cycling) preloadPieces: async function preloadPieces(pieceNames, onProgress) { if (!pieceNames || pieceNames.length === 0) return;
const baseUrl = getBuiltInPieceBaseUrl(); // Report fraction-complete (0..1) as each piece settles, so callers can // drive a real loading spinner (e.g. `merryo ^pads` preloading 47 pieces). let done = 0; const total = pieceNames.length; const bump = () => { done += 1; try { onProgress?.(done / total); } catch (e) {} };
const fetchPromises = pieceNames.map(async (piece) => { // Skip if already cached if (pieceCodeCache.has(piece)) { console.log(`🎄 Piece already cached: ${piece}`); bump(); return; }
try { // Try .mjs first, then .lua, then .lisp const mjsUrl = `${baseUrl}/aesthetic.computer/disks/${piece}.mjs?v=${Date.now()}`; let response = await fetch(mjsUrl, { cache: 'no-store' });
if (response.ok) { const code = await response.text(); pieceCodeCache.set(piece, { code, type: 'mjs' }); console.log(`🎄 Preloaded mjs: ${piece}`); return; }
// Try .lua const luaUrl = `${baseUrl}/aesthetic.computer/disks/${piece}.lua?v=${Date.now()}`; response = await fetch(luaUrl, { cache: 'no-store' });
if (response.ok) { const code = await response.text(); pieceCodeCache.set(piece, { code, type: 'lua' }); console.log(`🎄 Preloaded lua: ${piece}`); return; }
// Try .lisp const lispUrl = `${baseUrl}/aesthetic.computer/disks/${piece}.lisp?v=${Date.now()}`; response = await fetch(lispUrl, { cache: 'no-store' });
if (response.ok) { const code = await response.text(); pieceCodeCache.set(piece, { code, type: 'lisp' }); console.log(`🎄 Preloaded lisp: ${piece}`); return; }
console.warn(`🎄 Could not preload piece: ${piece}`); } catch (err) { console.warn(`🎄 Error preloading ${piece}:`, err); } finally { bump(); } });
await Promise.all(fetchPromises); console.log(`🎄 Preloaded ${pieceCodeCache.size} pieces`); },
// Get cached piece code (returns undefined if not cached) getCachedPieceCode: function (pieceName) { return pieceCodeCache.get(pieceName); },
canShare: false, // Whether navigator.share is enabled for mobile devices. leaving: isLeaving, handle: () => { return HANDLE; }, notice: (msg, color = ["white", "green"], opts) => { notice = msg; noticeColor = color; noticeOpts = opts; const sound = {}; if (color[0] === "yellow" && color[1] === "red") sound.tone = 300; noticeBell(cachedAPI, sound); // The host shell (prompt.ac) covers the pixel buffer with its DOM, so // notices also travel up to render as crisp DOM flashes. if (shellHTMLMode) { send({ type: "notice:shell", content: { text: msg, color } }); } }, // 🪟 A crisp vector overlay, drawn on the native UI canvas (uiCtx) above the pixel // buffer. Hand it a display-list of ops each frame and bios replays them with // anti-aliased Canvas 2D — rounded rects, text, lines. For UI that wants to look // like an app, not pixels. Set null / omit to draw nothing. // overlay([["rr", x,y,w,h,r, [r,g,b,a]], ["text","W", x,y, size, [r,g,b,a], "center"], ...]) overlay: (list) => { overlay2D = list || null; }, // ⌛ Delay a function by `time` number of sim steps. delay: (fun, time) => { hourGlasses.push(new gizmo.Hourglass(time, { completed: () => fun() })); }, // Different syntax than `delay` but the same with looped behavior. blink: (time, fun) => { hourGlasses.push( new gizmo.Hourglass(time, { completed: () => fun(), autoFlip: true }), ); }, // 🎟️ Open a ticketed paywall on the page. ticket: (name) => { send({ type: "ticket-wall", content: name }); }, // 🪙 Mint a url or the `pixels` that get passed into the argument to a // network of choice. mint: async (picture, progress, params) => { console.log("🪙 Minting...", picture); // Determine if picture is a string or an object.
// A record will be attached if one exists via the prompt and the user is // logged in. let filename; let zipped; if (picture.record && HANDLE) { const record = picture.record; filename = `painting-${record[record.length - 1].timestamp}.png`; // Set filename based on record.
zipped = await $commonApi.zip( { destination: "upload", painting: { record } }, (p) => { console.log("🤐 Zip progress:", p); progress?.(p); }, );
console.log("🤐 Zipped:", zipped); }
const data = await uploadPainting(picture, progress, HANDLE, filename);
let description;
if (picture.width && picture.height) { description = `A ${picture.width}x${picture.height} pixel painting made on [aesthetic computer](https://aesthetic.computer).`; } else { description = `A painting made on [aesthetic computer](https://aesthetic.computer).`; }
if (data) { // Redirect to Zora. if (HANDLE && zipped) { description = `[\` ${HANDLE}/${data.slug}\`](https://aesthetic.computer/painting~${HANDLE}/${data.slug})\n\n${description}`; }
if (HANDLE) data.slug = `${HANDLE}/painting/${data.slug}`;
const pixels = `https://aesthetic.computer/api/pixel/2048:contain/${data.slug}.${data.ext}`;
// TODO: Also add the `zip` here? // Look into writing custom chunks into PNGS: https://chat.openai.com/c/0ed77e57-c5d2-4827-a733-c024da4bebd7
$commonApi.jump( encodeURI( `https://zora.co/create/single-edition?image=${pixels}&name=${ params[0] || "Untitled Painting" }&symbol=$${data.slug}&description=${description}`, ), ); } }, // 🖨️ Print either a url or the `pixels` that get passed into // the argument, with N quantity. print: async (picture, quantity = 1, progress) => { console.log("🖨️ Printing:", picture, "Quantity:", quantity); const data = await uploadPainting(picture, progress); let pixels; if (data && data.code) { // Use the short code (2-6 chars) which pixel.js can look up in the database pixels = `${data.code}.${data.ext}`; } else if (data && data.slug) { // Fallback to slug if code is not available pixels = `${data.slug}.${data.ext}`; } else if (data) { pixels = data.url; } else { $commonApi.notice("UPLOAD ERROR", ["red", "yellow"]); return; }
try { const headers = { "Content-Type": "application/json" }; try { // Include authorization token if logged in. const token = await $commonApi.authorize(); // Get user token. if (token) headers.Authorization = `Bearer ${token}`; } catch (err) {} // Handled up-stream.
const res = await fetch(`/api/print?new=true&pixels=${pixels}`, { method: "POST", headers, body: JSON.stringify({ quantity, slug: $commonApi.slug }), // TODO: Add order info here. ^ });
const data = await res.json(); if (!res.ok) throw new Error( `🖨️ Print: HTTP error! Status: ${JSON.stringify(data)}`, ); console.log("🖨️ Print order:", data); $commonApi.jump(data.location); // Redirect to checkout. } catch (error) { console.error("🖨️ Print order error:", error); } }, // ☕ Mug - Print a painting on a ceramic mug with optional color. // `picture` can be a painting object OR a code string like "abc" or "#abc" mug: async (picture, color = "white", quantity = 1, progress) => { console.log("☕ Mug:", picture, "Color:", color, "Quantity:", quantity); let pixels;
// Check if picture is a code string (e.g., "abc" or "#abc") if (typeof picture === "string") { const code = picture.startsWith("#") ? picture.slice(1) : picture; pixels = `${code}.png`; // Assume PNG for existing paintings console.log("☕ Using existing painting code:", code); } else { // Upload the painting const data = await uploadPainting(picture, progress); if (data && data.code) { pixels = `${data.code}.${data.ext}`; } else if (data && data.slug) { pixels = `${data.slug}.${data.ext}`; } else if (data) { pixels = data.url; } else { $commonApi.notice("UPLOAD ERROR", ["red", "yellow"]); return; } }
try { const headers = { "Content-Type": "application/json" }; try { const token = await $commonApi.authorize(); if (token) headers.Authorization = `Bearer ${token}`; } catch (err) {}
const res = await fetch(`/api/mug?new=true&pixels=${pixels}&color=${color}`, { method: "POST", headers, body: JSON.stringify({ quantity, slug: $commonApi.slug }), });
const data = await res.json(); if (!res.ok) throw new Error( `☕ Mug: HTTP error! Status: ${JSON.stringify(data)}`, ); console.log("☕ Mug order:", data); $commonApi.jump(data.location); } catch (error) { console.error("☕ Mug order error:", error); } }, // Create a zip file of specified content. (Used for storing painting data.) zip: (content, progress) => { const prom = new Promise((resolve, reject) => { zipCreation = { resolve, reject }; });
if (content.destination === "upload") { serverUploadProgressReporter = progress; serverUploadProgressReporter?.(0); }
send({ type: "zip", content }); return prom; }, // Track device motion. motion: { on: false, start: () => { send({ type: "motion:start" }); }, stop: () => { send({ type: "motion:stop" }); // TODO: Automatically stop when changing a disk? }, current: {}, // Will get replaced by an update event. }, // 🔷 Tezos wallet management (via bios.mjs) wallet: { _state: { connected: false, address: null, balance: null, network: "ghostnet", domain: null, // .tez domain if resolved }, _stateRequested: false, // Track if we've requested state from bios // Get current wallet state (requests from bios if not yet synced) get: function () { // Request fresh state from bios on first call if (!$commonApi.wallet._stateRequested) { $commonApi.wallet._stateRequested = true; send({ type: "wallet:get-state" }); } return { ...$commonApi.wallet._state }; }, // Connect wallet (with optional address for manual entry) connect: function (options = {}) { // Support both connect(network) and connect({ address, network }) if (typeof options === "string") { send({ type: "wallet:connect", content: { network: options } }); } else { send({ type: "wallet:connect", content: options }); } }, // Disconnect wallet disconnect: function () { send({ type: "wallet:disconnect" }); }, // Refresh balance from RPC refreshBalance: function () { send({ type: "wallet:refresh-balance" }); }, // Sign a message with the connected wallet sign: function (message, callback) { if (callback) $commonApi.wallet._signCallback = callback; send({ type: "wallet:sign", content: { message } }); }, // Get pairing URI for mobile wallet QR code getPairingUri: function (network = "mainnet", callback) { if (callback) $commonApi.wallet._pairingCallback = callback; send({ type: "wallet:get-pairing-uri", content: { network } }); }, // Internal callbacks _pairingCallback: null, _signCallback: null, // Check if connected isConnected: function () { return $commonApi.wallet._state.connected; }, // Request state sync from bios sync: function () { send({ type: "wallet:get-state" }); }, }, // Speak an `utterance` aloud. speak: function speak(utterance, voice = "female:18", mode = "cloud", opts) { return send({ type: "speak", content: { utterance, voice, mode, opts } }); }, // Broadcast an event through the entire act system. act: (event, data = {}) => { // console.log("Acted:", event); Show the synthetic event. data.is = (e) => e === event; cachedAPI.event = data; try { act(cachedAPI); } catch (e) { console.warn("️ ✒ Act failure...", e); } }, // 🚥 `Get` api // Retrieve media assets from a user account. get: { picture: (url) => { return $commonApi.net.preload( encodeURI(url), true, undefined, // byOpts, ); }, painting: (code, opts) => { return { by: async function (handle = "anon", byOpts) { const extension = opts?.record ? "zip" : "png";
// Use the same origin-aware URL construction logic as module loading const { protocol, hostname } = getSafeUrlParts();
let baseUrl; // Check if we're in a development environment (localhost with port) const isDevelopment = hostname === 'localhost' && typeof location !== 'undefined' && location.port; if (isDevelopment) { // Use the local development server baseUrl = `${protocol}//${hostname}:${location.port}`; } else { // Use the production server for sandboxed iframes or production baseUrl = `https://aesthetic.computer`; }
// Detect timestamp format (YYYY.M.D.H.M.S.mmm or YYYY.MM.DD.HH.MM.SS.mmm) const isTimestamp = code.match(/^\d{4}\.\d{1,2}\.\d{1,2}\.\d{1,2}\.\d{1,2}\.\d{1,2}\.\d{1,3}$/);
let mediaUrl; if (isTimestamp && handle && handle !== "anon") { // Legacy user painting with timestamp - use old format const handleWithAt = handle.startsWith('@') ? handle : `@${handle}`; mediaUrl = `${baseUrl}/media/${handleWithAt}/painting/${code}.${extension}`; } else { // 3-letter code or anonymous painting - use new endpoint mediaUrl = `${baseUrl}/media/paintings/${code}.${extension}`; }
return $commonApi.net.preload( mediaUrl, true, undefined, byOpts, ); }, }; }, }, // ***Actually*** upload a file to the server. // 📓 The file name can have `media-` which will sort it on the server into // a directory via `presigned-url.js`. upload: async (filename, data, progress, bucket, recordingSlug, metadata) => { const prom = new Promise((resolve, reject) => { serverUpload = { resolve, reject }; }); serverUploadProgressReporter = progress; serverUploadProgressReporter?.(0);
console.log("Uploading:", filename, { width: data?.width, height: data?.height });
send({ type: "upload", content: { filename, data, bucket, recordingSlug, metadata } }); return prom; }, code: { channel: (chan) => { codeChannel = chan; // Set the current `codeChannel`. store["code-channel"] = codeChannel; // Store and keep it in the browser. store.persist("code-channel"); if (!codeChannel || codeChannel?.length === 0) { console.log("📭 Code channel cleared!"); } else { console.log("📬 Code channel set to:", codeChannel); } socket.send("code-channel:sub", codeChannel); // Tell any parent iframes that the channel has been updated. send({ type: "post-to-parent", content: { type: "setCode", value: codeChannel }, }); // ❤️🔥 // TODO: Should return a promise here, and wait for a `code-channel:subbed` // event, that way users get better confirmation if the socket // doesn't go through / there is a server issue. 23.07.04.18.01 }, // Store a painting code mapping (timestamp/slug -> 3-letter code) store: (slug, handle, code) => { codeCache.storeCode(slug, handle, code); }, // Get a painting code from timestamp/slug get: (slug, handle) => { return codeCache.getCode(slug, handle); }, }, encode: async (file) => { // File should be { type, data } where type is "png", "webp", "jef", etc. const prom = new Promise((resolve, reject) => { fileEncodeRequest = { resolve, reject }; }); send({ type: "file-encode:request", content: file }); return prom; }, // Open a local file picker. Pass { mode: "video" } (and optionally // { accept }) to pick a clip and resolve with // { kind:"video", data:ArrayBuffer, mime, name, duration }; the default // resolves with a bitmap (image picker). file: async (opts = {}) => { const prom = new Promise((resolve, reject) => { fileOpenRequest = { resolve, reject }; }); send({ type: "file-open:request", content: opts }); return prom; }, // Authorize a user. authorize: async () => { // TODO: This should always fail while running user code. // console.log("Sending auth request..."); const prom = new Promise((resolve, reject) => { authorizationRequests.push({ resolve, reject }); }); send({ type: "authorization:request" }); return prom; }, // Get a token for a logged in user. // Hand-tracking. 23.04.27.10.19 TODO: Move eventually. hand: { mediapipe: { screen: [], world: [], hand: "None" } }, hud: { label: (text, color, offset, plainTextOverride) => { currentHUDTxt = text; // Use plainTextOverride if provided, otherwise strip color codes from text currentHUDPlainTxt = plainTextOverride || stripColorCodes(text); // Store plain text version
if (!color) { currentHUDTextColor = currentHUDTextColor || graph.findColor(color); } else { currentHUDTextColor = graph.findColor(color); } currentHUDOffset = offset;
// Calculate and store dimensions for animation when directly called // (This ensures kidlisp pieces have proper animation geometry) if (currentHUDTxt && currentHUDTxt.length > 0) { // Use plain text (without color codes) for dimension calculations const textForMeasurement = currentHUDPlainTxt || currentHUDTxt;
// Detect if this is a KidLisp piece for more generous width allowance const sourceCode = currentText || currentHUDTxt; const isKidlispPiece = (currentPath && lisp?.isKidlispSource && lisp.isKidlispSource(currentPath) && !currentPath.endsWith('.lisp')) || currentPath === "(...)" || (sourceCode && sourceCode.startsWith("$")) || (currentPath && currentPath.includes("/disks/$")) || (sourceCode && lisp?.isKidlispSource && lisp.isKidlispSource(sourceCode));
// Use full screen width for text wrapping const maxWidth = cachedAPI.screen.width;
const labelBounds = cachedAPI.text.box( textForMeasurement, undefined, maxWidth, 1, // scale true, // wordWrap // TODO: This should check useTinyHudLabel like the main corner label, but that's not accessible here // For now, use default font to maintain consistency with current behavior undefined // Use default font - will need to be updated when useTinyHudLabel is globally accessible );
// Use the actual computed width from text.box instead of character count const measuredWidth = labelBounds.box.width; const fallbackShareWidth = (currentHUDLabelBlockWidth || DEFAULT_TYPEFACE_BLOCK_WIDTH) * "share ".length; const shareWidth = Math.max(currentHUDShareWidth || 0, fallbackShareWidth); currentHUDLeftPad = shareWidth; const baseLabelWidth = measuredWidth + shareWidth; const scrubExtension = Math.max(0, currentHUDScrub); const h = labelBounds.box.height + cachedAPI.typeface.blockHeight;
currentHUDLabelMeasuredWidth = baseLabelWidth; hudAnimationState.labelWidth = baseLabelWidth + scrubExtension; hudAnimationState.labelHeight = h; } }, // 📱 Set a QR code to display to the LEFT of the HUD label qr: (url) => { console.log("📱 HUD QR called with:", JSON.stringify(url), "current:", JSON.stringify(currentHUDQR)); if (url !== currentHUDQR) { currentHUDQR = url; currentHUDQRCells = null; // Clear cache to regenerate console.log("📱 HUD QR set to:", url); } }, // 🔤 Force MatrixChunky8 (tiny) font for HUD label tinyLabel: (enabled = true) => { forceTinyHudLabel = enabled; }, // 🌐 Set a compact suffix (e.g. ".com") after the HUD label. suffix: (text) => { currentHUDSuperscript = text || null; }, // Backward-compatible name for older pieces. superscript: (text) => { currentHUDSuperscript = text || null; }, currentStatusColor: () => currentHUDStatusColor, currentLabel: () => ({ text: currentHUDTxt, plainText: currentHUDPlainTxt, // Include plain text version btn: currentHUDButton, qrSize: currentHUDQRCells?.length || hudAnimationState?.qrSize || 0, leftPad: currentHUDLeftPad || 0, }), labelBack: () => { labelBack = true; // Store the source piece for special handling (e.g., chat returning to prompt) send({ type: "labelBack:source", content: { sourcePiece: $commonApi.piece } }); }, }, // 📱 LAN Dev mode identity (from session server) get devIdentity() { return devIdentity; }, send, platform, // USB flash support (Electron only) usb: { listDevices: () => new Promise((resolve) => { usbDeviceListResolution = resolve; send({ type: "usb:list-devices" }); }), flashImage: (opts) => new Promise((resolve) => { usbFlashResolution = resolve; send({ type: "usb:flash-image", content: opts }); }), }, history: [], // Populated when a disk loads and sets the former piece. // Trigger background music. // Eventually add an "@" style parameter similar to what a stamp system would have. bgm: { set: function (trackNumber, volume) { send({ type: "bgm-change", content: { trackNumber, volume } }); }, stop: () => send({ type: "bgm-stop" }), data: {}, }, system: { // 🐚 True when an HTML shell (prompt.ac) hosts the runtime and owns the // prompt / corner chrome in DOM — pieces skip their own low-res chrome. shellhtml: shellHTMLMode, // prompt: { input: undefined }, Gets set in `prompt_boot`. // 🎵 ac-electron drag-drop: set when a file is dropped onto the app // (Dock icon or running window). Pieces (notably `play`) read this on // boot; a `dropped:file` act event also fires for already-running // pieces. Cleared by the consumer. droppedFile: null, // 📦 Global update poll state — `ready` flips true when /api/version // long-poll detects a new deployment. Pieces can read this to render // their own update UI; the corner ↑ badge in disk.mjs is automatic. update: { get ready() { return globalUpdateReady; }, get versionInfo() { return globalVersionInfo; }, get recentCommits() { return globalRecentCommits; }, reload: () => send({ type: "window:reload" }), }, world: { // Populated in `world_boot` of `world.mjs`. teleported: false, telepos: undefined, teleport: (to, telepos) => { $commonApi.system.world.teleported = true; $commonApi.system.world.telepos = telepos; $commonApi.jump(to); }, }, nopaint: { //boot: nopaint_boot, // TODO: Why are these in the commonApi? 23.02.12.14.26 // act: nopaint_act, buffer: null, // An overlapping brush buffer that gets drawn on top of the // painting. piece: null, // Canonical code + pixel layer stack for No Paint paintings. syncWip: async (api = cachedAPI) => { if (!api) return null; const { syncACPaintingWip } = await import("./painting-wip.mjs"); return syncACPaintingWip(api); }, recording: false, record: [], // Store a recording here. gestureRecord: [], // Store the active gesture. startRecord: function (fullText) { const sys = $commonApi.system; sys.nopaint.record = []; // Clear any existing recording. sys.nopaint.recording = true; sys.nopaint.addToRecord({ label: fullText || "start", painting: { pixels: new Uint8ClampedArray(sys.painting.pixels), width: sys.painting.width, height: sys.painting.height, }, }); // console.log("🖌️🔴 Now recording:", sys.nopaint.record); }, addToRecord: function (record) { record.timestamp = num.timestamp(); // Insert the timestamp data. record.gesture = $commonApi.system.nopaint.gestureRecord.slice(); if (record.gesture.length === 0) delete record.gesture; $commonApi.system.nopaint.gestureRecord = []; $commonApi.system.nopaint.record.push(record); store["painting:record"] = $commonApi.system.nopaint.record; store.persist("painting:record", "local:db"); // console.log("🖌️🟠 Recorded a step:", record.label); }, is: nopaint_is, cancelStroke: nopaint_cancelStroke, undo: { paintings: undoPaintings }, needsBake: false, needsPresent: false, bakeOnLeave: false, addUndoPainting, // Regresses the system painting to a previous state. // Or the reverse... ("yes") no: ({ system, store, needsPaint }, yes = false) => { const paintings = system.nopaint.undo.paintings;
let dontRecord = false;
if (yes) { // ⏩ Fast-forward mode. undoPosition += 1; if (undoPosition > paintings.length - 1) { undoPosition = paintings.length - 1; dontRecord = true; } } else { // ⏪ Rewind mode. undoPosition -= 1; if (undoPosition < 0) { undoPosition = 0; dontRecord = true; } }
if (paintings.length > 1) { // Copy over the old picture here... const p = paintings[undoPosition];
const op = p.pixels; const pixels = new Uint8ClampedArray(op.length); pixels.set(op);
store["painting"] = { width: p.width, height: p.height, pixels, };
const resolutionChange = paintings[0].width !== paintings[1].width || paintings[0].height !== paintings[1].height;
// 🦢 Swap mode. // 'no' should swap... // const temp = paintings[0]; // paintings[0] = paintings[1]; // paintings[1] = temp;
store.persist("painting", "local:db");
// 🎨 Broadcast painting update to other tabs $commonApi.broadcastPaintingUpdate("updated", { source: "yes-no-decision" });
system.painting = store["painting"];
if (system.nopaint.recording && dontRecord === false) { const label = yes ? "yes" : "no"; system.nopaint.addToRecord({ label, //, // painting: { // width: system.painting.width, // height: system.painting.height, // pixels: new Uint8Array(system.painting.pixels), // }, }); }
if (resolutionChange) { system.nopaint.resetTransform({ system }); system.nopaint.storeTransform(store, system); }
needsPaint(); } }, // Center the picture within the screen / default translation. resetTransform: ({ system: sys }) => { sys.nopaint.zoomLevel = 1;
if (!sys.painting) { sys.nopaint.translation = { x: 0, y: 0 }; return; }
sys.nopaint.translation.x = floor( screen.width / 2 - sys.painting.width / 2, ); sys.nopaint.translation.y = floor( screen.height / 2 - sys.painting.height / 2, ); }, storeTransform: (store, sys) => { store["painting:transform"] = { translation: sys.nopaint.translation, zoom: sys.nopaint.zoomLevel, }; store.persist("painting:transform", "local:db"); }, translation: { x: 0, y: 0 }, zoomLevel: 1, translate: ({ system }, x, y) => { system.nopaint.translation.x += x; system.nopaint.translation.y += y; }, // zoom: ({ system }, dir) => { // system.nopaint.zoomLevel += dir === "in" ? 1 : -1; // console.log("🔭 Zoom level:", system.nopaint.zoomLevel); // if (system.nopaint.zoomLevel <= 0) system.nopaint.zoomLevel = 1; // // TODO: Adjust the translation based on system.nopaint.brush.x and y // // Which would serve as the zoom origin point. // }, zoom: ({ system }, dir, cursor) => { // Store old zoom level const oldZoomLevel = system.nopaint.zoomLevel;
// Adjust the zoom level system.nopaint.zoomLevel += dir === "in" ? 1 : -1;
// Ensure zoom level is at least 1 if (system.nopaint.zoomLevel <= 0) system.nopaint.zoomLevel = 1;
// Calculate the scaling factor based on the change in zoom levels const scale = system.nopaint.zoomLevel / oldZoomLevel;
// Adjust the translation based on the scaling factor and the cursor's position system.nopaint.translation.x = floor( cursor.x + (system.nopaint.translation.x - cursor.x) * scale, ); system.nopaint.translation.y = floor( cursor.y + (system.nopaint.translation.y - cursor.y) * scale, ); }, brush: { x: 0, y: 0, dragBox: undefined }, transform: (p) => { return { x: (p.x - nopaintAPI.translation.x) / nopaintAPI.zoomLevel, y: (p.y - nopaintAPI.translation.y) / nopaintAPI.zoomLevel, }; }, // Similar to `updateBrush` but for arbitrary points, // with no change to `system.nopaint.brush`. pointToPainting: ({ system, pen }) => { const zoom = system.nopaint.zoomLevel; const x = floor(((pen?.x || 0) - system.nopaint.translation.x) / zoom); const y = floor(((pen?.y || 0) - system.nopaint.translation.y) / zoom);
return { x, y }; }, updateBrush: ({ pen, event, system }, act) => { // Prefer the event's coordinates over $api.pen for robot-driven events // so the synthetic pen path doesn't get corrupted by hardware mouse moves. const source = (event && event.device === "robot") ? event : pen; // TODO: Use `pointToPainting` above. 23.10.11.08.49 const zoom = system.nopaint.zoomLevel; const x = floor(((source?.x || 0) - system.nopaint.translation.x) / zoom); const y = floor(((source?.y || 0) - system.nopaint.translation.y) / zoom);
if (act === "touch") { system.nopaint.startDrag = { x, y }; }
// Ensure startDrag exists before creating dragBox if (!system.nopaint.startDrag) { system.nopaint.startDrag = { x, y }; }
const dragBox = new geo.Box( system.nopaint.startDrag.x, system.nopaint.startDrag.y, x - system.nopaint.startDrag.x, y - system.nopaint.startDrag.y, );
system.nopaint.brush = { x, y, dragBox, pressure: source?.pressure || 0.5 };
// Call needsPaint during any pen movement to ensure continuous painting and HUD updates $commonApi.needsPaint(); },
// Helper to display the existing painting on the screen, with an // optional pan amount, that returns an adjusted pen pointer as `brush`.
// TODO: - [] Add Zoom // - [] And Rotation!
present: ( { system, screen, wipe, paste, ink, slug, dark, theme, blend }, tx, ty, ) => { system.nopaint.needsPresent = false;
// Guard: system.painting must exist for present to work if (!system.painting) { console.warn("🖼️ present: system.painting is undefined, skipping"); return; }
const x = tx || system.nopaint.translation.x; const y = ty || system.nopaint.translation.y;
system.nopaint.translation = { x, y };
const fullbleed = x === 0 && y === 0 && screen.width <= system.painting.width && screen.height <= system.painting.height;
if (fullbleed) { // If we are not panned and the painting fills the screen. paste(system.painting); paste(system.nopaint.buffer); } else { // If we are panned or the painting is a custom resolution.
wipe(theme[dark ? "dark" : "light"].wipeBG) .paste(system.painting, x, y, system.nopaint.zoomLevel);
// Normal alpha blending for overlay buffer paste(system.nopaint.buffer, x, y, system.nopaint.zoomLevel);
ink(128) .box( x, y, system.painting.width * system.nopaint.zoomLevel, system.painting.height * system.nopaint.zoomLevel, "outline", ); }
// 🎯 DEBUG: Draw dragBox overlay if one exists (COMMENTED OUT) /* if (system.nopaint.brush?.dragBox) { const dragBox = system.nopaint.brush.dragBox;
// Apply zoom and translation to the dragBox coordinates const overlayX = (dragBox.x * system.nopaint.zoomLevel) + x; const overlayY = (dragBox.y * system.nopaint.zoomLevel) + y; const overlayW = dragBox.w * system.nopaint.zoomLevel; const overlayH = dragBox.h * system.nopaint.zoomLevel;
// Draw the dragBox outline in bright green with some transparency ink(0, 255, 0, 180).box(overlayX, overlayY, overlayW, overlayH, "outline");
// Draw corner indicators const cornerSize = 4; ink(0, 255, 0, 220); // Top-left corner ink().box(overlayX - cornerSize/2, overlayY - cornerSize/2, cornerSize, cornerSize); // Top-right corner ink().box(overlayX + overlayW - cornerSize/2, overlayY - cornerSize/2, cornerSize, cornerSize); // Bottom-left corner ink().box(overlayX - cornerSize/2, overlayY + overlayH - cornerSize/2, cornerSize, cornerSize); // Bottom-right corner ink().box(overlayX + overlayW - cornerSize/2, overlayY + overlayH - cornerSize/2, cornerSize, cornerSize);
// Display coordinate info const coordText = `${dragBox.x},${dragBox.y} ${dragBox.w}x${dragBox.h}`; ink(0, 255, 0, 200).write(coordText, { x: overlayX, y: Math.max(overlayY - 15, 5) // Position above the box, but not off screen }, undefined, undefined, false, "MatrixChunky8"); } */
// Graph `zoomLevel` if (system.nopaint.zoomLevel !== 1 && slug !== "prompt") ink(255, 127).write(`${system.nopaint.zoomLevel}x`, { x: 6, y: 18 });
return { x, y, //, //brush: { x: (pen?.x || 0) - x, y: (pen?.y || 0) - y }, }; }, // Kill an existing painting. noBang: async ( { system, store, needsPaint, painting, theme, dark }, res = { w: screen.width, h: screen.height }, ) => { // console.log("deleting..."); const deleted = await store.delete("painting", "local:db"); await store.delete("painting:piece", "local:db"); await store.delete("painting:resolution-lock", "local:db"); await store.delete("painting:transform", "local:db"); system.nopaint.undo.paintings.length = 0; // Reset undo stack. system.painting = null; system.nopaint.piece = null; system.nopaint.resetTransform({ system, screen }); // Reset transform. needsPaint();
// Make a blank painting. // I don't like that these getters will not re-associate. system.painting = painting(res.w, res.h, ($) => { $.wipe(theme[dark ? "dark" : "light"].wipeNum); });
// Store only the pixel data, not the full painting object with functions store["painting"] = { width: system.painting.width, height: system.painting.height, pixels: system.painting.pixels, };
// Store the new painting BEFORE broadcasting store.persist("painting", "local:db");
// 🎨 Broadcast painting cleared to other tabs $commonApi.broadcastPaintingUpdate("cleared", { source: "clear", width: res.w, height: res.h, resetTransform: true // Flag that receiving tabs should reset their transform });
// Clear any existing painting recording in RAM and // storage. await store.delete("painting:record", "local:db"); if (system.nopaint.recording) { system.nopaint.recording = false; system.nopaint.record.length = 0; }
return deleted; }, // Replace a painting entirely, remembering the last one. // (This will always enable fixed resolution mode.) replace: ( { system, screen, store, needsPaint }, painting, message = "(replace)", ) => { store.delete("painting:piece", "local:db"); system.nopaint.piece = null; system.painting = painting; // Update references.
// Store only the pixel data, not the full painting object store["painting"] = { width: system.painting.width, height: system.painting.height, pixels: system.painting.pixels, };
store.persist("painting", "local:db"); // Persist to storage.
// 🎨 Broadcast painting replacement to other tabs $commonApi.broadcastPaintingUpdate("replaced", { source: "replace", message: message });
store["painting:resolution-lock"] = true; store.persist("painting:resolution-lock", "local:db"); system.nopaint.resetTransform({ system, screen }); // Reset transform. system.nopaint.storeTransform(store, system); system.nopaint.addUndoPainting(system.painting, message); system.nopaint.needsPresent = true; needsPaint(); }, abort: () => (NPnoOnLeave = true), }, }, // Paint all queued rendering commands immediately. flatten: () => { return painting.paint(true); }, connect: () => { const p = new Promise((resolve, reject) => { web3Response = { resolve, reject }; }); send({ type: "web3-connect" }); return p; }, // Tezos Wallet API tezos: { // Connect to a Tezos wallet (opens Beacon popup) connect: (network = "mainnet") => { const p = new Promise((resolve, reject) => { tezosConnectResponse = { resolve, reject }; }); send({ type: "tezos-connect", content: { network } }); return p; }, // Disconnect from Tezos wallet disconnect: () => { const p = new Promise((resolve, reject) => { tezosDisconnectResponse = { resolve, reject }; }); send({ type: "tezos-disconnect" }); return p; }, // Get current wallet address (null if not connected) address: () => { const p = new Promise((resolve, reject) => { tezosAddressResponse = { resolve, reject }; }); send({ type: "tezos-address" }); return p; }, // Sign a message with the connected wallet sign: (message) => { const p = new Promise((resolve, reject) => { tezosSignResponse = { resolve, reject }; }); send({ type: "tezos-sign", content: { message } }); return p; }, // Call a contract method call: (contractAddress, method, args, amount = 0) => { const p = new Promise((resolve, reject) => { tezosCallResponse = { resolve, reject }; }); send({ type: "tezos-call", content: { contractAddress, method, args, amount } }); return p; }, }, wiggle: function (n, level = 0.2, speed = 6) { wiggleAngle = (wiggleAngle + 1 * speed) % 360; const osc = sin(num.radians(wiggleAngle)); return n + n * level * osc; }, dark: true, // Dark mode. (Gets set on startup and on any change.) darkMode, // Toggle dark mode or set to `true` or `false`. // content: added programmatically: see Content class gpuReady: false, gpu: { message: (content) => { const p = new Promise((resolve, reject) => { gpuResponse = { resolve, reject }; }); send({ type: "gpu-event", content }); return p; }, }, // WebGPU 2D Renderer API webgpu: { enabled: false, // Flag to disable CPU renderer when true backend: null, // Preferred backend: "webgpu", "webgl2", "canvas2d", "vello", etc. // 🛑 Disable WebGPU and restore normal CPU rendering disable: () => { $commonApi.webgpu.enabled = false; $commonApi.webgpu.backend = null; send({ type: "webgpu-command", content: { type: "disable" } }); }, clear: (r = 0, g = 0, b = 0, a = 255) => { send({ type: "webgpu-command", content: { type: "clear", color: [r, g, b, a] } }); }, line: (x1, y1, x2, y2, r = 255, g = 255, b = 255, a = 255) => { send({ type: "webgpu-command", content: { type: "line", x1, y1, x2, y2, color: [r, g, b, a] } }); }, render: () => { send({ type: "webgpu-command", content: { type: "render" } }); }, // 📊 Toggle performance overlay perf: (enabled) => { send({ type: "webgpu-command", content: { type: "perf-overlay", enabled } }); }, }, // 📊 DOM-based stats overlay (always on top of everything, even GPU canvases) stats: { show: (data = {}) => { send({ type: "stats-overlay", content: { enabled: true, data } }); }, hide: () => { send({ type: "stats-overlay", content: { enabled: false } }); }, update: (data) => { send({ type: "stats-overlay", content: { data } }); }, }, // Deprecated in favor of `bios` -> `hitboxes`. (To support iOS) // clipboard: { // copy: (text) => { // send({ type: "copy", content: text }); // }, // }, text: { capitalize: text.capitalize, reverse: text.reverse, // Get the pixel width of a string of characters. width: (text, fontName) => { if (Array.isArray(text)) text = text.join(" ");
// Use specified font or fall back to current typeface const useTypeface = getTypefaceForMeasurement(fontName) || tf; if (!useTypeface) { return text.length * DEFAULT_TYPEFACE_BLOCK_WIDTH; }
const isProportional = useTypeface?.data?.proportional === true || !!useTypeface?.data?.advances || !!useTypeface?.data?.bdfFont;
if (isProportional && typeof useTypeface.getAdvance === "function") { let totalWidth = 0; for (const char of text) { if (char === "\n" || char === "\r") continue; const advance = useTypeface.getAdvance(char); totalWidth += typeof advance === "number" ? advance : useTypeface.blockWidth; } return totalWidth; } else { // For monospace fonts, use the typeface's block width const blockWidth = useTypeface.blockWidth || DEFAULT_TYPEFACE_BLOCK_WIDTH; return text.length * blockWidth; } }, height: (text) => { // Get the pixel height of a string of characters. return 10; }, // Return a text's bounding box. box: (text, pos = { x: 0, y: 0 }, bounds, scale = 1, wordWrap = true, fontName) => {
if (!text) { console.warn("⚠️ No text for `box`."); return; }
pos = { ...pos }; const absScale = abs(scale ?? 1);
let useTypeface = getTypefaceForMeasurement(fontName) || tf; if (!useTypeface) { useTypeface = tf; }
const baseBlockWidth = useTypeface?.blockWidth ?? tf?.blockWidth ?? DEFAULT_TYPEFACE_BLOCK_WIDTH; const baseBlockHeight = useTypeface?.blockHeight ?? tf?.blockHeight ?? DEFAULT_TYPEFACE_BLOCK_HEIGHT;
const blockHeight = baseBlockHeight * absScale;
const isProportional = useTypeface?.data?.proportional === true || !!useTypeface?.data?.advances || !!useTypeface?.data?.bdfFont;
const getAdvanceWidth = (char) => { if (!char) return baseBlockWidth * absScale; if (isProportional && typeof useTypeface?.getAdvance === "function") { const raw = useTypeface.getAdvance(char); if (typeof raw === "number") { return raw * absScale; } } return baseBlockWidth * absScale; };
const getCharWidth = (char) => { if (!char || char === "\n" || char === "\r") return 0; if (char === "\t") { return getAdvanceWidth(" ") * 4; } return getAdvanceWidth(char); };
if (bounds === undefined) { const sampleWidth = getAdvanceWidth("0"); bounds = (text.length + 2) * sampleWidth; } if (!(bounds > 0)) { bounds = Number.POSITIVE_INFINITY; }
const lines = [""]; const charMap = [[]]; let line = 0; let run = 0; let maxWidth = 0;
const commitLineWidth = () => { if (run > maxWidth) { maxWidth = run; } };
const newLine = () => { commitLineWidth(); line += 1; lines[line] = ""; charMap[line] = []; run = 0; };
const appendChar = (char, sourceIndex) => { const width = getCharWidth(char); const renderedChar = char === "\t" ? " " : char; lines[line] += renderedChar; charMap[line].push(sourceIndex); run += width; if (run > maxWidth) { maxWidth = run; } };
if (!wordWrap) { for (let idx = 0; idx < text.length; idx += 1) { const char = text[idx]; if (char === "\r") continue; if (char === "\n") { newLine(); continue; } appendChar(char, idx); } } else { const tokens = []; let idx = 0; while (idx < text.length) { const char = text[idx]; if (char === "\r") { idx += 1; continue; } if (char === "\n") { tokens.push({ type: "newline", indices: [idx] }); idx += 1; continue; } if (char === " " || char === "\t") { let textBuffer = ""; const indices = []; while (idx < text.length) { const c = text[idx]; if (c !== " " && c !== "\t") break; textBuffer += c; indices.push(idx); idx += 1; } tokens.push({ type: "whitespace", text: textBuffer, indices }); continue; } let textBuffer = ""; const indices = []; while (idx < text.length) { const c = text[idx]; if (c === "\n" || c === " " || c === "\t" || c === "\r") break; textBuffer += c; indices.push(idx); idx += 1; } if (textBuffer.length > 0) { tokens.push({ type: "word", text: textBuffer, indices }); } }
tokens.forEach((token) => { if (token.type === "newline") { newLine(); return; }
if (token.type === "whitespace") { for (let i = 0; i < token.text.length; i += 1) { const char = token.text[i]; const sourceIndex = token.indices[i]; const width = getCharWidth(char); if (run > 0 && run + width > bounds) { newLine(); } appendChar(char, sourceIndex); } return; }
if (token.type === "word") { const chars = token.text.split(""); const widths = chars.map((char) => getCharWidth(char)); const wordWidth = widths.reduce((acc, value) => acc + value, 0);
if (bounds > 0 && wordWidth > bounds) { chars.forEach((char, charIdx) => { const width = widths[charIdx]; if (run > 0 && run + width > bounds) { newLine(); } appendChar(char, token.indices[charIdx]); }); return; }
if (run > 0 && run + wordWidth > bounds) { newLine(); }
chars.forEach((char, charIdx) => { appendChar(char, token.indices[charIdx]); }); } }); }
commitLineWidth();
const lineHeightGap = 1 * absScale; const finalBlockHeight = blockHeight + lineHeightGap;
if (lines.length >= 1 && pos.center && pos.center.indexOf("y") !== -1) { pos.y = $activePaintApi.screen.height / 2 - (lines.length * finalBlockHeight) / 2 + finalBlockHeight / 2 + (pos.y || 0); }
const height = lines.length * finalBlockHeight;
const lineWidths = lines.map((lineText, index) => { if (!lineText) return 0; const indices = charMap[index] || []; let width = 0; for (let i = 0; i < lineText.length; i += 1) { const sourceIndex = indices[i]; if (typeof sourceIndex === "number" && sourceIndex >= 0 && sourceIndex < text.length) { width += getCharWidth(text[sourceIndex]); } else { width += getCharWidth(lineText[i]); } } return width; });
const maxLineWidth = lineWidths.reduce((acc, value) => Math.max(acc, value), 0);
const box = { x: pos.x, y: pos.y, width: maxLineWidth, height };
return { pos, box, lines, lineWidths, lineHeight: finalBlockHeight, charMap }; }, }, num: { add: num.add, wrap: num.wrap, even: num.even, odd: num.odd, clamp: num.clamp, wave: num.wave, rand: num.rand, randInt: num.randInt, randInd: num.randInd, randIntArr: num.randIntArr, randIntRange: num.randIntRange, rangedInts: num.rangedInts, multiply: num.multiply, perlin: num.perlin, dist: num.dist, dist3d: num.dist3d, radians: num.radians, degrees: num.degrees, lerp: num.lerp, map: num.map, arrMax: num.arrMax, arrCompress: num.arrCompress, Track: num.Track, timestamp: num.timestamp, p2: num.p2, midp: num.midp, number: num.number, intersects: num.intersects, signedCeil: num.signedCeil, signedFloor: num.signedFloor, vec2: vec2, vec3: vec3, vec4: vec4, mat3: mat3, mat4: mat4, quat: quat, parseColor: num.parseColor, findColor: num.findColor, saturate: num.saturate, desaturate: num.desaturate, shiftRGB: num.shiftRGB, rgbToHexStr: num.rgbToHexStr, hexToRgb: num.hexToRgb, blend: num.blend, rgbToHsl: num.rgbToHsl, hslToRgb: num.hslToRgb, rainbow: num.rainbow, zebra: num.zebra, resetZebraCache: num.resetZebraCache, }, geo: { Box: geo.Box, DirtyBox: geo.DirtyBox, Grid: geo.Grid, Circle: geo.Circle, linePointsFromAngle: geo.linePointsFromAngle, pointFrom: geo.pointFrom, Race: geo.Race, Quantizer: geo.Quantizer, }, ui: { Button: ui.Button, Slider: ui.Slider, TextButton: ui.TextButton, TextButtonSmall: ui.TextButtonSmall, TextInput: TextInput, TextFields: TextFields, }, help: { choose: help.choose, flip: help.flip, repeat: help.repeat, every: help.every, any: help.any, anyIndex: help.anyIndex, anyKey: help.anyKey, resampleArray: help.resampleArray, each: help.each, shuffleInPlace: help.shuffleInPlace, serializePainting: (painting) => { if (!painting) return; const pixels = uint8ArrayToBase64(painting.pixels); return { width: painting.width, height: painting.height, pixels }; }, deserializePainting: (painting) => { if (!painting) return; const pixels = base64ToUint8Array(painting.pixels); return { width: painting.width, height: painting.height, pixels }; }, }, gizmo: { Hourglass: gizmo.Hourglass, EllipsisTicker: gizmo.EllipsisTicker, Ticker: gizmo.Ticker }, rec: new Recorder(), robo: new Robo(), net: { signup: () => { send({ type: "signup" }); }, login: () => { store.delete("handle"); send({ type: "login" }); }, // { email } logout: () => { store.delete("handle"); send({ type: "logout" }); $commonApi.broadcast("logout:success"); chatClient.system?.server?.send("logout"); // Send a "logout" message here to the chat server.
// TODO: And probably the session server as well in // the future. 24.05.23.21.27 }, pieces: `${(() => { // In spider mode, always use main aesthetic.computer domain if (typeof window !== 'undefined' && window.acSPIDER) { return 'https://aesthetic.computer'; } const { protocol, hostname } = getSafeUrlParts(); return `${protocol}//${hostname}`; })()}/aesthetic.computer/disks`, parse, // Parse a piece slug. // lan: // Set dynamically. // host: // Set dynamically. // loadFailureText: // Set dynamically. // Make a user authorized / signed request to the api. // Used both in `motd` and `handle`. requestDocs: async () => { if (typeof docs === "object") return Promise.resolve(docs); return fetch("/docs.json") .then((response) => { if (response.status !== 200) { throw new Error("Network failure: " + response.status); } return response.json(); }) .then((d) => { docs = d; return docs; }) .catch((err) => console.error("🔴 📚 Couldn't get docs:", err)); }, // Get the authorization token for the current user (for streaming/custom requests) getToken: async () => { try { return await $commonApi.authorize(); } catch (error) { // Only log actual errors, not null (which just means not logged in) if (error) console.warn("🔑 getToken:", error); return null; } }, userRequest: async (method, endpoint, body) => { try { const token = await $commonApi.authorize(); // Get user token. if (!token) throw new Error("🧖 Not logged in.");
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json", };
const options = { method, headers }; if (body) options.body = JSON.stringify(body); const response = await fetch(endpoint, options);
if (response.status === 500) { try { const json = await response.json(); return { status: response.status, ...json }; } catch (e) { return { status: response.status, message: response.statusText }; } } else { const clonedResponse = response.clone(); try { return { ...(await clonedResponse.json()), status: response.status, }; } catch { return { status: response.status, body: await response.text() }; } } } catch (error) { // Mail request errors can contain submitted letters. Piece telemetry // captures this console, so discard provider text for these endpoints. if (/^\/api\/(?:mail|mail-status|tell)(?:[/?#]|$)/.test(endpoint)) { console.error("mail.request.failed"); } else { console.error("🚫 Error:", error); } return { message: "unauthorized" }; } }, // Loosely connect the UDP receiver. udp: (receive) => { udpReceive = receive; return udp; }, hiccup: (hiccupIn = 5, outageSeconds = 5) => { console.log("😵 Hiccuping in:", hiccupIn, "seconds."); clearTimeout(hiccupTimeout); hiccupTimeout = setTimeout(() => { console.log("😶🌫️ Hiccup!"); chatClient.system?.server.kill(outageSeconds); // Disconnect from chat. socket?.kill(outageSeconds); // Diconnect from socket session. udp?.kill(outageSeconds); // Disconnect from UDP. }, hiccupIn * 1000); }, // Remote debugging: Send log messages to session server for debugging on any device log: function(levelOrFilename, ...args) { // Handle both old API (filename, content) and new API (level, ...args) if (levelOrFilename.startsWith('/tmp/') || levelOrFilename.includes('.log')) { // Old API: filename, content const filename = levelOrFilename; const content = args[0];
// Send to session server if socket is available (removed verbose console log) if (socket && socket.send) { socket.send("dev-log", { level: "INFO", message: `${filename}: ${content}`, device: navigator.userAgent || "unknown", timestamp: Date.now() }); } } else { // New API: level, ...args const level = levelOrFilename;
// Serialize objects and create single-line message const serializedArgs = args.map(arg => { if (typeof arg === 'object' && arg !== null) { try { return JSON.stringify(arg); } catch (e) { return String(arg); } } return String(arg); });
const message = serializedArgs.join(" ");
// Always log locally first if (level === "warn") { console.warn(...args); } else if (level === "error") { console.error(...args); } else { console.log(...args); }
// Send to session server if socket is available if (socket && socket.send) { socket.send("dev-log", { level: level.toUpperCase(), message: message, device: navigator.userAgent || "unknown", timestamp: Date.now() }); } } }, }, needsPaint: () => { noPaint = false; if (system === "nopaint") { $commonApi.system.nopaint.needsPresent = true; } }, // TODO: Does "paint" needs this?
store, pieceCount: -1, // Incs to 0 when the first piece (usually the prompt) loads. // Increments by 1 each time a new piece loads. debug, nopaintPerf,
// 🎯 Cursor control API - pieces can request custom cursors cursor: (cursorStyle) => { send({ type: "cursor:set", content: { style: cursorStyle } }); },};
// Convenience methods for different log levels$commonApi.net.log.info = (...args) => $commonApi.net.log("info", ...args);$commonApi.net.log.warn = (...args) => $commonApi.net.log("warn", ...args);$commonApi.net.log.error = (...args) => $commonApi.net.log("error", ...args);
// 🎬 Recording UI overlays - UI elements drawn on screen but NOT captured in tape recordings// These are stored per-frame and cleared after sending to the main threadlet pendingRecordingUIOverlays = {};
// Add recording UI API to $commonApi$commonApi.recordingUI = { // Add an overlay that will be visible on screen but not recorded in tapes // name: unique identifier for the overlay // painting: a painting buffer created with $api.painting() // x, y: position to draw the overlay // opacity: optional opacity (0-1) add: function(name, painting, x, y, opacity = 1) { if (!painting || !painting.pixels) return; pendingRecordingUIOverlays[name] = { x: x, y: y, opacity: opacity, img: { width: painting.width, height: painting.height, pixels: painting.pixels, }, }; }, // Clear all recording UI overlays clear: function() { pendingRecordingUIOverlays = {}; }, // Get pending overlays (used internally when sending frame data) _getPending: function() { return pendingRecordingUIOverlays; }, // Clear pending overlays (used internally after sending) _clearPending: function() { pendingRecordingUIOverlays = {}; },};
chatClient.$commonApi = $commonApi; // Add a reference to the `Chat` module.
const nopaintAPI = $commonApi.system.nopaint;
// 🎨 Start painting change monitoring for cross-tab syncstartPaintingChangeMonitoring();
// Broadcast to other tabs in the same origin.const channel = new BroadcastChannel("aesthetic.computer");
channel.onmessage = (event) => { processMessage(event.data);};
async function processMessage(msg) { if (logs.messaging) console.log(`🗼 Processing broadcast: ${msg}`);
// 🎨 Handle painting updates (both new JSON format and legacy string format) if (msg.startsWith("painting:") || (msg.startsWith("{") && msg.includes('"type":"painting:updated"'))) { const isNopaintActive = $commonApi.system?.nopaint?.is?.("painting"); if (isNopaintActive) { console.log(`🚫 CROSS-TAB INTERFERENCE: Receiving painting update during nopaint operation - this may disrupt live preview!`, { msgPreview: msg.substring(0, 100) + '...', nopaintState: 'painting' }); } await handlePaintingUpdate(msg); return; }
if (msg.startsWith("handle:updated")) { // 👰♀️ Update the user handle if it changed. const newHandle = msg.split(":").pop(); HANDLE = "@" + newHandle; if (typeof window !== 'undefined') window.acHANDLE = HANDLE; // Expose for UDP identity if (typeof window !== 'undefined' && window.acBootCanvas?.setHandle) window.acBootCanvas.setHandle(HANDLE); send({ type: "handle", content: HANDLE }); store["handle:received"] = true; store["handle"] = newHandle; // store.persist("handle"); return; }
// Refresh the window if we logged in or out from another tab. if (msg === "login:success" && !USER) { $commonApi.net.refresh(); return; }
if (msg === "logout:success" && USER) { $commonApi.net.refresh(); // 🗒️ This should always be fast enough? return; }}
// 🎨 Handle painting update messages from other tabsasync function handlePaintingUpdate(msg) { try { // Parse the JSON message const data = JSON.parse(msg);
if (data.type !== "painting:updated") return;
// Skip if this message is from the current tab if (data.tabId === $commonApi._tabId) { console.log(`🎨 SKIPPED: Own message (${data.action})`); return; }
if (!$commonApi.system) { console.log(`🎨 SKIPPED: No system available`); return; }
// 📐 Handle resize events specifically if ((data.action === "resized" && data.metadata?.source === "screen_resize") || (data.action === "updated" && (data.source === "resize" || data.source === "crop"))) { const width = data.width || data.metadata?.width; const height = data.height || data.metadata?.height; console.log(`📐 DIMENSION CHANGE EVENT: ${width}x${height} (source: ${data.source || data.metadata?.source})`);
// For dimension change events, we need to wait for a "storage_complete" event $commonApi._processingResize = true; $commonApi._awaitingResizeStorage = { width, height, timestamp: data.timestamp };
console.log(`📐 Waiting for storage completion for ${data.source} ${width}x${height}...`); return; // Don't continue with regular painting sync for dimension change events }
// 💾 Handle storage completion events if (data.action === "storage_complete" && (data.source === "resize" || data.source === "crop")) { const awaitingResize = $commonApi._awaitingResizeStorage; console.log(`🪝 RECEIVED storage_complete:`, { source: data.source, timestamp: data.timestamp, awaitingResize: awaitingResize, timestampMatch: awaitingResize && data.timestamp === awaitingResize.timestamp });
if (awaitingResize && data.timestamp === awaitingResize.timestamp) { console.log(`📐 Storage completed for ${data.source} ${awaitingResize.width}x${awaitingResize.height}, applying...`);
try { const storedPainting = await store.retrieve("painting", "local:db"); if (storedPainting) { // Update system painting with the stored version $commonApi.system.painting = { width: storedPainting.width, height: storedPainting.height, pixels: new Uint8ClampedArray(storedPainting.pixels) };
// Store the painting data store["painting"] = { width: storedPainting.width, height: storedPainting.height, pixels: storedPainting.pixels };
// Update painting hash lastPaintingHash = generatePaintingHash($commonApi.system.painting);
// Update KidLisp with new painting reference const kidlispInstance = getGlobalKidLisp(); if (kidlispInstance) { kidlispInstance.setAPI($commonApi); console.log(`🎯 KidLisp API refreshed with ${data.source} painting: ${storedPainting.width}x${storedPainting.height}`); }
// Force repaint with new dimensions if ($commonApi.system.nopaint) { $commonApi.system.nopaint.needsPresent = true; } $commonApi.needsPaint();
console.log(`📐 Screen updated after ${data.source}: ${storedPainting.width}x${storedPainting.height}`); } } catch (error) { console.error(`📐 ERROR applying ${data.source} after storage completion:`, error); }
// Clean up delete $commonApi._awaitingResizeStorage; $commonApi._processingResize = false; return; } }
// Set processing flag to prevent feedback loops $commonApi._processingBroadcast = true;
try { // Load painting from storage (lightweight messages don't contain pixel data) const storedPainting = await store.retrieve("painting", "local:db"); if (storedPainting) { $commonApi.system.painting = { width: storedPainting.width, height: storedPainting.height, pixels: new Uint8ClampedArray(storedPainting.pixels) };
// Store only the pixel data, not the full painting object store["painting"] = { width: storedPainting.width, height: storedPainting.height, pixels: storedPainting.pixels };
// Update painting hash to prevent false positives lastPaintingHash = generatePaintingHash($commonApi.system.painting);
// 🎯 Update KidLisp with the new painting reference const kidlispInstance = getGlobalKidLisp(); if (kidlispInstance) { kidlispInstance.setAPI($commonApi); console.log(`🎯 KidLisp API refreshed with updated painting`); }
// Force repaint if ($commonApi.system.nopaint) { $commonApi.system.nopaint.needsPresent = true; } $commonApi.needsPaint();
// 🆕 Force nopan (reset transform) for clear/new actions or explicit flag if (((data.action === "cleared" || data.action === "new") || data.resetTransform) && $commonApi.system.nopaint) { console.log(`🎯 ${data.action?.toUpperCase() || 'TRANSFORM_RESET'}: Forcing nopan (reset transform) on receiver`); $commonApi.system.nopaint.resetTransform({ system: $commonApi.system, screen: $commonApi.screen }); }
console.log(`🎨 PAINTING SYNCED: ${storedPainting.width}x${storedPainting.height}`, { hash: generatePaintingHash($commonApi.system.painting)?.substr(0, 8), fromHash: data.hash || 'unknown', firstPixels: Array.from($commonApi.system.painting.pixels.slice(0, 12)) // First 3 pixels (RGBA) }); } else { // No painting found in storage } } catch (storageError) { console.error(`🎨 ERROR loading from storage:`, storageError); }
} catch (error) { console.error(`🎨 ERROR handling painting update:`, error);
// Fallback to old string-based format if (typeof msg === "string" && msg.startsWith("painting:")) { const action = msg.split(":")[1]; console.log(`🎨 FALLBACK: Processing legacy format ${action}`);
// Legacy handling - load from storage try { const storedPainting = await store.retrieve("painting", "local:db"); if (storedPainting) { $commonApi.system.painting = { width: storedPainting.width, height: storedPainting.height, pixels: new Uint8ClampedArray(storedPainting.pixels) };
// Store only the pixel data, not the full painting object store["painting"] = { width: storedPainting.width, height: storedPainting.height, pixels: storedPainting.pixels };
const kidlispInstance = getGlobalKidLisp(); if (kidlispInstance) { kidlispInstance.setAPI($commonApi); }
if ($commonApi.system.nopaint) { $commonApi.system.nopaint.needsPresent = true; } $commonApi.needsPaint(); } } catch (storageError) { console.error(`🎨 ERROR in fallback storage load:`, storageError); } } } finally { // Clear processing flag regardless of success or error setTimeout(() => { $commonApi._processingBroadcast = false; }, 50); }}
$commonApi.broadcast = (msg) => { processMessage(msg); // Process locally. channel.postMessage(msg);};
// 🎨 Broadcast painting updates to other tabs$commonApi.broadcastPaintingUpdate = (action, data = {}) => { if (!$commonApi.system?.painting) return;
// Check if we're in a nopaint operation and log it const isNopaintActive = $commonApi.system?.nopaint?.is?.("painting"); if (isNopaintActive) { console.log(`🚫 NOPAINT INTERFERENCE: Attempting to broadcast "${action}" during nopaint operation - this may disrupt live preview!`, { action, source: data.source, nopaintState: 'painting' }); }
// Throttle broadcasts to prevent excessive messages const now = Date.now(); if (now - lastBroadcastTime < broadcastThrottleDelay) { return; } lastBroadcastTime = now;
// Generate unique tab ID to prevent self-processing if (!$commonApi._tabId) { $commonApi._tabId = Math.random().toString(36).substr(2, 9); }
// Create lightweight notification message (no pixel data) const message = { type: "painting:updated", action, tabId: $commonApi._tabId, timestamp: now, width: $commonApi.system.painting.width, height: $commonApi.system.painting.height, ...data };
// Only send to other tabs (exclude self) channel.postMessage(JSON.stringify(message));};
// 🚀 Immediate painting broadcast - bypasses throttling for instant sync$commonApi.broadcastPaintingUpdateImmediate = (action, data = {}) => { if (!$commonApi.system?.painting || $commonApi._processingBroadcast) return;
// Check for nopaint interference const isNopaintActive = $commonApi.system?.nopaint?.is?.("painting"); if (isNopaintActive) { console.log(`🚫 IMMEDIATE NOPAINT INTERFERENCE: Attempting immediate broadcast "${action}" during nopaint operation!`, { action, source: data.source, nopaintState: 'painting' }); }
// Performance: Skip if we just broadcasted very recently (debounce) const now = Date.now(); if (now - (lastBroadcastTime || 0) < 16) { // ~60fps max broadcast rate return; }
// Generate unique tab ID if needed if (!$commonApi._tabId) { $commonApi._tabId = Math.random().toString(36).substr(2, 9); }
const paintingHash = generatePaintingHash($commonApi.system.painting); const message = { type: "painting:updated", action, tabId: $commonApi._tabId, timestamp: now, width: $commonApi.system.painting.width, height: $commonApi.system.painting.height, hash: paintingHash?.substr(0, 8), immediate: true, ...data };
// Immediate broadcasting silently
// Store painting immediately (async to not block) setTimeout(() => { store["painting"] = { width: $commonApi.system.painting.width, height: $commonApi.system.painting.height, pixels: $commonApi.system.painting.pixels, }; store.persist("painting", "local:db");
// 🪝 Fire storage completion hook for resize and crop events if (data.source === "resize" || data.source === "crop") { const storageCompleteMessage = { type: "painting:updated", action: "storage_complete", source: data.source, tabId: message.tabId, timestamp: message.timestamp, width: $commonApi.system.painting.width, height: $commonApi.system.painting.height };
channel.postMessage(JSON.stringify(storageCompleteMessage)); console.log(`🪝 STORAGE COMPLETE hook fired for ${data.source} ${$commonApi.system.painting.width}x${$commonApi.system.painting.height}`, { timestamp: message.timestamp, tabId: message.tabId.substr(0, 4) + '...' }); }
}, 0);
// Update hash to prevent duplicate detection lastPaintingHash = paintingHash; lastBroadcastTime = now;
channel.postMessage(JSON.stringify(message));};
// 🎨 Painting change detection systemfunction generatePaintingHash(painting) { if (!painting?.pixels) return null;
// Simple hash using width, height, and sample of pixels let hash = painting.width * 31 + painting.height * 37;
// Sample every 100th pixel to create a lightweight hash for (let i = 0; i < painting.pixels.length; i += 100) { hash = ((hash << 5) - hash + painting.pixels[i]) & 0xffffffff; }
return hash.toString(36);}
function startPaintingChangeMonitoring() { if (paintingChangeCheckInterval) return; // Already monitoring
// Painting change monitoring initialized silently
// Initialize hash baseline if painting exists if ($commonApi.system?.painting) { lastPaintingHash = generatePaintingHash($commonApi.system.painting); }
// Enable frame-based monitoring for immediate detection enableFrameBasedMonitoring();
// Keep interval-based monitoring as backup (lower frequency) paintingChangeCheckInterval = setInterval(() => { if (!$commonApi.system?.painting || frameBasedMonitoring) return; // Skip if frame monitoring is active
const currentHash = generatePaintingHash($commonApi.system.painting);
// Only broadcast if hash actually changed AND we're not currently processing a broadcast if (lastPaintingHash !== null && currentHash !== lastPaintingHash && !$commonApi._processingBroadcast) {
// Set flag to prevent feedback loops during broadcast processing $commonApi._processingBroadcast = true;
// Store updated painting store["painting"] = { width: $commonApi.system.painting.width, height: $commonApi.system.painting.height, pixels: $commonApi.system.painting.pixels, }; store.persist("painting", "local:db");
// Broadcast the change $commonApi.broadcastPaintingUpdate("updated", { source: "interval_monitor", hash: currentHash.substr(0,8) });
// Clear the processing flag after a brief delay setTimeout(() => { $commonApi._processingBroadcast = false; }, 100); }
lastPaintingHash = currentHash; }, 1000); // Slower backup monitoring (1 second)}
function stopPaintingChangeMonitoring() { frameBasedMonitoring = false; // Stop frame-based monitoring if (paintingChangeCheckInterval) { clearInterval(paintingChangeCheckInterval); paintingChangeCheckInterval = null; }}
// Spawn a session backend for a piece.async function session(slug, forceProduction = false, service) { let endPoint = "/session/" + slug; const params = { service }; if (forceProduction) params.forceProduction = 1; endPoint += "?" + new URLSearchParams(params);
const req = await fetch(endPoint);
let session; if (req.status === 200 || req.status === 304) { session = await req.text().then((text) => { try { return JSON.parse(text); } catch (e) { return text; } }); } else { session = await req.text(); }
if (typeof session === "string") return session;
// Session server is always ready (no Jamsocket polling needed) // Just return the session object with URLs return session;}
// Just for "update".const $updateApi = {};
// 🖼 Painting
// Pre-fab models:const QUAD = { type: "quad", positions: [ // Triangle 1 (Left Side) [-1, -1, 0, 1], // Bottom Left [-1, 1, 0, 1], // Top Left [1, 1, 0, 1], // Top Right // Triangle 2 (Right Side) [-1, -1, 0, 1], // Bottom Left [1, -1, 0, 1], // Bottom Right [1, 1, 0, 1], // Top Right ], indices: [ // These are not re-used for now. // One 0, 1, 2, //Two 3, 4, 5, ],};
// A cube of lines.const CUBEL = { type: "line", positions: [ // Back [-0.5, -0.5, 0.5, 1], // Down [-0.5, 0.5, 0.5, 1],
[-0.5, 0.5, 0.5, 1], // Across [0.5, 0.5, 0.5, 1],
[0.5, 0.5, 0.5, 1], // Up [0.5, -0.5, 0.5, 1],
[0.5, -0.5, 0.5, 1], // Back [-0.5, -0.5, 0.5, 1], // Front [-0.5, -0.5, -0.5, 1], // Down [-0.5, 0.5, -0.5, 1],
[-0.5, 0.5, -0.5, 1], // Across [0.5, 0.5, -0.5, 1],
[0.5, 0.5, -0.5, 1], // Up [0.5, -0.5, -0.5, 1],
[0.5, -0.5, -0.5, 1], // Back [-0.5, -0.5, -0.5, 1], // Bars (back to front) [-0.5, -0.5, 0.5, 1], // Top Left [-0.5, -0.5, -0.5, 1],
[-0.5, 0.5, 0.5, 1], // Bottom Left [-0.5, 0.5, -0.5, 1],
[0.5, 0.5, 0.5, 1], // Up [0.5, 0.5, -0.5, 1],
[0.5, -0.5, 0.5, 1], // Back [0.5, -0.5, -0.5, 1], ],};
const ORIGIN = { type: "line", positions: [ [-0.5, 0, 0, 1], // Horizontal X [0.5, 0, 0, 1], [0, 0, -0.5, 1], // Horizontal Z [0, 0, 2, 1], [0, -0.5, 0, 1], // Vertical [0, 0.5, 0, 1], ], colors: [ [255, 0, 0, 255], [255, 0, 0, 255], [0, 255, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [0, 0, 255, 255], ],};
const TRI = { type: "triangle", positions: [ [-1, -1, 0, 1], // Bottom Left [0, 1, 0, 1], // Top Left [1, -1, 0, 1], // Top Right // Triangle 2 (Right Side) ], indices: [0, 1, 2],};
const LINE = { type: "line", positions: [ [0, 0, 0, 1], // Bottom [0, 1, 0, 1], // Top ], indices: [0, 1],};
// Inputs: (r, g, b), (r, g, b, a) or an array of those.// (rgb) for grayscale or (rgb, a) for grayscale with alpha.// Or hex with "#000000" or "0x000000" or 0x000000.// TODO: Add `erase` anc all css color alpha support. 23.07.20.14.45// TODO: Add transparency and short hex to hex support.// TODO: Add better hex support via: https://stackoverflow.com/a/53936623/8146077
function ink() { // Fast path: plain numeric rgb / rgba goes straight to the colour setter. // Shaded 3D pieces call this once per triangle, so the general parser // (and its argument copies) would otherwise run thousands of times a frame. const n = arguments.length; if ((n === 3 || n === 4) && !inkFloodLoggingEnabled()) { const r = arguments[0], g = arguments[1], b = arguments[2]; const a = n === 4 ? arguments[3] : 255; if ( typeof r === "number" && typeof g === "number" && typeof b === "number" && typeof a === "number" && r === r && g === g && b === b && a === a ) return graph.color(r, g, b, a); } const foundColor = graph.findColor(...arguments); if (inkFloodLoggingEnabled()) { console.log( `${inkFloodLogPrefix()}🖍️ INK DEBUG`, { args: cloneArgsForLog(arguments), resolved: cloneColorForLog(foundColor) } ); } const result = graph.color(...foundColor); if (inkFloodLoggingEnabled()) { console.log( `${inkFloodLogPrefix()}🖍️ INK APPLIED`, { color: cloneColorForLog(result) } ); } return result;}
function ink2() { if (arguments[0] === null) { return graph.color2(null); } else { return graph.color2(...graph.findColor(...arguments)); }}
// 🎯 Global KidLisp Instance Managementfunction initializeGlobalKidLisp(api) { if (!globalKidLispInstance) { // console.log("🚀 Initializing global KidLisp instance"); if (typeof lisp.ensureGlobalInstance === "function") { globalKidLispInstance = lisp.ensureGlobalInstance(); } else if (typeof lisp.getGlobalInstance === "function") { globalKidLispInstance = lisp.getGlobalInstance() || new lisp.KidLisp(); } else { globalKidLispInstance = new lisp.KidLisp(); } globalKidLispInstance.setAPI(api); // Expose to window for bios.mjs acCLEAR_BAKE_LAYERS if (typeof window !== 'undefined') { window.__acGlobalKidLispInstance = globalKidLispInstance; } // Enable performance HUD if PERF_MODE is active if (PERF_MODE) { globalKidLispInstance.startPerformanceMonitoring(); } // Enable auto-density scaling if AUTO_SCALE_MODE is active if (AUTO_SCALE_MODE) { globalKidLispInstance.enableAutoDensity(); } } return globalKidLispInstance;}
function getGlobalKidLisp() { return globalKidLispInstance;}
// 🎵 Update KidLisp audio globals (safe for worker contexts)function updateKidLispAudio(audioData) { const payload = audioData && typeof audioData === "object" ? { ...audioData, __source: audioData.__source || "embedded:paintApi" } : audioData; if (globalKidLispInstance && globalKidLispInstance.updateAudioGlobals) { globalKidLispInstance.updateAudioGlobals(payload); } else if (payload?.amp > 0) { console.warn("🔊 updateKidLispAudio: no instance!", !!globalKidLispInstance, !!globalKidLispInstance?.updateAudioGlobals); }}
// 🔎 PAINTAPIconst $paintApi = { // 1. Composite functions (that use $activePaintApi) // (Must be unwrapped) // Prints a line of text using the default / current global font. // Argument options: // text, pos: {x, y, center}, bg (optional)
// Parameters: // text, x, y, options, wordWrap, customTypeface // text, pos, bg, bounds, wordWrap = true, customTypeface write: function () { let text = arguments[0], pos, bg, bounds, wordWrap = true, customTypeface = null, rotation = 0;
if (text === undefined || text === null || text === "" || !tf) return $activePaintApi; // Fail silently if no text.
text = typeof text === "object" && text !== null ? JSON.stringify(text) : text.toString();
// Assume: text, x, y, options, wordWrap, customTypeface if (typeof arguments[1] === "number") { pos = { x: arguments[1], y: arguments[2] }; const options = arguments[3]; bg = options?.bg; bounds = options?.bounds; wordWrap = options?.wordWrap === undefined ? wordWrap : options.wordWrap; customTypeface = options?.typeface; rotation = options?.rotation ?? options?.angle ?? 0; } else { pos = arguments[1]; bg = arguments[2]; bounds = arguments[3]; wordWrap = arguments[4] === undefined ? wordWrap : arguments[4]; customTypeface = arguments[5]; // Extract rotation from pos object if it exists rotation = pos?.rotation ?? pos?.angle ?? 0; }
if (customTypeface) { const resolvedTypeface = resolveTypefaceInstance(customTypeface); if (resolvedTypeface) { ensureTypefaceLoaded(resolvedTypeface); customTypeface = resolvedTypeface; } }
// 🎨 Color code processing const hasColorCodes = textContainsColorCodes(text);
if (hasColorCodes) {
// Remember the current ink color to restore it later const originalColor = $activePaintApi.inkrn();
// Process color codes into per-character color array let cleanText = ""; let charColors = []; let currentColor = null;
// Split text by color codes and process each segment. // [text, code, text, code, …] — the text halves arrive unescaped, so a // literal backslash the author typed lands in cleanText as one character. const segments = splitColorCodes(text);
for (let i = 0; i < segments.length; i++) { if (i % 2 === 0) { // This is regular text const segment = segments[i]; for (let j = 0; j < segment.length; j++) { cleanText += segment[j]; charColors.push(currentColor);
} } else { // This is a color name or RGB value (from the captured group) const colorStr = segments[i];
if (!colorStr) { continue; }
const normalized = colorStr.trim(); const lower = normalized.toLowerCase();
if (!normalized) { continue; }
if (lower === "reset" || lower === "default" || lower === "base") { currentColor = null; continue; }
if (normalized.includes(",")) { // RGB/RGBA format like "255,0,0" or "255,0,0,128" const parts = normalized.split(",").map((n) => { const parsed = parseInt(n.trim(), 10); return Number.isFinite(parsed) ? parsed : 0; }); while (parts.length < 3) parts.push(0); if (parts.length === 3) parts.push(255); currentColor = parts.slice(0, 4); } else if (lower === "transparent" || lower === "clear") { currentColor = [0, 0, 0, 0]; } else { const resolved = graph.findColor(normalized); if (Array.isArray(resolved)) { currentColor = resolved.slice(); } else if (resolved && typeof resolved === "object") { currentColor = { ...resolved }; } else if (resolved !== undefined) { currentColor = resolved; } else { currentColor = null; } }
} }
// Check if we have any actual text to display after removing color codes if (cleanText.trim().length === 0) { return $activePaintApi; // Exit silently if no text content remains }
// Render with colors - same logic as original text processing const scale = pos?.size || 1;
if (bounds) { const tb = $commonApi.text.box(cleanText, pos, bounds, scale, wordWrap, customTypeface); if (!tb || !tb.lines) { return $activePaintApi; // Exit silently if text.box fails }
const charMap = tb.charMap || [];
tb.lines.forEach((lineText, index) => { const renderedLine = typeof lineText === "string" ? lineText : lineText?.join?.(" ") || ""; const sourceIndices = charMap[index] || []; const lineColors = [];
for (let i = 0; i < renderedLine.length; i++) { const sourceIndex = sourceIndices[i]; if ( typeof sourceIndex === "number" && sourceIndex >= 0 && sourceIndex < charColors.length ) { lineColors.push(charColors[sourceIndex]); } else { lineColors.push(null); } }
// Add rotation to tb.pos const posWithRotation = { ...tb.pos, rotation };
(customTypeface || tf)?.print( $activePaintApi, posWithRotation, index, renderedLine, bg, lineColors, ); }); } else { // Break on `\n` and handle separate lines if (cleanText.indexOf("\n") !== -1) { const lines = cleanText.split("\n"); const lineHeightGap = 2; let charIndex = 0;
lines.forEach((line, index) => { const lineColors = charColors?.slice( charIndex, charIndex + line.length, ); (customTypeface || tf)?.print( $activePaintApi, { x: pos?.x, y: pos ? pos.y + index * (customTypeface || tf).blockHeight + lineHeightGap : undefined, rotation, }, 0, line, bg, lineColors, ); charIndex += line.length + 1; // +1 for the newline character }); } else { // Add rotation to pos const posWithRotation = { ...pos, rotation }; (customTypeface || tf)?.print($activePaintApi, posWithRotation, 0, cleanText, bg, charColors); } }
const typefaceName = typeof customTypeface === "string" ? customTypeface : customTypeface?.name; if (typefaceName === "MatrixChunky8" || typefaceName === "unifont") { const hasColorAssignments = charColors?.some((color) => { if (!color) return false; if (Array.isArray(color)) { return color.some((component) => component !== null && component !== undefined); } if (typeof color === "object") { return Object.keys(color).length > 0; } return typeof color === "string"; });
if (!hasColorAssignments) { const snippet = cleanText.length > 120 ? `${cleanText.slice(0, 117)}…` : cleanText; if (lastMatrixChunkyWriteDiagnosticLog !== snippet) { lastMatrixChunkyWriteDiagnosticLog = snippet; console.warn("🎨 MatrixChunky8 HUD charColors missing", { snippet, charColorsLength: charColors?.length, cleanTextLength: cleanText.length, }); } } }
// Restore the original ink color $activePaintApi.ink(...originalColor); return $activePaintApi; }
// 🎁 Original code for text without color codes // See if the text length is greater than the bounds, and if it is then // print on a new line. const scale = pos?.size || 1;
if (bounds) { const tb = $commonApi.text.box(text, pos, bounds, scale, wordWrap, customTypeface); // TODO: Get the current ink color, memoize it, and make it static here. // 23.10.12.22.04 tb.lines.forEach((lineText, index) => { const renderedLine = typeof lineText === "string" ? lineText : lineText?.join?.(" ") || ""; // Add rotation to tb.pos const posWithRotation = { ...tb.pos, rotation }; (customTypeface || tf)?.print($activePaintApi, posWithRotation, index, renderedLine, bg); }); } else { // Break on `\n` and handle separate lines if (text.indexOf("\n") !== -1) { const lines = text.split("\n"); // Split text on new line characters const lineHeightGap = 2; lines.forEach((line, index) => { (customTypeface || tf)?.print( $activePaintApi, { x: pos?.x, y: pos ? pos.y + index * (customTypeface || tf).blockHeight + lineHeightGap : undefined, rotation, }, 0, line, bg, ); // Adjust `lineHeight` as needed based on your text spacing }); } else { //if (text === "POW") console.log($activePaintApi.screen); 24.12.10.07.26 - Get write working with deferred rendering and page.
const actualFont = customTypeface || tf; // Add rotation to pos const posWithRotation = { ...pos, rotation }; actualFont?.print($activePaintApi, posWithRotation, 0, text, bg); // Or print a single line. } }
return $activePaintApi; }, // 2. Image Utilities clonePixels: graph.cloneBuffer, colorsMatch: graph.colorsMatch, color: graph.findColor, resize: graph.resize, // 3. 3D Classes & Objects Camera: graph.Camera, Form: graph.Form, Dolly: graph.Dolly, sign: graph.sign, cacheSignGlyph: graph.cacheSignGlyph, // Helper to get glyphs from a typeface for use with sign() glyphs: (fontName = "MatrixChunky8") => { const typeface = typefaceCache.get(fontName); return typeface?.glyphs || {}; }, TRI, QUAD, LINE, CUBEL, ORIGIN, // Project text glyphs onto a 3D plane, drawing each character's line/point // commands through a projection function. // Usage: ink(r,g,b,a).write3D("text", { origin, right, down, project, typeface }, scale) // origin: [x,y,z] — top-left of the first character in world space // right: [x,y,z] — unit-ish direction for text flow (gets scaled by glyph size) // down: [x,y,z] — unit-ish direction for descending lines // project: ([x,y,z]) => [screenX, screenY] — world-to-screen projection // typeface: optional typeface name (e.g. "MatrixChunky8") — defaults to active tf // scale: multiplier for glyph coordinate spacing (default 1) write3D: function (text, plane, scale = 1) { if (!text || !plane) return $activePaintApi; const { origin, right, down, project } = plane; if (!origin || !right || !down || !project) return $activePaintApi;
// Resolve typeface: use specified one from cache, or fall back to active tf let face = tf; if (plane.typeface && typefaceCache.has(plane.typeface)) { face = typefaceCache.get(plane.typeface); } if (!face) return $activePaintApi;
const blockW = face.blockWidth || face.data?.glyphWidth || 6; const fontData = face.data || {}; const advances = fontData.advances || {}; let cursorX = 0;
for (let ci = 0; ci < text.length; ci++) { const char = text[ci]; const glyph = face.getGlyph?.(char) || face.glyphs?.[char]; const advance = advances[char] || face.getAdvance?.(char) || blockW;
if (glyph) { // Apply glyph offset (baselineOffset preferred, then offset) let offX = 0, offY = 0; if (glyph.baselineOffset) { offX = glyph.baselineOffset[0] * scale; offY = glyph.baselineOffset[1] * scale; } else if (glyph.offset) { offX = glyph.offset[0] * scale; offY = glyph.offset[1] * scale; }
// Compute projected pixel size once for this glyph // by measuring the screen distance of one glyph-pixel step const projectPt = (gx, gy) => { const px = cursorX + (gx * scale + offX); const py = gy * scale + offY; const wx = origin[0] + px * right[0] + py * down[0]; const wy = origin[1] + px * right[1] + py * down[1]; const wz = origin[2] + px * right[2] + py * down[2]; return project([wx, wy, wz]); };
// Measure projected pixel size (distance for 1 glyph-pixel in each axis) const [refX, refY] = projectPt(0, 0); const [refRX, refRY] = projectPt(1, 0); const [refDX, refDY] = projectPt(0, 1); const pxW = Math.sqrt((refRX - refX) ** 2 + (refRY - refY) ** 2) || 1; const pxH = Math.sqrt((refDX - refX) ** 2 + (refDY - refY) ** 2) || 1; const pxSize = Math.max(1, Math.round((pxW + pxH) / 2));
// Optional per-pixel callback (lets caller vary ink per pixel) const pixelCb = plane.pixelCallback;
// Render each "on" pixel as a box at its projected center const renderPixel = (gx, gy) => { const [sx, sy] = projectPt(gx + 0.5, gy + 0.5); const bx = Math.floor(sx - pxSize / 2); const by = Math.floor(sy - pxSize / 2); if (pixelCb) pixelCb(bx, by, gx, gy, ci); $activePaintApi.box(bx, by, pxSize, pxSize); };
// BDF pixel-based glyphs (MatrixChunky8, etc.) if (glyph.pixels) { for (let row = 0; row < glyph.pixels.length; row++) { const pixelRow = glyph.pixels[row]; if (!Array.isArray(pixelRow)) continue; for (let col = 0; col < pixelRow.length; col++) { if (pixelRow[col] === 1) renderPixel(col, row); } } }
// Vector command-based glyphs (font_1, etc.) if (glyph.commands) { for (const { name, args } of glyph.commands) { if (name === "point") { renderPixel(args[0], args[1]); } else if (name === "line") { // Render pixel boxes at both endpoints + line between renderPixel(args[0], args[1]); renderPixel(args[2], args[3]); const [s1x, s1y] = projectPt(args[0], args[1]); const [s2x, s2y] = projectPt(args[2], args[3]); $activePaintApi.line( Math.floor(s1x), Math.floor(s1y), Math.floor(s2x), Math.floor(s2y), ); } } } } cursorX += advance * scale; } return $activePaintApi; },};
// TODO: Eventually move this to `num`. 24.07.23.18.52function normalizeAngle(angle) { return ((angle % 360) + 360) % 360;}
let turtleAngle = 270;let turtleDown = false;let turtlePosition = { x: 0, y: 0 };
// This is where I map the API functions that anyone can use, to the internal// code that represents them...
// Rendering of 3D forms.
const formsToClear = [];let backgroundColor3D = [0, 0, 0, 255];let formsSent = {}; // TODO: This should be cleared more often...
// `cpu: true` enabled software renderingfunction form( forms, cam = $commonApi.system.fps.doll.cam, { cpu, background } = { cpu: true, keep: true, background: backgroundColor3D, },) { // Exit silently if no forms are present. if (forms == null || forms?.length === 0) return;
if (cpu === true) { if (formReframing) { cam.resize(); formReframing = false; } if (Array.isArray(forms)) forms.filter(Boolean).forEach((form) => form.graph(cam)); else forms.graph(cam); } else { // GPU forms. if (!Array.isArray(forms)) forms = [forms];
// Clear out any forms that need deleting. formsToClear.forEach((id) => delete formsSent[id]); formsToClear.length = 0;
// Build a list of forms to send, ignoring already sent ones by UID. const formsToSend = [];
forms.filter(Boolean).forEach((form) => { // Clear out any trash in `formsSent` that do not have IDs left in forms. //if (formsSent[forms.uid])
// A. If the form has not been sent yet... if (formsSent[form.uid] === undefined && form.vertices.length > 0) { // Set the form to expire automatically if keep is false. formsToSend.push(form); formsSent[form.uid] = form; //console.log("Forms sent:", Object.keys(formsSent).length); form.gpuVerticesSent = form.vertices.length; form.gpuReset = false; } else { // B. If the form has been sent, but the form has changed and // needs a partial state update or is simply being redrawn. let msgCount = 0;
if (form.gpuRecolored === true) { formsToSend.push({ update: "form:color", uid: form.uid, color: form.color, }); msgCount += 1; form.gpuRecolored = false; }
// Transform the geometry. if (form.gpuTransformed === true) { formsToSend.push({ update: "form:transform", uid: form.uid, rotation: form.rotation, position: form.position, scale: form.scale, }); form.gpuTransformed = false; msgCount += 1; }
if (form.vertices.length > form.gpuVerticesSent || form.gpuReset) { // Add vertices to buffered forms. formsToSend.push({ update: "form:buffered:add-vertices", uid: form.uid, reset: form.gpuReset, vertices: form.vertices.slice(form.gpuVerticesSent), length: form.vertices.length, // TODO: These aren't being used anymore / they are generated from the GPU. pastLength: form.gpuVerticesSent, });
// Update form state now that we are sending the message. // TODO: Put these both under a "gpu" object in form. //console.log(form.gpuReset); form.gpuReset = false; form.gpuVerticesSent = form.vertices.length; msgCount += 1; }
if (msgCount === 0) { // Simply tell the system we are still drawing the form... otherwise // it will get cleared. formsToSend.push({ update: "form:touch", uid: form.uid, }); } } });
if (formsToSend.length === 0) return;
// console.log("Sending form...", performance.now())
// Only send a background update if the value changed. if (background !== backgroundColor3D) { send({ type: "gpu-event", content: { type: "background-change", content: background, }, }); backgroundColor3D = background; }
send({ type: "forms", content: { forms: formsToSend, cam: { position: cam.position, rotation: cam.rotation, scale: cam.scale, fov: cam.fov, near: cam.near, far: cam.far, }, color: graph.color(), }, });
// paintFormsResolution?.(); // return new Promise((resolve) => { // paintFormsResolution = resolve; // }); }}
// 🔑 Code resolution cache for #code -> @handle/slug lookupsconst codeCache = { memory: new Map(), // code -> @handle/slug resolution cache reverseMemory: new Map(), // @handle/slug -> code reverse lookup cache
async resolve(code) { // Return cached resolution if available if (this.memory.has(code)) { return this.memory.get(code); }
// Query painting-code API which returns slug and handle const response = await fetch(`/api/painting-code?code=${code}`); if (!response.ok) { throw new Error(`Code #${code} not found`); }
const data = await response.json();
// Build the path format: @handle/slug or just slug for anonymous let path; if (data.handle && data.handle !== 'anon') { path = `@${data.handle}/${data.slug}`; } else { path = data.slug; }
this.memory.set(code, path); this.reverseMemory.set(path, code); return path; },
// Store a mapping from timestamp to code (called by profile.mjs and other pieces) storeCode(slug, handle, code) { if (!slug || !code) return;
// Normalize the key format to match what we use elsewhere const normalizedHandle = handle?.replace(/^@+/, ''); const key = normalizedHandle ? `@${normalizedHandle}/${slug}` : slug;
this.reverseMemory.set(key, code); this.memory.set(code, key); },
// Get code from timestamp+handle getCode(slug, handle) { const normalizedHandle = handle?.replace(/^@+/, ''); const key = normalizedHandle ? `@${normalizedHandle}/${slug}` : slug; return this.reverseMemory.get(key); }};
// 🔑 Expose code cache debug utilityif (typeof window !== "undefined") { window.acCodeCacheStats = () => { console.log("🔑 Code resolution cache stats:", { cacheSize: codeCache.memory.size, codes: Array.from(codeCache.memory.entries()).map(([code, path]) => ({ code: `#${code}`, resolvedTo: path })) }); };}
// Used by `paste` and `stamp` to prefetch bitmaps of the network as needed.// Occurs also when loading a piece's source code.async function prefetchPicture(code) { const originalCode = code;
// Strip # prefix if present to get the actual code let actualCode = code; if (code.startsWith('#')) { actualCode = code.slice(1); }
if (paintings[code] === "fetching") return;
// 🖼️ Check multi-level cache first // Level 1: Check if already in paintings cache if (paintings[code] && paintings[code] !== "fetching") { return; }
// Level 2: Check in-memory image cache if (imageCache.has(code)) { paintings[code] = imageCache.get(code); return; }
// Level 3: Check persistent storage (IndexedDB) if (imageCache.isCacheable(code)) { const cachedImage = await imageCache.loadFromPersistent(code); if (cachedImage) { // console.log("🖼️ Loaded from cache:", code); paintings[code] = cachedImage; return; } }
// Level 3.5: Check for embedded painting bitmaps (pack mode / bundled HTML) const globalScope = (function() { if (typeof globalThis !== 'undefined') return globalThis; if (typeof self !== 'undefined') return self; if (typeof window !== 'undefined') return window; return {}; })();
if (globalScope.acEMBEDDED_PAINTING_BITMAPS) { // Check with various key formats const embeddedBitmap = globalScope.acEMBEDDED_PAINTING_BITMAPS[code] || globalScope.acEMBEDDED_PAINTING_BITMAPS['#' + actualCode] || globalScope.acEMBEDDED_PAINTING_BITMAPS[actualCode]; if (embeddedBitmap) { console.log("🖼️ Loaded from embedded bundle:", code); paintings[code] = embeddedBitmap; return; } }
// Level 4: Fetch from network // console.log("🖼️ Prefetching from network...", code); paintings[code] = "fetching";
const cacheAndStore = async (img) => { paintings[code] = img; // Only cache images from aesthetic.computer media backend if (imageCache.isCacheable(code)) { await imageCache.set(code, img); } };
if (actualCode.startsWith("http")) { $commonApi.get .picture(actualCode) .then(({ img }) => cacheAndStore(img)) .catch(() => { delete paintings[code]; }); } else if (actualCode.includes("/")) { // Format: @handle/timestamp or handle/timestamp const [author, paintingSlug] = actualCode.split("/"); $commonApi.get .painting(paintingSlug) .by(author) .then(({ img }) => cacheAndStore(img)) .catch(() => { delete paintings[code]; }); } else if (actualCode.match(/^\d{4}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{3}$/)) { // Timestamp format without handle (e.g., "2025.11.01.23.55.14.266") // This shouldn't happen - timestamps need a handle console.warn(`⚠️ Painting timestamp without handle: ${actualCode}`); delete paintings[code]; } else if (actualCode.length === 3 && actualCode.match(/^[a-zA-Z0-9]{3}$/)) { // 3-letter code — the `/media/paintings/<code>.png` endpoint resolves // the slug + user server-side, so we don't need a separate painting-code // lookup (which returned a fully-qualified slug and broke `.by()` URL // construction). $commonApi.get .painting(actualCode) .by() .then(({ img }) => cacheAndStore(img)) .catch(() => { delete paintings[code]; }); } else { // Unknown format - try as direct code anyway $commonApi.get .painting(actualCode) .by() // No author needed - endpoint resolves it .then(({ img }) => cacheAndStore(img)) .catch(() => { delete paintings[code]; }); }}
const $paintApiUnwrapped = { // Turtle graphics: 🐢 crawl, left, right, up, down, goto, face // Move the turtle forward based on angle, wrapping at screen edges. crawl: (steps = 1) => { const w = screen.width, h = screen.height; const x2 = turtlePosition.x + steps * cos(num.radians(turtleAngle)); const y2 = turtlePosition.y + steps * sin(num.radians(turtleAngle)); const wx2 = ((x2 % w) + w) % w; const wy2 = ((y2 % h) + h) % h; // Only draw if we didn't cross a screen edge (avoids diagonal wrap lines). const wrapped = Math.abs(wx2 - x2) > 0.5 || Math.abs(wy2 - y2) > 0.5; if (turtleDown && !wrapped) { graph.line(turtlePosition.x, turtlePosition.y, wx2, wy2); } turtlePosition.x = wx2; turtlePosition.y = wy2; return { x: turtlePosition.x, y: turtlePosition.y }; }, // Turn turtle left n degrees. left: (d = 1) => { turtleAngle = normalizeAngle(turtleAngle - d); return turtleAngle; }, // Turn turtle right n degrees. right: (d = 1) => { turtleAngle = normalizeAngle(turtleAngle + d); return turtleAngle; }, // Turtle pen up. up: () => { turtleDown = false; // console.log("🐢 Up"); }, // Turtle pen down. down: () => { turtleDown = true; // console.log("🐢 Down"); }, // Teleport the turtle position (wraps to screen bounds). goto: (x = screen.width / 2, y = screen.height / 2) => { const w = screen.width, h = screen.height; const wx = ((x % w) + w) % w; const wy = ((y % h) + h) % h; if (turtleDown) { graph.line(turtlePosition.x, turtlePosition.y, wx, wy); } turtlePosition.x = wx; turtlePosition.y = wy; return { x: turtlePosition.x, y: turtlePosition.y }; }, face: (angle = 0) => { turtleAngle = normalizeAngle(angle); return turtleAngle; }, // Shortcuts // l: graph.line, // i: ink, // Defaults blend: graph.blendMode, setEraseTarget: graph.setEraseTarget, page: function () { if (arguments[0]?.api) { // console.log("New paint api?", arguments[0].api); // $activePaintApi = arguments[0].api; }
const buf = arguments[0];
// If this is the main screen object (by identity), restore its true state. // Brushes like smear call page(system.painting) which corrupts screen.pixels/ // width/height. When they later call page(screen), we recognise the original // screen object and restore from the frame-start snapshot so subsequent code // (including other brushes and the nopaint pre-processing restore at line ~12788) // sees correct dimensions immediately rather than at end-of-frame. if (buf === _mainScreenObject && _trueScreenPixels) { buf.pixels = _trueScreenPixels; buf.width = _trueScreenWidth; buf.height = _trueScreenHeight; } else { // Switching to a sub-buffer: propagate its dimensions into screen so that // code reading screen.width/height while on this buffer gets correct values. $activePaintApi.screen.width = buf.width; $activePaintApi.screen.height = buf.height; $activePaintApi.screen.pixels = buf.pixels; }
graph.setBuffer(buf); }, edit: graph.changePixels, // Edit pixels by pasing a callback. // Color ink: function () { const out = ink(...arguments); twoDCommands.push(["ink", ...out]); }, ink2: function () { const out = ink2(...arguments); twoDCommands.push(["ink2", ...(out || [])]); }, // inkrn: () => graph.c.slice(), // Get current inkColor. // 2D wipe: function () { const cc = graph.c.slice(0);
// Preserve fade alpha during wipe operations to prevent clearing it const preserveFadeAlpha = getPreserveFadeAlpha?.() || false; if (!preserveFadeAlpha && typeof setPreserveFadeAlpha === 'function') { setPreserveFadeAlpha(true); }
// Default to white if no arguments provided if (arguments.length === 0) { ink(255, 255, 255); } else { ink(...arguments); }
// 🎨 WebGPU clear if enabled if ($commonApi.webgpu.enabled) { send({ type: "webgpu-command", content: { type: "clear", color: graph.c.slice(0) } }); ink(...cc); if (!preserveFadeAlpha && typeof setPreserveFadeAlpha === 'function') { setPreserveFadeAlpha(false); } return; }
// 🚀 Use optimized clear() which writes directly to pixels array // clear() already handles fade colors and uses copyWithin doubling for solid colors // This is much faster than box() which goes through plot/blend per pixel graph.clear();
twoDCommands.push(["wipe", ...graph.c]); ink(...cc);
// Restore previous preservation state if (!preserveFadeAlpha && typeof setPreserveFadeAlpha === 'function') { setPreserveFadeAlpha(false); }
// 🍞 LAYER 0: Also clear layer 0 if it exists (for KidLisp) // This ensures wipe() clears the persistent layer, not just the screen if (this.kidlispInstance?.layer0) { this.kidlispInstance.layer0.pixels.fill(0); } }, // Set background fill color for reframe operations (especially for KidLisp pieces) backgroundFill: function (color) { // This function should fill transparent areas with the background color // without clearing existing painted content. // // For now, we use a simple approach: only do a full wipe during initial boot, // but skip it during reframe to preserve content.
const cc = graph.c.slice(0); // Save current ink color
if (arguments.length === 0) { ink(255, 255, 255); // Default to white } else { ink(...arguments); }
// TODO: Implement proper transparent-area-only filling // For now, always do a full clear graph.clear(); twoDCommands.push(["backgroundFill", ...graph.c]); ink(...cc); // Restore previous ink color }, // Erase the screen. clear: function () { const cc = graph.c.slice(0); ink(0, 0); graph.clear(); ink(...cc); }, copy: graph.copy, paste: function paste() { if (typeof arguments[0] === "string") { // Check to see if the bitmap has been cached by this piece already. const code = arguments[0]; if (paintings[code] && paintings[code] !== "fetching") { graph.paste(paintings[code], ...[...arguments].slice(1)); } else if (paintings[code] !== "fetching") { prefetchPicture(code); } } else { graph.paste(...arguments); } }, // Similar to paste, but always draws from the center of x, y. // Has partial support for {center, bottom}. 24.02.15.12.19 stamp: function stamp() { let params; // Parse the parameters and lay out the stamp. function makeLayout() { if (typeof params[0] === "object") { const layout = params[0]; if (layout.center === "x") { params[0] = $activePaintApi.screen.width / 2; } else { params[0] = 0; } if (layout.bottom !== undefined) { params[1] = $activePaintApi.screen.height - layout.bottom - paintings[code].height / 2; } else { params[1] = 0; } } } if (typeof arguments[0] === "string") { // Check to see if the bitmap has been cached by this piece already. const code = arguments[0]; params = [...arguments].slice(1); if (paintings[code] && paintings[code] !== "fetching") { makeLayout(); graph.stamp(paintings[code], ...params); } else if (paintings[code] !== "fetching") { prefetchPicture(code); } } else { params = [...arguments].slice(1); if (params.length === 0) params = [0, 0]; makeLayout(); graph.stamp(arguments[0], ...params); } }, pixel: graph.pixel, plot: function () { if (arguments.length === 1) { graph.plot(arguments[0].x, arguments[0].y); } else { graph.plot(...arguments); } }, // TODO: Should this be renamed to set? flood: graph.flood, compositeLayers: graph.compositeLayers, // GPU-accelerated multi-layer compositing batchedEffects: graph.batchedEffects, // GPU-accelerated batched effects (zoom+scroll+contrast+brightness in one pass) point: function () { const out = graph.point(...arguments); twoDCommands.push(["point", ...out]); }, line: function() { // 🎨 WebGPU line if enabled if ($commonApi.webgpu.enabled && arguments.length >= 4) { const [x1, y1, x2, y2] = arguments; const color = graph.c.slice(0); send({ type: "webgpu-command", content: { type: "line", x1, y1, x2, y2, color } }); return; } return graph.line(...arguments); }, lineAngle: graph.lineAngle, pline: graph.pline, // Set the alpha of every non-transparent pixel in a buffer to a uniform value. // Useful for per-stroke alpha — must be called after drawing on the buffer. setBufferAlpha: function (buffer, alpha) { if (!buffer || !buffer.pixels) return; const target = Math.round(alpha * 255); const px = buffer.pixels; for (let i = 3; i < px.length; i += 4) { if (px[i] > 0) px[i] = target; } }, pppline: graph.pixelPerfectPolyline, oval: graph.oval, circle: graph.circle, pie: graph.pie, tri: graph.tri, poly: graph.poly, box: graph.box, shape: graph.shape, grid: graph.grid, draw: graph.draw, setShowClippedWireframes: graph.setShowClippedWireframes, clearWireframeBuffer: graph.clearWireframeBuffer, drawBufferedWireframes: graph.drawBufferedWireframes, getRenderStats: graph.getRenderStats, printLine: graph.printLine, // TODO: This is kind of ugly and I need a state machine for type. form, pan: graph.pan, unpan: graph.unpan, savepan: graph.savepan, loadpan: graph.loadpan, mask: graph.mask, unmask: graph.unmask, steal: graph.steal, putback: graph.putback, skip: graph.skip, scroll: graph.scroll, flip: graph.flip, spin: graph.spin, sort: graph.sort, zoom: graph.zoom, suck: graph.suck, blur: function(radius = 1) { // 🔧 FIX: Ensure blur operates on current buffer context // When called from within a painting() context, the graph module // should already have the correct buffer set via setBuffer() return graph.blur(radius); }, sharpen: function(strength = 1) { // Apply sharpening filter to enhance edges and details // When called from within a painting() context, the graph module // should already have the correct buffer set via setBuffer() return graph.sharpen(strength); }, invert: function() { // Invert RGB colors (255 - value) while preserving alpha // When called from within a painting() context, the graph module // should already have the correct buffer set via setBuffer() return graph.invert(); }, contrast: graph.contrast, shear: graph.shear, resetScrollState: function() { // Reset scroll accumulator - used by burn to ensure clean state return graph.resetScrollState(); }, noise16: graph.noise16, noise16DIGITPAIN: graph.noise16DIGITPAIN, noise16Aesthetic: graph.noise16Aesthetic, noise16Sotce: graph.noise16Sotce, noiseTinted: graph.noiseTinted, // 🎨 Alpha-blended paste for crossfade compositing pasteWithAlpha: function pasteWithAlpha(source, x, y, alpha) { if (!source || !source.pixels || alpha <= 0) return; const dst = $activePaintApi.screen; if (!dst || !dst.pixels) return; const srcPixels = source.pixels; const dstPixels = dst.pixels; const sw = source.width, sh = source.height; const dw = dst.width, dh = dst.height; if (alpha >= 255) { $activePaintApi.paste(source, x, y); return; } const alphaFactor = alpha / 255.0; for (let sy = 0; sy < sh; sy++) { const dy = sy + y; if (dy < 0 || dy >= dh) continue; for (let sx = 0; sx < sw; sx++) { const dx = sx + x; if (dx < 0 || dx >= dw) continue; const si = (sy * sw + sx) * 4; const di = (dy * dw + dx) * 4; let srcA = srcPixels[si + 3]; if (srcA === 0 && (srcPixels[si] || srcPixels[si + 1] || srcPixels[si + 2])) { srcA = 255; // Promote opaque RGB with zero alpha } if (srcA === 0) continue; const ea = (srcA * alphaFactor + 1) | 0; const inv = 256 - ea; dstPixels[di] = (ea * srcPixels[si] + inv * dstPixels[di]) >> 8; dstPixels[di + 1] = (ea * srcPixels[si + 1] + inv * dstPixels[di + 1]) >> 8; dstPixels[di + 2] = (ea * srcPixels[si + 2] + inv * dstPixels[di + 2]) >> 8; dstPixels[di + 3] = Math.min(255, dstPixels[di + 3] + ea); } } }, // 🎯 Simplified KidLisp integration using global singleton instance kidlisp: function kidlisp(x = 0, y = 0, width, height, source, options = {}) { // Initialize global instance if needed if (!globalKidLispInstance) { initializeGlobalKidLisp($activePaintApi); }
// Default dimensions to screen size if not provided if (!width) width = $activePaintApi.screen.width; if (!height) height = $activePaintApi.screen.height;
// console.log(`🎯 Simple kidlisp call: ${width}x${height} at (${x},${y})`);
// Extract options const { noCache = false, accumulate: forceAccumulate = false, noPaste = false } = options;
try { // Initialize persistent paintings cache if needed if (!globalKidLispInstance.persistentPaintings) { globalKidLispInstance.persistentPaintings = new Map(); }
// For dollar codes, check if we already have a resolved version cached let resolvedSource = source; if (source && source.startsWith && source.startsWith('$') && source.length > 1) { const cacheId = source.slice(1);
// Use a singleton-specific cache to avoid conflicts with normal prompt loading if (!globalKidLispInstance.singletonDollarCodeCache) { globalKidLispInstance.singletonDollarCodeCache = new Map(); }
// Track loading states to prevent concurrent requests if (!globalKidLispInstance.loadingDollarCodes) { globalKidLispInstance.loadingDollarCodes = new Set(); }
// First check if we have a painting with the resolved source already if (globalKidLispInstance.singletonDollarCodeCache.has(cacheId)) { resolvedSource = globalKidLispInstance.singletonDollarCodeCache.get(cacheId); // Note: regionKey will be set later after accumulation detection
// If we already have this resolved painting, we'll check after regionKey is set // console.log(`🎯 Using cached source: ${resolvedSource}`); } else if (globalKidLispInstance.loadingDollarCodes.has(cacheId)) { // Already loading this dollar code, wait for it to complete // console.log(`🎯 ${source} already loading, waiting...`); return null; } else { // Start loading and mark as loading console.log(`🎯 Loading ${source} for first time...`); globalKidLispInstance.loadingDollarCodes.add(cacheId);
getCachedCodeMultiLevel(cacheId).then(loadedSource => { if (loadedSource) { console.log(`🎯 ✅ Loaded source for ${cacheId}: ${loadedSource.length} chars`); globalKidLispInstance.singletonDollarCodeCache.set(cacheId, loadedSource); } else { console.warn(`❌ Could not load source for ${cacheId}`); } }).catch(error => { console.error(`❌ Error loading ${cacheId}:`, error); }).finally(() => { // Remove from loading set when done (success or failure) console.log(`🎯 Finished loading attempt for ${cacheId}`); globalKidLispInstance.loadingDollarCodes.delete(cacheId); });
// Return early - no painting this frame, wait for cache return null; } }
// Guard: if no resolved source, bail early (e.g., disk-based piece, not raw KidLisp) if (!resolvedSource || typeof resolvedSource !== "string") { return null; }
// 🎨 AUTO-DETECT ACCUMULATION: Check if source starts with a color word or forced via option const colorWords = ['red', 'blue', 'green', 'yellow', 'purple', 'orange', 'cyan', 'magenta', 'black', 'white', 'gray', 'grey', 'brown', 'pink']; const firstWord = resolvedSource.trim().split(/\s+/)[0]?.toLowerCase(); const startsWithColor = colorWords.includes(firstWord);
// Auto-generate accumulation settings (can be forced via option) const accumulate = forceAccumulate || startsWithColor; const accumulateKey = accumulate ? `auto_${x}_${y}_${width}_${height}_${firstWord}` : null;
// Create persistent painting key with accumulation support let regionKey; // Build a dimension-independent content key for finding previous paintings after resize let contentKey; if (accumulate && accumulateKey) { regionKey = `${x},${y},${width},${height}:ACCUMULATE:${accumulateKey}`; contentKey = `ACCUMULATE:auto_${x}_${y}_${firstWord}`; } else { regionKey = `${x},${y},${width},${height}:${resolvedSource}`; contentKey = resolvedSource; }
// 🔄 RESIZE RESILIENCE: If no cached painting at current dimensions, // look for a previous painting with the same source at different dimensions. // This preserves animation continuity when the screen resizes. let resizedPreviousPainting = null; let resizedPreviousKey = null; if (!globalKidLispInstance.persistentPaintings.has(regionKey)) { for (const [key, val] of globalKidLispInstance.persistentPaintings) { // Match keys that share the same content suffix but differ in dimensions if (key !== regionKey && key.endsWith(`:${contentKey}`)) { resizedPreviousPainting = val; resizedPreviousKey = key; break; } } }
// Check if the code contains frame-dependent randomization commands const hasFrameDependentCommands = /\(\s*ink\s*\)|\(\s*color\s*\)|\(\s*rand\s*\)/.test(resolvedSource);
// Check if we need to reset (resolved source contains 'wipe') const shouldReset = resolvedSource.includes('wipe') && !accumulate; // Don't reset in accumulation mode
// Check if source contains animation-related commands that need fresh execution const animationCommands = ['rainbow', 'zebra', 'time', 'random', 'noise', 'clock', 'scroll', 'zoom', 'flip', 'contrast', 'fade']; const hasTimingCommands = /\d+\.?\d*s\b/.test(resolvedSource); // Detect timing like "0.15s", "1s", etc. const hasAnimationCommands = animationCommands.some(cmd => resolvedSource.includes(cmd)); const isDollarCode = source && source.startsWith && source.startsWith('$'); // Dollar codes should always refresh // Don't force fresh execution for timing commands - they need to run continuously // In accumulate mode, prefer building on existing surface unless explicitly forced const needsFreshExecution = !accumulate && (noCache || (isDollarCode && !hasTimingCommands) || (hasAnimationCommands && !hasTimingCommands));
let painting;
// Create fresh painting if: reset needed, no cached version exists, or animation commands need updating if (shouldReset || needsFreshExecution || !globalKidLispInstance.persistentPaintings.has(regionKey)) { // Create fresh painting (first time, after wipe, or for animations) const reason = shouldReset ? 'wipe command' : needsFreshExecution ? 'animation content' : 'first time'; // console.log(`🎨 Creating fresh painting for key: ${regionKey.slice(0, 50)}... (${reason})`);
// For animations, start with previous frame if available (unless wiping) // Also check for resized previous painting from a different dimension const previousPainting = !shouldReset && globalKidLispInstance.persistentPaintings.has(regionKey) ? globalKidLispInstance.persistentPaintings.get(regionKey) : (!shouldReset ? resizedPreviousPainting : null);
// Clean up the old dimension key if we found a resized previous if (resizedPreviousKey && resizedPreviousPainting) { globalKidLispInstance.persistentPaintings.delete(resizedPreviousKey); }
painting = $activePaintApi.painting(width, height, (api) => { // Paste previous frame to maintain animation continuity (including across resizes) if (previousPainting && needsFreshExecution && !shouldReset) { api.paste(previousPainting); }
globalKidLispInstance.setAPI(api);
// Add basic timing support for KidLisp commands if (!api.clock) { api.clock = { time: () => new Date() }; } // Override zoom and scroll functions to implement actual effects in painting context const originalZoom = api.zoom; const originalScroll = api.scroll || $activePaintApi.scroll;
api.zoom = (...args) => { // console.log(`🔍 Zoom called in painting context: args=${JSON.stringify(args)}`); // Just call the original zoom function - it will operate on the current buffer if (originalZoom && typeof originalZoom === 'function') { return originalZoom(...args); } };
api.scroll = (dx, dy) => { // console.log(`📜 Scroll called in painting context: dx=${dx}, dy=${dy}`); // Call the real scroll function from the main paint API if (originalScroll && typeof originalScroll === 'function') { return originalScroll(dx, dy); } else if ($activePaintApi.scroll && typeof $activePaintApi.scroll === 'function') { return $activePaintApi.scroll(dx, dy); } }; // Add randomization support for ink() and other commands if (!api.num) { api.num = $activePaintApi.num || { random: () => Math.random(), randInt: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min, rainbow: num.rainbow, // Use the actual rainbow function from num.mjs zebra: num.zebra // Use the actual zebra function from num.mjs }; } // Add color support for proper ink randomization if (!api.color) { api.color = $activePaintApi.color || { random: () => [ Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), Math.floor(Math.random() * 256) ] }; }
// 🎨 Add CSS color functions to make unquoted color words work // Import CSS colors from num.mjs and add them as functions to the API const cssColors = num.cssColors; if (cssColors) { Object.keys(cssColors).forEach(colorName => { if (!api[colorName]) { api[colorName] = () => cssColors[colorName]; } }); }
// Add rainbow and zebra as top-level functions for unquoted usage if (!api.rainbow) { api.rainbow = () => num.rainbow(); } if (!api.zebra) { api.zebra = () => num.zebra(); } // Reset KidLisp color state to prevent cross-contamination between regions globalKidLispInstance.currentInk = null;
// Save original state before configuring execution mode const originalInEmbedPhase = globalKidLispInstance.inEmbedPhase; const originalIsNestedInstance = globalKidLispInstance.isNestedInstance; const originalEmbeddedLayers = globalKidLispInstance.embeddedLayers; const originalIsEmbeddedContext = globalKidLispInstance.isEmbeddedContext;
// 🎯 CRITICAL: Set isEmbeddedContext=true so effects (zoom, blur, scroll, etc.) // execute immediately instead of being deferred to postCompositeCommands // This aligns embedded kidlisp() with the main KidLisp paint behavior globalKidLispInstance.isEmbeddedContext = true;
// Check if source contains timing expressions that need proper scheduling const hasTimingExpressions = /\d+\.?\d*s(\.\.\.|!)?/.test(resolvedSource); // Check if source contains scroll/zoom/flip that needs deferred execution const hasScrollZoom = /\(\s*(scroll|zoom|flip)\s/.test(resolvedSource);
if (hasTimingExpressions) { // console.log(`🎯 Detected timing expressions, preserving normal execution flow`); // Don't force immediate execution for timing expressions globalKidLispInstance.embeddedLayers = null; // Still clear embedded layers } else if (hasScrollZoom) { // console.log(`🎯 Detected scroll/zoom/flip, allowing deferred execution`); // Allow scroll/zoom/flip to use deferred execution but clear embedded layers globalKidLispInstance.inEmbedPhase = false; // Allow deferring for scroll/zoom/flip globalKidLispInstance.isNestedInstance = false; // Allow deferring for scroll/zoom/flip globalKidLispInstance.embeddedLayers = null; // Clear any leftover embedded layers } else { // console.log(`🎯 No timing/scroll/zoom/flip detected, forcing immediate execution`); // Force immediate execution mode for simplified kidlisp() calls without timing or scroll/zoom/flip globalKidLispInstance.inEmbedPhase = true; // Prevent deferring globalKidLispInstance.isNestedInstance = true; // Enable immediate execution globalKidLispInstance.embeddedLayers = null; // Clear any leftover embedded layers }
executeLispCode(resolvedSource, api, false); // false = not accumulating, fresh painting
// Restore original state to avoid interfering with other KidLisp operations globalKidLispInstance.inEmbedPhase = originalInEmbedPhase; globalKidLispInstance.isNestedInstance = originalIsNestedInstance; globalKidLispInstance.embeddedLayers = originalEmbeddedLayers; globalKidLispInstance.isEmbeddedContext = originalIsEmbeddedContext;
globalKidLispInstance.setAPI($activePaintApi); }); // Always cache the painting for next frame continuity (even for animations) globalKidLispInstance.persistentPaintings.set(regionKey, painting); } else { // Build on existing painting (accumulate effects) const existingPainting = globalKidLispInstance.persistentPaintings.get(regionKey); // console.log(`🎨 Accumulating on existing painting for key: ${regionKey.slice(0, 50)}...`);
painting = $activePaintApi.painting(width, height, (api) => { // Paste previous state first api.paste(existingPainting); // Then add new effects on top globalKidLispInstance.setAPI(api);
// Add basic timing support for KidLisp commands if (!api.clock) { api.clock = { time: () => new Date() }; } // Add scroll and zoom support for animations const originalZoom = api.zoom; const originalScroll = api.scroll || $activePaintApi.scroll;
api.zoom = (...args) => { // console.log(`🔍 Zoom called in accumulation context: args=${JSON.stringify(args)}`); if (originalZoom && typeof originalZoom === 'function') { return originalZoom(...args); } };
api.scroll = (dx, dy) => { // console.log(`📜 Scroll called in accumulation context: dx=${dx}, dy=${dy}`); if (originalScroll && typeof originalScroll === 'function') { return originalScroll(dx, dy); } else if ($activePaintApi.scroll && typeof $activePaintApi.scroll === 'function') { return $activePaintApi.scroll(dx, dy); } }; // Add randomization support for ink() and other commands if (!api.num) { api.num = $activePaintApi.num || { random: () => Math.random(), randInt: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min, rainbow: num.rainbow, // Use the actual rainbow function from num.mjs zebra: num.zebra // Use the actual zebra function from num.mjs }; } // Add color support for proper ink randomization if (!api.color) { api.color = $activePaintApi.color || { random: () => [ Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), Math.floor(Math.random() * 256) ] }; }
// 🎨 Add CSS color functions to make unquoted color words work // Import CSS colors from num.mjs and add them as functions to the API const cssColors = num.cssColors; if (cssColors) { Object.keys(cssColors).forEach(colorName => { if (!api[colorName]) { api[colorName] = () => cssColors[colorName]; } }); }
// Add rainbow and zebra as top-level functions for unquoted usage if (!api.rainbow) { api.rainbow = () => num.rainbow(); } if (!api.zebra) { api.zebra = () => num.zebra(); } // Reset KidLisp color state to prevent cross-contamination between regions globalKidLispInstance.currentInk = null;
// Save original state before configuring execution mode const originalInEmbedPhase = globalKidLispInstance.inEmbedPhase; const originalIsNestedInstance = globalKidLispInstance.isNestedInstance; const originalEmbeddedLayers = globalKidLispInstance.embeddedLayers; const originalIsEmbeddedContext = globalKidLispInstance.isEmbeddedContext;
// 🎯 CRITICAL: Set isEmbeddedContext=true so effects (zoom, blur, scroll, etc.) // execute immediately instead of being deferred to postCompositeCommands // This aligns embedded kidlisp() with the main KidLisp paint behavior globalKidLispInstance.isEmbeddedContext = true;
// Check if source contains timing expressions that need proper scheduling const hasTimingExpressions = /\d+\.?\d*s(\.\.\.|!)?/.test(resolvedSource); // Check if source contains scroll/zoom/flip that needs deferred execution const hasScrollZoom = /\(\s*(scroll|zoom|flip)\s/.test(resolvedSource);
// Only log occasionally to reduce console spam
if (hasTimingExpressions) { // console.log(`🎯 Detected timing expressions in accumulation, preserving normal execution flow`); // Don't force immediate execution for timing expressions globalKidLispInstance.embeddedLayers = null; // Still clear embedded layers } else if (hasScrollZoom) { // console.log(`🎯 Detected scroll/zoom/flip in accumulation, allowing deferred execution`); // Allow scroll/zoom/flip to use deferred execution but clear embedded layers globalKidLispInstance.inEmbedPhase = false; // Allow deferring for scroll/zoom/flip globalKidLispInstance.isNestedInstance = false; // Allow deferring for scroll/zoom/flip globalKidLispInstance.embeddedLayers = null; // Clear any leftover embedded layers } else { // console.log(`🎯 No timing/scroll/zoom/flip in accumulation, forcing immediate execution`); // Force immediate execution mode for simplified kidlisp() calls without timing or scroll/zoom/flip globalKidLispInstance.inEmbedPhase = true; // Prevent deferring globalKidLispInstance.isNestedInstance = true; // Enable immediate execution globalKidLispInstance.embeddedLayers = null; // Clear any leftover embedded layers }
executeLispCode(resolvedSource, api, true); // true = accumulating on existing painting
// Restore original state to avoid interfering with other KidLisp operations globalKidLispInstance.inEmbedPhase = originalInEmbedPhase; globalKidLispInstance.isNestedInstance = originalIsNestedInstance; globalKidLispInstance.embeddedLayers = originalEmbeddedLayers; globalKidLispInstance.isEmbeddedContext = originalIsEmbeddedContext;
globalKidLispInstance.setAPI($activePaintApi); }); // Update the cache with the new accumulated state (unless noCache is true or animation content) if (!noCache || !needsFreshExecution) { globalKidLispInstance.persistentPaintings.set(regionKey, painting); } }
// Paste the painting to the specified location (unless noPaste is set) if (!noPaste && $activePaintApi.paste && painting) { // console.log(`🎯 Pasting painting to (${x},${y})`); $activePaintApi.paste(painting, x, y); }
return painting;
} catch (error) { console.error("🚫 Simple KidLisp error:", error);
// Draw error directly to main screen (skip in projection/embed mode like kidlisp.com) if (!projectionMode) { const originalInk = $activePaintApi.ink(); $activePaintApi.ink(255, 0, 0); if ($activePaintApi.write) { $activePaintApi.write("KidLisp Error", x, y); } $activePaintApi.ink(originalInk); }
return null; } }, // 🎵 Update KidLisp audio globals (accessible to all pieces) updateKidLispAudio: updateKidLispAudio, // glaze: ...};
// 🎨 Expose wipe function globally for KidLisp reframe operationsif (typeof globalThis !== "undefined") { globalThis.$paintApiUnwrapped = $paintApiUnwrapped; globalThis.wipe = $paintApiUnwrapped.wipe;}
// Helper function to execute KidLisp codefunction executeLispCode(source, api, isAccumulating = false) { try { // Check if trace is enabled (set by kidlisp.com visualization) const shouldTrace = $commonApi?.kidlispEnableTrace; if (shouldTrace) { enableKidlispTrace(); clearExecutionTrace(); } else { disableKidlispTrace(); }
// Parse and evaluate the source directly without going through module lifecycle // console.log(`🔍 Parsing KidLisp source:`, source);
// Clear previous first-line color state for fresh detection
globalKidLispInstance.firstLineColor = null;
// Store source for error location lookup globalKidLispInstance.currentSource = source;
globalKidLispInstance.parse(source); // console.log(`🔍 Generated AST:`, globalKidLispInstance.ast);
if (globalKidLispInstance.ast) { // Detect first-line color but only apply it if not accumulating globalKidLispInstance.detectFirstLineColor();
if (globalKidLispInstance.firstLineColor && !isAccumulating) { // console.log(`🎨 Applying first-line color background: ${globalKidLispInstance.firstLineColor}`); api.wipe(globalKidLispInstance.firstLineColor); }
// Execute the parsed AST using the main evaluate method // console.log(`🔍 Executing AST using main evaluate method...`); // console.log(`🔍 AST contains:`, globalKidLispInstance.ast.map(expr => Array.isArray(expr) ? expr[0] : expr));
// Set up proper timing environment by ensuring clock and frameCount are available if (!api.clock) { api.clock = { time: () => new Date() }; }
// Increment frame count for timing expressions if (typeof globalKidLispInstance.frameCount !== 'number') { globalKidLispInstance.frameCount = 0; } globalKidLispInstance.frameCount++;
// Normal KidLisp evaluation (dollar codes already resolved) const result = globalKidLispInstance.evaluate(globalKidLispInstance.ast, api, globalKidLispInstance.localEnv); // console.log(`🔍 Evaluation result:`, result);
// Post execution trace if enabled (only on first frame to avoid spam) if (shouldTrace && globalKidLispInstance.frameCount === 1) { postExecutionTrace(); } } else { // console.log(`⚠️ No AST generated from source`); } } catch (evalError) { console.error("🚫 KidLisp evaluation error:", evalError); // Draw error indicator (skip in projection/embed mode like kidlisp.com) if (!projectionMode) { api.wipe(60, 0, 0); api.ink(255, 255, 255); if (api.write) { api.write("KidLisp Eval Error", 5, 15); } } }}
// TODO: Eventually restructure this a bit. 2021.12.16.16.0// Should global state like color and transform be stored here?
let $activePaintApi;
let paintingAPIid = 0n;
const twoDCommands = [];graph.twoD(twoDCommands); // Set a global for passing 2d commands here.
class Painting { #layers = []; #layer = 0; api = {}; inkrn;
// panrn; // In order for this feature to work, translation needs to be stored outside of graph / captured differently?
constructor() { Object.assign(this.api, $paintApi); const p = this;
p.api.index = paintingAPIid; paintingAPIid += 1n;
p.inkrn = graph.c.slice(); // Init global state machine read-outs. p.pagern = graph.getBuffer(); // p.panrn = graph.getPan();
// Filter for and then wrap every rendering behavior of $paintApi into a // system to be deferred in groups, using layer. // ⛓️ This wrapper also makes the paint API chainable.
function globals(k, args) { if (k === "page") p.pagern = args[0]; // TODO: 😅 Add other state globals like line thickness? 23.1.25 }
for (const k in $paintApiUnwrapped) { if (typeof $paintApiUnwrapped[k] === "function") { // Wrap and then transfer to #api. p.api[k] = function () { if (k === "ink") { $paintApiUnwrapped[k](...arguments); p.inkrn = graph.c.slice(); } else { globals(k, arguments); // Keep track of global state, like page, via `pagern`. } // Create layer if necessary. if (notArray(p.#layers[p.#layer])) p.#layers[p.#layer] = []; const callArgs = arguments; // Add each deferred paint api function to the layer, to be run // all at once in `paint` on each frame update. p.#layers[p.#layer].push([ k, () => { if (k === "ink") { $paintApiUnwrapped[k](...callArgs); p.inkrn = graph.c.slice(); } else { globals(k, callArgs); // Update globals again on chainable calls. $paintApiUnwrapped[k](...callArgs); } }, ]); return p.api; }; } }
// Ensure critical graphics functions are available (fallback if wrapping fails) if (!p.api.box && typeof graph.box === "function") { p.api.box = function () { const callArgs = arguments; if (notArray(p.#layers[p.#layer])) p.#layers[p.#layer] = []; p.#layers[p.#layer].push([ "box", () => graph.box(...callArgs), ]); return p.api; }; } if (!p.api.line && typeof graph.line === "function") { p.api.line = function () { const callArgs = arguments; if (notArray(p.#layers[p.#layer])) p.#layers[p.#layer] = []; p.#layers[p.#layer].push([ "line", () => graph.line(...callArgs), ]); return p.api; }; } if (!p.api.wipe && typeof $paintApiUnwrapped.wipe === "function") { p.api.wipe = function () { const callArgs = arguments; if (notArray(p.#layers[p.#layer])) p.#layers[p.#layer] = []; p.#layers[p.#layer].push([ "wipe", () => $paintApiUnwrapped.wipe(...callArgs), ]); return p.api; }; } if (!p.api.ink && typeof $paintApiUnwrapped.ink === "function") { p.api.ink = function () { $paintApiUnwrapped.ink(...arguments); p.inkrn = graph.c.slice(); return p.api; }; }
// Allows grouping & composing painting order using an AofA (Array of Arrays). // n: 0-n (Cannot be negative.) // fun: A callback that contains $paintApi commands or any other code. this.api.layer = function (n) { p.#layer = n; // TODO: ❤️🔥 Current layer needs to be set on each API state...! return p.api; };
// Creates a new pixel buffer with its own layering wrapper / context // on top of the base painting API. this.api.painting = function painting() { const oldActivePaintApi = $activePaintApi; const painting = new Painting(); $activePaintApi = painting.api; // Mock out the screen here using the arguments. $activePaintApi.screen = { width: arguments[0], height: arguments[1], // pix gets added in the makeBuffer... }; const pix = graph.makeBuffer(...arguments, painting, $activePaintApi); $activePaintApi = oldActivePaintApi; return pix; };
this.api.pixel = function () { return graph.pixel(...arguments); };
this.api.inkrn = () => this.inkrn; // Return current ink color. // this.api.panrn = graph.getPan; // Return current panTranslation. this.api.pagern = () => this.pagern; // Return current page buffer.
// This links to abstract, solitary graph functions that do not need // to be wrapped or deferred for rendering. // TODO: Maybe these functions should be under a graphics algorithms label? this.api.abstract = { bresenham: graph.bresenham }; }
// Paints every layer. //async paint(immediate = false) { paint(immediate = false) { for (let layer of this.#layers) { layer ||= []; // Evaporate empty layers. for (const paint of layer) { // if (immediate) { // console.log("Label:", paint[0]); paint[1](); // } else { // await paint(); // } } } this.#layers.length = 0; this.#layer = 0; }}
const painting = new Painting();
let glazeAfterReframe;
// *** Resolution ***// Accepts width, height and gap either as numbers or as// an object with those keys.//// Usage: resolution(64);// resolution(320, 240);// resolution(display); // "display" is a global object whose width// and height matches the hardware display// hosting aesthetic.computer.let lastGap = 8;$commonApi.resolution = function (width, height = width, gap = 8) { if (typeof width === "object") { const props = width; height = props.height; width = props.width || props.height; gap = props.gap === 0 ? 0 : props.gap || 8; }
if (typeof width === "number" && typeof height === "number") { width = round(width); height = round(height); }
// Don't do anything if there is no change and no gap update. if (screen.width === width && screen.height === height && gap === lastGap) return;
lastGap = gap;
// width = width || currentDisplay.innerWidth; // height = height || currentDisplay.innerHeight;
// TODO: Paint anything that needs to be painted before resizing... // TODO: Does this even work right now? painting.paint();
if (width === undefined && height === undefined) { // 1. Generate a new width and height. width = round(currentDisplay.width / currentDisplay.subdivisions); height = round(currentDisplay.height / currentDisplay.subdivisions); // Build a reframe request that will be sent to the main thread, mirroring this. reframe = { width: undefined, height: undefined, gap, }; } else { // 2. Manually set the width and height. reframe = { width, height, gap }; }
// console.log( // "🖼 Reframe to:", // width, // height, // "from", // screen.width, // screen.height, // );
// 3. Assign the generated or manual width and height. const oldScreen = { width: screen.width, height: screen.height, pixels: screen.pixels, };
screen.width = width; screen.height = height;
// Reset / recreate the depth buffer. (This is only used for the 3D software renderer in `graph`) graph.depthBuffer.length = screen.width * screen.height; graph.depthBuffer.fill(Number.MAX_VALUE); graph.writeBuffer.length = 0; //screen.width * screen.height; // graph.writeBuffer.fill(Number.MAX_VALUE);
screen.pixels = new Uint8ClampedArray(screen.width * screen.height * 4); screen.pixels.fill(255);
graph.setBuffer(screen); graph.paste({ painting: oldScreen, crop: new geo.Box(0, 0, oldScreen.width, oldScreen.height), });
// 🎨 Fill any new pixels with background color after screen expansion if (width > oldScreen.width || height > oldScreen.height) { const persistentColor = getPersistentFirstLineColor(); console.log("🎨 Resolution function: Screen expansion detected"); console.log("🎨 Resolution function: Old screen:", oldScreen.width, "x", oldScreen.height); console.log("🎨 Resolution function: New screen:", width, "x", height); console.log("🎨 Resolution function: Persistent color found:", persistentColor);
let fillSpec = resolveBackgroundFillSpec(persistentColor); if (!fillSpec && typeof globalKidLispInstance?.getBackgroundFillColor === "function") { const fallbackColor = globalKidLispInstance.getBackgroundFillColor(); console.log("🎨 Post-reframe: Fallback background candidate:", fallbackColor); fillSpec = resolveBackgroundFillSpec(fallbackColor); }
if (fillSpec) { if (fillSpec.type === "fade") { console.log("🎨 Post-reframe: Applying fade background to expansions"); fillExpandedWithFadePixels( screen, width, height, oldScreen.width, oldScreen.height, fillSpec.fadeInfo, ); } else { console.log("🎨 Post-reframe: Filling expansions with solid color", fillSpec.rgba); fillExpandedWithSolidPixels( screen, width, height, oldScreen.width, oldScreen.height, fillSpec.rgba, ); } } else { console.log("🎨 Post-reframe: No background fill spec available for expansion"); } } else { console.log("🎨 Resolution function: No screen expansion needed"); }};
// Add new content to the DOM.// (Requires `send`)class Content { nodes = []; #id = 0;
constructor() {}
// Make a request to add new content to the DOM. add(content) { // if (debug) console.log("📃 Adding content:", content); this.nodes.push({ id: this.#id }); this.#id = this.nodes.length - 1; send({ type: "content-create", content: { id: this.#id, content } }); return this.nodes[this.nodes.length - 1]; }
remove() { send({ type: "content-remove" }); this.nodes = []; this.#id = 0; }
receive({ id, response }) { this.nodes[id].response = response; }
//update({ id, msg }) { // send({ type: "content-update", content: { id, msg } }); //}}
// 🔈 Sound
// Microphone State (Audio Input)class Microphone { amplitude = 0; waveform = []; pitch = 0; connected = false; // Flips to true on a callback message from `bios`. recording = false; recordingPromise; permission = ""; permissionPending = false; permissionChecked = false; recordingBuffer = null; // Live recording buffer for preview
requestPermission() { if (this.permissionPending || this.permissionChecked) return this; this.permissionPending = true; send({ type: "microphone-permission-request" }); return this; }
// Note: can send `{monitor: true}` in `options` for audio feedback. connect(options) { send({ type: "microphone", content: options }); return this; }
disconnect() { send({ type: "microphone", content: { detach: true } }); }
poll() { send({ type: "get-microphone-amplitude" }); send({ type: "get-microphone-waveform" }); send({ type: "get-microphone-pitch" }); if (this.recording) { send({ type: "get-microphone-recording-buffer" }); } }
// Start recording. rec() { this.recording = true; send({ type: "microphone-record" }); }
// Stop recording. cut() { const prom = new Promise((resolve, reject) => { this.recordingPromise = { resolve, reject }; }); send({ type: "microphone-cut" }); this.recording = false; return prom; }}
class Speaker { waveforms = { left: [], right: [] }; amplitudes = { left: [], right: [] }; frequencies = { left: [], right: [] }; beat = { detected: false, strength: 0, timestamp: 0 }; // Add beat detection data
poll() { send({ type: "get-waveforms" }); send({ type: "get-amplitudes" }); send({ type: "get-frequencies" }); }}
let sound, // soundClear, // Used by receivedBeat and defined in first frame update. soundId = 0n, // Increment each sound / give it an id in the `bios`. soundTime; // Used by `$sound.synth` for global timing.
sound = { bpm: undefined, sounds: [], bubbles: [], farts: [], organics: [], kills: [],};
const speaker = new Speaker();const microphone = new Microphone();
// 🎮 Game Boy Emulator APIconst gameboy = { frame: null, // Current frame data (Uint8ClampedArray) width: 160, // Game Boy screen width height: 144, // Game Boy screen height romName: null, // Currently loaded ROM name isPlaying: false // Whether emulator is running};
// Resolve a user code (e.g. "ac25namuc") to a handle via the permahandle API.// Returns the handle string (e.g. "jeffrey") or null if not a user code / not found.async function resolveUserCode(slug) { if (!slug || slug.length !== 9 || !slug.startsWith("ac")) return null; if (!/^ac[0-9]{2}[a-z]{5}$/.test(slug)) return null; try { const res = await fetch(`/api/permahandle/${slug}`); if (!res.ok) return null; const data = await res.json(); return data.handle || null; } catch { return null; }}
// 2. ✔ Loading the disk.let originalHost;let firstLoad = true;
let notice, noticeTimer, noticeColor, noticeOpts; // Renders a full-screen notice on piece-load if present.let overlay2D = null; // A per-frame vector display-list drawn on uiCtx (api.overlay).
async function load( parsed, // If parsed is not an object, then assume it's source code. fromHistory = false, alias = false, devReload = false, loadedCallback, forceKidlisp = false, // Force interpretation as kidlisp even without prefix forceP5 = false, // Force interpretation as a p5.js sketch (dropped .js files)) { const loadFunctionStartTime = performance.now(); diskTimings.loadStarted = Math.round(loadFunctionStartTime - diskTimingStart); // Silent: load() started
let fullUrl, source; let params, search, colon, hash, path, host = originalHost, text, slug, clockShortcode = null; // Track original *code for URL display
// 🕷️ SPIDER MODE: Always log what piece we're loading if (typeof window !== 'undefined' && window.acSPIDER) { console.log("🕷️ SPIDER: Starting load() with:", { parsed, fromHistory, alias, devReload, forceKidlisp }); }
// Check for stuck loading state before proceeding checkLoadingTimeout();
if (loading === false) { loading = true; loadingStartTime = Date.now(); } else { // TODO: If the piece is different, then there should be a way to abort // or ignore a current load. console.warn( "Coudn't load:", parsed.path || parsed.name, "(Already loading.)", ); return true; }
const previewIdentity = await previewEvidence.begin(parsed.source, parsed.aeselPreview);
// Reload a previously sideloaded piece on subsequent loads. if ( !parsed.source && store["publishable-piece"] && parsed.piece === store["publishable-piece"].slug ) { parsed.source = store["publishable-piece"].source; parsed.name = store["publishable-piece"].slug; parsed.ext = store["publishable-piece"].ext; }
// 🕸️ Loading over the network from a parsed path object with no source code. if (!parsed.source) { params = parsed.params; path = parsed.path; search = parsed.search; colon = parsed.colon; hash = parsed.hash; host = parsed.host; slug = parsed.text;
if (typeof window !== 'undefined' && window.acSPIDER) { console.log("🕷️ SPIDER: Parsed slug/path:", { slug, path, parsedText: parsed.text, parsedPath: parsed.path, hash, params }); }
// 📡 A piece published under a handle owns the code channel named after // its own route. `/run` accepts a push there only from that handle's token, // so the name is free to be public — which is what lets the address someone // scanned be the channel too. Loading @handle/piece is therefore enough to // start watching it live, and the session server replays the last source to // a late joiner, so a phone arriving mid-session gets the current piece // without waiting for the next save. // // Subscribed alongside this browser's own channel rather than replacing it, // and deliberately never persisted: this is someone else's piece, not this // browser's identity. const routeChannel = (parsed.piece || slug || "").split(":")[0]; if (routeChannel.startsWith("@") && routeChannel.indexOf("/") !== -1) { const owned = routeChannel.slice(1); if (owned !== pieceCodeChannel) { pieceCodeChannel = owned; socket?.send("code-channel:sub", pieceCodeChannel); } // 🩺 Report on this piece to whoever owns it. Scoped to the piece's own // channel, which only that handle's token can listen to — a piece being // authored is the only thing that ever gets reported on. diagnostics.channel(pieceCodeChannel); } else { pieceCodeChannel = undefined; diagnostics.channel(""); }
// 👱 Route to the `profile` piece if we are just hitting an empty // username. if (slug.startsWith("@") && slug.indexOf("/") === -1) { params = [slug, ...params]; // Rewrite all params for `@user` slug urls. //slug = "profile"; // Go to `profile` instead of the `@user`. const hiddenSlug = "profile"; // Rewrite path to `profile`. console.log("Profile Path:", path); path = [...path.split("/").slice(0, -1), hiddenSlug].join("/"); }
// ⏰ Normalize *clock shortcodes to the clock piece and let clock.mjs fetch // the stored melody by code. This avoids intercepting later in the loader // and ensures the correct module is fetched. if (slug && slug.startsWith("*") && slug.length > 1) { clockShortcode = slug; // Preserve original *code for URL const cacheId = slug.slice(1); const clockParam = `*${cacheId}`; params = [clockParam, ...(params || [])]; slug = "clock"; path = "aesthetic.computer/disks/clock"; parsed.text = slug; parsed.path = path; parsed.params = params; }
// if (debug) console.log(debug ? "🟡 Development" : "🟢 Production"); if (host === "") host = originalHost; loadFailure = undefined; host = host.replace(/\/$/, ""); // Remove any trailing slash from host. // Note: This fixes a preview bug on teia.art. 2022.04.07.03.00 if (path === "") path = ROOT_PIECE; // Set bare path to what "/" maps to. // if (path === firstPiece && params.length === 0) params = firstParams;
// Check if the path already has a language extension and use it directly, otherwise default to .mjs // Handle sandboxed environments where location.protocol might be "null:" // For aesthetic.computer pieces, determine the correct server const { protocol, hostname } = getSafeUrlParts();
// If we're loading an aesthetic.computer piece, choose the appropriate server let baseUrl; if (path.startsWith('aesthetic.computer/')) { // Check if we're in OBJKT mode - use local bundled files if (typeof window !== 'undefined' && window.acSPIDER) console.log("🕷️ getPackMode():", getPackMode(), "window.acPACK_MODE:", window.acPACK_MODE, "checkPackMode():", checkPackMode()); if (getPackMode()) { // In OBJKT mode, use relative paths to load bundled pieces baseUrl = "."; } else { // Check if we're in a development environment (localhost with port) baseUrl = getBuiltInPieceBaseUrl(); } } else { baseUrl = `${protocol}//${hostname}`; }
if (typeof window !== 'undefined' && window.acSPIDER) console.log("�️ SPIDER MODE Debug:", { protocol, hostname, baseUrl, isSandboxed: isSandboxed(), path, isDevelopment: hostname === 'localhost' && typeof location !== 'undefined' && location.port });
// Check if path already includes the hostname to avoid double paths let resolvedPath = path; if (getPackMode() && path.startsWith('aesthetic.computer/')) { // In OBJKT mode, keep the full path since files are bundled with directory structure resolvedPath = path; } else if (baseUrl === 'https://aesthetic.computer' && path.startsWith('aesthetic.computer/')) { // Strip "aesthetic.computer/" prefix when using production domain to avoid doubling resolvedPath = path.substring('aesthetic.computer/'.length); }
// if (debug) console.log("🔍 Debug path resolution:", { originalPath: path, resolvedPath, hostname, baseUrl });
if (path.endsWith('.lisp') || path.endsWith('.lua')) { if (getPackMode()) { // In OBJKT mode, use absolute path from iframe origin fullUrl = "/" + resolvedPath + "?v=" + Date.now(); } else { fullUrl = baseUrl + "/" + resolvedPath + "?v=" + Date.now(); } } else { if (getPackMode()) { // In OBJKT mode, navigate up from lib directory to aesthetic.computer root, then to target const relativePath = resolvedPath.startsWith('aesthetic.computer/') ? resolvedPath.substring('aesthetic.computer/'.length) : resolvedPath; fullUrl = "../" + relativePath + ".mjs" + "?v=" + Date.now(); } else { fullUrl = baseUrl + "/" + resolvedPath + ".mjs" + "?v=" + Date.now(); } } // The query param `v` busts the cache so that the environment is // reset if a disk is re-entered while the system is running. // Note: Hash fragments (#) don't bust browser cache, only query strings (?) do. // if (debug) console.log("🕸", fullUrl); } else { // 📃 Loading with provided source code. // This could either be JavaScript or LISP.
if ( devReload === true && parsed.codeChannel && parsed.codeChannel !== codeChannel && parsed.codeChannel !== pieceCodeChannel ) { console.warn( "🙅 Not reloading, code channel invalid:", codeChannel || "N/A", ); return; } // console.log("📃 Loading from source:", JSON.stringify(parsed)); // console.log("📃 Source content to run:", JSON.stringify(parsed.source)); source = parsed.source; params = parsed.params; search = parsed.search; colon = parsed.colon || []; hash = parsed.hash; // tood: these probably don't work? 24.07.09.23.46 host = parsed.host; slug = parsed.name; // not 'text' for this.
// ⏰ Normalize *clock shortcodes for source-based loads too. if (slug && slug.startsWith("*") && slug.length > 1) { clockShortcode = slug; // Preserve original *code for URL const cacheId = slug.slice(1); const clockParam = `*${cacheId}`; params = [clockParam, ...(params || [])]; slug = "clock"; if (slug !== "(...)" && !path) path = "aesthetic.computer/disks/clock"; parsed.name = slug; parsed.path = path || "aesthetic.computer/disks/clock"; parsed.params = params; }
// 🔍 Enable tracing for kidlisp.com visualization if requested if (parsed.enableTrace) { enableKidlispTrace(); } else { disableKidlispTrace(); }
if (slug !== "(...)") path = parsed.path; //"aesthetic.computer/disks/" + slug; // 📓 Might need to fill in hash, path, or slug here. 23.06.24.18.49 }
let prefetches; // Will be acted on after `hotSwap`.
// 🅱️ Load the piece.
// const moduleLoadTime = performance.now();
let blobUrl, sourceCode, originalCode; try { // If this is a reload (with no source change) then just create a new // blobURL off the old source. // TODO: Cache piece code locally / in an intelligent way, // and then receive socket updates when it changes on the server? if ( slug?.split("~")[0] === currentText?.split("~")[0] && sourceCode == currentCode && !devReload ) { const blob = new Blob([currentCode], { type: "application/javascript" }); blobUrl = URL.createObjectURL(blob); sourceCode = currentCode; originalCode = sourceCode; } else { let sourceToRun; let fetchStartTime = performance.now(); // Initialize timing at the start
// 💾 Check if this is a cached kidlisp code (starts with $ followed by a cache ID) if (slug && slug.startsWith("$") && slug.length > 1) { const cacheId = slug.slice(1).split(":")[0]; // Remove $ prefix and any :fps colon params
// Clear author/hits immediately to prevent stale data showing during load currentHUDAuthor = null; currentHUDHits = null;
// First check if we have this in objktKidlispCodes (offline bundle) const globalScope = (function () { if (typeof window !== 'undefined') return window; if (typeof globalThis !== 'undefined') return globalThis; if (typeof global !== 'undefined') return global; if (typeof self !== 'undefined') return self; return {}; })();
if (globalScope.objktKidlispCodes && globalScope.objktKidlispCodes[cacheId]) { sourceToRun = globalScope.objktKidlispCodes[cacheId]; currentOriginalCodeId = slug; currentHUDAuthor = null; // No author info in offline bundles currentHUDHits = null; } else { console.log("💾 Loading cached kidlisp code:", cacheId); try { console.log("🔍 Fetching cached code from API..."); sourceToRun = await getCachedCodeMultiLevel(cacheId); if (!sourceToRun) { throw new Error(`Cached code not found: ${cacheId}`); } // Track the original $code identifier for sharing currentOriginalCodeId = slug; // Keep the full $code format console.log("✅ Successfully loaded cached code:", cacheId, `(${sourceToRun.length} chars)`);
// 👤 Fetch author metadata in background (for logging only) fetchKidlispMetadata(cacheId).then(meta => { if (meta) { // Note: author/hits are fetched but not displayed for KidLisp pieces console.log(`👤 Author: ${meta.handle || 'anonymous'}, Hits: ${meta.hits}`); } }).catch(err => { console.warn("⚠️ Failed to fetch KidLisp metadata:", err); }); } catch (error) { console.error("❌ Failed to load cached code:", cacheId, error); throw new Error(`Failed to load cached code: ${cacheId}`); } } } else if (fullUrl) { // 🎄 Check if piece code is cached (for merry preloading) const pieceSlug = slug?.split("~")[0]; const cachedPiece = pieceSlug ? pieceCodeCache.get(pieceSlug) : null;
if (cachedPiece) { console.log(`🎄 Using cached code for: ${pieceSlug}`); sourceToRun = cachedPiece.code; // Skip fetch, use cached code directly } else { // In OBJKT mode, try direct import first for .mjs files to avoid CSP issues // Extract the filename from URL, handling ./ prefix and hash fragments const urlWithoutHash = fullUrl.split('#')[0]; const filename = urlWithoutHash.split('/').pop();
if (getPackMode() && filename.endsWith('.mjs')) { // In OBJKT mode, skip dynamic import and use fetch directly since files are bundled locally // Will proceed to fetch() below }
let response; if (logs.loading) console.log("📥 Loading from url:", fullUrl); // if (debug) console.log("🔍 Debug: Constructed fullUrl:", fullUrl);
// Notify boot progress const fetchStartTime = performance.now(); send({ type: "boot-log", content: `fetching: ${path}` });
// Use cache: 'no-store' to force fresh fetch, especially important for LAN IP access // Retry on network failure (bad connections drop fetches) { const maxAttempts = 3; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { response = await fetch(fullUrl, { cache: 'no-store' }); break; } catch (fetchErr) { if (attempt >= maxAttempts) throw fetchErr; console.warn(`🔄 Piece fetch attempt ${attempt}/${maxAttempts} failed, retrying in ${attempt}s...`); await new Promise(r => setTimeout(r, 1000 * attempt)); } } } const fetchEndTime = performance.now(); diskTimings.fetchComplete = Math.round(fetchEndTime - diskTimingStart); // Silent: fetch completed if (response.status === 404 || response.status === 403) { const extension = path.endsWith('.lisp') ? '.lisp' : path.endsWith('.lua') ? '.lua' : '.mjs'; // Handle sandboxed environments for anon URL construction const { protocol } = getSafeUrlParts(); const anonUrl = protocol + "//" + "art.aesthetic.computer" + "/" + path.split("/").pop() + extension + "#" + Date.now(); if (logs.loading) console.log("🧑🤝🧑 Attempting to load piece from anon url:", anonUrl); response = await fetch(anonUrl, { cache: 'no-store' }); if (response.status === 404 || response.status === 403) throw new Error(response.status); } sourceToRun = await response.text(); } } else { sourceToRun = source; }
// 🔥 Idea // One should be able to drag a piece in, then be able to load the piece // go back to the prompt, and return to it and it should still load // the modded code! // Then refresh should be able to function as well? // ⚠️ Detect if we are running `kidlisp` or JavaScript syntax. // Note: This may not be the most reliable way to detect `kidlisp`? // 🚗 Needs to know if the source was from a prompt with a lisp module. // console.log("🔍 Checking if kidlisp source:", JSON.stringify(sourceToRun)); if ( sourceToRun.startsWith("(") || sourceToRun.startsWith(";") || forceKidlisp || slug === "(...)" || path === "(...)" || (path && path.endsWith(".lisp")) || (slug && slug.startsWith("$") && slug.length > 1) || // Cached codes are always kidlisp // Pack mode KidLisp pieces - but NOT if acPACK_COLOPHON explicitly says isKidLisp: false (typeof window !== 'undefined' && window.acPACK_PIECE && slug === window.acPACK_PIECE && window.acPACK_COLOPHON?.piece?.isKidLisp !== false) ) { // Only use basic detection, not the broader isKidlispSource function // which can incorrectly detect JavaScript as kidlisp, unless forceKidlisp is true // or this came from parse function as kidlisp (slug/path === "(...)") // Assume lisp. // console.log( // "🐍 Lisp piece detected (slug:", // slug, // "path:", // path, // "forceKidlisp:", // forceKidlisp, // ")", // ); sourceCode = sourceToRun; originalCode = sourceCode;
// 🔒 KidLisp pieces are always restricted (safe by design) pieceMetadata = { code: slug || "kidlisp", trustLevel: "kidlisp", anonymous: true };
const isPackMode = (typeof window !== "undefined" && window.acPACK_MODE) || (typeof globalThis !== "undefined" && globalThis.acPACK_MODE); if (!isPackMode) { log.lisp.log("Initializing KidLisp piece..."); }
// Notify boot progress const compileStartTime = performance.now(); const fetchElapsed = Math.round(compileStartTime - fetchStartTime); // console.log("📢 Disk sending boot-log: compiling kidlisp"); // Ensure send is available before using it if (typeof send === 'function') { send({ type: "boot-log", content: `compiling kidlisp (fetch: ${fetchElapsed}ms)` }); } else { log.lisp.warn("send function not available for boot-log"); }
// Initialize persistent cache for $codes (only needs to be done once) initPersistentCache(store);
// Initialize persistent image cache for paste/stamp (only needs to be done once) imageCache.init(store);
loadedModule = lisp.module(sourceToRun, path && path.endsWith(".lisp"));
const compileEndTime = performance.now(); const compileElapsed = Math.round(compileEndTime - compileStartTime); diskTimings.compileComplete = Math.round(compileEndTime - diskTimingStart); // Silent: KidLisp compiled if (!isPackMode) { log.lisp.success(`KidLisp module loaded (${compileElapsed}ms)`); }
// Notify boot progress // console.log("📢 Disk sending boot-log: kidlisp compiled"); send({ type: "boot-log", content: `kidlisp compiled (${compileElapsed}ms)` });
// Send source file to boot canvas for display send({ type: "boot-file", content: { filename: path, source: sourceCode.slice(0, 8000) } });
if (devReload) { store["publishable-piece"] = { slug, source: sourceToRun, ext: "lisp", }; if (logs.loading) console.log("💌 Publishable:", store["publishable-piece"]); } } else if ( forceP5 || parsed?.ext === "js" || (path && path.endsWith(".js")) ) { // 🎨 p5.js sketch (dropped .js file) — run real p5 in this worker // against an OffscreenCanvas blitted into screen.pixels (Option B), // mirroring the URL fallback path for .js pieces. sourceCode = sourceToRun; originalCode = sourceCode; pieceMetadata = { code: slug || "p5", trustLevel: "p5", anonymous: true, };
const compileStartTime = performance.now(); const fetchElapsed = Math.round(compileStartTime - fetchStartTime); send({ type: "boot-log", content: `compiling p5 (fetch: ${fetchElapsed}ms)`, });
loadedModule = await makeP5WorkerModule({ slug, source: sourceToRun });
const compileEndTime = performance.now(); const compileElapsed = Math.round(compileEndTime - compileStartTime); diskTimings.compileComplete = Math.round(compileEndTime - diskTimingStart);
send({ type: "boot-log", content: `p5 compiled (${compileElapsed}ms)`, });
send({ type: "boot-file", content: { filename: path, source: sourceCode.slice(0, 8000) }, });
if (devReload) { store["publishable-piece"] = { slug, source: sourceToRun, ext: "js", }; if (logs.loading) console.log("💌 Publishable:", store["publishable-piece"]); } } else if ( parsed?.ext === "lua" || (path && path.endsWith(".lua")) || (fullUrl && fullUrl.includes(".lua")) || (sourceToRun.trim().startsWith("--") && /(?:^|\n)\s*function\s+(setup|draw)\s*\(/.test(sourceToRun)) ) { sourceCode = sourceToRun; originalCode = sourceCode; pieceMetadata = { code: slug || "l5", trustLevel: "l5", anonymous: true, };
const compileStartTime = performance.now(); const fetchElapsed = Math.round(compileStartTime - fetchStartTime); send({ type: "boot-log", content: `compiling lua (fetch: ${fetchElapsed}ms)` });
loadedModule = await l5.module(sourceToRun);
const compileEndTime = performance.now(); const compileElapsed = Math.round(compileEndTime - compileStartTime); diskTimings.compileComplete = Math.round(compileEndTime - diskTimingStart);
send({ type: "boot-log", content: `lua compiled (${compileElapsed}ms)` });
send({ type: "boot-file", content: { filename: path, source: sourceCode.slice(0, 8000) } });
if (devReload) { store["publishable-piece"] = { slug, source: sourceToRun, ext: "lua", }; if (logs.loading) console.log("💌 Publishable:", store["publishable-piece"]); } } else { if (devReload) { store["publishable-piece"] = { slug, source: sourceToRun }; if (logs.loading) console.log("💌 Publishable:", store["publishable-piece"].slug); }
// Initialize persistent image cache for paste/stamp (also for .mjs pieces) imageCache.init(store);
// 🔒 Determine piece trust level // Built-in disk pieces (aesthetic.computer/disks/*) are always trusted const isBuiltInDisk = path && path.includes("aesthetic.computer/disks/"); if (isBuiltInDisk) { pieceMetadata = { code: slug || "system", trustLevel: "trusted", anonymous: false }; } else if (slug && !devReload) { try { // Clear permissions from previous piece if (pieceMetadata?.code) { clearPermissions(pieceMetadata.code); }
pieceMetadata = await fetchPieceMetadata(slug); if (logs.loading) { console.log(`🔒 Piece metadata:`, { code: pieceMetadata.code, trustLevel: pieceMetadata.trustLevel, anonymous: pieceMetadata.anonymous }); } } catch (err) { console.warn(`⚠️ Failed to fetch piece metadata, defaulting to untrusted:`, err); pieceMetadata = { code: slug, trustLevel: "untrusted", anonymous: true }; } } else { // Dev reload or no slug - default to untrusted pieceMetadata = { code: slug || "unknown", trustLevel: "untrusted", anonymous: true }; }
originalCode = sourceToRun; const updatedCode = updateCode(sourceToRun, host, debug);
prefetches = updatedCode .match(/"(@\w[\w.]*\/[^"]*)"/g) ?.map((match) => match.slice(1, -1)); // for "@name/code".
const blob = new Blob([updatedCode], { type: "application/javascript", });
blobUrl = URL.createObjectURL(blob);
// Notify boot progress const importStartTime = performance.now(); const importFetchElapsed = Math.round(importStartTime - fetchStartTime); // console.log("📢 Disk sending boot-log: importing module"); send({ type: "boot-log", content: `importing module (fetch: ${importFetchElapsed}ms)` });
sourceCode = updatedCode; // Retry dynamic import on network failures (sub-dependency fetches can fail on bad connections) { const maxImportAttempts = 3; for (let attempt = 1; attempt <= maxImportAttempts; attempt++) { try { loadedModule = await import(blobUrl); break; } catch (importErr) { const isNetworkError = importErr.toString().includes("Failed to fetch"); if (attempt < maxImportAttempts && isNetworkError) { console.warn(`🔄 Module import attempt ${attempt}/${maxImportAttempts} failed, retrying in ${1.5 * attempt}s...`); URL.revokeObjectURL(blobUrl); blobUrl = URL.createObjectURL(new Blob([updatedCode], { type: "application/javascript" })); await new Promise(r => setTimeout(r, 1500 * attempt)); } else { throw importErr; } } } }
const importEndTime = performance.now(); const importElapsed = Math.round(importEndTime - importStartTime); diskTimings.compileComplete = Math.round(importEndTime - diskTimingStart); // Silent: JS module imported if (logs.loading) console.log(`✅ Module imported (${importElapsed}ms)`); send({ type: "boot-log", content: `module imported (${importElapsed}ms)` });
// Send source file to boot canvas for display send({ type: "boot-file", content: { filename: path, source: sourceCode.slice(0, 8000) } // Limit size }); } } } catch (err) { const moduleLoadErrorTime = performance.now(); // console.log(`⏰ Module load error caught at ${moduleLoadErrorTime}ms`); console.log("🟡 Error loading mjs module:", err);
// Determine if this was a fetch failure (404/403) vs a module import/compile error. // Only try language fallbacks if this was a missing .mjs path. const isFetchError = err.message === "404" || err.message === "403"; const isNetworkImportError = err.toString().includes("Failed to fetch dynamically imported module"); const isModuleImportError = isNetworkImportError || err.toString().includes("Unexpected token") || err.toString().includes("SyntaxError") || err.toString().includes("Cannot use import");
// If the .mjs file was fetched but failed to import (JS error), don't try fallbacks. if (fullUrl && !fullUrl.includes('.lisp') && !fullUrl.includes('.lua') && !fullUrl.includes('.js?') && isFetchError && !isModuleImportError) { try { const fallbackExts = [".js", ".lua", ".lisp"]; let loadedFromFallback = false; let fallbackError = null;
for (const fallbackExt of fallbackExts) { let fallbackUrl = fullUrl.replace(".mjs", fallbackExt); if (logs.loading) { console.log(`📥 Loading ${fallbackExt.slice(1)} from url:`, fallbackUrl); }
let response = await fetch(fallbackUrl, { cache: 'no-store' }); let resolvedUrl = fallbackUrl;
if (response.status === 404 || response.status === 403) { // Handle sandboxed environments for anon URL construction const { protocol } = getSafeUrlParts(); const anonUrl = protocol + "//" + "art.aesthetic.computer" + "/" + path.split("/").pop() + fallbackExt + "#" + Date.now(); if (logs.loading) { console.log("🧑🤝🧑 Attempting to load piece from anon url:", anonUrl); } response = await fetch(anonUrl, { cache: 'no-store' });
if (response.status === 404 || response.status === 403) { fallbackError = new Error(response.status); continue; } resolvedUrl = anonUrl; }
sourceCode = await response.text(); originalCode = sourceCode;
if (fallbackExt === ".js") { // Plain JavaScript / p5-style sketch — run real p5 in this worker // against an OffscreenCanvas blitted into screen.pixels (Option B). pieceMetadata = { code: slug || "p5", trustLevel: "p5", anonymous: true, }; loadedModule = await makeP5WorkerModule({ slug, source: sourceCode }); if (devReload) { store["publishable-piece"] = { slug, source: sourceCode, ext: "js" }; } } else if (fallbackExt === ".lua") { pieceMetadata = { code: slug || "l5", trustLevel: "l5", anonymous: true, }; loadedModule = await l5.module(sourceCode); if (devReload) { store["publishable-piece"] = { slug, source: sourceCode, ext: "lua" }; } } else { pieceMetadata = { code: slug || "kidlisp", trustLevel: "kidlisp", anonymous: true }; loadedModule = lisp.module(sourceCode, true); if (devReload) { store["publishable-piece"] = { slug, source: sourceCode, ext: "lisp" }; } }
loadedFromFallback = true; break; }
if (!loadedFromFallback) { throw fallbackError || new Error("404"); }
if (devReload && logs.loading) { console.log("💌 Publishable:", store["publishable-piece"]); } } catch (err) { // Continue with current module if one has already loaded. console.error( `😡 "${path}" load failure:`, err, "💾 First load:", firstLoad, ); loadFailure = err; $commonApi.net.loadFailureText = err.message + "\n" + sourceCode; loading = false;
// Only return a 404 if the error type is correct. if (firstLoad && (err.message === "404" || err.message === "403")) { // Check if slug is a user code (e.g. "ac25namuc") and redirect to their profile. const handle = await resolveUserCode(slug); if (handle) { $commonApi.jump(`@${handle}`); } else { $commonApi.jump(`404~${slug}`); } } else { // At the prompt, a not-found entry isn't a failure — its helper shows // a "thinking" character + LLM reply instead of a sad face. if (currentText !== "prompt") $commonApi.notice(":(", ["red", "yellow"]); } return false; } } else { // Module import/compile error - the .mjs file exists but has errors // Don't try language fallbacks, just show the error if (isNetworkImportError) { console.error( `😡 "${path}" module import failed (network error loading dependencies after ${3} retries):`, err, ); } else if (isModuleImportError) { console.error( `😡 "${path}" module import failed (JS error in the piece):`, err, ); } else { console.error( `😡 "${path}" load failure:`, err, "💾 First load:", firstLoad, ); } loadFailure = err; $commonApi.net.loadFailureText = err.message + "\n" + (sourceCode || ""); loading = false;
// Only return a 404 if the error type is correct. if (firstLoad && (err.message === "404" || err.message === "403")) { // Check if slug is a user code (e.g. "ac25namuc") and redirect to their profile. const handle = await resolveUserCode(slug); if (handle) { $commonApi.jump(`@${handle}`); } else { $commonApi.jump(`404~${slug}`); } } else { $commonApi.notice(":(", ["red", "yellow"]); } return false; } }
// console.log("Module load time:", performance.now() - moduleLoadTime, module); // 🧨 Fail out if no module is found. const moduleCheckTime = performance.now(); // console.log(`⏰ Module check at ${moduleCheckTime}ms, module loaded: ${loadedModule !== undefined}`);
if (loadedModule == null) { loading = false; leaving = false; return false; }
// 🧩 Piece code has been loaded... // Now we can instantiate the piece.
pieceHistoryIndex += fromHistory === true ? 0 : 1; // Adjust the history.
if (!debug && !firstLoad) { // console.clear(); headers($commonApi.dark); // Clear console and re-print headers if we are in production. }
// console.log("🧩", path, "🌐", host);
$commonApi.net.devReload = devReload; // Expose to the piece if it was // reloaded by the developer, for special logic per piece.
// Add debug to the common api. $commonApi.debug = debug;
// Add reload to the common api. $commonApi.reload = function reload({ piece, name, source, codeChannel, createCode, codeId, // The $code identifier (e.g., "inz") from kidlisp.com authToken, // Auth token from kidlisp.com login enableTrace, // Enable execution trace for kidlisp.com visualization language, // Optional source language (e.g., "lua") ext, // Optional source extension hint (e.g., "lua") liveName, // Optional stable live piece name } = {}) { // console.log("⚠️ Reloading:", piece, name, source);
// Check for global loading timeout first if (checkLoadingTimeout()) { console.log("🔴 Recovered from stuck loading state"); }
if (loading && source && !name && !piece) { // If a piece is loading and we have new source code, queue the reload // But only queue up to 10 times (1 second max wait), then force through const queueCount = (reload._queueCount || 0) + 1; reload._queueCount = queueCount;
if (queueCount > 10) { // Too many retries - force reset loading state and proceed console.warn("🔴 Reload queue timeout - forcing loading reset"); loading = false; loadingStartTime = null; reload._queueCount = 0; } else { console.log("🟡 Queueing reload until current load completes..."); setTimeout( () => reload({ source, codeId, createCode, authToken, enableTrace, language, ext, liveName, }), 100, ); return; } } else { // Reset queue counter when not queuing reload._queueCount = 0; }
if (loading) { console.log("🟡 A piece is already loading."); return; } if (piece === "*refresh*") { // padding: 8px; border-radius: 2px;console.log("💥️ Restarting system..."); send({ type: "refresh" }); // Refresh the browser. } else if (piece === "*piece-reload*") { // console.log("🎨 Reloading current piece..."); // Reload the current piece without refreshing the entire page $commonApi.load( { path: currentPath, host: currentHost, search: currentSearch, colon: currentColon, params: currentParams, hash: currentHash, text: currentText, }, true, // fromHistory - don't add to history stack alias, true, // devReload ); } else if (source && !name && !piece) { // Live reload with new source code (e.g., from kidlisp.com editor) const isLuaReload = language === "lua" || ext === "lua";
// Just pass the source directly without path/text currentText = isLuaReload ? (liveName || "l5-live") : source; currentPath = isLuaReload ? (liveName || "l5-live") : source;
if (isLuaReload) { $commonApi.load( { source, name: liveName || "l5-live", ext: "lua", search: currentSearch, colon: currentColon, params: currentParams, hash: currentHash, }, true, // fromHistory - don't add to history stack alias, true, // devReload ); return; }
// Store the $code identifier for screenshots/sharing if (codeId) { currentOriginalCodeId = `$${codeId}`; }
// Store createCode flag globally so kidlisp can access it if (createCode) { $commonApi.kidlispCreateCode = true; } else { // console.log('⚠️ createCode flag is false or undefined:', createCode); $commonApi.kidlispCreateCode = false; }
// Store auth token globally so kidlisp can use it for saving if (authToken) { $commonApi.kidlispAuthToken = authToken; }
// Store trace flag globally for kidlisp visualization $commonApi.kidlispEnableTrace = enableTrace || false;
// Reset frameCount so trace is posted on first frame of new code if (globalKidLispInstance) { globalKidLispInstance.frameCount = 0; }
$commonApi.load( { source: source, // Pass as source so it's used directly name: "kidlisp-live", // Give it a name so slug won't be undefined search: currentSearch, colon: currentColon, params: currentParams, hash: currentHash, enableTrace: enableTrace, // Pass trace flag }, true, // fromHistory - don't add to history stack alias, true, // devReload undefined, // loadedCallback true, // forceKidlisp - always treat as KidLisp code ); } else if (name && source) { // TODO: Check for existence of `name` and `source` is hacky. 23.06.24.19.27 // TODO: 🔥 This should somehow keep current commands or params, etc.
// console.log( // "🪷 Current: params:", // currentParams, // "text:", // currentText, // "path:", // currentPath, // );
// Note: This is used for live development via the socket server. // // A push names the piece by its bare slug (`balozo`), but this browser // may be sitting on that piece's handle route (`@jeffrey/balozo`). The // name given here becomes `currentText`, and `bios.mjs` rewrites the // address bar from it — so taking the pushed slug at face value renamed // the URL to `/balozo`, and the next refresh resolved that as a system // piece, failed, and landed on `404~balozo`. It also dropped // `pieceCodeChannel`, which quietly ended live updates. // // When the route we are on is that same piece, the route is its name. const routed = currentText?.startsWith("@") && currentText.split("/")[1]?.split(":")[0] === name ? currentText : name; $commonApi.load({ source, name: routed, codeChannel }, false, false, true); // Load source code. } /*if (piece === "*" || piece === undefined /*|| currentText === piece*/ /*) {*/ else { // console.log("💾️ Reloading:", piece, "Params:", currentParams); // $commonApi.pieceCount = -1; // Reset pieceCount on developer reload. // (This can be disabled while testing pieces // that rely on pieceCount increments) // ❤️🔥 TODO: Reloading the same piece should not effect piece count.
$commonApi.load( { path: currentPath, host: currentHost, search: currentSearch, colon: currentColon, params: currentParams, hash: currentHash, text: currentText, }, // Use the existing contextual values when live-reloading in debug mode. true, // (fromHistory) ... never add any reload to the history stack alias, true, // devReload ); } //else if (piece !== undefined) { //console.log("Reloading pieceeeeeeee:", piece, parse(piece)); //$commonApi.load(parse(piece), false, false, true); //} };
// Start the socket server // TODO: Before we load the disk, in case of needing to reload remotely on failure? 23.01.27.12.48 let receiver; // Handles incoming messages from the socket. const forceProd = false; // For testing prod socket servers in development. // TOOD: Hoist this to an environment variable?
// Requests a session-backend and connects via websockets. function startSocket() { // Skip socket connections in OBJKT mode or SPIDER mode if (getPackMode() || (typeof window !== 'undefined' && window.acSPIDER)) { return; }
// 📡 Set up remote logging for LAN Dev mode setupRemoteLogging();
if ( //parsed.search?.startsWith("preview") || //parsed.search?.startsWith("icon") previewOrIconMode ) { console.log("🧦 Sockets disabled, just grabbing screenshots. 😃"); return; } // Never open socket server in icon / preview mode. // if (debug && logs.session) console.log("🫂 Finding session server..."); socket = new Socket(debug, send); // Then redefine and make a new socket.
const monolith = "monolith"; // "monolith"; // or `undefined` for horizontal scaling via // jamstack
session(slug, forceProd, monolith) .then(async (sesh) => { if (typeof sesh === "string") throw new Error(sesh); // Cancel if error. const url = new URL(sesh.url); // Parse the url. const udpUrl = new URL(sesh.udp); // Parse the udp url.
// console.log("Session URL:", url);
// 🩰 Fetch dev-info to get TURN host (for local dev with Docker) // Only attempt this in dev mode (localhost) - not in production let turnHost = null; const isLocalDev = url.hostname === "localhost" || url.hostname.endsWith(".local"); if (isLocalDev) { try { const devInfoRes = await fetch(`${sesh.url}/dev-info`); if (devInfoRes.ok) { const devInfo = await devInfoRes.json(); if (devInfo.ip) { turnHost = devInfo.ip; log.socket.debug("Got TURN host from dev-info:", turnHost); } } } catch (e) { log.socket.debug("Could not fetch dev-info for TURN host:", e.message); } }
// 🩰 UDP... (via `bios`) // Use the protocol from the session response (http for localhost, https for prod) log.socket.debug("Sending udp:connect to BIOS:", { url: `${udpUrl.protocol}//${udpUrl.hostname}`, port: udpUrl.port || (udpUrl.protocol === 'https:' ? '443' : '80'), turnHost, }); send({ type: "udp:connect", content: { url: `${udpUrl.protocol}//${udpUrl.hostname}`, port: udpUrl.port || (udpUrl.protocol === 'https:' ? '443' : '80'), turnHost, // Pass TURN host separately for ICE servers }, });
let slugBroadcastInterval;
// 🕸️ Web Sockets socket?.connect( url.host + url.pathname, (id, type, content) => { // Globally receivable messages... // (There are also some messages handled in `Socket`) // 😱 Scream at everyone who is connected! if (type === "scream" && socket?.id !== id) { console.log("😱 Scream:", content, "❗"); scream = content; } // 🎯 Jump to a specific piece! if (type === "jump") { $commonApi.jump(content.piece); } // 📱 Handle dev identity from session server (LAN Dev mode) if (type === "dev:identity") { devIdentity = content; remoteLogSocket = socket; // Store socket reference for remote logging flushRemoteLogQueue(); // Send any queued logs $commonApi?.needsPaint?.(); return; } // 🧩 Pieces get all other messages not caught in `Socket`. receiver?.(id, type, content); // Run the piece receiver. }, $commonApi.reload, "wss", () => { // 🔩 Connected! (Post-connection logic.) if (USER) socket?.send("login", { user: USER });
// Broadcast current location. if (HANDLE) { // console.log("🗼 Broadcasting slug:", currentText, "for:", HANDLE); socket?.send("location:broadcast", { handle: HANDLE, slug: currentText, user: USER, // Include user for identity resolution }); slugBroadcastInterval = setInterval(() => { socket?.send("location:broadcast", { handle: HANDLE, slug: "*keep-alive*", user: USER, // Include user for identity resolution }); }, 2500); }
// Subscribe to code-channel as needed. if (codeChannel) socket?.send("code-channel:sub", codeChannel); // …and to the loaded piece's own channel, which a reconnect would // otherwise drop silently — the piece would keep rendering and // simply stop updating. if (pieceCodeChannel) socket?.send("code-channel:sub", pieceCodeChannel);
updateHUDStatus(); $commonApi.needsPaint(); codeChannelAutoLoader?.(); // setTimeout(function () { // currentHUDStatusColor = undefined; // }, 250); }, () => { // 💔 Disconnected! (Post-disconnection logic.) updateHUDStatus(); clearInterval(slugBroadcastInterval); }, ); }) .catch((err) => { console.error("Session connection error:", err); }); }
// End the socket connection before switching pieces if one exists. // socket?.kill(); // udp?.kill(); // socket = undefined;
// Delay session server by .75 seconds in order to prevent redundant // connections being opened as pieces are quickly re-routing and jumping. clearTimeout(socketStartDelay); socket?.kill(); // Kill any already open socket from a previous disk. udp?.kill(); socket = undefined; socketStartDelay = setTimeout(() => startSocket(), 250);
$commonApi.net.socket = function (receive) { receiver = receive || (() => {}); if (!socket) { // Just in case we init. in a `boot` before the timeout fires above. clearTimeout(socketStartDelay); startSocket(); } else { // Return the server then send an already connected message. setTimeout(() => { if (socket?.id) receiver(socket.id, "connected:already"); }, 10); } return socket; };
// ***Client Metadata Fields*** // Set default metadata fields for SEO and sharing, // (requires serverside prerendering, also via `index.js`). let meta;
if (alias === false) { // Parse any special piece metadata.
// Check if we're in OBJKT mode and have colophon data let objktContext = null; if (checkPackMode() && typeof window !== 'undefined' && window.acOBJKT_COLOPHON) { objktContext = { author: window.acOBJKT_COLOPHON.build.author }; }
// Use parsed.piece for the title (just the piece name, not full slug with params) const titleSlug = parsed.piece || slug;
const { title, desc, ogImage, twitterImage, icon } = metadata( location.host, // "aesthetic.computer", titleSlug, loadedModule.meta?.({ ...parsed, num: $commonApi.num, store: $commonApi.store, }) || inferTitleDesc(originalCode), location.protocol, // Pass the current protocol objktContext // Pass OBJKT context if available );
meta = { title, desc, // Note: This doesn't auto-update externally hosted module descriptions, and may never need to? 22.07.19.06.00 img: { og: ogImage, twitter: twitterImage, icon, }, url: "https://aesthetic.computer/" + slug, }; }
// Add meta to the common api so the data can be overridden as needed. $commonApi.meta = (data) => send({ type: "meta", content: data });
// Rewrite a new URL / parameter path without affecting the history. $commonApi.net.rewrite = (path, historical = false) => { if (historical) $commonApi.history.push(path); send({ type: "rewrite-url-path", content: { path, historical } }); // Jump the browser to a new url. };
// Add host to the networking api. $commonApi.net.host = host;
// Jump the browser to a new url. $commonApi.net.web = (url, jumpOut) => { send({ type: "web", content: { url, blank: jumpOut } }); };
$commonApi.net.refresh = () => { send({ type: "refresh" }); };
// Tell the system to wait until preloading is finished before painting. $commonApi.net.waitForPreload = () => { send({ type: "wait-for-preload", content: true }); };
// Tell the system that all preloading is done. $commonApi.net.preloaded = () => { send({ type: "preload-ready", content: true }); };
$commonApi.content = new Content();
$commonApi.dom = {};
$commonApi.dom.clear = () => { $commonApi.content.remove(); };
$commonApi.dom.html = (strings, ...vars) => { const processed = defaultTemplateStringProcessor(strings, ...vars); $commonApi.content.add(processed); };
$commonApi.dom.css = (strings, ...vars) => { const processed = defaultTemplateStringProcessor(strings, ...vars); $commonApi.content.add(`<style>${processed}</style>`); };
$commonApi.dom.javascript = (strings, ...vars) => { const processed = defaultTemplateStringProcessor(strings, ...vars); $commonApi.content.add(`<script>${processed}</script>`); }; // 💾 Uploading + Downloading // Add download event to trigger a file download from the main thread. $commonApi.download = (filename, data, modifiers) => { send({ type: "download", content: { filename, data, modifiers } }); };
// * Preload * // Add preload to the boot api. // Accepts paths local to the original disk server, full urls, and demos. // Usage: preload("demo:drawings/2021.12.12.17.28.16.json") // pre-included // preload("https://myserver.com/test.json") // remote // preload("drawings/default.json") // hosted with disk // Results: preload().then((r) => ...).catch((e) => ...) // via promise
$commonApi.net.preload = async function ( path, parseJSON = true, progressReport, options = {}, ) { let extension;
const rejection = (reject) => { reject(new DOMException("Aborted", "AbortError")); };
// 🎒 PACK mode: Check VFS for embedded files first (used for offline bundles) const packModeActive = (typeof window !== "undefined" && window.acPACK_MODE) || (typeof globalThis !== "undefined" && globalThis.acPACK_MODE); const packVfs = (typeof window !== "undefined" && window.VFS) || (typeof globalThis !== "undefined" && globalThis.VFS);
if (packModeActive && packVfs) { let vfsPath = typeof path === "object" ? path.path : path; // Normalize path: strip leading slash and "aesthetic.computer/" prefix if (vfsPath.startsWith('/')) vfsPath = vfsPath.slice(1); if (vfsPath.startsWith('aesthetic.computer/')) vfsPath = vfsPath.slice('aesthetic.computer/'.length);
const vfsEntry = packVfs[vfsPath]; if (vfsEntry) { const ext = typeof path === "object" ? path.extension : vfsPath.split(".").pop().split("?")[0]; progressReport?.(1); // Report completion immediately for VFS files
if (ext === "json") { // JSON files in VFS: { content: "...", type: 'json' } or { content: "...", type: 'base64' } let jsonStr; if (typeof vfsEntry === 'object' && vfsEntry.content !== undefined) { if (vfsEntry.type === 'base64') { jsonStr = atob(vfsEntry.content); } else { jsonStr = vfsEntry.content; // type: 'json' stores content as string } } else if (typeof vfsEntry === 'string') { jsonStr = vfsEntry; } if (jsonStr !== undefined) { return parseJSON ? JSON.parse(jsonStr) : jsonStr; } } else if (ext === "xml") { let xmlStr; if (typeof vfsEntry === 'object' && vfsEntry.content !== undefined) { xmlStr = vfsEntry.type === 'base64' ? atob(vfsEntry.content) : vfsEntry.content; } else { xmlStr = vfsEntry; } const parser = new DOMParser(); return parser.parseFromString(xmlStr, "text/xml"); } // For other types, fall through to normal handling if not JSON/XML } }
if (soundWhitelist.includes(path)) { extension = "ogg"; } else { if (typeof path === "object") { extension = path.extension; path = path.path; } else { extension = path.split(".").pop().split("?")[0]; }
if (path.indexOf("/") === 0) path = path.slice(1);
try { const url = new URL(path); if (url.protocol === "demo:") { path = `/demo/${url.pathname}`; } else if (url.protocol !== "https:") { } } catch { // Handle sandboxed environments for path construction const { protocol, hostname, host } = getSafeUrlParts(); const originalPath = path;
// Apply the same origin-aware logic as in module loading let baseUrl; if (path.startsWith('aesthetic.computer/')) { // Check if we're in a development environment (localhost with port) baseUrl = getBuiltInPieceBaseUrl();
// Only strip "aesthetic.computer/" if we're using the main production domain if (baseUrl === 'https://aesthetic.computer') { path = path.substring('aesthetic.computer/'.length); } } else { baseUrl = `${protocol}//${host || hostname}`; }
path = `${baseUrl}/${path}`;
// Removed debug log for font glyph preload } }
if (extension === "xml" || extension === undefined) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest();
options.signal?.addEventListener("abort", () => { xhr.abort(); rejection(reject); });
xhr.open("GET", path, true); xhr.onprogress = function (event) { const progress = min(event.loaded / event.total, 1); if (debug && logs.download) { console.log(`💈 XML Download: ${progress * 100}%`); } progressReport?.(progress); }; xhr.onload = function () { if (xhr.status === 200 || xhr.status === 304) { resolve(xhr.responseXML || xhr.responseText); } else { reject(xhr.status); } }; xhr.onerror = reject; xhr.send(); }); } else if (extension === "json") { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest();
options.signal?.addEventListener("abort", () => { xhr.abort(); rejection(reject); });
xhr.open("GET", path, true); xhr.onprogress = function (event) { const progress = min(event.loaded / event.total, 1); if (debug && logs.download) { console.log(`💈 JSON Download: ${progress * 100}%`); } progressReport?.(progress); }; xhr.onload = function () { if (xhr.status === 200 || xhr.status === 304) { resolve(parseJSON ? JSON.parse(xhr.response) : xhr.response); } else { reject(xhr.status); } }; xhr.onerror = reject; xhr.send(); }); } else if ( extension === "webp" || extension === "jpg" || extension === "gif" || extension === "jpeg" || extension === "png" ) { return new Promise((resolve, reject) => { if (options.signal?.aborted) { rejection(reject); return; }
send({ type: "load-bitmap", content: path }); preloadPromises[path] = { resolve, reject };
options.signal?.addEventListener("abort", () => { send({ type: "load:abort", content: path }); rejection(reject); }); }); } else if ( extension === "m4a" || extension === "ogg" || extension === "webm" || extension === "wav" || extension === "mp3" ) { return new Promise((resolve, reject) => { if (options.signal?.aborted) { rejection(reject); return; }
preloadPromises[path] = { resolve, reject }; // Register the receiver before asking BIOS to load. BIOS deliberately // acknowledges an HTMLAudioElement immediately, so sending first can // race the reply and leave this promise pending forever. send({ type: "sfx:load", content: path });
options.signal?.addEventListener("abort", () => { rejection(reject); }); }); } else if (extension === "zip") { return new Promise((resolve, reject) => { if (options.signal?.aborted) { rejection(reject); return; }
send({ type: "zip:load", content: path }); preloadPromises[path] = { resolve, reject };
options.signal?.addEventListener("abort", () => { rejection(reject); }); }); } };
// 🎬 Load animated WebP and decode all frames $commonApi.net.preloadAnimatedWebp = async function (url) { return new Promise((resolve, reject) => { send({ type: "load-animated-webp", content: url }); preloadPromises[url] = { resolve, reject }; }); };
$commonApi.slug = slug; $commonApi.piece = slug?.split("~")[0].split(":")[0]; $commonApi.query = Object.fromEntries(new URLSearchParams(search)); $commonApi.params = params || []; $commonApi.colon = colon || []; $commonApi.hash = hash; // Add hash support for painting codes like #2ug
// Initialize duration tracking from query parameters if ($commonApi.query.duration) { const duration = parseFloat($commonApi.query.duration); if (!isNaN(duration) && duration > 0) { durationTotal = duration; durationStartTime = pageLoadTime; // Start from page load time immediately durationProgress = 0; durationCompleted = false; durationBlinkState = false; console.log("⏱️ Duration parameter detected:", duration, "seconds - starting from page load time:", pageLoadTime); } }
$commonApi.load = async function () { // Load a piece, wrapping it in a leave function so a final frame // plays back. leaving = true; graph.unmask(); // Clear any active mask when leaving a piece
return new Promise((resolve) => { leaveLoad = async () => { const loaded = await load(...arguments); resolve(loaded); // Resolve with `true` or `false`. }; }); };
// 💡 Eventually this could merge with net.web so there is one command // to either go to a piece within the system if one loads... or an entirely // different url somehow! 23.02.07.21.21
$commonApi.alias = function alias(name, colon, params) { $commonApi.jump( name + colon.map((c) => `:` + c).join("") + params.map((p) => `~` + p).join(""), true, true, ); };
// Go back to the previous piece, or to the prompt if there is no history. $commonApi.back = () => { if (pieceHistoryIndex > 0) { send({ type: "back-to-piece" }); } else { $commonApi.jump("prompt"); } };
if (!alias) $commonApi.pieceCount += 1; // Don't bump pieceCount if aliased.
// Load typeface if it hasn't been yet. // (This only has to happen when the first piece loads.) // Skip expensive typeface loading - use on-demand loading for all glyphs const skipTypefacePreload = true; // Always skip preload, load on-demand instead // console.log(`⏰ Typeface skip check: skipTypefacePreload=${skipTypefacePreload}, tf exists=${!!tf}`);
const typefaceLoadStartTime = performance.now(); if (!tf && !skipTypefacePreload) { tf = await new Typeface(/*"unifont"*/).load($commonApi.net.preload); const typefaceLoadEndTime = performance.now(); // console.log(`⏰ Typeface load: ${Math.round(typefaceLoadEndTime - typefaceLoadStartTime)}ms`); } else if (skipTypefacePreload) { // Create a Typeface instance and call load() to set up Proxy tf = new Typeface(/*"unifont"*/); // In TV/headless mode (oven captures), await font load to ensure glyphs // are available before first paint. Otherwise load on-demand (fire-and-forget). const isHeadless = typeof window !== "undefined" && new URLSearchParams(window.location?.search).get("tv") === "true"; if (isHeadless) { try { await tf.load($commonApi.net.preload); if (typeof window !== "undefined") { window.acFontsReady = true; window.__acTypeface = tf; } console.log("🔤 font_1 awaited load complete (tv mode)"); } catch (err) { console.error("🔤 tf.load() FAILED:", err?.message || err); if (typeof window !== "undefined") window.acFontsReady = "error"; } } else { tf.load($commonApi.net.preload).then(() => { if (typeof window !== "undefined") { window.acFontsReady = true; window.__acTypeface = tf; } }).catch(() => {}); } const typefaceLoadEndTime = performance.now(); // console.log(`⏰ Typeface load skipped (on-demand mode), Proxy initialized in ${Math.round(typefaceLoadEndTime - typefaceLoadStartTime)}ms`); } else { // console.log(`⏰ Typeface already loaded`); }
$commonApi.typeface = tf; // Expose a preloaded typeface globally. if (typeof window !== "undefined") window.__acTypeface = tf; // Expose for oven diagnostics ui.setTypeface(tf); // Set the default `ui` typeface.
// Initialize HUD metrics now that the primary typeface is available currentHUDLabelBlockWidth = tf.blockWidth; currentHUDLabelBlockHeight = tf.blockHeight; currentHUDShareWidth = tf.blockWidth * "share ".length; currentHUDEditWidth = tf.blockWidth;
// Initialize MatrixChunky8 font for QR code text rendering and keep it warm across pieces // Always register the font so kidlisp.com previews still render MatrixChunky8 labels const matrixFontStartTime = performance.now(); let matrixFont = typefaceCache.get("MatrixChunky8");
if (!matrixFont) { matrixFont = new Typeface("MatrixChunky8"); typefaceCache.set("MatrixChunky8", matrixFont); }
if (!matrixFont.__loadPromise) { matrixFont.__loadPromise = matrixFont.load($commonApi.net.preload); }
// Only run the heavier glyph preload pass when auth UI is enabled; kidlisp.com preview just needs the font registered const skipMatrixGlyphPreload = typeof window !== "undefined" && window.acNOAUTH;
if (!skipMatrixGlyphPreload) { await matrixFont.__loadPromise;
if (matrixFont && !matrixFont.__preloadedCommonGlyphs) { const glyphPreloadStartTime = performance.now(); // Pre-load common QR code characters to avoid fallback during rendering const commonQRChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz$";
// Access each character to trigger the Proxy loading mechanism and collect promises const glyphPromises = [];
for (const char of commonQRChars) { // Accessing the glyph triggers the Proxy to start loading it const glyph = matrixFont.glyphs[char];
// If it's a Promise, collect it to wait for loading if (glyph && typeof glyph.then === "function") { glyphPromises.push(glyph); } }
// Wait for all common glyphs to finish loading if (glyphPromises.length > 0) { try { await Promise.all(glyphPromises); } catch (e) { console.warn("🔤 Some MatrixChunky8 glyphs failed to preload:", e); } }
const glyphPreloadEndTime = performance.now(); // console.log(`⏰ MatrixChunky8 glyph preload: ${Math.round(glyphPreloadEndTime - glyphPreloadStartTime)}ms`); matrixFont.__preloadedCommonGlyphs = true; }
const matrixFontEndTime = performance.now(); // console.log(`⏰ MatrixChunky8 total: ${Math.round(matrixFontEndTime - matrixFontStartTime)}ms`); } else { // console.log("⏰ MatrixChunky8 preload skipped (noauth mode)"); }
/** * @function video * @descrption Make a live video feed. Returns an object that links to current frame. * @param {string} type - "camera" or "camera-update" or see below. 💡 * @param {object} options - *unimplemented* { src, width, height } */
let videoTimeout; let constraintTimeout; let lastConstraintSendTime = 0; const CONSTRAINT_SEND_INTERVAL = 60; // ms between zoom/torch tweak sends.
$commonApi.video = function (type, options) { // TODO: ❤️🔥 Prevent fast multiple taps while camera is updating...
// TODO: Options could eventually be { src, width, height } // const vid = video("youtube-link"); // const vid = video("tiktok:@whistlegraph"); // https://codepen.io/oceangermanique/pen/LqaPgO
if (videoSwitching === false) { // Torch and zoom adjust constraints on the live track — the stream // never restarts, so frames keep flowing and there is nothing to wait // for. Only facing/size changes swap the stream and need the // frame-hold (`videoSwitching`) treatment; holding for a zoom tweak // froze the camera preview mid-recording because zoom-only updates // never emit `camera:updated` to release the hold. const keys = options ? Object.keys(options) : []; const constraintOnly = type === "camera:update" && keys.length > 0 && keys.every((key) => key === "torch" || key === "zoom");
if (constraintOnly) { // Leading + trailing throttle on its own timer. A trailing-only // debounce starves a continuous zoom slide — every new touch value // resets the timer, so nothing sends until the finger pauses and the // recorded zoom then lurches in one big step. Sending immediately // (rate-limited) keeps the ramp tracking the finger; the trailing // send delivers the final resting value. const now = performance.now(); const since = now - lastConstraintSendTime; clearTimeout(constraintTimeout); if (since >= CONSTRAINT_SEND_INTERVAL) { lastConstraintSendTime = now; send({ type: "video", content: { type, options } }); } else { constraintTimeout = setTimeout(() => { lastConstraintSendTime = performance.now(); send({ type: "video", content: { type, options } }); }, CONSTRAINT_SEND_INTERVAL - since); } return videoFrame; }
if (type === "camera:update") { lastActiveVideo = activeVideo || lastActiveVideo; activeVideo = null; }
clearTimeout(videoTimeout); videoTimeout = setTimeout(() => { send({ type: "video", content: { type, options } }); }, 50);
if (type === "camera:update") videoSwitching = true; }
// Return an object that can grab whatever the most recent frame of // video was. return videoFrame; };
function videoFrame(shader) { if (activeVideo) { const { width, pixels } = activeVideo;
if (shader) { for (let i = 0; i < pixels.length; i += 4) { const c = pixels.subarray(i, i + 4); const p = { x: (i / 4) % width, y: floor(i / 4 / width) }; shader(p, c); } } } // With no fresh frame (e.g. mid camera switch), hand back the last real // frame untouched so the preview holds instead of flashing a placeholder. return activeVideo || lastActiveVideo; }
// This function actually hotSwaps out the piece via a callback from `bios` once fully loaded via the `loading-complete` message. hotSwap = () => { loadedCallback?.(); // Run the optional load callback. (See also: `jump`)
$commonApi.rec.loadCallback?.(); // Start any queued tape. $commonApi.rec.loadCallback = null;
// 📤 Send ready signal to parent window (e.g., kidlisp.com editor) // Note: disk.mjs runs in a Web Worker, so window may not exist const hasWindow = typeof window !== 'undefined';
try { if (hasWindow && window.parent && window.parent !== window) { window.parent.postMessage({ type: "kidlisp-ready" }, "*"); } // If no window (worker context), silently skip } catch (e) { // Silently fail if in worker context or cross-origin }
// Process any queued piece-reload that arrived before piece was ready if (pendingPieceReload && $commonApi.reload) { log.piece.log("Processing queued piece-reload from hotSwap"); const pending = pendingPieceReload; pendingPieceReload = null; // Use setTimeout to ensure all initialization is complete setTimeout(() => { $commonApi.reload({ source: pending.source, codeId: pending.codeId, createCode: pending.createCode, authToken: pending.authToken, enableTrace: pending.enableTrace, language: pending.language, ext: pending.ext, liveName: pending.liveName, }); }, 50); }
module = loadedModule;
// 🧹 Reset KidLisp baked layers when returning to the prompt to avoid stale flashes if (module?.system?.startsWith("prompt") && globalKidLispInstance?.clearBakedLayers) { globalKidLispInstance.clearBakedLayers(); graph.unmask(); // Clear mask when returning to prompt from KidLisp }
if (!module.system?.startsWith("world")) $commonApi.system.world.teleported = false;
// 📚 nopaint system if ( !Array.isArray(module) && (module.system?.startsWith("nopaint") || typeof module?.brush === "function" || typeof module?.lift === "function" || typeof module?.filter === "function") ) { // If there is no painting is in ram, then grab it from the local store, // or generate one. const modsys = module.system || "nopaint";
// TODO: 🖌️💬 Integrate nopaint system with chat. // console.log(chat);
const chatEnabled = false;
$commonApi.system.nopaint.bakeOnLeave = modsys.split(":")[1] === "bake-on-leave"; // The default is to `bake` at the end of each gesture aka `bake-on-lift`.
boot = async ($) => { // Reset scroll state when a new piece boots graph.resetScrollState(); // Reset mask state to prevent masks from persisting across pieces graph.unmask();
const booter = module.boot || nopaint_boot; booter($); if (chatEnabled) { try { const chatModule = await import("../disks/chat.mjs"); chatModule.boot($); } catch (err) { console.log("💬 Chat disabled in OBJKT mode"); } } };
sim = module.sim || defaults.sim; receive = module.receive || defaults.receive; paint = async ($) => { let painted = false;
if (module.paint) { painted = module.paint($); if ($.system?.nopaint) { $.system.nopaint.needsPresent = true; } }
// 🎨 OVERLAY SUPPORT: Call overlay function for preview rendering with painting coordinates if (module.overlay && $.system.nopaint?.brush?.dragBox) { // Transform painting coordinates to screen coordinates for direct screen rendering const originalDragBox = $.system.nopaint.brush.dragBox; const zoom = $.system.nopaint.zoomLevel; const tx = $.system.nopaint.translation.x; const ty = $.system.nopaint.translation.y;
// Create screen-space dragBox const screenDragBox = { x: originalDragBox.x * zoom + tx, y: originalDragBox.y * zoom + ty, w: originalDragBox.w * zoom, h: originalDragBox.h * zoom };
// Temporarily replace the dragBox with screen coordinates const originalBrush = $.system.nopaint.brush; $.system.nopaint.brush = { ...originalBrush, dragBox: screenDragBox };
// Add color word for overlay function $.color = $.system.nopaint.color;
// Add mark word for overlay function (preview dragBox) $.mark = $.system.nopaint.brush?.dragBox;
// Render overlay directly to screen buffer module.overlay($);
// Restore original dragBox $.system.nopaint.brush = originalBrush; }
// 📊 Always render nopaint performance HUD if enabled (regardless of module.paint result) if (system === "nopaint" && nopaintPerf) { nopaint_renderPerfHUD($); // Force continuous painting for nopaint system when perf HUD is enabled painted = true; }
if (painted) { return painted; }
// TODO: Pass in extra arguments here that flag the wipe. if (chatEnabled) { try { const chatModule = await import("../disks/chat.mjs"); chatModule.paint($, { embedded: true }); // Render any chat interface necessary. } catch (err) { console.log("💬 Chat disabled in OBJKT mode"); } } }; beat = module.beat || defaults.beat; brush = module.brush; lift = module.lift; filter = module.filter; act = ($) => { nopaint_act($); // Inherit base functionality.
// 🎯 IMMEDIATE BAKING: Fix race condition by triggering bake immediately after lift const np = $.system?.nopaint; if (np?.needsBake === true && bake) { // 📊 Trigger bake flash effect and beep sound nopaint_triggerBakeFlash();
// 🔊 Microwave-style beep for bake completion (only for robo) if (currentPath?.includes('robo')) { $commonApi.sound.synth({ tone: 800, // Higher pitched beep like a microwave duration: 0.1, attack: 0.01, decay: 0.5, volume: 0.3, }); }
// 🎨 ELEGANT BRUSH PATTERN: Call brush or lift function for final painting if (brush || lift) { const finalBrushApi = { ...$ }; // Add top-level 'color' word that maps to system.nopaint.color finalBrushApi.color = $.system.nopaint.color;
// Add top-level 'mark' word that maps to finalDragBox for brush finalBrushApi.mark = $.system.nopaint.finalDragBox;
// 🎯 Use preserved coordinates since brush is null at bake time const preservedDragBox = $.system.nopaint.finalDragBox; const preservedStartDrag = $.system.nopaint.finalStartDrag;
if (preservedDragBox) { // Create a pen object with the preserved coordinates finalBrushApi.pen = { dragBox: preservedDragBox, x: preservedStartDrag?.x || preservedDragBox.x, y: preservedStartDrag?.y || preservedDragBox.y }; } else { finalBrushApi.pen = $.system.nopaint.brush; // Fallback to original (likely null) }
finalBrushApi.lift = true; // 🎯 Single clean flag for final brush call
$.page($.system.painting); // Set context to painting surface
// Call lift function if it exists, otherwise fall back to brush if (lift) { lift(finalBrushApi); // Call lift for final painting } else if (brush) { brush(finalBrushApi); // Call brush for final painting (legacy) }
$.page($.screen); // Reset context }
$.page($.system.painting); bake($);
// 🧹 CLEAR BUFFER: Prevent flickering by clearing buffer BEFORE presentation $.page($.system.nopaint.buffer).wipe(255, 255, 255, 0);
$.page($.screen); np.present($); np.needsBake = false;
// 📸 Snapshot for undo after every stroke bake. addUndoPainting($.system.painting, "stroke");
// 🚀 Broadcast bake completion $commonApi.broadcastPaintingUpdateImmediate("baked", { source: "immediate_bake", piece: loadedModule?.meta?.()?.title || "unknown" }); }
if (module.act) { return module.act($); } else { return defaults.act($); } }; leave = ($) => { module.leave?.($); // Run the custom leave. nopaint.leave($); // And the inherited default leave from nopaint. }; bake = module.bake || nopaint.bake; system = "nopaint"; } else if (module.system?.startsWith("prompt")) { // 📖 prompt system // Default wrap to "word" if using `prompt:character`. const wrap = module.wrap || (module.system.indexOf("character") > -1 ? "word" : undefined);
if (module.scheme) { if ( module.scheme.dark === undefined && module.scheme.light === undefined ) { const defaultScheme = { ...module.scheme }; module.scheme.dark = module.scheme.light = defaultScheme; keys(module.scheme).forEach((key) => { if (key !== "dark" && key !== "light") delete module.scheme[key]; }); } }
boot = async ($) => { await prompt.prompt_boot( $, { prompt: module.prompt, program: { before: module.before, after: module.after, }, hint: module.system.split(":").slice(1).join(":"), // See `ask.ts`. forgetful: module.forgetful || false, memory: module.memory || Infinity, gutterMax: module.gutterMax, lineSpacing: module.lineSpacing, }, module.reply, module.halt, module.scheme, wrap, module.copied, module.activated, ); await module.boot?.($); };
sim = ($) => { module.sim?.($); if (!shellHTMLMode) prompt.prompt_sim($); else shellPromptSync($); // mirror reply text / thinking to the shell };
paint = ($) => { let noPaint = module.paint?.($); // Carry the return.
// Paint an illustration if it exists and `paint` is not defined. if (!module.paint && module.illustration) { $.wipe(...module.scheme.dark.background.slice(0, 3)); $.stamp(module.illustration, { center: "x", bottom: 0 }); noPaint = true; }
// shellhtml: the host page's DOM layer IS the prompt — never composite // the low-res typable input over the stage beneath it. const promptPainted = shellHTMLMode ? false : prompt.prompt_paint($); return noPaint || promptPainted; };
beat = module.beat || defaults.beat;
act = ($) => { module.act?.($); if (!shellHTMLMode) prompt.prompt_act($); // shell DOM owns typing };
leave = ($) => { module.leave?.($); prompt.prompt_leave($); };
// console.log("📨 Module exports check:", { // hasReceive: !!module.receive, // receiveType: typeof module.receive, // receiveIsDefault: module.receive === defaults.receive, // });
receive = module.receive || defaults.receive;
system = "prompt"; } else if (module.system?.startsWith("world")) { // 🗺️ world system boot = async ($) => { await world.world_boot($, module.world); await module.boot?.($); };
sim = ($) => { world.world_sim($); module.sim?.($); };
paint = ($) => { if (!world.coversScreen($.screen)) module.background?.($); world.world_paint($, module.paint, module.curtain); };
beat = module.beat || defaults.beat;
act = ($) => { world.world_act($); module.act?.($); };
leave = ($) => { world.world_leave($); module.leave?.($); };
system = "world"; } else if (module.system?.startsWith("fps")) { // 🧊 fps system let doll;
boot = ($) => { // Default camera opts; pieces can override via exported `fpsOpts` to // enable grounded physics (gravity/jump/run/crouch) or just retune FOV. const defaults = { fov: 80, z: 0, y: 0, sensitivity: 0.002 }; const opts = { ...defaults, ...(module.fpsOpts || {}) }; doll = new CamDoll($.Camera, $.Dolly, opts); $commonApi.system.fps = { doll, renderStats: graph.renderStats }; module?.boot?.($); };
sim = ($) => { doll?.sim(); module?.sim?.($); };
act = ($) => { doll?.act($.event); module?.act?.($); };
paint = module.paint || defaults.paint; leave = module.leave || defaults.leave;
system = "fps"; } else if (module.system?.startsWith("game")) { // 🎲 🎮 game system console.log("game!"); // TODO: ⚠️ Make game template. 25.06.05.09.19 } else { const moduleAssignStartTime = performance.now(); // console.log(`⏰ Starting module function assignment at ${moduleAssignStartTime}ms`);
// 🧩 piece // Reset scroll state when a piece loads graph.resetScrollState(); // Reset mask state to prevent masks from persisting across pieces graph.unmask();
boot = module.boot || defaults.boot; sim = module.sim || defaults.sim; paint = module.paint || defaults.paint; beat = module.beat || defaults.beat; act = module.act || defaults.act; leave = module.leave || defaults.leave;
// console.log("📨 Module exports check:", { // hasReceive: !!module.receive, // receiveType: typeof module.receive, // receiveIsDefault: module.receive === defaults.receive // });
receive = module.receive || defaults.receive; // Handle messages from BIOS
// ⏱️ Background mode: pieces can export `background = true` to keep sim() // running when the page is hidden (e.g. clock.mjs for audio scheduling) pieceBackground = !!module.background;
// 🎨 AUTO-DETECT BRUSH FUNCTIONS: If a piece exports a brush or lift function, automatically use nopaint system system = module.system || (module.brush || module.lift ? "nopaint" : null);
// 🎨 AUTO-GENERATE BAKE: If using brush/lift but no explicit bake function, create a default one if ((module.brush || module.lift) && !module.bake) { bake = ({ paste, system, page }) => { paste(system.nopaint.buffer); page(system.nopaint.buffer).wipe(255, 255, 255, 0); }; }
const moduleAssignEndTime = performance.now(); // console.log(`⏰ Module function assignment complete (${Math.round(moduleAssignEndTime - moduleAssignStartTime)}ms)`);
// delete $commonApi.system.name; // No system in use. }
preview = module.preview || defaults.preview; // Set preview method. icon = module.icon || defaults.icon; // Set preview method.
// ♻️ Reset global state for this piece. paintCount = 0n; paintingAPIid = 0n; simCount = 0n; booted = false; // initialSim = true; activeVideo = null; videoSwitching = false; lastActiveVideo = null; keys(preloadPromises).forEach((key) => preloadPromises[key].reject(key)); preloadPromises = {}; noPaint = false; hdUnload(); // 🖼️ Drop the previous piece's HD layer (hides it in bios).
// 🎞️ Reset piece-level FPS control pieceFPS = null; lastPaintTime = 0; lastPaintOut = undefined; shouldSkipPaint = false; pieceFrameCount = 0; // Reset acPieceReady so the signal fires again for the next piece if (typeof window !== "undefined") { window.acPieceReady = false; }
// 🎬 Piece Transition: Capture CURRENT screen pixels BEFORE clearing // This enables piece-to-piece morphing transitions (not just noise16→piece) // Skip entirely if TRANSITION_TYPE is "none" if (TRANSITION_TYPE !== "none") { // Check if buffer is valid (not detached from transfer to main thread) try { if (screen?.pixels && screen.width > 0 && screen.height > 0 && screen.pixels.buffer && !screen.pixels.buffer.detached && screen.pixels.byteLength > 0) { golTransition.overlayPixels = new Uint8ClampedArray(screen.pixels); golTransition.width = screen.width; golTransition.height = screen.height;
// Initialize and START transition immediately (loading phase) initGOLCells(golTransition.width, golTransition.height); golTransition.generation = 0; golTransition.active = true; console.log(`🎬 Transition: Started ${TRANSITION_TYPE} (loading phase)`, screen.width, "x", screen.height); } } catch (e) { // Buffer may be detached if pixels were transferred - skip transition console.log("🎬 Transition: Could not capture pixels (buffer detached), skipping"); golTransition.overlayPixels = null; } }
// Note: transition state is now set above when starting
// 🧹 Clear screen buffer when loading a new piece to prevent stale pixels // This fixes the blank screen issue when backspacing from kidlisp to prompt graph.clear();
formsSent = {}; // Clear 3D list for GPU. currentPath = path; currentHost = host; currentSearch = search; // console.log("Set currentSearch to:", search); firstPreviewOrIcon = true;
// Clear clock piece cached code when leaving (will be set by clock piece if it caches) cachedClockCode = null;
// Parse search parameters properly to check for nolabel // Also hide label in pack mode (standalone bundles have no URL params) hideLabel = (typeof window !== "undefined" && window.acPACK_MODE && !window.acKEEP_LABEL) || hideLabelForFrame; if (parsed.search) { const searchParams = new URLSearchParams(parsed.search); if (searchParams.has("nolabel")) hideLabel = hideLabelForFrame = true; if (searchParams.has("autoreload")) autoUpdateForFrame = true; } if (shellHTMLMode) hideLabel = true; // the shell's DOM corner overlay replaces it shellLabelLast = null; // a fresh piece re-sends its label even if the text repeats
currentColon = colon; currentParams = params; currentHash = hash; // sound = null; glazeEnabled = null; // soundClear = null; hourGlasses.length = 0; // labelBack = false; // Now resets after a jump label push. 25.03.22.21.36
// Parse search parameters properly to check for preview and icon previewMode = false; iconMode = false; if (parsed.search) { const searchParams = new URLSearchParams(parsed.search); previewMode = searchParams.has("preview"); iconMode = searchParams.has("icon"); }
// console.log("🔴 PREVIEW OR ICON:", PREVIEW_OR_ICON, "Preview mode:", previewMode, "Icon mode:", iconMode); // console.log("📑 Search:", parsed.search); // console.log("🖼️ ICON MODE:", iconMode);
previewOrIconMode = previewMode || iconMode; paintings = {}; // Reset painting cache. // Reset KidLisp persistent paintings cache to avoid stale embedded content if (globalKidLispInstance?.persistentPaintings) { globalKidLispInstance.persistentPaintings.clear(); } prefetches?.forEach((p) => prefetchPicture(p)); // Prefetch parsed media. graph.color2(null); // Remove any secondary color that was added from another piece. // 🐢 Reset turtle state. turtleAngle = 270; turtleDown = false; turtlePosition = { x: screen.width / 2, y: screen.height / 2 };
//$api.fps = function (newFps) { // No longer reset global FPS when loading new pieces // send({ type: "fps-change", content: undefined }); //};
// 🪧 See if notice needs to be shown. if ($commonApi.query.notice === "success") { notice = "PRINTED!"; noticeColor = ["white", "green"]; noticeBell(cachedAPI); } else if ($commonApi.query.notice === "cancel") { notice = "CANCELLED"; noticeColor = ["yellow", "red"]; noticeBell(cachedAPI, { tone: 300 }); } else if ($commonApi.query.notice === "email-verified") { notice = "Email verified!"; noticeColor = ["white", "blue"]; noticeBell(cachedAPI, { tone: 300 }); } else if ($commonApi.query.notice?.length > 0) { notice = $commonApi.query.notice; noticeColor = ["white", "green"]; noticeBell(cachedAPI, { tone: 300 }); }
if (!alias) { currentHUDTxt = slug; // Update hud if this is not an alias.
// Special HUD label formatting for specific pieces if (slug === "r8dio") { // Display as "r8Dio" with "8D" in magenta currentHUDTxt = "r\\255,0,255\\8D\\255,200,220\\io"; currentHUDPlainTxt = "r8Dio"; } else { currentHUDPlainTxt = stripColorCodes(slug); }
// Hide HUD label for RGB-only kidlisp pieces (e.g., "255 0 0") // since they're just background colors and don't need to be displayed if (sourceCode && lisp?.isValidRGBString && lisp.isValidRGBString(sourceCode.trim())) { currentHUDTxt = undefined; currentHUDPlainTxt = undefined; } } if (module.nohud || system === "prompt") { currentHUDTxt = undefined; currentHUDPlainTxt = undefined; } // Clear original code ID for non-$code pieces (unless this was a $code load) if (!slug?.startsWith("$")) { currentOriginalCodeId = undefined; currentHUDAuthor = null; // Reset author for non-KidLisp pieces currentHUDHits = null; } currentHUDOffset = undefined; // Always reset these to the defaults. currentHUDTextColor = undefined; currentHUDStatusColor = "red"; //undefined; currentHUDButton = undefined; currentHUDHovered = false; currentHUDHoverAmount = 0; currentHUDScrub = 0; currentHUDLeftPad = 0; currentHUDQR = null; // Reset QR code when loading new piece currentHUDQRCells = null; currentHUDSuperscript = null; // Reset custom superscript forceTinyHudLabel = false; // Reset tiny label flag // currentPromptButton = undefined;
// Push last piece to a history list, skipping prompt and repeats. if ( !fromHistory && currentText && currentText !== "prompt" && currentText !== $commonApi.history[$commonApi.history.length - 1] ) { $commonApi.history.push(currentText); }
currentText = slug; currentCode = sourceCode; previewEvidence.activate(previewIdentity);
// 🧾 Begin piece-run telemetry (batched console capture → /api/piece-log) try { pieceRuns.start({ slug, params, colon, host, user: USER?.sub || USER?.handle || null, }); } catch (err) { // never break piece load }
// Broadcast location change to session server if (HANDLE && socket?.connected && slug !== "*keep-alive*") { socket.send("location:broadcast", { handle: HANDLE, slug: slug, user: USER, }); if (logs.socket) console.log("📍 Broadcasting location:", slug); }
if (screen) screen.created = true; // Reset screen to created if it exists.
cursorCode = "precise"; // Set default cursor.
if (firstLoad === true) { firstLoad = false; // firstPiece = path; // firstParams = params; // firstSearch = search; } };
send({ type: "disk-loaded", content: { path, host, search, params, hash, text: slug, pieceCount: $commonApi.pieceCount, pieceHasSound: true, // TODO: Make this an export flag for pieces that don't want to enable the sound engine. 23.07.01.16.40 background: pieceBackground, // ⏱️ Piece wants sim() to keep running when hidden // 📓 Could also disable the sound engine if the flag is false on a subsequent piece, but that would never really make practical sense? fromHistory, alias, meta, taping: $commonApi.rec.loadCallback !== null || $commonApi.rec.recording, // 🎏 Hacky flag. 23.09.17.05.09 // noBeat: beat === defaults.beat, clockShortcode, // ⏰ Original *code for URL display (null if not a clock shortcode) }, });
// Notify parent window of progress via bios relay const loadCompleteTime = performance.now(); // console.log("⏰ load() completed at", loadCompleteTime); // console.log("📢 Disk sending boot-log: loaded"); send({ type: "boot-log", content: `loaded: ${slug || path}` });
return true; // Loaded successfully.}
const isWorker = typeof importScripts === "function";const _workerReadyTime = performance.now();// Only log worker ready if there was significant import timeif (_workerReadyTime - _importStart > 50) { console.log(`📦 [DISK] Ready: ${(_workerReadyTime - _importStart).toFixed(0)}ms`);}
// 📦 Long-poll `/api/version` to detect new deployments. Runs once for the// life of the worker so the green ↑ badge can appear in any piece.async function startGlobalVersionPoll() { if (updatePollStarted) return; updatePollStarted = true;
const fetchVersion = async () => { try { const res = await fetch("/api/version", { cache: "no-store" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); globalVersionInfo = debug ? { ...data, deployed: data.deployed === "unknown" ? "dev" : data.deployed, status: "local", behindBy: 0, } : data; globalRecentCommits = data.recentCommits || []; try { $commonApi.needsPaint?.(); } catch {} } catch (e) { console.warn("📦 Could not fetch version info:", e); } };
await fetchVersion();
if (debug) { // Local dev: poll once a minute instead of long-polling. setInterval(fetchVersion, 60 * 1000); return; }
// Long-poll loop: only a different, valid deployed hash signals an update. while (true) { try { if (!globalVersionInfo?.deployed) { await new Promise((r) => setTimeout(r, 5000)); await fetchVersion(); continue; } updatePollController = new AbortController(); const res = await fetch( `/api/version?current=${globalVersionInfo.deployed}`, { signal: updatePollController.signal, cache: "no-store" }, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); if (data.changed === false) continue; // Server held the request open. if (!/^[a-f0-9]{7}$/i.test(data.deployed || "") || data.deployed === globalVersionInfo.deployed) { // Cached full responses and transient version lookup failures are not // deployments. Back off if a proxy answered without long-polling. await new Promise((r) => setTimeout(r, 5000)); continue; } if (!/^[a-f0-9]{7}$/i.test(globalVersionInfo.deployed)) { globalVersionInfo = data; continue; // Establish a baseline after an unavailable initial lookup. } globalUpdateReady = true; globalVersionInfo = data; globalRecentCommits = data.recentCommits || []; console.log("📦 New deployment detected:", data.deployed); try { send({ type: "sw:clear-cache" }); } catch (e) { console.warn("📦 Could not request SW cache clear:", e); } try { $commonApi.needsPaint?.(); } catch {} break; // Stop polling — update is available } catch (e) { if (e.name === "AbortError") break; console.warn("📦 Version poll error:", e); await new Promise((r) => setTimeout(r, 10000)); } }}
// ***Bootstrap***// Start by responding to a load message, then change// the message response to makeFrame.if (isWorker) { // Signal to bios.mjs that disk.mjs has loaded successfully postMessage({ type: "worker-ready" });
onmessage = (e) => { // DEBUG: Log DAW messages only when debugging if (e.data?.type?.startsWith?.("daw:") && logs.daw) { console.log("🎹 WORKER:", e.data?.type, e.data); } makeFrame(e); };} else { noWorker.onMessage = (d) => makeFrame({ data: d });}
// The main messaging function to comumunicate back with the main thread.function send(data, shared = []) { if (isWorker) { if (shared[0] === undefined) shared = []; postMessage(data, shared); } else { if (noWorker.postMessage) { noWorker.postMessage({ data }); } else { console.warn("⚠️ noWorker.postMessage is not defined yet, cannot send:", data); } }}
// Used to subscribe to live coding / development reloads.let codeChannel, codeChannelAutoLoader;// The channel named by the piece currently loaded from a handle route,// held separately from this browser's own `codeChannel` because it belongs// to whoever published the piece. Never persisted.let pieceCodeChannel;
// Queue for export events that arrive when actEvents isn't availablelet pendingExportEvents = [];
// 4. ✔ Respond to incoming messages, and probably produce a frame.// Boot procedure:// First `paint` happens after `boot`, then any `act` and `sim`s each frame// before `paint`ing occurs. One `sim` always happens after `boot` and before// any `act`. `paint` can return false to stop drawing every display frame,// then, it must be manually restarted via `needsPaint();`). 2022.01.19.01.08// 🔥// TODO: makeFrame is no longer a great name for this function, which actually// receives every message from the main thread, one of which renders a// frame.// TODO: Make simple needsPaint example.// TODO: Try to remove as many API calls from here as possible.
async function makeFrame({ data: { type, content } }) { // DEBUG: Log ALL messages starting with "pedal:" or "daw:" if (type?.startsWith?.("pedal:") || type?.startsWith?.("daw:") || type?.startsWith?.("spreadnob:")) { console.log("🎹🎹🎹 makeFrame received:", type, JSON.stringify(content)); }
// Handle permission responses from bios.mjs if (type === "permission-response") { resolvePermissionRequest(content.requestId, content.granted); return; }
// Navigate to a new piece (device.html slideshow uses this for instant transitions) if (type === "navigate") { const target = content?.to; console.log("🧭 Navigate received:", target, "jump available:", !!$commonApi?.jump, "loading:", loading, "leaving:", leaving); if (target && $commonApi?.jump) { $commonApi.jump(target); } else { console.warn("🧭 Navigate failed: target=", target, "$commonApi=", !!$commonApi, "jump=", !!$commonApi?.jump); } return; }
// Native DOM prompt bridge (prompt.ac). Keep the TextInput and prompt piece // as the source of truth while leaving their low-resolution composition off. if (type === "prompt:dom-input") { const promptInput = $commonApi?.system?.prompt?.input; if (!promptInput || $commonApi?.slug !== "prompt") { // The bridge comes up before the prompt piece is current, so a submit // sent in that window must not vanish — reroute it through an autorun // prompt load, which executes the command once the piece is ready. // Never jump mid-load though: interrupting an in-flight load wedges the // loader ("Already loading"), and the shell re-sends once ac:piece says // the prompt is live. const text = String(content?.text ?? "").trim(); if (content?.submit === true && text && !loading && !leaving) { $commonApi?.jump?.(`prompt~${text}~!autorun`); } return; }
const text = String(content?.text ?? ""); promptInput.text = text; promptInput.addUserText?.(text); promptInput.runnable = text.trim().length > 0; promptInput.canType = content?.active !== false; if (promptInput.enter?.btn) { promptInput.enter.btn.disabled = !promptInput.runnable; } promptInput.snap?.(); $commonApi.needsPaint?.();
if (content?.submit === true && promptInput.runnable) { await promptInput.run($commonApi.store); } return; }
// 🎵 ac-electron drag-drop: a file was dropped onto the app icon or // window. Stash it on system.droppedFile, fire a dropped:file act event // for the live piece, and auto-jump to `dj` unless a player piece (dj or // play) is already running and will swap tracks itself. if (type === "dropped:file") { if (!content || !content.url) return; if ($commonApi?.system) $commonApi.system.droppedFile = content; actAlerts.push("dropped:file"); try { const slug = $commonApi?.slug || ""; const inPlayer = ["dj", "play"].some( (p) => slug === p || slug.startsWith(p + "~") || slug.startsWith(p + ":") || (currentPath && currentPath.endsWith("/" + p)), ); if (!inPlayer && $commonApi?.jump) { $commonApi.jump("dj"); } } catch (e) { console.warn("🎵 dropped:file auto-jump failed:", e?.message || e); } return; }
// Warm the kidlisp code cache with pre-fetched sources from parent if (type === "warm-kidlisp-cache") { const codes = content?.codes; if (codes) { let count = 0; for (const [codeId, source] of Object.entries(codes)) { saveCodeToAllCaches(codeId, source); count++; } console.log(`🔥 Warmed kidlisp cache with ${count} codes`); } return; }
// Runs once on boot. if (type === "piece-source") { // Late arrival of the shell's prefetched piece source (see init-from-bios). if (content?.slug && typeof content.code === "string" && !pieceCodeCache.has(content.slug)) { pieceCodeCache.set(content.slug, { code: content.code, type: "mjs" }); } return; }
if (type === "init-from-bios") { debug = content.debug; setDebug(content.debug); ROOT_PIECE = content.rootPiece; hdPixelRatio = content.pixelRatio || 1; // 🖼️ For the hd() layer's scale. if (content.bootId && typeof self !== "undefined") self.acBOOT_ID = content.bootId; // 🎁 Piece source the shell fetched at parse time: seed the piece-code // cache so load() takes its "Using cached code" path and never fetches. if (content.pieceSource?.slug && typeof content.pieceSource.code === "string") { pieceCodeCache.set(content.pieceSource.slug, { code: content.pieceSource.code, type: "mjs" }); }
// 📦 Kick off global version polling (skip preview/icon/pack/objkt where // the user can't reload anyway). if ( !content.previewOrIcon && !content.objktMode && !content.packMode ) { startGlobalVersionPoll(); }
if (content.debugHud) { globalThis.debugHudHitbox = true; console.log("🔍 HUD Hitbox Debug: ON (init-from-bios)"); $commonApi.needsPaint(); }
// Store noauth flag for iframe embedding (kidlisp.com) // In noauth mode, mark session as started immediately so piece loading // doesn't wait for the session:started message (resilience when session server is down) if (content.noauth) { globalThis.NOAUTH_MODE = true; sessionStarted = true; }
USER = content.user; // if (USER) socket.send("login", { user: USER });
LAN_HOST = content.lanHost; SHARE_SUPPORTED = content.shareSupported; PREVIEW_OR_ICON = content.previewOrIcon; VSCODE = content.vscode; setPackMode( Boolean( content.objktMode || content.packMode || (typeof globalThis !== "undefined" && globalThis.acPACK_MODE), ), ); // Store PACK/OBJKT mode flag in worker global scope if (content.objktKidlispCodes) { const globalScope = (function() { if (typeof globalThis !== 'undefined') return globalThis; if (typeof self !== 'undefined') return self; if (typeof global !== 'undefined') return global; return {}; })(); globalScope.objktKidlispCodes = content.objktKidlispCodes; }
TV_MODE = content.resolution?.tv === true; DEVICE_MODE = content.resolution?.device === true; SOLO_MODE = content.resolution?.solo === true;
// Parse highlight parameter const highlightParam = content.resolution?.highlight; if (highlightParam) { HIGHLIGHT_MODE = true; if (highlightParam !== true && typeof highlightParam === 'string') { // If it's a color string, parse it if (highlightParam.startsWith('#')) { // Convert hex to RGB const hex = highlightParam.slice(1); const r = parseInt(hex.slice(0, 2), 16); const g = parseInt(hex.slice(2, 4), 16); const b = parseInt(hex.slice(4, 6), 16); HIGHLIGHT_COLOR = `${r},${g},${b}`; } else if (highlightParam.includes(',')) { // Already RGB format HIGHLIGHT_COLOR = highlightParam; } else { // Named color or other format - use CSS color parser or default HIGHLIGHT_COLOR = highlightParam; } } } else { HIGHLIGHT_MODE = false; }
// Parse perf parameter for KidLisp performance HUD PERF_MODE = content.resolution?.perf === true;
// Parse auto-scale parameter for automatic density scaling AUTO_SCALE_MODE = content.resolution?.autoScale === true; SPOOF_AUDIO_MODE = content.resolution?.spoofaudio === true; NOGAP_MODE = content.resolution?.gap === 0;
microphone.permission = content.microphonePermission;
$commonApi.canShare = SHARE_SUPPORTED; $commonApi.vscode = VSCODE; // Add vscode flag to the common api. $commonApi.net.lan = LAN_HOST; $commonApi.user = USER; $commonApi.net.iframe = content.iframe; $commonApi.net.sandboxed = content.sandboxed; $commonApi.net.tvMode = TV_MODE; // Add TV mode flag to common API
// 🔑 Hand `/api/ask` a way to learn who is asking. Signing in or out is // picked up on the next ask because the provider resolves the token at call // time rather than caching one here. setAskTokenProvider(() => $commonApi.authorize());
codeChannelAutoLoader = null; codeChannel = await store.retrieve("code-channel"); if (!codeChannel || codeChannel?.length === 0) { codeChannel = nanoid(); store["code-channel"] = codeChannel; store.persist("code-channel"); }
// console.log("💻 Code channel:", codeChannel);
// Always send the codeChannel up to any parent window. codeChannelAutoLoader = () => { send({ type: "post-to-parent", content: { type: "setCode", value: codeChannel }, }); codeChannelAutoLoader = null; };
// 🚀 Eagerly initialize GPU effects module for faster spin/blur/zoom // Currently GPU effects are disabled due to Y-flip issues, so this is non-blocking // TODO: Re-enable await when GPU effects Y-coordinate handling is fixed if (typeof graph.initGpuEffects === 'function') { graph.initGpuEffects().catch(e => { console.warn('🎮 GPU Effects initialization failed:', e); }); }
// console.log("Init:", content); // await handle(); // Get the user's handle. // console.log("🟢 Loading after preamble:", content.parsed);
originalHost = content.parsed.host; loadAfterPreamble = () => { const loadCallStartTime = performance.now(); // console.log("⏰ loadAfterPreamble called at", loadCallStartTime); loadAfterPreamble = null; load(content.parsed); // Load after some of the default frames run. };
if (PREVIEW_OR_ICON) { console.log("💬 Chat disabled, just grabbing screenshots. 😃"); } else if (getPackMode()) { // Skip chat connection in PACK mode - offline bundle } else { chatClient.connect("system"); // Connect to `system` chat. }
send({ type: "disk-defaults-loaded" }); return; }
// Enable KidLisp-only console channel (forwarded from boot.mjs). if (type === "kidlisp-console-enable") { // Call the kidlisp.mjs enableKidlispConsole() to set the module-level flag // This is needed because bios.mjs imports isKidlispConsoleEnabled() from kidlisp.mjs enableKidlispConsole(); return; }
// � Toggle HUD hitbox debug visualization from main thread if (type === "debug:hud-hitbox:toggle") { globalThis.debugHudHitbox = !globalThis.debugHudHitbox; console.log("🔍 HUD Hitbox Debug:", globalThis.debugHudHitbox ? "ON" : "OFF"); $commonApi.needsPaint(); return; }
// �🛑 Watchdog ping - respond immediately with pong if (type === "watchdog:ping") { send({ type: "watchdog:pong", content: { timestamp: content?.timestamp } }); return; }
if (type === "logout:broadcast:subscribe") { console.log("🏃♂️ Broadcasting logout:", content); socket?.send("logout:broadcast:subscribe", content); return; }
// Get visualViewport update, for keyboard overlays, etc. if (type === "viewport-height:changed" && booted) { const $api = cachedAPI; const data = { ...content }; Object.assign(data, { is: (e) => e === type, }); $api.event = data; try { act($api); } catch (e) { console.warn("️ ✒ Act failure...", e); } return; }
// Receive a midi input message. if (type === "midi:keyboard" && booted) { // console.log("🎹 Keyboard:", content.data); const $api = cachedAPI; const data = { ...content }; Object.assign(data, { device: "midi:keyboard", is: (e) => e === type, }); $api.event = data; try { act($api); } catch (e) { console.warn("️ ✒ Act failure...", e); } return; }
if (type === "clock:rate") { if (playbackClock.setRate(content?.rate, content?.reset === true)) { send({ type: "clock:state", content: { rate: playbackClock.rate, time: playbackClock.time() } }); } return; }
if (type === "audio:sample-rate") { AUDIO_SAMPLE_RATE = content; return; }
// 🎹 DAW Sync messages (from Max for Live via bios.mjs) // Use persistentDawState since $commonApi.sound gets recreated every frame if (type === "daw:tempo") { // Send acknowledgement back to main thread for debugging send({ type: "daw:tempo:ack", content: { bpm: content?.bpm } }); sound.bpm = content.bpm; // Store in persistent state (survives frame updates) persistentDawState.bpm = content.bpm; return; }
if (type === "daw:transport") { // Send acknowledgement back to main thread for debugging send({ type: "daw:transport:ack", content: { playing: content?.playing } }); // Store in persistent state (survives frame updates) persistentDawState.playing = content.playing; return; }
if (type === "daw:phase") { // Store in persistent state (survives frame updates) persistentDawState.time = content.time; return; }
if (type === "daw:samplerate") { // Store in persistent state (survives frame updates) persistentDawState.sampleRate = content.rate; return; }
// 🎛️ Spreadnob messages (from M4L via bios) — stored on persistentDawState if (type === "spreadnob:note") { persistentDawState.snNote = content.note; return; } if (type === "spreadnob:target") { persistentDawState.snTarget = content.name; return; } if (type === "spreadnob:value") { persistentDawState.snValue = content.value; return; } if (type === "spreadnob:active") { persistentDawState.snActive = content.active; return; } if (type === "spreadnob:min") { persistentDawState.snMin = content.min; return; } if (type === "spreadnob:max") { persistentDawState.snMax = content.max; return; } if (type === "spreadnob:state") { persistentDawState.snRawNote = content.raw; persistentDawState.snNormalizedNote = content.note; persistentDawState.snShift = content.shift; persistentDawState.snLocked = content.locked; persistentDawState.snAmbiguous = content.ambiguous; return; } if (type === "spreadnob:qwerty-range") { persistentDawState.snQwertyLow = content.low; persistentDawState.snQwertyHigh = content.high; return; }
// 🎸 Pedal messages (for audio effect visualization) if (type === "pedal:peak") { // Forward to the piece's receive function if it exists if (typeof receive === "function") { receive({ type: "pedal:peak", peak: content.peak }); } return; }
if (type === "pedal:envelope") { // Forward to the piece's receive function if it exists if (typeof receive === "function") { receive({ type: "pedal:envelope", ...content }); } return; }
// Update the logged in user after initialization. if (type === "session:started") { // console.log("🟢 Session starting..."); USER = content.user; $commonApi.user = USER; // User will be set to "null" here // it it doesn't exist.
if (USER) { // console.log("Getting handle..."); await handle(); // Get the user's handle. // console.log("Handle recived:", HANDLE); const displayName = HANDLE || USER.email; console.log(`%c🔐 auth%c Welcome back %c${displayName}`, 'background: #ec4899; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: bold;', 'color: inherit;', 'color: #ec4899; font-weight: bold;' ); // Log tab ID for debugging if (!$commonApi._tabId) { $commonApi._tabId = Math.random().toString(36).substr(2, 9); } // Broadcast to other tabs... $commonApi.broadcast("login:success"); } else { // console.log("🔐 You are not logged in."); // Generate tab ID for debugging (anonymous user) if (!$commonApi._tabId) { $commonApi._tabId = Math.random().toString(36).substr(2, 9); } } sessionStarted = true; diskTimings.sessionStarted = Math.round(performance.now() - diskTimingStart); // Silent: sessionStarted return; }
if (type === "microphone-permission") { microphone.permission = content; microphone.permissionPending = false; microphone.permissionChecked = true; if ( content === "granted" && globalKidLispInstance?.containsMicrophoneFunctions?.( globalKidLispInstance.ast, ) ) { globalKidLispInstance.requestMicrophoneConnection?.($activePaintApi); } return; }
// Handle live reload from kidlisp.com editor if (type === "piece-reload") { log.piece.log("Reloading with new code:", content.source?.substring(0, 30) + "..."); if ($commonApi.reload) { $commonApi.reload({ source: content.source, codeId: content.codeId, // The $code identifier (e.g., "inz") createCode: content.createCode, authToken: content.authToken, // Pass token from kidlisp.com login enableTrace: content.enableTrace, // Enable execution trace for visualization language: content.language, ext: content.ext, liveName: content.liveName, }); } else { // Queue the reload request to be processed once the piece is ready // Always update the queue with the latest code (overwrite previous) log.piece.warn("Reload function not ready yet, queuing:", content.source?.substring(0, 20)); pendingPieceReload = { source: content.source, codeId: content.codeId, createCode: content.createCode, authToken: content.authToken, enableTrace: content.enableTrace, language: content.language, ext: content.ext, liveName: content.liveName, }; } return; }
// 🎚️ Slide mode update - re-evaluate code but preserve buffers/state if (type === "piece-slide") { if (content.source) { // Use the exported slideUpdate from kidlisp.mjs to update the correct instance // (The one used by lisp.module() for loaded pieces) lisp.slideUpdate(content.source); } return; }
// 🎵 Audio data from jukebox - update KidLisp audio state if (type === "kidlisp-audio") { lisp.updateKidLispAudio?.(content); return; }
// Confirming if the pen has been locked or unlocked by the Pointer Lock API. if (type === "pen:locked" || type === "pen:unlocked") { actAlerts.push(type); return; }
// Capture a link from the docs system. if (type === "docs:link") { console.log("📚 Doc link captured:", content); $commonApi.jump("prompt~" + content); return; }
// Capture the browser scroll wheel / scroll effect. if (type === "scroll") { if (!cachedAPI) return; // Guard against undefined cachedAPI const $api = cachedAPI; const data = { ...content }; Object.assign(data, { device: "wheel", is: (e) => e === type, }); $api.event = data; try { act($api); } catch (e) { console.warn("️ ✒ Act failure...", e); } return; }
// Jump to any piece slug from the bios. if (type === "jump") { let ahistorical, alias; if (content.ahistorical === undefined) { ahistorical = true; } else ahistorical = content.ahistorical; if (content.alias === undefined) { alias = true; } else alias = content.alias; $commonApi.jump(content.piece, ahistorical, alias); return; }
// Create a notice. if (type === "notice") { $commonApi.notice(content, ["white", "maroon"]); return; }
if (type === "loading-complete") { leaving = false; hotSwap?.(); // Actually swap out the piece functions and reset the state. loading = false; loadingStartTime = null; noPaint = false; // 🎨 Ensure first frame paints after piece swap return; }
if (type === "udp:receive") { udp.receive(content); return; }
if (type === "udp:connected") { console.log("🩰 DISK received udp:connected! Setting udp.connected = true"); udp.connected = true; updateHUDStatus(); $commonApi.needsPaint(); return; }
if (type === "udp:disconnected") { console.log("🩰 DISK received udp:disconnected! Setting udp.connected = false"); udp.connected = false; updateHUDStatus(); $commonApi.needsPaint(); return; }
if (type === "microphone-disconnect") { microphone.connected = false; actAlerts.push("microphone-disconnect"); return; }
// if (type === "hand-tracking-data") { // $commonApi.hand = { mediapipe: content }; // return; // }
// Load the source code for a dropped `.mjs` or KidLisp file. if (type === "dropped:piece") { // Pass forceKidlisp=true if this was detected as KidLisp (e.g., from .lisp.html bundle) const forceKidlisp = content.isKidLisp === true; // Pass forceP5=true if this was a dropped .js sketch (run via the p5 worker). const forceP5 = content.isP5 === true; load(content, false, false, true, undefined, forceKidlisp, forceP5); return; }
// Run dropped text as a prompt command (navigation / piece name). if (type === "dropped:text") { $commonApi.jump(content.text); return; }
if (type === "dropped:bitmap") { if (currentPath === "aesthetic.computer/disks/prompt") { $commonApi.system.nopaint.replace( { system: $commonApi.system, store, needsPaint: $commonApi.needsPaint }, content.source, ); } else { const $api = cachedAPI; const data = { name: content.name, painting: content.source }; Object.assign(data, { device: "none", is: (e) => e === type, }); $api.event = data; try { act($api); } catch (e) { console.warn("️ ✒ Act failure...", e); } } return; }
// Handle dropped ALS files - route to piece receive function if available if (type === "dropped:als") { console.log("🎵 DISK.MJS: Received dropped:als message:", content);
// First try to call the piece's receive function if it exists if (typeof receive === "function") { console.log("🎵 DISK.MJS: Calling piece receive function"); try { receive({ type, content }); } catch (e) { console.warn("🎵 DISK.MJS: Piece receive function error:", e); } } else { console.log("🎵 DISK.MJS: No piece receive function found, triggering act event"); // Fall back to act event pattern const $api = cachedAPI; if ($api) { const data = { name: content.name, xmlData: content.xmlData, type: "dropped:als" }; Object.assign(data, { device: "none", is: (e) => e === type, }); $api.event = data; try { act($api); } catch (e) { console.warn("️ ✒ Act failure...", e); } } } return; }
// Handle dropped WAV files - route to piece receive function if available if (type === "dropped:wav") { console.log("🎵 DISK.MJS: Received dropped:wav message:", content);
// First try to call the piece's receive function if it exists if (typeof receive === "function") { console.log("🎵 DISK.MJS: Calling piece receive function for WAV"); try { receive({ type, content }); } catch (e) { console.warn("🎵 DISK.MJS: Piece receive function error for WAV:", e); } } else { console.log("🎵 DISK.MJS: No piece receive function found for WAV, triggering act event"); // Fall back to act event pattern const $api = cachedAPI; if ($api) { const data = { name: content.name || content.originalName, id: content.id, size: content.size, type: "dropped:wav" }; Object.assign(data, { device: "none", is: (e) => e === type, }); $api.event = data; try { act($api); } catch (e) { console.warn("️ ✒ Act failure...", e); } } } return; }
// Speech boundaries let pieces synchronize their own captions or animation. if (type === "speech:started" || type === "speech:word") { actAlerts.push({ name: type, content }); return; }
// 🗣️ An act that fires when an utterance has ended in the Web Speech API. if (type === "speech:completed") { actAlerts.push("speech:completed"); return; }
// 🗣️ A cloud utterance that never came back: `content` carries the HTTP // status (0 for a network drop) and the provider, so a piece can tell // "sign in" apart from "the voice service is down". if (type === "speech:error") { actAlerts.push({ name: "speech:error", content }); return; }
// 🎵 Clock piece sent its cached code for QR display if (type === "clock:cached") { cachedClockCode = content?.code || null; console.log(`🎵 Clock cached code received: *${cachedClockCode}`); return; }
// When inputting text into the prompt. if ( type === "prompt:text:replace" || type === "prompt:text:select" || type === "prompt:text:cursor" ) { const $api = cachedAPI; const data = { ...content }; Object.assign(data, { device: "none", is: (e) => e === type, }); $api.event = data; try { act($api); } catch (e) { console.warn("️ ✒ Act failure...", e); } return; }
// Handles: clipboard:paste:pasted, clipboard:paste:pasted:empty if (type.startsWith("paste:pasted")) { actAlerts.push("clipboard:" + type); return; }
if (type === "paste:failed") { actAlerts.push("clipboard:paste:failed"); return; }
if (type === "copy:copied") { actAlerts.push("clipboard:copy:copied"); return; }
if (type === "aesthetic-parent:focused") { actAlerts.push("aesthetic-parent:focused"); return; }
if (type === "copy:failed") { actAlerts.push("clipboard:copy:failed"); return; }
if (type === "upload:progress") { serverUploadProgressReporter?.(content); // Report file upload progress if needed.
// Also forward to current piece (especially video.mjs for tape uploads) if (booted && receive !== defaults.receive) { console.log(`📤 Forwarding upload:progress to piece: ${content}`); try { receive({ type: "upload:progress", content, is: (name) => name === "upload:progress" }); } catch (error) { console.warn("📤 Error forwarding upload:progress to piece:", error); } }
return; }
if (type === "upload:status") { // Forward backend upload status stages to the active piece. if (booted && receive !== defaults.receive) { try { receive({ type: "upload:status", content, is: (name) => name === "upload:status", }); } catch (error) { console.warn("📤 Error forwarding upload:status to piece:", error); } } return; }
if (type === "focus-change") { if (!cachedAPI) return; // Hacky... 23.04.21.14.59 const $api = cachedAPI; if (content !== inFocus) { inFocus = content; const data = {}; Object.assign(data, { device: "none", is: (e) => e === (inFocus === true ? "focus" : "defocus"), }); $api.event = data; try { act($api); } catch (e) { console.warn("️ ✒ Act failure...", e); } } }
// Max for Live → piece: track color push (bios forwards as this // message type after Max's live.observer fires into jweb via // window.acSetLiveTrackColor). Pieces read e.trackColor in act(). if (type === "live:track-color") { if (!cachedAPI) return; const $api = cachedAPI; const data = { device: "live", trackColor: content, is: (e) => e === "live:track-color", }; $api.event = data; try { act($api); } catch (e) { console.warn("️ ✒ Act failure (live:track-color)", e); } }
// Build env info from bios: { packMode, packGit, packDate, latestCommit } // Piece reads e.env in act() to know whether it's in the packed offline // bundle or the live URL, and whether its build hash matches the latest // the server is serving (for an "update available" nudge). if (type === "env:info") { if (!cachedAPI) return; const $api = cachedAPI; const data = { device: "env", env: content, is: (e) => e === "env:info", }; $api.event = data; try { act($api); } catch (e) { console.warn("️ ✒ Act failure (env:info)", e); } }
if (type === "visibility-change") { // 🧨 Just in case of a regression... 23.06.02.21.12 // Because the `bios` focus event changed from visibility behavior. // if (!lastActAPI) return; // Hacky... 23.04.21.14.59 // const $api = lastActAPI; // Focus change events have an empty API. // if (content !== inFocus) { // inFocus = content; // const data = {}; // Object.assign(data, { // device: "none", // is: (e) => e === (inFocus === true ? "focus" : "defocus"), // }); // $api.event = data; // try { // act($api); // } catch (e) { // console.warn("️ ✒ Act failure...", e); // } // }
// Reset KidLisp timing state when becoming visible again // This prevents animation "catch up" after tab was hidden // Skip reset for pieces with background mode — they kept running while hidden. if (content === true && visible === false && globalKidLispInstance && !pieceBackground) { // Becoming visible - reset timing expressions globalKidLispInstance.lastSecondExecutions = {}; globalKidLispInstance.instantTriggersExecuted = {}; if (globalKidLispInstance.sequenceCounters) { globalKidLispInstance.sequenceCounters.clear(); } }
visible = content; }
if (type === "before-unload") { // This has to be synchronous (no workers) to work, and is also often unreliable. // I should not design around using this event, other than perhaps // sending a beacon at the end. 22.11.03.14.53 // See also: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon
/* try { leave({ store, screen, system: $commonApi.system }); // Trigger leave. } catch (e) { console.warn("👋 Leave failure...", e); } */ return; }
// Get the updated device motion. if (type === "motion:update") { $commonApi.motion.on = true; $commonApi.motion.current = content; return; }
if (type === "motion:enabled") { $commonApi.motion.on = true; return; }
// 🔷 Tezos wallet state updates from bios.mjs if (type === "wallet:state") { // Update the wallet state from bios (full state broadcast) Object.assign($commonApi.wallet._state, content); // Clear any error after successful update if (content.connected) delete $commonApi.wallet._state.error; log.wallet.debug("Wallet state updated:", $commonApi.wallet._state); return; }
// 🔷 Wallet pairing URI response (for mobile QR code) if (type === "wallet:pairing-uri-response") { if ($commonApi.wallet._pairingCallback) { $commonApi.wallet._pairingCallback(content); $commonApi.wallet._pairingCallback = null; } return; }
// 🔷 Wallet sign response if (type === "wallet:sign-response") { if ($commonApi.wallet._signCallback) { $commonApi.wallet._signCallback(content); $commonApi.wallet._signCallback = null; } return; }
if (type === "gpu-rendered-once") { $commonApi.gpuReady = true; return; }
if (type === "gpu-forms-removed") { // Delete forms from the sent list that have been removed from the GPU scene. content.forEach((id) => { formsToClear.push(id); }); return; }
if (type === "dark-mode") { darkMode(content.enabled); return; }
if (type === "gpu-config") { graph.setGpuConfig(content); return; }
if (type === "forms:baked") { //console.log("🍞 Forms baked:", content); //noPaint = false;
// if (content.pixels) { // graph.paste(content, 0, 0, 1, true); // }
// paintFormsResolution?.(); return; }
// Media Recorder Events
if (type === "recorder:transcode-progress") { if (debug) console.log("📼 Transcode progress:", content); $commonApi.rec.printProgress = content; if (content === 1) { send({ type: "signal", content: "recorder:transcoding-done" }); // TODO: Is this the best place for this signal to be sent? // Maybe it should go back in the BIOS? 22.08.19.13.44 } // DON'T return here - let it continue to the piece forwarding logic below! // return; // ❌ REMOVED - this was preventing messages from reaching the piece }
// Forward recorder export events to the current piece, but only when not in export mode // During export (when actEvents is undefined), let events pass through directly to main thread if ( type === "recorder:export-status" || type === "recorder:export-progress" || type === "recorder:transcode-progress" || type === "recorder:export-complete" || type === "tape:posted" || type === "tape:post-error" || type === "tape:draft-progress" || type === "tape:draft-ready" || type === "tape:draft-error" || type === "tape:download-progress" || type === "tape:load-progress" || type === "tape:preloaded" || type === "tape:preload-error" || type === "tape:frames" || type === "tape:info-reply" || // tape:get-info answer → video.mjs tapeInfo type === "tape:audio-context-state" || type === "audio:latency-info" || type === "audio:probe" || type === "audio:reinit-complete" || type === "tape:playback-progress" || // Video-backed (kind:"mp4") tape playback callbacks → video.mjs. type === "tape:mp4-ready" || type === "tape:mp4-playing" || type === "tape:mp4-paused" || // Camera-roll clip upload result → chat.mjs (insert !code). type === "chat:video-tape:posted" ) { // Check if actEvents exists without causing ReferenceError try { const hasActEvents = typeof actEvents !== 'undefined' && actEvents && Array.isArray(actEvents);
// If actEvents is available, route to the piece as normal if (hasActEvents) { const event = { is: (eventType) => eventType === type, type: type, content: content, progress: type === "recorder:transcode-progress" ? content : undefined };
actEvents.push(event); if (debug) console.log("📼 ✅ Used actEvents for export event:", type); return; // Don't let it pass through to main thread } } catch (e) { // actEvents doesn't exist - that's fine, we'll call receive directly }
// If actEvents isn't available, try calling the piece's receive function directly // But only if the piece is booted AND has a custom receive (not the default) if (typeof receive === "function" && booted && receive !== defaults.receive) { const event = { is: (eventType) => eventType === type, type: type, content: content, progress: type === "recorder:transcode-progress" ? content : undefined }; try { if (debug && !getPackMode()) console.log("📼 ✅ Calling receive directly for export event:", type); receive(event); return; // Successfully delivered to piece } catch (e) { console.warn("📼 ❌ Error calling receive:", e); } } else { // Log why we're NOT calling receive if (!getPackMode()) { const reasons = []; if (typeof receive !== "function") reasons.push("receive not a function"); if (!booted) reasons.push("not booted"); if (receive === defaults.receive) reasons.push("using default receive"); console.log(`📼 ⏸️ Cannot call receive for ${type}: ${reasons.join(", ")}`); } }
// If we can't deliver it (piece not booted or using default receive), queue it for later const event = { is: (eventType) => eventType === type, type: type, content: content, progress: type === "recorder:transcode-progress" ? content : undefined }; pendingExportEvents.push(event); if (!getPackMode()) { console.log("📼 📥 Queued export event for later delivery:", type, "queue size:", pendingExportEvents.length); } return; }
if ( type === "recorder:rolling:started" || type === "recorder:rolling:resumed" ) { $commonApi.rec.recording = true; $commonApi.rec.cleanMode = content.cleanMode || false; $commonApi.rec.rollingCallback?.(content.time); return; }
if (type === "recorder:rolling:ended") { $commonApi.rec.recording = false; $commonApi.rec.recorded = true; // Also cleared when a recording "slates". // Don't reset cleanMode here - it's needed during GIF export/playback
if ($commonApi.rec.cutCallback) { try { $commonApi.rec.cutCallback(); // Clear the callback after execution to prevent duplicate calls $commonApi.rec.cutCallback = null; } catch (error) { console.error(`🎬 ❌ Error in cutCallback:`, error); console.error(`🎬 ❌ Stack trace:`, error.stack); } } else { console.warn(`🎬 ⚠️ No cutCallback available when rolling ended`); // Fallback: try to jump to video directly if no callback is set $commonApi.jump("video"); } return; }
if (type === "recorder:printing:started") { return; }
if (type === "recorder:printed") { $commonApi.rec.printed = true; $commonApi.rec.printCallback?.(content); return; }
if (type === "recorder:presented") { $commonApi.rec.presenting = true; $commonApi.rec.presentProgress = 0; $commonApi.rec.tapeProgress = 0; // Also surface as an act event — cap uses it to drop its frozen frame // and reveal the already-playing tape underneath while `video` loads. actAlerts.push("recorder:presented"); return; }
if (type === "recorder:presented:failure") { $commonApi.rec.presenting = false; $commonApi.rec.presentProgress = 0; $commonApi.rec.tapeProgress = 0; return; }
if (type === "recorder:frames-response") { // First chunk - initialize accumulation if (content.totalChunks > 1) { console.log(`📦 Receiving chunked frames: chunk 1/${content.totalChunks}`); $commonApi.rec.frameChunks = [content.frames]; $commonApi.rec.totalChunks = content.totalChunks; $commonApi.rec.receivedChunks = 1; $commonApi.rec.rawAudio = content.rawAudio; } else { // Single chunk, call callback immediately $commonApi.rec.framesCallback?.(content); } return; }
if (type === "recorder:frames-chunk") { // Subsequent chunks - accumulate if ($commonApi.rec.frameChunks) { $commonApi.rec.frameChunks.push(content.frames); $commonApi.rec.receivedChunks++;
console.log(`📦 Received chunk ${$commonApi.rec.receivedChunks}/${$commonApi.rec.totalChunks}`);
// All chunks received - combine and call callback if ($commonApi.rec.receivedChunks === $commonApi.rec.totalChunks) { const allFrames = $commonApi.rec.frameChunks.flat(); console.log(`✅ All ${$commonApi.rec.totalChunks} chunks received, total frames: ${allFrames.length}`);
$commonApi.rec.framesCallback?.({ frames: allFrames, rawAudio: $commonApi.rec.rawAudio });
// Clean up delete $commonApi.rec.frameChunks; delete $commonApi.rec.totalChunks; delete $commonApi.rec.receivedChunks; delete $commonApi.rec.rawAudio; } } return; }
if (type === "recorder:present-progress") { $commonApi.rec.presentProgress = content; return; }
if (type === "recorder:present-playing") { $commonApi.rec.playing = true; return; }
if (type === "recorder:present-paused") { $commonApi.rec.playing = false; return; }
if (type === "recorder:unpresented") { $commonApi.rec.presenting = false; $commonApi.rec.presentProgress = 0; $commonApi.rec.tapeProgress = 0; return; }
if (type === "signal") { signals.push(content); return; }
if (type === "piece:act-alert") { if (content !== undefined && content !== null) { if (typeof content === "string") { actAlerts.push(content); } else if (typeof content.name === "string" && content.name.length > 0) { const { name, ...extra } = content; if (keys(extra).length > 0) { actAlerts.push({ name, ...extra }); } else { actAlerts.push(name); } } } return; }
// Menu Fighter keeps RTCPeerConnection on the main thread, then forwards // signaling, channel state, and input packets back through BIOS. Deliver // those messages through the normal piece event queue so worker pieces can // consume them in act() without giving arbitrary BIOS messages that power. if (type === "fight:rtc:event") { actAlerts.push({ name: type, content }); return; }
if (type === "store:retrieved") { storeRetrievalResolutions[content.key]?.(content.data); delete storeRetrievalResolutions[content.key]; return; }
if (type === "store:deleted") { storeDeletionResolutions[content.key]?.(content.data); delete storeDeletionResolutions[content.key]; return; }
// USB flash support (Electron only) if (type === "usb:devices-listed") { if (usbDeviceListResolution) { usbDeviceListResolution(content); usbDeviceListResolution = null; } return; }
if (type === "usb:flash-progress") { actAlerts.push({ name: "usb:flash-progress", ...content }); return; }
if (type === "usb:flash-complete") { if (usbFlashResolution) { usbFlashResolution({ success: true, ...content }); usbFlashResolution = null; } return; }
if (type === "usb:flash-error") { if (usbFlashResolution) { usbFlashResolution({ success: false, error: content.error }); usbFlashResolution = null; } return; }
if (type === "content-created") { $commonApi.content.receive(content); return; }
// 📺 A piece's DOM overlay (e.g. chat.mjs's YouTube modal) closed itself on // the main thread and is letting the piece know via an act alert. if (type === "youtube:modal:closed") { actAlerts.push({ name: "youtube:modal:closed" }); return; }
if (type === "leave") { //const $api = {}; console.log("🏃♂️ Leave:", content); return; }
if (type === "sfx:killed") { sfxKillReceivers[content.id]?.(); return; }
if (type === "sfx:got-sample-data") { sfxSampleReceivers[content.id]?.(content.data); return; }
if (type === "sfx:progress:report") { sfxProgressReceivers[content.id]?.(content); // Resolve the progress report. return; }
if (type === "sfx:got-duration") { sfxDurationReceivers[content.id]?.(content.duration); return; }
// 🎵 Streaming Audio messages - forward to piece receive function // (the whole family: playing/paused/stopped/error/state, time-data, // seeked, frequencies/waveform/peaks-data, speed-data) if (type.startsWith("stream:") || type === "kidlisp:data") { if (typeof receive === "function") { try { receive({ type, content }, cachedAPI); } catch (e) { console.warn("🎵 Stream receive error:", e); } } return; }
if (type === "microphone-amplitude") { microphone.amplitude = content; return; }
if (type === "microphone-waveform") { microphone.waveform = content; return; }
if (type === "microphone-recording-buffer") { microphone.recordingBuffer = content; return; }
if (type === "waveforms") { speaker.waveforms = content; return; }
if (type === "amplitudes") { speaker.amplitudes = content; return; }
if (type === "frequencies") { speaker.frequencies = content; // Extract beat data if it exists in the content if (content.beat) { speaker.beat = content.beat; } return; }
// Handle Game Boy frame data if (type === "gameboy:frame-data") { // Store frame data and metadata in gameboy API object gameboy.frame = content.pixels; gameboy.romName = content.romName; gameboy.title = content.title; gameboy.isGameBoyColor = content.isGameBoyColor; gameboy.isPlaying = true;
return; }
if (type === "microphone-pitch") { microphone.pitch = content; return; }
if (type === "microphone-recording:complete") { microphone.recordingPromise?.resolve(content); return; }
if (type === "microphone-connect:success") { microphone.connected = true; actAlerts.push("microphone-connect:success"); return; }
if (type === "microphone-connect:failure") { microphone.connected = false; actAlerts.push({ name: "microphone-connect:failure", ...content }); return; }
// 1a. Import // One send (returns afterwards) // Here we are receiving file data from main thread that was requested // by $api.upload 😱. We check to see if the upload promise exists and then // use it and/or throw it away. if (type === "import" && fileImport) { if (content.result === "success") { fileImport?.resolve(content.data); } else if (content.result === "error") { console.error("File failed to load:", content.data); fileImport?.reject(content.data); } fileImport = undefined; return; }
// Resolve a web3 connection message. if (type === "web3-connect-response" && web3Response) { if (content.result === "success") { web3Response?.resolve(content.id); } else if (content.result === "error") { web3Response?.reject("error"); } web3Response = undefined; return; }
// Resolve Tezos wallet connection if (type === "tezos-connect-response" && tezosConnectResponse) { if (content.result === "success") { tezosConnectResponse?.resolve(content.address); } else { tezosConnectResponse?.reject(content.error || "Connection failed"); } tezosConnectResponse = undefined; return; }
// Resolve Tezos wallet disconnection if (type === "tezos-disconnect-response" && tezosDisconnectResponse) { if (content.result === "success") { tezosDisconnectResponse?.resolve(); } else { tezosDisconnectResponse?.reject(content.error || "Disconnect failed"); } tezosDisconnectResponse = undefined; return; }
// Resolve Tezos address request if (type === "tezos-address-response" && tezosAddressResponse) { if (content.result === "success") { tezosAddressResponse?.resolve(content.address); } else { tezosAddressResponse?.reject(content.error || "Failed to get address"); } tezosAddressResponse = undefined; return; }
// Resolve Tezos message signing if (type === "tezos-sign-response" && tezosSignResponse) { if (content.result === "success") { tezosSignResponse?.resolve(content.signature); } else { tezosSignResponse?.reject(content.error || "Signing cancelled"); } tezosSignResponse = undefined; return; }
// Resolve Tezos contract call if (type === "tezos-call-response" && tezosCallResponse) { if (content.result === "success") { tezosCallResponse?.resolve(content.opHash); } else { tezosCallResponse?.reject(content.error || "Contract call failed"); } tezosCallResponse = undefined; return; }
// Resolve a gpu message if (type === "gpu-response" && gpuResponse) { if (content.result === "success") { gpuResponse?.resolve(content.data); } else if (content.result === "error") { gpuResponse?.reject(content.data); } gpuResponse = undefined; return; }
// Resolve a server uploaded file. if (type === "upload" && serverUpload) { if (content.result === "success") { // 🗄️ After S3 upload succeeds, create MongoDB record with short code // Extract extension from filename to determine media type const filename = content.data.filename || ""; const ext = filename.split(".").pop();
// Only track paintings and pieces in database if (!content.data.code && (ext === "png" || ext === "mjs" || ext === "lisp" || ext === "lua")) { try { // Call track-media POST to create database record with short code const trackResponse = await $commonApi.net.userRequest("POST", "/api/track-media", { slug: content.data.slug, ext: ext, });
if (trackResponse.status === 401) { // Session expired - trigger logout const errorData = await trackResponse.json(); if (errorData.code === "SESSION_EXPIRED") { console.warn("🔒 Session expired, logging out..."); await $commonApi.net.logout(); console.log("✅ Logged out due to expired session"); } return; // Stop processing this upload }
if (trackResponse.status === 200) { const trackData = await trackResponse.json(); // Add the short code to the response data content.data.code = trackData.code; console.log(`✅ ${ext === "png" ? "Painting" : "Piece"} tracked: ${content.data.slug} → #${trackData.code}`);
// Fire off ATProto sync (don't await - let it happen in background) if (ext === "png") { $commonApi.net.userRequest("POST", "/api/sync-painting-atproto", { slug: content.data.slug, code: trackData.code, paintingId: trackData.paintingId, bucket: content.data.bucket || "user-aesthetic-computer", }).then(response => { if (response.status === 200) { console.log("✅ ATProto sync initiated for", content.data.slug); } }).catch(err => { console.warn("⚠️ ATProto sync failed (non-critical):", err); }); } } else { console.warn(`⚠️ Failed to track media in database:`, trackResponse.status); } } catch (err) { console.error("❌ Error tracking media in database:", err); // Don't fail the upload if database tracking fails } }
serverUpload?.resolve(content.data); } else if (content.result === "error") { console.error("File failed to load:", content); serverUpload?.reject(new Error(content.error || "Upload failed")); } serverUpload = undefined; return; }
if (type === "zipped" && zipCreation) { if (content.result === "success") { zipCreation.resolve(content.data); } else if (content.result === "error") { console.error("Zip failed to be created:", content); zipCreation?.reject(new Error(content.error || "Painting steps upload failed")); } zipCreation = undefined; return; }
// Run when a painting record ZIP is succesfully parsed after being // dragged into the A.C window. if (type === "painting:record:dropped") { // Replace the active nopaint record with the loaded one. // $commonApi.system.nopaint.recording = true; $commonApi.system.nopaint.record = content; if ($commonApi.slug !== "painting") $commonApi.jump("painting"); return; }
// Resolve a locally requested file. if (type === "file-open:response" && fileOpenRequest) { if (content.result === "success") { fileOpenRequest?.resolve(content.data); } else if (content.result === "error") { console.error("Failed to open file.", content); fileOpenRequest?.reject(content.data); } fileOpenRequest = undefined; return; }
// Resolve a file encoding request. if (type === "file-encode:response" && fileEncodeRequest) { if (content.result === "success") { fileEncodeRequest?.resolve(content.data); } else if (content.result === "error") { console.error("Failed to encode file.", content); fileEncodeRequest?.reject(content.data); } fileEncodeRequest = undefined; return; }
// Resolve an authorization request. if (type === "authorization:response" && authorizationRequests.length) { const request = authorizationRequests.shift(); if (content.result === "success") { request.resolve(content.data); } else if (content.result === "error") { request.reject(content.data); } return; }
// 1b. Video frames. if (type === "video-frame") { if (!videoSwitching) activeVideo = content; return; }
if (type === "video-devices") { videoDeviceCount = content; $commonApi.cameras = videoDeviceCount; return; }
if (type === "camera:updated") { videoSwitching = false; actAlerts.push("camera:mode:" + content); return; }
if (type === "camera:denied") { actAlerts.push("camera:denied"); return; }
if ( type === "camera:capabilities" || type === "camera:torch" || type === "camera:zoom" ) { actAlerts.push({ name: type, content }); return; }
if (type === "recorder:av-sync") { const status = content?.aligned ? "PASS" : "FAIL"; console.log( `🎞️ A/V sync ${status}: ${content?.offsetMs?.toFixed?.(1)}ms ` + `(tolerance ±${content?.toleranceMs}ms)`, ); actAlerts.push({ name: type, content }); return; }
if (type === "recorder:compact") { actAlerts.push({ name: type, content }); return; }
// 📊 Camera diagnostics from bios — log directly here so they land in // the piece-runs silo via the patched console. Avoids needing to wire // every diagnostic type into the actAlerts queue (which only carries // string keys, not the full content payload we need for debugging). if (type === "camera:debug" || type === "camera:debug:frame") { try { console.log(`📷 ${type}:`, JSON.stringify(content)); } catch { console.log(`📷 ${type}:`, content); } return; }
// 1c. Loading from History if (type === "history-load") { if (debug && logs.history) console.log("⏳ History:", content); $commonApi.load(content, true); return; }
// 1d. Loading Bitmaps if (type === "loaded-bitmap-success") { // Logging the bitmap itself makes piece-run telemetry JSON-encode every // pixel. No Paint's sprite sheets can stall the worker for seconds. if (debug) console.log("🖼️ Bitmap loaded:", content.url, { width: content.img?.width, height: content.img?.height, }); preloadPromises[content.url]?.resolve(content); delete preloadPromises[content]; return; }
if (type === "loaded-bitmap-rejection") { if (debug) console.error("🖼️ Bitmap load failure:", content); preloadPromises[content.url]?.reject(content.url); delete preloadPromises[content.url]; return; }
// 1d2. Loading Animated WebP if (type === "loaded-animated-webp-success") { console.log("🎬 Animated WebP loaded:", content.frameCount, "frames"); preloadPromises[content.url]?.resolve(content); delete preloadPromises[content.url]; return; }
if (type === "loaded-animated-webp-rejection") { console.error("🎬 Animated WebP load failure:", content); preloadPromises[content.url]?.reject(content.error); delete preloadPromises[content.url]; return; }
// 1e. Loading Sound Effects if (type === "loaded-sfx-success") { if (debug && logs.audio) console.log("Sound load success:", content); // BIOS acknowledges twice — instantly, then again once the buffer is // decoded and its length is known. The second one is where duration lands. if (content.duration) sfxDurations[content.sfx] = content.duration; preloadPromises[content.sfx]?.resolve(content.sfx); delete preloadPromises[content]; return; }
if (type === "loaded-sfx-rejection") { if (debug && logs.audio) console.error("Sound load failure:", content); preloadPromises[content.sfx]?.reject(content.sfx); delete preloadPromises[content.sfx]; return; }
// 1f. Loading ZIP files. if (type === "loaded-zip-success") { if (debug) console.log("🤐 Zip load success:", content.url); preloadPromises[content.url]?.resolve(content.data); delete preloadPromises[content.url]; return; }
if (type === "loaded-zip-rejection") { if (debug) console.warn("🤐 Zip load failure:", content.url); preloadPromises[content.url]?.reject(content.url); delete preloadPromises[content.url]; return; }
// Request a repaint (runs when the window is resized.) if (type === "needs-paint") { noPaint = false; return; }
if (type === "update:auto-state") { globalAutoReload = content === true; $commonApi.needsPaint?.(); return; }
if (type === "reframed") { // Always update the currentDisplay settings for synchronous // screen buffer updates. currentDisplay = { width: content.innerWidth, height: content.innerHeight, subdivisions: content.subdivisions, pixelRatio: content.pixelRatio, // For the `hd()` native-resolution layer. }; $commonApi.display = currentDisplay;
// IMPORTANT: Update screen dimensions immediately so the next paint uses correct size if (content.width !== undefined && content.height !== undefined && screen) { const oldWidth = screen.width; const oldHeight = screen.height; const oldBufferSize = screen.pixels.length;
screen.width = content.width; screen.height = content.height; // Create fresh buffer - bios.mjs freeze frame provides visual continuity // The piece's paint() will fill correct content on next frame screen.pixels = new Uint8ClampedArray(content.width * content.height * 4);
if ($commonApi.rec.presenting && (oldWidth !== content.width || oldHeight !== content.height)) { console.log('📐 REFRAME: Worker dimensions updated', oldWidth, 'x', oldHeight, '→', content.width, 'x', content.height); console.log(' Buffer:', oldBufferSize, '→', screen.pixels.length, '| Expected:', content.width * content.height * 4); } }
// Only trigger a reframe event if we have already passed `boot` (painted // at least once) if (booted) { reframed = true; formReframing = true; } return; }
// Handle QR code fullscreen toggle from hitbox taps if (type === "qr:toggle-fullscreen") { toggleQRFullscreen(); return; }
// 1. Beat if (type === "beat") { if (!sound) return; // Just in case no `frame` has been sent yet. try { beat($activePaintApi); } catch (e) { console.warn(" 💗 Beat failure...", e); }
send({ type: "beat", content: sound }); // soundClear?.(); sound.sounds.length = 0; // Empty the sound command buffer. sound.bubbles.length = 0; sound.farts.length = 0; sound.organics.length = 0; sound.kills.length = 0; return; }
// 2. Frame // Where each piece action (boot, sim, paint, etc...) is run. if (type === "frame") { // Update loop paused state from bios loopPaused = content.paused || false;
// Take hold of a previously worker transferrable screen buffer // and re-assign it. let pixels; if (content.pixels && screen) { pixels = new Uint8ClampedArray(content.pixels); // Only use the transferred buffer if it matches current screen dimensions // After a reframe, the buffer from bios may have old dimensions const expectedLength = (content.width ?? screen.width) * (content.height ?? screen.height) * 4; if (pixels.length === expectedLength) { screen.pixels = pixels; } else { if ($commonApi.rec.presenting) { console.log('⚠️ FRAME: Ignoring mismatched buffer from bios. Got:', pixels.length, '| Expected:', expectedLength, '| Keeping reframed buffer'); } // Screen recreation below also reads this local. A rejected buffer // must not be reused there with the new frame's width and height. pixels = undefined; // Keep the current screen.pixels buffer (from reframe) } }
function getHudEditScrubThreshold() { const promptLength = Math.max( 1, (currentHUDPlainTxt || currentHUDTxt || currentText || "").length, ); const blockWidth = currentHUDLabelBlockWidth || tf?.blockWidth || DEFAULT_TYPEFACE_BLOCK_WIDTH; const baseCaretWidth = blockWidth + 2; const maxCaretWidth = blockWidth * 3; if (promptLength <= 4) return baseCaretWidth; if (promptLength <= 8) return Math.floor(baseCaretWidth * 1.5); return Math.min(maxCaretWidth, Math.floor(promptLength * blockWidth * 0.25)); }
function triggerBackspaceAction(overrideContent) { $commonApi.sound.synth({ tone: 800, beats: 0.1, attack: 0.01, decay: 0.5, volume: 0.15, }); send({ type: "keyboard:unlock" });
const content = overrideContent || currentHUDPlainTxt || currentHUDTxt || currentText; let promptSlug = "prompt"; if (content) { if (lisp.isKidlispSource(content)) { promptSlug += "~" + lisp.encodeKidlispForUrl(content); } else { promptSlug += "~" + content.replace(/~/g, " "); } }
$commonApi.jump(promptSlug); send({ type: "keyboard:open" }); currentHUDScrub = 0; currentHUDScrubReadyState = null; $commonApi.needsPaint?.(); }
function triggerShareAction() { $commonApi.sound.synth({ tone: 1800, beats: 0.15, attack: 0.01, decay: 0.5, volume: 0.15, }); $commonApi.sound.synth({ tone: 1800 / 2, beats: 0.15 * 2, attack: 0.01, decay: 0.5, volume: 0.1, });
// Prefer existing cached short codes so we don't re-embed long inline source const cachedShortCode = getCachedCode(currentCode); if (cachedShortCode) { $commonApi.jump("share~$" + cachedShortCode); } else if (currentOriginalCodeId && currentOriginalCodeId.startsWith("$")) { $commonApi.jump("share~" + currentOriginalCodeId); } else { const textToShare = currentHUDPlainTxt || currentHUDTxt || currentText || ""; $commonApi.jump("share~" + lisp.encodeKidlispForUrl(textToShare)); } currentHUDScrub = 0; currentHUDScrubReadyState = null; $commonApi.needsPaint?.(); }
// 🌟 Global Keyboard Shortcuts (these could also be seen via `act`) content.keyboard.forEach((data) => { if (currentText && currentText.indexOf("botce") > -1) return; // No global keys on `botce`. 23.11.12.23.38
// 📼 [Ctrl+R+R] Quick tape - start recording without leaving the piece // Handle this BEFORE the keyboard:down check since it's a special event if (data.name === "keyboard:quick-tape" && !getPackMode()) { console.log("📼 Quick tape: Handler triggered!"); console.log("📼 Quick tape: currentText =", currentText); console.log("📼 Quick tape: currentParams =", currentParams); console.log("📼 Quick tape: $commonApi.rec =", $commonApi.rec);
// Play a sound to indicate recording started $commonApi.sound.synth({ tone: 2000, duration: 0.05, attack: 0.01, decay: 0.5, volume: 0.25, });
// Set up the recording with default 7 second duration const defaultDuration = 7; const kidlispFps = (typeof window !== 'undefined' && window.currentKidlispFps) || null;
console.log("📼 Quick tape: About to call rec.rolling()...");
// Set up recording options and callback $commonApi.rec.rolling( { type: "video", pieceName: currentText || "quick-tape", pieceParams: currentParams ? currentParams.join("~") : "", originalCommand: currentText, intendedDuration: defaultDuration, frameMode: false, frameCount: null, kidlispFps: kidlispFps, cleanMode: false, showTezosStamp: false, mystery: false }, (time) => { // This callback is invoked after recording starts console.log(`📼 Quick tape: Timer callback invoked! time=${time}`); $commonApi.rec.tapeTimerSet(defaultDuration, time, false); console.log(`📼 Quick tape: Timer set complete`); }, );
console.log("📼 Quick tape: rec.rolling() called");
// Mark as recording $commonApi.rec.recording = true;
console.log(`📼 Quick tape: Recording initiated! Duration=${defaultDuration}s`); return; // Don't process this event further }
if (data.name.indexOf("keyboard:down") === 0) { // [Escape] (Deprecated on 23.05.22.19.33) // If not on prompt, then move backwards through the history of // previously loaded pieces in a session. // if ( // data.key === "Escape" && // currentPath !== "aesthetic.computer/disks/prompt" // ) { // if (pieceHistoryIndex > 0) { // send({ type: "back-to-piece" }); // } else { // // Load the prompt automatically. // // $api.load("prompt"); Disabled on 2022.05.07.03.45 // } // }
if ((data.key === "$" || data.key === "Home") && !getPackMode()) { if (data.ctrl || data.alt) { const sys = $commonApi.system; // Make it a painting. sys.nopaint.replace( cachedAPI, graph.cloneBuffer(screen), "$creenshot", ); $commonApi.jump("prompt"); } else { downloadScreenshot(); // 🖼️ Take a screenshot. } $commonApi.sound.synth({ tone: 1600, duration: 0.02, attack: 0.01, decay: 0.5, volume: 0.25, }); }
// ⛈️ Jump back to the `prompt` from anywhere.. if ( !getPackMode() && // Disable navigation keys in OBJKT mode !SOLO_MODE && // Disable navigation keys in solo mode (data.key === "`" || data.key === "Enter" || data.key === "Backspace" || data.key === "Escape") && system !== "prompt" && system !== "world" && currentText !== "chat" && currentText !== "laer-klokken" && currentText !== "laklok" && currentText !== "lairk" && // Enter opens its chat, Escape closes it currentText !== "aa" && currentText !== "mail" && // composing — it leaves on its own terms currentText !== "amail" && currentText !== "sign" && currentText !== "jas" && currentText !== "nopaint" && // owns Enter/Escape and locks input while saving // numrank keeps Backspace/Enter for gameplay; ` and Escape still leave. !( currentText === "numrank" && (data.key === "Backspace" || data.key === "Enter") ) && currentPath !== "aesthetic.computer/disks/prompt" ) { // If recording, Escape should stop the tape instead of jumping to prompt if (data.key === "Escape" && $commonApi.rec.recording) { console.log("🎬 Escape pressed during recording - stopping tape");
// Cut the recording first $commonApi.rec.cut();
// Play the sound AFTER a brief delay so it doesn't get recorded setTimeout(() => { $commonApi.sound.synth({ tone: 600, duration: 0.15, attack: 0.01, decay: 0.8, volume: 0.2, }); }, 100); // 100ms delay to ensure recording has stopped
return; // Don't proceed with navigation } // 🎬 Stop demoplay on Escape if (data.key === "Escape" && typeof $commonApi.system.stopDemoplay === "function") { leaving = false; $commonApi.system.stopDemoplay(); return; } // Stop merry pipeline for both Escape and Backspace let merryOriginalCommand = null; if (data.key === "Escape" || data.key === "Backspace") { const stopMerry = $commonApi.system.stopMerryPipeline; if (typeof stopMerry === "function") { // Capture original command before stopping (for backspace editing) if (data.key === "Backspace" && $commonApi.system.merry?.originalCommand) { merryOriginalCommand = $commonApi.system.merry.originalCommand; } // Reset leaving flag to allow merry stop to jump leaving = false; stopMerry({ reason: data.key === "Escape" ? "escape" : "backspace", jumpAfter: false, jumpTarget: null, cutTape: true, }); } else if ($commonApi.system.merry) { $commonApi.system.merry.running = false; delete $commonApi.system.merry; } }
$commonApi.sound.synth({ tone: data.key === "Backspace" ? 800 : 1200, beats: 0.1, attack: 0.01, decay: 0.5, volume: 0.15, });
send({ type: "keyboard:unlock" });
// 🌐 Branded domain: Escape/backtick/Enter navigates to aesthetic.computer if (data.key !== "Backspace" && redirectIfBrandedDomain()) return;
const promptSlug = "prompt"; if (!labelBack || data.key === "Backspace" || data.key === "Escape") { if (data.key === "Backspace") { triggerBackspaceAction(merryOriginalCommand); } else { $commonApi.jump(promptSlug); send({ type: "keyboard:open" }); } } else { if ($commonApi.history.length > 0) { send({ type: "back-to-piece" }); // $commonApi.jump( // $commonApi.history[$commonApi.history.length - 1], // ); } else { $commonApi.jump(promptSlug); send({ type: "keyboard:open" }); } } }
// [Tab] Toggle HUD label and QR overlay visibility with smooth animation // Disabled in pack mode (OBJKT bundles) // Only works in KidLisp pieces (to avoid conflict with autocomplete) if (data.key === "Tab" && !getPackMode()) { const isKidLispPiece = detectKidLispPiece({ currentPath, currentHUDTxt, currentText, cleanText: currentHUDTxt }); if (isKidLispPiece) { const currentTime = performance.now(); const timeSinceLastTab = currentTime - (hudAnimationState.lastTabTime || 0); const isDoubleTap = timeSinceLastTab < 300; // 300ms double-tap window
hudAnimationState.lastTabTime = currentTime; // Skip sound only for embedded/inline KidLisp (kidlisp.com editor, PJ KidLisp) const isEmbedded = detectEmbeddedKidLisp({ currentPath, currentHUDTxt, currentText }); toggleHUDVisibility(isDoubleTap, isEmbedded); } }
// [Shift] Toggle QR code fullscreen mode for KidLisp pieces // if (data.key === "Shift") { // console.log("⌨️ [Shift Key] Received! getPackMode():", getPackMode()); // } if (data.key === "Shift" && !getPackMode()) { toggleQRFullscreen(); }
// [Ctrl + X] // Enter and exit fullscreen mode. if (data.key === "x" && data.ctrl && currentText !== "notepat" && !getPackMode()) { send({ type: "fullscreen-enable" }); }
// [Ctrl + D] // Toggle auto-density override — force full density even when FPS is low. if (data.key === "d" && data.ctrl && globalKidLispInstance) { if (globalKidLispInstance.autoDensityOverride) { // Re-enable auto-density globalKidLispInstance.autoDensityOverride = false; if (typeof window !== 'undefined') window.acAutoDensityOverride = false; globalKidLispInstance.enableAutoDensity(); } else { // Force full density — disable auto-scaling globalKidLispInstance.autoDensityOverride = true; if (typeof window !== 'undefined') window.acAutoDensityOverride = true; globalKidLispInstance.disableAutoDensity(); } } } });
// Add 'loading' status to $commonApi. $commonApi.loading = loading; // Let the piece know if we are already // loading another piece.
// Globalize any background music data, retrievable via bgm.data $commonApi.bgm.data = { amplitude: content.audioMusicAmplitude, sample: content.audioMusicSampleData, };
// Synthetic audio for headless captures — feed KidLisp audio globals if (SPOOF_AUDIO_MODE && content.audioMusicAmplitude > 0) { const norm = content.audioMusicAmplitude / 255; // 0–1 updateKidLispAudio({ amp: norm * 10, leftAmp: norm * 10, rightAmp: norm * 10, __source: "spoof", }); }
// Hand-tracking if (content.hand) $commonApi.hand = { mediapipe: content.hand };
// Pens if (content.pen) { const primaryPointer = help.findKeyAndValue( content.pen.pointers, "isPrimary", true, );
// Returns all [pens] if n is undefined, or can return a specific pen by 1 based index. // [pens] are sorted by `pointerIndex`
// TODO: Including "help.findKeyAndValue" seems to bring a lot of // allocation here because it keeps the whole API around? // Re-test this when pointers is not empty! 22.11.12.20.02 const pointers = content.pen.pointers; const pointersValues = Object.values(pointers);
// Make all available dragBoxes into `Box` instances. pointersValues.forEach((p) => { if (p.dragBox) p.dragBox = geo.Box.from(p.dragBox); });
const pens = pointersValues.reduce((arr, value) => { arr[value.pointerNumber] = value; return arr; }, []);
// if (pens.length > 0 && debug) // console.log("Pens:", pens, content.pen.events);
$commonApi.pens = function (n) { if (n === undefined) return pens; return help.findKeyAndValue(pointers, "pointerNumber", n - 1) || {}; };
if (pointersValues.length > 1 && primaryPointer) primaryPointer.multipen = true; // Set a flag for multipen activity on main pen API object.
$commonApi.pen = primaryPointer;
if ( screen && primaryPointer && (primaryPointer.delta?.x !== 0 || primaryPointer.delta?.y !== 0) ) { // Skip sending fairy points in KidLisp pieces. const isKidlisp = detectKidLispPiece({ currentPath, currentHUDTxt, currentText }) || (currentPath && currentPath.endsWith('.lisp')); if (!isKidlisp) { udp?.send("fairy:point", { x: primaryPointer.x / screen.width, y: primaryPointer.y / screen.height, }); } } }
// 🕶️ VR Pen $commonApi.pen3d = content.pen3d?.pen;
// Add upload event to allow the main thread to open a file chooser. // type: Accepts N mimetypes or file extensions as comma separated string. // Usage: upload(".jpg").then((data) => ( ... )).catch((err) => ( ... )); $commonApi.sideload = (type) => { const prom = new Promise((resolve, reject) => { fileImport = { resolve, reject }; }); send({ type: "import", content: type }); return prom; };
// 🤖 Sim // no send $commonApi.seconds = function (s) { return s * 120; // TODO: Get 120 dynamically from the Loop setting. 2022.01.13.23.28 };
// 🔈 Sound // TODO: Most of the $sound api doesn't need to be generated per // frame. 24.01.14.15.19
// For reference in `freq` below. const noteFrequencies = { c: 16.35, "c#": 17.32, db: 17.32, d: 18.35, "d#": 19.45, eb: 19.45, e: 20.6, f: 21.83, "f#": 23.12, gb: 23.12, g: 24.5, "g#": 25.96, ab: 25.96, a: 27.5, "a#": 29.14, bb: 29.14, b: 30.87, };
const $sound = { get time() { const currentTime = content.audioTime; // Add comprehensive logging for audio timing (throttled to avoid spam) // if (debug && Math.floor(currentTime * 100) % 100 === 0) { // Log every 10ms when rounded // console.log(`🎵 AUDIO_TIME: ${currentTime.toFixed(6)}s (getter called from ${(new Error().stack.split('\n')[2] || 'unknown').trim()})`); // } return currentTime; }, // 🎹 DAW state (populated via daw:tempo, daw:transport, daw:phase, daw:samplerate messages from M4L) // Uses persistentDawState which survives frame updates (unlike $commonApi.sound which is recreated) get daw() { return persistentDawState; }, // 🎛️ Spreadnob state (populated via spreadnob:* messages from M4L) get spreadnob() { return persistentSpreadnobState; }, // Get the bpm with bpm() or set the bpm with bpm(newBPM). bpm: function (newBPM) { if (newBPM) sound.bpm = newBPM; return sound.bpm; }, enabled: () => { return AUDIO_SAMPLE_RATE > 0; }, // Compute the frequency of a musical note. // 🗒️ Can take a number or formatted octave string such as 5C# or even C#5 for C sharp in fifth octave. freq: function (input) { // console.log("🎵 Note to check:", input); // Return if it's just a number or parses as one. if (typeof input === "number") return input; if (input === null || input === undefined) return null;
const trimmed = String(input).trim(); if (trimmed === "") return null;
if (!isNaN(parseFloat(trimmed)) && isFinite(trimmed)) return Number(trimmed);
let octave = 4; let note = trimmed.toLowerCase(); // Downcase everything.
// Handle special tokens that shouldn't resolve to a frequency if (note === "rest" || note === "pause" || note === "_" || note === "speech") return null;
const prefixMatch = note.match(/^(-?\d+)([a-z#/+\-]+)$/); const suffixMatch = note.match(/^([a-z#/+\-]+)(-?\d+)$/);
if (prefixMatch) { octave = parseInt(prefixMatch[1], 10); note = prefixMatch[2]; } else if (suffixMatch) { note = suffixMatch[1]; octave = parseInt(suffixMatch[2], 10); }
// Remove trailing + or - markers and other notation artifacts note = note.replace(/[+\-]+$/g, "");
// Replace notepat "s"/"f" suffixes with standard accidentals when appropriate note = note.replace(/^([a-g])s$/, "$1#").replace(/^([a-g])f$/, "$1b");
const frequency = noteFrequencies[note]; // Look up freq for the note. if (!frequency) throw new Error("Note not found in the list");
// Calculate the frequency for the given octave const finalFreq = frequency * Math.pow(2, octave); return finalFreq; }, // Calculate a musical note from a frequency. note: function (frequency) { let closestNote = "", minDiff = Infinity; for (let octave = 0; octave <= 8; octave++) { for (let note in noteFrequencies) { const noteFrequency = noteFrequencies[note] * Math.pow(2, octave); const diff = Math.abs(frequency - noteFrequency); if (diff < minDiff) { minDiff = diff; closestNote = note + octave; } } }
return closestNote.toUpperCase(); }, // MIDI midi: { connect: () => send({ type: "midi:connect" }), // Convert MIDI note number to note string note: function (midiNumber) { const noteNames = [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", ]; const octave = floor(midiNumber / 12) - 1; const noteIndex = midiNumber % 12; return noteNames[noteIndex] + octave; }, }, // Rendering paint: { bars: function paintSound( { ink, box, screen, num }, amplitude, waveform, x, y, width, height, color, options = { noamp: false, nobounds: false }, ) { const yMid = round(y + (height - 2) / 2), yMax = round((height - 2) / 2); let lw = options.noamp ? 0 : 4; // levelWidth; const xStep = (width - lw) / waveform.length;
// Vertical bounds. if (!options.nobounds) { ink("yellow") .line(x + lw, y, x + width - 1, y) .line(x + lw, y + height, x + width - 1, y + height); }
// Level meter. if (!options.noamp) { ink("black").box(x, y, lw, height + 1); ink("green").box(x, y + height, lw, -amplitude * height); }
// Filled waveform const waves = waveform.map((v, i) => { if (v < -1) v = -1; if (v > 1) v = 1; return [x + lw + i * xStep, yMid + v * yMax]; });
ink(options.secondaryColor || "black").box( x + lw, y + 1, width - lw, height - 1, );
ink(options.primaryColor || "white");
let remainder = 0; let totalWidthCovered = 0;
waves.forEach((point, index) => { let bx = x + lw + totalWidthCovered; if (bx >= x + width) return; // Compute the pixel-aligned width for the current bar. let barWidth = Math.floor(xStep + remainder); remainder = (xStep + remainder) % 1; // Collect the fractional remainder. // Ensure we don't exceed the full width for the last bar. if (index === waves.length - 1 || bx + barWidth >= x + width) barWidth = x + width - bx; box( bx, y + point[1] + 1 - y, barWidth, y + (height - 1) - point[1], ); totalWidthCovered += barWidth; });
// Waveform // ink("lime", 255).poly( // waveform.map((v, i) => [x + lw + i * xStep, yMid + v * yMax]), // );
// TODO: Fill a point above this line and below. // ink("blue").flood(x + 7, y + 1); // ink("teal").flood(x + 7, y + height - 2);
// const my = screen.height - mic.amplitude * screen.height; // ink("yellow", 128).line(0, my, screen.width, my); // Horiz. line for amplitude. },
// Paints a waveform with a bounding box based on amplitude. waveform: function paintWaveform( { ink }, amplitude, waveform, x, y, width, height, color = "yellow", options, ) { if (waveform?.length < 1) return; const direction = options?.direction || "left-to-right"; if (direction === "left-to-right") { const xStep = width / (waveform.length - 1);
const yMid = y + height / 2, yMax = height / 2;
ink(color, 128).poly( waveform.map((v, i) => { const p = [x + i * xStep, yMid + (v || 0) * yMax]; return p; }), ); } else if (direction === "bottom-to-top") { const yStep = height / (waveform.length - 1); const xMid = x + width / 2, xMax = width;
ink("blue", 128).poly( waveform.map((v, i) => { const p = [xMid + (v || 0) * xMax, y + height - i * yStep]; return p; }), ); } else { console.warn("🌊 Unsupported direction."); } },
// Renders an "AUDIO ENGINE OFF" badge when the audio context is // suspended. Returns { visible, x, y, width, height } so callers // can react to its layout. audioEngineBadge: function paintAudioEngineBadge( { ink, screen }, speaker, x, y, options = {}, ) { const waveforms = speaker?.waveforms?.left; const amp = speaker?.amplitudes?.left; const ready = waveforms && typeof waveforms.length === "number" && waveforms.length > 0 && typeof amp === "number" && Number.isFinite(amp);
if (ready) return { visible: false, x: 0, y: 0, width: 0, height: 0 };
const maxWidth = options.maxWidth != null ? options.maxWidth : screen.width - 20; const padding = 4; const badgeHeight = options.height || 12; const align = options.align || "center"; // "center" | "left" | "right"
// Responsive text choices const texts = ["AUDIO ENGINE OFF", "ENGINE OFF", "OFF"]; let chosen = texts[0]; for (const t of texts) { // Approximate glyph width for MatrixChunky8 is 6px per char + 1px gap const tw = t.length * 7; if (tw + padding * 2 <= maxWidth) { chosen = t; break; } } const textWidth = chosen.length * 7; const totalWidth = textWidth + padding * 2;
let bx = x; if (align === "center") { bx = x != null ? x : Math.floor((screen.width - totalWidth) / 2); } else if (align === "right") { bx = (x != null ? x : screen.width) - totalWidth; }
ink(180, 0, 0, 240).box(bx, y, totalWidth, badgeHeight); ink(255, 255, 0).write( chosen, { x: bx + padding, y: y + 3 }, undefined, undefined, false, "MatrixChunky8", );
return { visible: true, x: bx, y, width: totalWidth, height: badgeHeight }; }, }, };
$sound.microphone = microphone; $sound.speaker = speaker; $sound.gameboy = gameboy; $sound.sampleRate = AUDIO_SAMPLE_RATE;
// TODO: Generalize square and bubble calls. // TODO: Move this stuff to a "sound" module. sound.bpm = content.audioBpm;
// Clear synchronized audio triggers. // soundClear = () => { // sound.sounds.length = 0; // sound.bubbles.length = 0; // sound.kills.length = 0; // };
// Trigger a named audio sample to playback in the `bios`. // options: { volume: 0-n, pan: 0-2?, loop: Bool, ...(there is more) }
$sound.getSampleData = async function getSampleData(id) { const prom = new Promise((resolve, reject) => { sfxSampleReceivers[id] = resolve; return { resolve, reject }; }); send({ type: "sfx:get-sample-data", content: { id } }); return prom; };
$sound.registerSample = function registerSample(id, data, sampleRate) { if (!id || !data) return; send({ type: "sfx:register", content: { id, data, sampleRate } }); };
// 🔄 Live buffer update - updates sample data for currently playing sounds $sound.updateSample = function updateSample(id, data, sampleRate) { if (!id || !data) return; send({ type: "sfx:update-sample", content: { id, data, sampleRate } }); };
soundTime = content.audioTime;
$sound.play = function play(sfx, options, callbacks) { const id = sfx + "_" + $sampleCount; // A *unique id for this sample. $sampleCount += 1n;
send({ type: "sfx:play", content: { sfx, id, options } });
const playingSound = { options, // Allow the options passed to BIOS to be inspected. startedAt: soundTime, // performance.now(), killed: false, kill: (fade) => { send({ type: "sfx:kill", content: { id, fade } }); }, progress: async () => { if (playingSound.killed) return { progress: 0 }; const prom = new Promise((resolve, reject) => { sfxProgressReceivers[id] = (progressData) => { // Enhanced logging for audio progress tracking if (debug && progressData?.progress !== undefined) { const timeElapsed = soundTime - playingSound.startedAt; // console.log(`🎵 AUDIO_PROGRESS: id=${id.substring(id.lastIndexOf('_') + 1)}, progress=${progressData.progress.toFixed(6)}, elapsed=${timeElapsed.toFixed(3)}s, started=${playingSound.startedAt.toFixed(3)}s, current=${soundTime.toFixed(3)}s`);
// Detect potential timing issues - compare in consistent units if (progressData.progress > 0 && timeElapsed > 0 && progressData.duration) { // Convert audio progress to actual time for comparison const actualAudioTime = progressData.progress * progressData.duration; const timeDrift = Math.abs(actualAudioTime - timeElapsed); const driftPercent = (timeDrift / progressData.duration) * 100;
if (driftPercent > 5) { // 5% drift threshold console.warn(`🎵 SYNC_DRIFT: actual=${actualAudioTime.toFixed(3)}s, expected=${timeElapsed.toFixed(3)}s, drift=${timeDrift.toFixed(3)}s (${driftPercent.toFixed(1)}%)`); } } } resolve(progressData); }; return { resolve, reject }; }); send({ type: "sfx:progress", content: { id } }); return prom; }, update: function (properties) { if (properties.shift || properties.sampleSpeed !== undefined) { send({ type: "sfx:update", content: { id, properties }, }); } }, };
sfxKillReceivers[id] = () => { callbacks?.kill?.(); playingSound.killed = true; };
return playingSound; };
$sound.skip = function () { send({ type: "beat:skip" }); };
$sound.at = function (timeToRun, callback) { // TODO: Finish this implementation. // timeToRun; // content.audioTime; };
// How long a preloaded sound is, in seconds, or undefined until BIOS has // decoded it. A plain read, unlike `getDuration` — nothing to await and // nothing to hang on, so a piece can ask again next frame. $sound.duration = function duration(id) { return sfxDurations[id]; };
$sound.getDuration = async function getDuration(id) { const prom = new Promise((resolve, reject) => { sfxDurationReceivers[id] = resolve; return { resolve, reject }; }); send({ type: "sfx:get-duration", content: { id } }); return prom; };
$sound.synth = function synth({ tone = 440, // hz, or musical note // "sine", "triangle", "square", "sawtooth", "noise-white" (alias // "noise" — tone sets the filter centre), "harp" (aliases "pluck", // "guitar", "string"), "whistle" (aliases "ocarina", "flute"), // "sample", "custom" type = "square", duration = 0.1, // In seconds... (where beats is a shortcut) beats = undefined, // 🧧 Should this be deprecated? attack = 0.01, // How quickly the sound starts. decay = 0.9, // A multiplier for when the sound fades. volume, pan = 0, immediate = false, // Bypass the next frame batch for latency-critical input. probe = null, // Opt-in main-thread → AudioWorklet profiling metadata. generator = null, // Custom waveform generator function for type "custom" // 🐦 Expression — all optional; see lib/sound/synth.mjs `_express`. slide, // hz or note to glide to (exponentially) over slideDuration slideDuration, // seconds; defaults to the note length (0.25 when held) vibrato, // semitones at 5 Hz, or { rate, depth, delay } tremolo, // depth 0..1 at 6 Hz, or { rate, depth } drift, // semitones of slow random pitch wander noise, // 0..1 white-noise breath mixed into the source lowpass, // cutoff hz, or { cutoff, resonance, sweep, sweepDuration } formant, // "a"|"e"|"i"|"o"|"u", bands, or { vowel|bands, scale } } = {}) { const id = soundId; if (volume === undefined) volume = 1; if (duration === "🔁") duration = Infinity; // First emoji in the API. 24.07.03.02.26 if (beats === undefined && duration !== undefined) beats = (duration * sound.bpm) / 60;
tone = $sound.freq(tone); // console.log("⛈️ Tone:", tone); // Add generator to sound data for custom type const soundData = { id, type, tone, beats, attack, decay, volume, pan, probe }; // `tone` stays top-level (bios telemetry reads it); the expression // rides in `options`, which is what the worklet hands to Synth. const options = { tone }; if (slide !== undefined) options.slide = $sound.freq(slide); for (const [key, val] of Object.entries({ slideDuration, vibrato, tremolo, drift, noise, lowpass, formant })) { if (val !== undefined) options[key] = val; } if (Object.keys(options).length > 1) soundData.options = options; if (type === "custom" && generator) { soundData.generator = generator.toString(); // Convert function to string for postMessage }
if (immediate) send({ type: "synth:trigger", content: soundData }); else sound.sounds.push(soundData); soundId += 1n; let seconds; if (beats === undefined && duration !== undefined) seconds = duration; else seconds = (60 / sound.bpm) * beats; // console.log("Beats:", beats, "Duration:", duration, "Seconds:", seconds, "BPM:", sound.bpm);
const end = soundTime + seconds;
return { startedAt: soundTime, // performance.now(), id, kill: function (fade) { if (immediate) send({ type: "synth:kill", content: { id, fade } }); else sound.kills.push({ id, fade }); }, progress: function (time) { return 1 - max(0, end - time) / seconds; }, update: function (properties) { if (properties.tone) properties.tone = $sound.freq(properties.tone); if (properties.slide) properties.slide = $sound.freq(properties.slide); send({ type: "synth:update", content: { id, properties }, }); }, updateGenerator: function (newGenerator) { if (type === "custom") { send({ type: "update-generator", content: { id, generator: newGenerator.toString() }, // Convert function to string }); } }, }; }; $sound.bubble = function ({ radius, rise, volume = 1, pan = 0 } = {}) { const id = soundId; sound.bubbles.push({ id, radius: radius, rise, volume, pan }); soundId += 1n;
return { startedAt: soundTime, id, kill: function (fade) { sound.kills.push({ id, fade }); }, update: function (properties) { send({ type: "bubble:update", content: { id, properties }, }); }, enableSustain: function () { send({ type: "bubble:update", content: { id, properties: { sustain: true } }, }); }, disableSustain: function () { send({ type: "bubble:update", content: { id, properties: { sustain: false } }, }); }, }; };
$sound.fart = function ({ pressure = 1, pitch = 60, rasp = 0.5, volume = 1, pan = 0 } = {}) { const id = soundId; sound.farts = sound.farts || []; sound.farts.push({ id, pressure, pitch, rasp, volume, pan }); soundId += 1n;
return { startedAt: soundTime, id, kill: function (fade) { sound.kills.push({ id, fade }); }, update: function (properties) { send({ type: "fart:update", content: { id, properties }, }); }, enableSustain: function () { send({ type: "fart:update", content: { id, properties: { sustain: true } }, }); }, disableSustain: function () { send({ type: "fart:update", content: { id, properties: { sustain: false } }, }); }, }; };
// 🐾 Organic voices (lib/sound/organic.mjs) all ride one record shape, // so a new generator needs no new plumbing here or in the bios. function organic(kind, params) { if (params.duration === "🔁") params = { ...params, duration: Infinity }; const id = soundId; sound.organics.push({ id, kind, params }); soundId += 1n; return { startedAt: soundTime, id, kind, kill: function (fade) { sound.kills.push({ id, fade }); }, update: function (properties) { send({ type: "synth:update", content: { id, properties } }); }, }; } $sound.growl = (p = {}) => organic("growl", p); $sound.breath = (p = {}) => organic("breath", p); $sound.howl = (p = {}) => organic("howl", p); $sound.chirp = (p = {}) => organic("chirp", p);
$sound.kill = function (id, fade) { sound.kills.push({ id, fade }); };
// 🏠 Room/Reverb API - global reverb effect $sound.room = { // Toggle room reverb on/off toggle: function () { send({ type: "room:toggle" }); }, // Enable room reverb on: function () { send({ type: "room:set", content: { enabled: true } }); }, // Disable room reverb off: function () { send({ type: "room:set", content: { enabled: false } }); }, // Set room parameters: { enabled, mix (0-1), feedback (0-0.95) } set: function (options = {}) { send({ type: "room:set", content: options }); }, // Get current room state (async via callback) get: function () { send({ type: "room:get" }); }, };
// 🧩 Glitch API - global raw synth effect $sound.glitch = { // Toggle glitch on/off toggle: function () { send({ type: "glitch:toggle" }); }, // Enable glitch on: function () { send({ type: "glitch:set", content: { enabled: true } }); }, // Disable glitch off: function () { send({ type: "glitch:set", content: { enabled: false } }); }, // Set glitch parameters: { enabled, mix (0-1), crush (2-12), rate (Hz), jitter (0-1) } set: function (options = {}) { send({ type: "glitch:set", content: options }); }, // Get current glitch state (async via callback) get: function () { send({ type: "glitch:get" }); }, };
$commonApi.sound = $sound;
// System beep. $commonApi.beep = (tone = 1200) => { $sound.synth({ tone, beats: 0.1, attack: 0.01, decay: 0.5, volume: 0.25, }); };
// Act & Sim (Occurs after first boot and paint, `boot` occurs below.) if (booted && paintCount > 0n /*&& !leaving*/) { let $api = {}; keys($commonApi).forEach((key) => ($api[key] = $commonApi[key])); keys($updateApi).forEach((key) => ($api[key] = $updateApi[key])); keys(painting.api).forEach((key) => ($api[key] = painting.api[key])); $api.api = $api; // Add a reference to the whole API.
// 🎮 Add gamepad helper API (separate event streams per gamepad) $api.gamepads = createGamepadAPI(content.gamepads);
// Set the robo API context so it can send events $api.robo.setAPI($api);
$api.inFocus = inFocus;
// 🔒 Apply security restrictions for untrusted pieces if (pieceMetadata && shouldRestrictPiece(pieceMetadata)) { $api = createRestrictedApi($api, pieceMetadata); }
cachedAPI = $api; // Remember this API for any other acts outside // of this loop, like a focus change or custom act broadcast.
// Use screen.width/height instead of content.width/height to get the most up-to-date dimensions // content.width/height can be stale if a reframe just happened $api.screen = { width: screen.width, height: screen.height, pixels: screen.pixels, };
$api.cursor = (code) => { cursorCode = code; send({ type: "cursor:set", content: { style: code } }); };
// 📻 Signaling $api.signal = (content) => { send({ type: "signal", content }); };
// Deprecated on 23.07.01.15.31 (Remove later if no regressions.) // if (initialSim) { // console.log("initial", initialSim, content.updateCount, 'paintcount', paintCount); // simCount += 1n; // $api.simCount = simCount; // try { // sim($api); // } catch (e) { // console.warn("🧮 Sim failure...", e); // } // initialSim = false; // } else
if (content.updateCount > 0 && paintCount > 0n) { // Run `sim` the number of times as requested from `bios`. for (let i = content.updateCount; i--; ) { simCount += 1n; $api.simCount = simCount; try { sim($api); noticeTimer?.step(); // Globally tick the noticeTimer if it exists. // ⌛ Run through all the global hourglass timers. for (let i = hourGlasses.length - 1; i >= 0; i--) { hourGlasses[i].step(); if (hourGlasses[i].complete && !hourGlasses[i].autoFlip) hourGlasses.splice(i, 1); } $api.rec.tapeTimerStep($api); } catch (e) { console.warn("🧮 Sim failure...", e); } } }
// 🌟 Act // *Device Event Handling*
// TODO: Shouldn't all these events come in as part of one array to // keep their order of execution across devices? // TODO: Could "device" be removed in favor of "device:event" strings and // if needed, a device method?
// Window Events
// Reframing the piece... (resizing the window). if (reframed === true) {
$api.event = { device: "none", is: (e) => e === "reframed", }; try { act($api); } catch (e) { console.warn("️ ✒ Act failure...", e); } reframed = false;
// Global reframings. // currentPromptButton?.reposition({ // left: 6, // bottom: 6, // screen: $api.screen, // }); }
// If a disk failed to load, then notify the disk that loaded it // by checking to see if loadFailure has anything set. if (loadFailure) { $api.event = { error: loadFailure, is: (e) => e === "load-error", }; try { act($api); } catch (e) { console.warn("️ ✒ Act failure...", e); } send({ type: "load-failure" }); loadFailure = undefined; }
// Signaling if (signals.length) { const data = { signal: signals }; Object.assign(data, { device: "none", is: (e) => e === "signal", }); $api.event = data; try { act($api); } catch (e) { console.warn("️ ✒ Act failure...", e); } signals.length = 0; }
// Keyboard Paste Event // if (content.clipboardText) { // const data = { text: content.clipboardText }; // Object.assign(data, { // device: "none", // is: (e) => e === "pasted:text", // }); // $api.event = data; // try { // act($api); // } catch (e) { // console.warn("️ ✒ Act failure...", e); // } // }
// *** Pen Events *** // Ingest all pen input events by running act for each event. // TODO: I could also be transforming pen coordinates here... // TODO: Keep track of lastPen to see if it changed. const startTime = performance.now(); let penEventCount = 0;
content.pen?.events.forEach((data) => { penEventCount++; Object.assign(data, { device: data.device, hudButtonActive: currentHUDButtonActive, // Add global HUD button flag is: (e) => { let [name, pointer] = e.split(":"); // 👆 Swipe events carry a *direction* in the second slot // (swipe:left / swipe:right / swipe:up / swipe:down), not a pointer // index. Bare `swipe` (or `swipe:any`) matches any direction. if (data.name === "swipe") { if (name !== "swipe") return false; if (!pointer || pointer === "any") return data.isPrimary === true; return data.direction === pointer && data.isPrimary === true; } if (pointer) { if (pointer === "any") { return name === data.name; } else { return name === data.name && data.index === parseInt(pointer); } } else { return name === data.name && data.isPrimary === true; } }, });
//console.log(data) $api.event = data; // 🌐🖋️️ Global pen events. try { // Always check to see if there was a tap on the corner. const { event: e, jump, send, sound, system, piece } = $api; let originalColor; let masked = false;
if ( e.is("touch:5") && piece !== "notepat" && piece !== "stample" && piece !== "toss" ) { sound.synth({ tone: 1600, duration: 0.02, attack: 0.01, decay: 0.5, volume: 0.25, }); system.nopaint.replace( cachedAPI, graph.cloneBuffer(screen), "$creenshot", ); jump("prompt"); }
// Corner prompt button. if (!TV_MODE) { currentHUDButton?.act(e, { // Passive rollover — the label tints and shakes like a // TextButton so it reads as pressable before you commit. hover: () => { currentHUDHovered = true; $api.needsPaint(); }, leave: () => { currentHUDHovered = false; $api.needsPaint(); }, down: () => { originalColor = currentHUDTextColor; currentHUDScrub = 0; currentHUDScrubReadyState = null; currentHUDTextColor = [0, 255, 0]; send({ type: "keyboard:enabled" }); // Enable keyboard flag. send({ type: "keyboard:unlock" }); $api.needsPaint();
// Set global flag to block other button interactions currentHUDButtonActive = true; // Track that this was a direct touch on the HUD button currentHUDButtonDirectTouch = currentHUDButton.box.contains(e);
// Mask unless we are in the camera. if ($api.slug !== "camera") masked = true;
$api.sound.synth({ tone: 600, beats: 0.1, attack: 0.01, decay: 0.5, volume: 0.25, }); }, push: (btn) => { // Block HUD label navigation in solo mode if (SOLO_MODE) return;
const fallbackShareWidth = tf.blockWidth * "share ".length; const shareWidth = Math.max(currentHUDShareWidth || 0, fallbackShareWidth); const editWidth = Math.max(currentHUDEditWidth || 0, getHudEditScrubThreshold());
// 📦 PACK mode: no scrub-to-share, tap refreshes page if (getPackMode()) { currentHUDButtonActive = false; currentHUDButtonDirectTouch = false; currentHUDScrub = 0; currentHUDScrubReadyState = null; $api.sound.synth({ tone: 1200, beats: 0.1, attack: 0.01, decay: 0.5, volume: 0.15, }); if (typeof window !== 'undefined') window.location.reload(); masked = true; $api.needsPaint(); return; }
// If scrubbed to reveal "share", jump to share piece if (currentHUDScrub >= shareWidth) { currentHUDButtonActive = false; currentHUDButtonDirectTouch = false; triggerShareAction(); return; }
// If scrubbed left to edit threshold, trigger backspace-to-edit if (currentHUDScrub <= -editWidth) { currentHUDButtonActive = false; currentHUDButtonDirectTouch = false; triggerBackspaceAction(); return; }
// If scrubbed but not to max, cancel the push if ( (currentHUDScrub > 0 && currentHUDScrub < shareWidth) || (currentHUDScrub < 0 && currentHUDScrub > -editWidth) ) { btn.actions.cancel?.(); return; }
// Clear global HUD button flags currentHUDButtonActive = false; currentHUDButtonDirectTouch = false;
$api.sound.synth({ tone: 1200, beats: 0.1, attack: 0.01, decay: 0.5, volume: 0.15, });
// 🌐 Branded domain: tapping HUD label navigates to aesthetic.computer if (redirectIfBrandedDomain()) return;
if (!labelBack) { jump("prompt"); } else { labelBack = false; // Reset `labelBack` after jumping. if ($commonApi.history.length > 0) { send({ type: "back-to-piece" }); } else { jump("prompt"); } } $api.needsPaint(); masked = true; currentHUDScrub = 0; currentHUDScrubReadyState = null; }, scrub: (btn) => { if (getPackMode()) return; // No scrub-to-share in bundled mode. if (piece === "share") return; // No need to share scrub while in share. if (currentHUDQR || currentHUDQRCells?.length || hudAnimationState.qrFullscreen) return; // Disable swipe-to-share when QR is shown.
// Only allow scrubbing if this was a direct touch on the HUD button if (!currentHUDButtonDirectTouch) return;
if (btn.over || currentHUDScrub !== 0) { const deltaX = Number(e?.delta?.x) || 0; currentHUDScrub += deltaX; }
const fallbackShareWidth = (currentHUDLabelBlockWidth || DEFAULT_TYPEFACE_BLOCK_WIDTH) * "share ".length; const shareWidth = Math.max(currentHUDShareWidth || 0, fallbackShareWidth); const editWidth = Math.max(currentHUDEditWidth || 0, getHudEditScrubThreshold());
if (currentHUDScrub >= 0) { const fallbackWidth = hudAnimationState.labelWidth ? Math.max(hudAnimationState.labelWidth - currentHUDScrub, 0) : currentHUDLabelBlockWidth * (currentHUDPlainTxt?.length || currentHUDTxt.length); const baseWidth = currentHUDLabelMeasuredWidth ?? fallbackWidth; btn.box.w = baseWidth + currentHUDScrub; // console.log(btn.b); }
if (currentHUDScrub > shareWidth) currentHUDScrub = shareWidth; if (currentHUDScrub < -editWidth) currentHUDScrub = -editWidth;
let nextReadyState = null; if (currentHUDScrub >= shareWidth) nextReadyState = "share"; if (currentHUDScrub <= -editWidth) nextReadyState = "edit";
if (nextReadyState !== currentHUDScrubReadyState && nextReadyState !== null) { $api.sound.synth({ tone: nextReadyState === "share" ? 1600 : 900, beats: 0.07, attack: 0.01, decay: 0.2, volume: 0.12, }); } currentHUDScrubReadyState = nextReadyState;
if (currentHUDScrub >= shareWidth) { currentHUDScrub = shareWidth; currentHUDTextColor = [0, 255, 255]; } else if (currentHUDScrub <= -editWidth) { currentHUDScrub = -editWidth; currentHUDTextColor = [255, 255, 0]; } else if (currentHUDScrub > 0) { currentHUDTextColor = [0, 200, 200]; } else if (currentHUDScrub < 0) { currentHUDTextColor = [255, 120, 180]; } else if (currentHUDScrub === 0) { if (btn.over) { currentHUDTextColor = [0, 255, 0]; } else { currentHUDTextColor = [255, 0, 0]; } }
$api.needsPaint(); }, cancel: () => { currentHUDTextColor = originalColor;
// Clear global HUD button flags currentHUDButtonActive = false; currentHUDButtonDirectTouch = false;
const fallbackShareWidth = tf.blockWidth * "share ".length; const shareWidth = Math.max(currentHUDShareWidth || 0, fallbackShareWidth); const editWidth = Math.max(currentHUDEditWidth || 0, getHudEditScrubThreshold()); if (currentHUDScrub === shareWidth) { triggerShareAction(); return; }
if (currentHUDScrub === -editWidth) { triggerBackspaceAction(); return; }
if (currentHUDScrub !== 0) { $api.sound.synth({ tone: 260, beats: 0.09, attack: 0.01, decay: 0.2, volume: 0.12, }); }
currentHUDScrub = 0; currentHUDScrubReadyState = null; // TODO: This might break on pieces where the keyboard is already // open. send({ type: "keyboard:disabled" }); // Disable keyboard flag. send({ type: "keyboard:lock" }); $api.needsPaint(); $api.sound.synth({ tone: 200, beats: 0.1, attack: 0.01, decay: 0.5, volume: 0.15, }); }, rollover: (btn) => { if (btn) { send({ type: "keyboard:unlock" }); if (btn.down) { currentHUDTextColor = [0, 255, 0]; } } }, rollout: () => { // console.log("rolled out..."); currentHUDTextColor = [200, 80, 80]; send({ type: "keyboard:lock" }); }, }); }
if (!masked && !TV_MODE) act($api); // Run the act function for all pen events (skip in TV mode). } catch (e) { console.warn("️ ✒ Act failure...", e); } });
// *** 3D Pen Events *** content.pen3d?.events?.forEach((data) => { Object.assign(data, { is: (e) => { let [prefix, event, pointer] = e.split(":"); if ( prefix === "3d" && event === data.name && (pointer === undefined || parseInt(pointer) === data.pointer) ) return true; }, }); $api.event = data; try { if (!TV_MODE) act($api); // Skip 3D pen events in TV mode } catch (e) { console.warn("️ ✒ Act failure...", e); } });
// if (penEventCount > 0) { // const processingTime = performance.now() - startTime; //if (processingTime > 5) { // Only log if processing took more than 5ms //console.log("🖋️ Pen event processing:", { avgPerEvent: (processingTime / penEventCount).toFixed(2) + "ms" //}); //} // }
// 🎬 Inject demoplay synthetic keyboard events if ($commonApi.system.demoplay?.syntheticKeyboard?.length) { const synth = $commonApi.system.demoplay.syntheticKeyboard.splice(0); content.keyboard = [...(content.keyboard || []), ...synth]; }
// Ingest all keyboard input events by running act for each event. content.keyboard?.forEach((data) => { Object.assign(data, { device: "keyboard", is: (e) => { const parts = e.split(":"); if (parts.length > 2) { // Check for an exact match if `keyboard:action:?` return data.name === e; } else { // Or a subtring match if `keyboard:action` return data.name.indexOf(e) === 0; } }, }); $api.event = data; try { if (!TV_MODE) act($api); // Execute piece shortcut (skip in TV mode). } catch (e) { console.warn("️ ✒ Act failure...", e); } });
// Ingest all gamepad input events by running act for each event. content.gamepad?.forEach((data) => { Object.assign(data, { device: "gamepad", is: (e) => data.name.indexOf(e) === 0, }); $api.event = data; try { if (!TV_MODE) act($api); // Execute piece shortcut (skip in TV mode). } catch (e) { console.warn("️ ✒ Act failure...", e); } });
// *** UI Cancel Interactions *** (For edge detection) if (content.type === "ui:cancel-interactions" && content.content) { const cancelData = { name: "ui:cancel-interactions", x: content.content.x, y: content.content.y, pointer: content.content.pointer, reason: content.content.reason, is: (e) => e === "ui:cancel-interactions", };
$api.event = cancelData; try { if (!TV_MODE) act($api); // Execute cancel for all buttons (skip in TV mode) } catch (e) { console.warn("️ ✒ Act failure (UI cancel)...", e); } }
// *** Act Alerts *** (Custom events defined in here.) // These do not run in the initial loader / preview piece. actAlerts.forEach((action) => { // Check if `name`'s not a string, and if not, attach arbitrary data. let name, extra = {}; if (typeof action !== "string") { // Make extra be an object with every key on action other than 'name'. ({ name, ...extra } = action); } else { name = action; }
const data = { name, is: (e) => e === name, of: (e) => name.startsWith(e), ...extra, };
// TODO: All all fields from 'extra' into 'data'.
$api.event = data; try { // In TV mode, only allow certain system events, not user input events const allowInTvMode = name.startsWith('speech:') || name.startsWith('dark-mode') || name.startsWith('light-mode') || name.startsWith('microphone-') || name.startsWith('aesthetic-parent:');
if (!TV_MODE || allowInTvMode) { act($api); } } catch (e) { console.warn("️ ✒ Act failure...", e); } }); // 🎬 Forward actAlerts to demoplay conductor for wait-for actions if ($commonApi.system.demoplay?.eventCallback) { actAlerts.forEach((action) => { const name = typeof action === "string" ? action : action.name; if (name) $commonApi.system.demoplay.eventCallback(name); }); } //if (actAlerts.length > 0) console.log(actAlerts, booted); actAlerts.length = 0; // Clear act alerts. }
// 🖼 Paint if (content.needsRender) { let $api = {}; keys($commonApi).forEach((key) => ($api[key] = $commonApi[key])); keys(painting.api).forEach((key) => ($api[key] = painting.api[key])); $api.api = $api; // Add a reference to the whole API.
// 🎮 Add gamepad helper API (separate event streams per gamepad) $api.gamepads = createGamepadAPI(content.gamepads);
// Set the robo API context so it can send events $api.robo.setAPI($api);
// 🔒 Apply security restrictions for untrusted pieces if (pieceMetadata && shouldRestrictPiece(pieceMetadata)) { $api = createRestrictedApi($api, pieceMetadata); }
cachedAPI = $api; // Remember this API for any other acts outside // of this loop, like a focus change or custom act broadcast.
// Object.assign($api, $commonApi); // Object.assign($api, painting.api);
// Use piece-level frame counter that only increments when piece actually paints // This ensures frame-based calculations (like in kidlisp) work correctly with FPS limiting $api.paintCount = pieceFrameCount;
$api.inFocus = content.inFocus;
// Make a screen buffer or resize it automatically if it doesn't exist.
if ( !screen || screen.width !== content.width || screen.height !== content.height ) { const hasScreen = screen !== undefined;
screen = { pixels: pixels || new Uint8ClampedArray(content.width * content.height * 4), width: content.width, height: content.height, load: function load(name) { if (store[name]?.pixels) { this.pixels = new Uint8ClampedArray(store[name].pixels); this.width = store[name].width; this.height = store[name].height; $commonApi.resize(this.width, this.height);
// 🎨 Broadcast screen resize from load operation if ($commonApi.broadcastPaintingUpdate) { $commonApi.broadcastPaintingUpdate("resized", { width: this.width, height: this.height, source: "screen_load" }); }
return true; } else { return false; } }, save: function save(name) { store[name] = { pixels: new Uint8ClampedArray(this.pixels), width: this.width, height: this.height, }; }, };
screen[hasScreen ? "resized" : "created"] = true; // Screen change type.
// 🎨 Broadcast screen resize to other tabs if (hasScreen && $commonApi.broadcastPaintingUpdate) { $commonApi.broadcastPaintingUpdate("resized", { width: screen.width, height: screen.height, source: "screen_resize" }); }
// TODO: Add the depth buffer back here. // Reset the depth buffer. // TODO: I feel like this is causing a memory leak... graph.depthBuffer.length = screen.width * screen.height; graph.depthBuffer.fill(Number.MAX_VALUE);
graph.writeBuffer.length = 0; //screen.width * screen.height; // graph.writeBuffer.fill(0); }
// TODO: Disable the depth buffer for now... it doesn't need to be // regenerated on every frame. // TODO: This only needs to run if 'form' is running in a piece. 25.03.20.19.27 graph.depthBuffer.fill(Number.MAX_VALUE); // Clear depthbuffer. graph.writeBuffer.length = 0; //fill(0); // Clear writebuffer.
$api.screen = screen; $api.screen.center = { x: screen.width / 2, y: screen.height / 2 };
// Snapshot true screen state each frame (after content.pixels transfer). // page(sub-buffer) mutates screen in place; _mainScreenObject lets page() // detect by identity when we're returning to the main screen and restore. _trueScreenPixels = screen.pixels; _trueScreenWidth = screen.width; _trueScreenHeight = screen.height; _mainScreenObject = screen;
$api.fps = function (newFps) { // Use piece-level timing instead of changing global render loop if (newFps === undefined || newFps === null) { if (pieceFPS !== null) { pieceFPS = null; // Remove FPS limit only if it was previously set } } else { // Only initialize timing if FPS wasn't set before, or if it's a different value if (pieceFPS === null) { // First time setting FPS - initialize timing pieceFPS = newFps; lastPaintTime = 0; // This will trigger "first frame" logic on next paint } else if (pieceFPS !== newFps) { // FPS changed to a different value - reset timing pieceFPS = newFps; lastPaintTime = 0; // This will trigger "first frame" logic on next paint } // If pieceFPS === newFps, do nothing (cache logic) } // Don't send fps-change to global render loop anymore // send({ type: "fps-change", content: newFps }); };
$api.cursor = (code) => { cursorCode = code; send({ type: "cursor:set", content: { style: code } }); };
graph.setBuffer(screen);
// API Stops being modified here... /*if (!$activePaintApi)*/ $activePaintApi = $api;
// Add paintings cache to API for KidLisp ready? function $activePaintApi.paintings = paintings;
// 🎯 Initialize global KidLisp instance with the main API initializeGlobalKidLisp($api);
// TODO: Set bpm from boot. /* $api.sound = { time: content.time, bpm: function (newBPM) { if (newBPM) { content.bpm[0] = newBPM; } return content.bpm[0]; }, }; */
// TODO: Boot's painting is currently bound by whatever dirtyBox gets // set to at the end of `paint`.
// Run boot only once before painting for the first time. if (paintCount === 0n && loading === false) { const dark = await store.retrieve("dark-mode"); // Read dark mode. if (dark === true || dark === false) $commonApi.dark = dark;
// System specific preloaders. //if ($commonApi?.system?.name === "nopaint" || currentText === "prompt") {
// Create a new painting if one doesn't already exist. if (!store["painting"]) { store["painting"] = (await store.retrieve("painting", "local:db")) || painting.api.painting(screen.width, screen.height, ($) => { $.wipe(64); });
store["painting:resolution-lock"] = await store.retrieve( "painting:resolution-lock", "local:db", );
store["painting:transform"] = await store.retrieve( "painting:transform", "local:db", );
addUndoPainting(store["painting"]); }
const sys = $commonApi.system; sys.painting = store["painting"]; if (!Object.hasOwn(store, "painting:piece")) { store["painting:piece"] = await store.retrieve("painting:piece", "local:db"); } sys.nopaint.piece = store["painting:piece"] || null;
// Set the painting record if one is in storage. if (!sys.nopaint.recording) { sys.nopaint.record = (await store.retrieve("painting:record", "local:db")) || [];
if (sys.nopaint.record.length === 0) { $commonApi.system.nopaint.startRecord("new"); }
sys.nopaint.recording = sys.nopaint.record.length > 0; }
sys.nopaint.translation = store["painting:transform"]?.translation || sys.nopaint.translation; sys.nopaint.zoomLevel = store["painting:transform"]?.zoom || sys.nopaint.zoomLevel;
try { const bootStartTime = performance.now(); diskTimings.bootStarted = Math.round(bootStartTime - diskTimingStart); // Silent: boot() starting
// Reset zebra cache at the beginning of boot to ensure consistent state $api.num.resetZebraCache();
// Notify boot progress // console.log("📢 Disk sending boot-log: running boot"); send({ type: "boot-log", content: "running boot" });
if (system === "nopaint") nopaint_boot({ ...$api, params: $api.params, colon: $api.colon }); await boot($api); if (system === "nopaint" && !/^nopaint(?:[:~]|$)/.test($api.slug || "")) { sys.nopaint.syncWip($api).catch((error) => console.warn("Painting WIP:", error.message)); } const bootEndTime = performance.now(); diskTimings.bootComplete = Math.round(bootEndTime - diskTimingStart); // Silent: boot() completed booted = true; log.boot.success("Boot completed, booted =", booted, "pending events:", pendingExportEvents.length);
// 📨 Flush any pending export events that arrived during boot if (pendingExportEvents.length > 0) { log.boot.debug(`Flushing ${pendingExportEvents.length} pending export events`); log.boot.verbose("receive === defaults.receive:", receive === defaults.receive); log.boot.verbose("typeof receive:", typeof receive); const eventsToFlush = [...pendingExportEvents]; pendingExportEvents = []; eventsToFlush.forEach(event => { if (typeof receive === "function" && receive !== defaults.receive) { try { log.boot.verbose("Flushing queued event:", event.type); receive(event); } catch (e) { console.warn("📼 ❌ Error flushing event:", event.type, e); } } }); } } catch (e) { console.warn("🥾 Boot failure...", e); } send({ type: "disk-loaded-and-booted" }); }
// Paint a frame, which can return false to enable caching via noPaint and by // default returns undefined (assume a repaint). // Once paint returns false and noPaint is marked true, `needsPaint` must be called. // Note: Always marked false on a disk's first frame.
let painted = false; let dirtyBox;
// Render a thumbnail instead of the piece. if (previewMode) { try { // Assign a default resolution on first preview, // which can be over-ridden using `resolution` inside the // `preview` function. if (firstPreviewOrIcon) { if (currentSearch === "preview") { $api.resolution(1200 / 8, 630 / 8, 0); } else { $api.resolution( ...currentSearch .split("=")[1] .split("x") .map((n) => floor(parseInt(n) / 8)), 0, ); } firstPreviewOrIcon = false; }
preview($api); painting.paint(true); painted = true; paintCount += 1n; } catch (err) { console.warn("🖼️ Preview failure...", err); previewMode = false; previewOrIconMode = previewMode || iconMode; } } else if (iconMode) { // Render a favicon instead of the piece. try { if (firstPreviewOrIcon) { $api.resolution(128, 128, 0); if (currentSearch === "icon") { $api.resolution(128, 128, 0); } else { console.log("Current:", currentSearch); $api.resolution( ...currentSearch .split("=")[1] .split("x") .map((n) => parseInt(n)), 0, ); } firstPreviewOrIcon = false; } icon($api); painting.paint(true); painted = true; paintCount += 1n; } catch (err) { console.warn("🪷 Icon failure...", err); iconMode = false; previewOrIconMode = previewMode || iconMode; } }
// Attempt a paint. if ( previewMode === false && iconMode === false && (noPaint === false || scream || fairies.length > 0 || (system === "nopaint" && nopaintPerf)) && booted ) { let paintOut;
// Restore kidlisp's accumulated pan state from previous frame $api.loadpan();
try { // 📓 Bake any painting from the nopaint system before anything else. if (system === "nopaint" && $api.system?.nopaint) { const np = $api.system.nopaint; // No Paint: baking
if ( (brush || filter) && $api.pen?.drawing /*&& currentHUDButton.down === false*/ ) { const brushFilterApi = { ...$api }; // Add top-level 'color' word that maps to system.nopaint.color brushFilterApi.color = np.color; if (currentHUDButton.down === false) { brushFilterApi.pen = np.brush; if (brush) { // $api.page($api.system.nopaint.buffer); $api.page($api.system.painting); // 🔥 // TODO: Use the pen data here to get an interpolation, // then pan to each interpolated point and repaint. // console.log( // "🖌️ Brush:", // brushFilterApi.pen, // "🖊️ Pen:", // $api.pen, // ); brush(brushFilterApi); } if (filter) { $api.page($api.system.painting); filter(brushFilterApi); } $api.page(screen); } }
// 🎯 BAKING MOVED TO ACT PHASE: Removed duplicate baking logic // Baking now happens immediately in act() to fix race conditions if (np.is("painting") || np.needsPresent) { np.present($api); // No Paint: prepaint } } // All: Paint
// 🎞️ Piece-level FPS timing control const now = performance.now(); let shouldPaint = true;
if (pieceFPS !== null && pieceFPS > 0) { const targetFrameTime = 1000 / pieceFPS; // milliseconds per frame
// If this is the first paint (lastPaintTime === 0), always allow it if (lastPaintTime === 0) { lastPaintTime = now; shouldPaint = true; } else { const timeSinceLastPaint = now - lastPaintTime;
if (timeSinceLastPaint < targetFrameTime) { shouldPaint = false; // Skip this frame } else { lastPaintTime = now; // Update last paint time shouldPaint = true; } } }
// Only call paint() if timing allows or no FPS limit is set if (shouldPaint) { // Reset zebra cache at the start of each frame so it can advance once per frame $api.num.resetZebraCache();
// Always call paint() - piece paints underneath, GOL overlays on top paintOut = paint($api); // Returns `undefined`, `false`, or `DirtyBox`. if (paint !== defaults.paint) previewEvidence.paint(); graph.gpuLogTick(); // 📊 GPU effect logging every 8 frames // Increment piece frame counter only when we actually paint pieceFrameCount++;
// 🎬 Piece Transition: Also capture noise16 frame as fallback (for initial page load) if (paint === defaults.paint && $api.screen?.pixels) { // Capture the noise16 state in case no piece was running before (initial load) golTransition.overlayPixels = new Uint8ClampedArray($api.screen.pixels); golTransition.width = $api.screen.width; golTransition.height = $api.screen.height; }
// 🎬 Piece Transition: When first piece paint happens, switch to reveal phase // Skip for KidLisp pieces - they have their own rendering that conflicts const isKidLispPiece = currentPath && ( lisp.isKidlispSource(currentPath) || currentPath === "(...)" || currentPath.includes("$") || currentPath.endsWith('.lisp') );
// Skip transitions entirely if TRANSITION_TYPE is "none" if (TRANSITION_TYPE !== "none" && pieceFrameCount === 1 && paint !== defaults.paint && golTransition.overlayPixels && $api.screen?.pixels && !isKidLispPiece) { const screenW = $api.screen.width; const screenH = $api.screen.height; const overlayW = golTransition.width; const overlayH = golTransition.height;
// If dimensions differ, scale the overlay to match the new screen if (screenW !== overlayW || screenH !== overlayH) { console.log(`🎬 Transition: Scaling overlay from ${overlayW}x${overlayH} to ${screenW}x${screenH}`); golTransition.overlayPixels = scaleOverlayPixels( golTransition.overlayPixels, overlayW, overlayH, screenW, screenH ); golTransition.width = screenW; golTransition.height = screenH; // Re-initialize with new dimensions if needed if (golTransition.active) { initGOLCells(screenW, screenH); } }
// If transition was already started during load, switch to reveal phase // Otherwise start it now (fallback for cases where capture didn't happen) if (golTransition.active) { transitionPieceLoaded(); // Switch from loading to reveal phase } else { // Fallback: start transition now if it wasn't started during load initGOLCells(golTransition.width, golTransition.height); golTransition.generation = 0; golTransition.active = true; golTransition.phase = "revealing"; // Skip loading, go straight to reveal console.log(`🎬 Transition: Started ${TRANSITION_TYPE} (direct reveal)`, golTransition.width, "x", golTransition.height); } }
// Log first piece paint if (pieceFrameCount === 1) { diskTimings.firstPaint = Math.round(performance.now() - diskTimingStart); // Silent: first piece paint }
// 🎨 Signal to hide boot canvas when piece's paint takes over from default noise16 // Only signal once when piece has a custom paint function (not defaults.paint) // Note: pieceFrameCount may already be >1 if defaults.paint ran while the module // was loading (e.g., KidLisp pieces that take several seconds to initialize). // Use acPieceReady itself as the "already fired" guard instead of pieceFrameCount. if (paint !== defaults.paint && !(typeof window !== "undefined" && window.acPieceReady)) { send({ type: "piece-paint-ready" }); // 🔥 Global signal for Puppeteer/Oven to detect piece is ready for screenshot if (typeof window !== "undefined") { window.acPieceReady = true; window.acPieceReadyTime = Date.now(); const isPackMode = (typeof window !== "undefined" && window.acPACK_MODE) || (typeof globalThis !== "undefined" && globalThis.acPACK_MODE); if (!isPackMode) { console.log("🟢 acPieceReady = true (first paint complete)"); } } }
// TODO: Remove old embedded layer rendering - using simplified approach now // globalThis.renderKidlispProgrammaticLayers(); } else { // When skipping paint, use undefined to ensure continuous rendering // This maintains the render loop while skipping the actual paint call paintOut = undefined; }
// Store paintOut for next frame (only when we actually painted) if (shouldPaint) { lastPaintOut = paintOut; }
// Save kidlisp's accumulated pan state for next frame $api.savepan(); // Reset pan for system UI rendering $api.unpan(); } catch (e) { console.warn("🎨 Paint failure...", e); }
// `DirtyBox` and `undefined` always set `noPaint` to `true`. noPaint = paintOut === false || (paintOut !== undefined && paintOut !== true);
// 🧬 GOL Transition: Force pixels to be sent while transition is active if (golTransition.active) { noPaint = false; // Override - GOL modified pixels, must send them }
// Run everything that was queued to be painted, then devour paintLayers. //await painting.paint();
// Upper layer. const { page, layer, ink, needsPaint, pieceCount } = $api;
// Restore screen if page(sub-buffer) calls during paint left it corrupted. // Brushes like smear call page(system.painting) which mutates screen.pixels/width/height. // page(screen) at the end of those brushes can't restore because screen is already corrupted. if (_trueScreenPixels && screen.pixels !== _trueScreenPixels) { screen.pixels = _trueScreenPixels; screen.width = _trueScreenWidth; screen.height = _trueScreenHeight; }
page($api.screen); // Make sure we're on the right bufer.
layer(1000); // Always make sure this stuff draws on top.
// const piece = $api.slug?.split("~")[0]; // if ( // !previewMode && // !iconMode && // !hideLabel && // system !== "prompt" && // piece !== "textfence" && // piece !== "bleep" && // piece !== undefined && // piece.length > 0 && // piece !== "painting" && // pieceCount > 0 // ) { // currentPromptButton = // currentPromptButton || // new $api.ui.TextButton("Back", { // left: 6, // bottom: 6, // screen: $api.screen, // }); // currentPromptButton.paint($api); // }
// 😱 Scream - Paint a scream if it exists. // TODO: Should this overlay after the fact and not force a paint? 23.05.23.19.21 // Yes probably, because of layering issues? if (scream || screaming) { ink("yellow").write(scream, { x: 6, y: 18 }, "red"); //ink("red").write(scream, { x: 6 + 1, y: 18 + 1 });
/* ink(255) .wipe(255, 0, 0) .write( scream, { center: "xy", size: 3, thickness: 1 }, undefined, $api.screen.width - 8, needsPaint(), ); */ if (!screaming) { screaming = true; clearTimeout(screamingTimer); screamingTimer = setTimeout(() => { screaming = false; scream = null; }, 1000); } }
// 🧚 Ambient Pen Points - Paint if they exist. fairies.forEach(({ x, y }) => { ink().point(x * screen.width, y * screen.height); }); if (fairies.length > 0) { needsPaint(); // if (system === "nopaint") $api.system.nopaint.needsPresent = true; } fairies.length = 0;
// 🔴 Show a cross-piece "Recording" indicator. // Currently only implemented for `painting:record`. 23.08.20.21.36 if ( $api.system?.nopaint?.recording && !hideLabel && pieceHistoryIndex > -1 && !loading ) { // ink("red").box(screen.width - 3, 1, 2); }
// Show a notice if necessary. if (notice) { ink(noticeColor[0]) //.pan(help.choose(-1, 0, 1), help.choose(-1, 0, 1)) .write( notice, { center: "xy", size: 2 }, // { center: "x", y: 32, size: 2 }, noticeColor[1], $api.screen.width - 8, noticeOpts?.wrap === "char" ? false : true, ); //.unpan(); }
layer(0);
painting.paint(true);
// 🎬 Piece Transition: Step and apply overlay AFTER painting.paint() executes // This ensures the overlay is composited on TOP of the piece's rendered frame if (golTransition.active && screen?.pixels) { // Check dimensions still match if (screen.width !== golTransition.width || screen.height !== golTransition.height) { console.warn("🎬 Transition: Screen dimensions changed! Aborting."); golTransition.active = false; golTransition.overlayPixels = null; golTransition.blockData = null; golTransition.dripOffsets = null; golTransition.dripSpeeds = null; golTransition.dripDirections = null; } else { // Step the transition const stillActive = golStep(screen.pixels);
// Apply overlay on top of piece's paint applyGOLOverlay(screen.pixels);
// End transition when complete if (!stillActive) { console.log(`🎬 Transition: ${TRANSITION_TYPE} complete!`); golTransition.active = false; golTransition.overlayPixels = null; golTransition.blockData = null; golTransition.dripOffsets = null; golTransition.dripSpeeds = null; golTransition.dripDirections = null; } } }
painted = true; if (system === "video") console.log('🟢 DISK: painted set to TRUE, paintCount:', paintCount); paintCount = paintCount + 1n;
// Check for frame-based recording completion (runs on every painted frame) if ($api.rec.tapeFrameMode && $api.rec.tapeFrameTarget > 0) { const currentFrame = Number(paintCount); const framesPassed = currentFrame - $api.rec.tapeFrameStart;
$api.rec.tapeProgress = framesPassed / $api.rec.tapeFrameTarget;
if (framesPassed >= $api.rec.tapeFrameTarget) { console.log(`🎬 Frame-based recording complete: ${framesPassed}/${$api.rec.tapeFrameTarget} frames`);
// Clear failsafe since normal timer completed if ($api.rec.failsafeTimeout) { clearTimeout($api.rec.failsafeTimeout); $api.rec.failsafeTimeout = null; }
// Reset frame-based recording state $api.rec.tapeProgress = 0; $api.rec.tapeFrameMode = false; $api.rec.tapeFrameStart = 0; $api.rec.tapeFrameTarget = 0;
// Trigger cut and jump to video if (typeof $api.rec.cut === 'function') { $api.rec.cut(() => { $commonApi.jump("video"); }); } else { log.tape.warn("Cut function not available"); $commonApi.jump("video"); } } }
if (paintOut) dirtyBox = paintOut;
delete screen.resized; // Remove status from screen after painting. delete screen.created;
//console.log("bake") //send({ type: "3d-bake" }); } else { if (system === "video") { console.log('❌ DISK: PAINT BLOCK SKIPPED - previewMode:', previewMode, 'iconMode:', iconMode, 'noPaint:', noPaint, 'booted:', booted); } }
// 🏷️ corner-label: Draw any Global UI / HUD in an overlay buffer that will get // composited by the other thread.
// TODO: ❤️🔥 Why is this being composited by a different thread? // Also... where do I put a scream?
// 🐚 shellhtml: the composited label stands down, so hand the hosting // shell (prompt.ac) the same string the raster would paint — color // codes and all — whenever it changes. The shell's DOM corner label // then shows exactly what aesthetic.computer shows, kidlisp source // included, instead of re-deriving a label from the slug. if (shellHTMLMode) shellLabelSync();
// System info label (addressability). let label; const piece = currentHUDTxt?.split("~")[0]; const defo = 6; // Default offset
if ( !previewMode && !iconMode && !hideLabel && piece !== undefined && piece.length > 0 ) {
// Use plain text for width calculation to avoid counting color codes const textForWidthCalculation = currentHUDPlainTxt || currentHUDTxt;
// Double-check: strip color codes directly if they're still present // Handle multiple color code formats: \colorname\, \255,255,255\, etc. const colorCodeRegex = /\\[^\\]*\\/g; // Matches \anything\ format let cleanText = textForWidthCalculation.replace(colorCodeRegex, '');
// Also try removing RGB color codes like \255,255,255\ if any remain cleanText = cleanText.replace(/\\\d+,\d+,\d+(,\d+)?\\/g, '');
// Remove any remaining single backslashes that might be color code remnants cleanText = cleanText.replace(/\\/g, '');
// Analyze visible lines for accurate sizing const rawLines = cleanText.split('\n'); const visibleLines = rawLines.map((line) => line.replace(/\s+$/, '')); const visibleLineCount = visibleLines.length;
const defaultTypeface = tf; const matrixTypeface = typefaceCache.get("MatrixChunky8");
const isKidlispPiece = detectKidLispPiece({ currentPath, currentHUDTxt, currentText, cleanText, });
// 🎨 KidLisp HUD color: green when running, yellow when paused if (isKidlispPiece && !currentHUDTextColor) { if (loopPaused) { currentHUDTextColor = [255, 220, 80]; // Yellow when paused (matches pause button #ffd93d) } else { currentHUDTextColor = [128, 255, 128]; // Green when running } }
// 🔤 Font Preloading: Ensure MatrixChunky8 starts loading early if available if (matrixTypeface && !matrixTypeface.__loadPromise) { ensureTypefaceLoaded(matrixTypeface); }
const measureLineWidth = (line, typeface) => { if (!line || !line.length || !typeface) return 0; const advances = typeface.data?.advances; const defaultAdvance = typeface.blockWidth || DEFAULT_TYPEFACE_BLOCK_WIDTH; let width = 0; for (let i = 0; i < line.length; i++) { const ch = line[i]; width += advances && advances[ch] !== undefined ? advances[ch] : defaultAdvance; } return width; };
const buildLayout = (typeface) => { if (!typeface) return null;
const fontIdentifier = typeface === defaultTypeface ? undefined : typeface.name; const blockWidth = typeface.blockWidth || DEFAULT_TYPEFACE_BLOCK_WIDTH; const blockHeight = typeface.blockHeight || DEFAULT_TYPEFACE_BLOCK_HEIGHT; const lineWidths = visibleLines.map((line) => measureLineWidth(line, typeface)); const longestLineWidth = lineWidths.reduce((max, width) => Math.max(max, width), 0); // 🎨 Use screen width minus padding for KidLisp wrapping // Account for left margin (HUD_LABEL_TEXT_MARGIN) in measurement bounds const bounds = isKidlispPiece ? ($api.screen.width - 6 - HUD_LABEL_TEXT_MARGIN) : Math.max($api.screen.width * 4, 1000);
const naturalBounds = $api.text.box( cleanText, { x: 0, y: 0 }, bounds, 1, true, // Enable wrapping for proper measurement fontIdentifier, );
const wrappedLines = Array.isArray(naturalBounds?.lines) ? naturalBounds.lines.map((line) => { if (typeof line === "string") return line; return line?.join?.(" ") || ""; }) : visibleLines; const wrappedLineWidths = wrappedLines.map((line) => measureLineWidth((line || "").replace(/\s+$/, ""), typeface), ); const wrappedLineStep = blockHeight + 1; const wrappedLastLineIndex = Math.max(0, wrappedLines.length - 1); const wrappedLastLineWidth = wrappedLineWidths[wrappedLastLineIndex] || 0; const wrappedLastLineY = wrappedLastLineIndex * wrappedLineStep;
if (fontIdentifier === "MatrixChunky8") { for (const line of visibleLines) { for (let i = 0; i < line.length; i++) { typeface.glyphs?.[line[i]]; } } }
return { typeface, fontIdentifier, blockWidth, blockHeight, lineWidths, longestLineWidth, textBoxWidth: naturalBounds?.box?.width ?? 0, textBoxHeight: naturalBounds?.box?.height ?? 0, naturalBounds, wrappedLines, wrappedLineWidths, wrappedLastLineWidth, wrappedLastLineY, }; };
const defaultLayout = buildLayout(defaultTypeface); let selectedLayout = defaultLayout;
// 🔤 Force MatrixChunky8 if hud.tinyLabel() was called if (forceTinyHudLabel && matrixTypeface) { const matrixLayout = buildLayout(matrixTypeface); if (matrixLayout) { selectedLayout = matrixLayout; } }
// Disabled: MatrixChunky8 breakpoint removed per user request // if ( // isKidlispPiece && // matrixTypeface && // defaultLayout && // defaultLayout.longestLineWidth > $api.screen.width // ) { // const matrixLayout = buildLayout(matrixTypeface); // if (matrixLayout) { // selectedLayout = matrixLayout; // } // }
if (!selectedLayout && matrixTypeface) { selectedLayout = buildLayout(matrixTypeface); }
if (!selectedLayout) { selectedLayout = defaultLayout; }
const selectedTypeface = selectedLayout?.typeface || defaultTypeface; const selectedHudFont = selectedLayout?.fontIdentifier; const hudBlockWidth = selectedLayout?.blockWidth ?? DEFAULT_TYPEFACE_BLOCK_WIDTH; const hudBlockHeight = selectedLayout?.blockHeight ?? DEFAULT_TYPEFACE_BLOCK_HEIGHT; const textBounds = selectedLayout?.naturalBounds; const textBoxWidth = selectedLayout?.textBoxWidth ?? 0; const textBoxHeight = selectedLayout?.textBoxHeight ?? 0; const longestVisibleLineWidth = selectedLayout?.longestLineWidth ?? 0; const wrappedLineCount = textBounds?.lines?.length ?? visibleLineCount; const measuredTextHeight = textBoxHeight || (wrappedLineCount * hudBlockHeight); currentHUDTextBoxWidth = textBounds?.box?.width || 0; currentHUDTextBoxHeight = textBounds?.box?.height || 0; currentHUDTextHeight = measuredTextHeight; const wrappedLastLineWidth = selectedLayout?.wrappedLastLineWidth ?? 0; const wrappedLastLineY = selectedLayout?.wrappedLastLineY ?? 0;
// 🔤 Font Loading Check: If MatrixChunky8 was selected but not loaded, skip rendering HUD // This prevents pop-in where font_1 renders briefly before MatrixChunky8 glyphs load let skipHudRender = false; if (selectedHudFont === "MatrixChunky8" && matrixTypeface) { // Check if actual glyph data exists (glyphs are loaded on-demand via Proxy) // Test a common character like '$' to verify glyphs are accessible // A real glyph will have properties like dwidth, advance, or resolution // The Proxy returns null for missing glyphs (typeof null === 'object') const dollarGlyph = matrixTypeface.glyphs && matrixTypeface.glyphs['$']; const hasGlyphs = dollarGlyph && dollarGlyph !== null && (dollarGlyph.dwidth || dollarGlyph.advance !== undefined || dollarGlyph.resolution);
if (!hasGlyphs) { // Trigger loading if not started if (!matrixTypeface.__loadPromise) { ensureTypefaceLoaded(matrixTypeface); }
// Wait for font to load if (matrixTypeface.__loadPromise) { matrixTypeface.__loadPromise.then(() => { matrixTypeface.__loaded = true; // Trigger repaint once loaded if (typeof window !== "undefined" && window.$activePaintApi?.needsPaint) { window.$activePaintApi.needsPaint(); } }).catch((err) => { console.warn("Failed to load MatrixChunky8:", err); matrixTypeface.__loaded = true; // Mark as loaded to prevent infinite waiting }); } // Skip rendering this frame - wait for font to load skipHudRender = true; } }
// Only proceed with HUD rendering if font is ready if (!skipHudRender) {
const measuredShareWidth = measureLineWidth("share ", selectedTypeface); const fallbackShareWidth = hudBlockWidth * "share ".length; const effectiveShareWidth = measuredShareWidth > 0 ? Math.max(measuredShareWidth, fallbackShareWidth) : fallbackShareWidth; const measuredEditWidth = measureLineWidth("edit ", selectedTypeface); const fallbackEditWidth = hudBlockWidth * "edit".length; const dynamicEditWidth = getHudEditScrubThreshold(); const effectiveEditWidth = Math.max( measuredEditWidth > 0 ? Math.max(measuredEditWidth, fallbackEditWidth) : fallbackEditWidth, dynamicEditWidth, ); currentHUDShareWidth = effectiveShareWidth; currentHUDEditWidth = effectiveEditWidth; const shareWidth = effectiveShareWidth; currentHUDLeftPad = shareWidth; const scrubExtension = Math.max(0, currentHUDScrub); const editExtension = currentHUDScrub < 0 ? effectiveEditWidth : 0;
currentHUDLabelFontName = selectedHudFont; currentHUDLabelBlockWidth = hudBlockWidth; currentHUDLabelBlockHeight = hudBlockHeight;
// Prefer visible line width for KidLisp pieces to avoid oversized buffers // When wrapping is enabled, use textBoxWidth (wrapped width) instead let w = isKidlispPiece ? textBoxWidth : textBoxWidth; if (!w || w <= 0) { w = textBoxWidth; }
// Add extra padding to buffer width to prevent font_1 character cutoff const bufferWidthPadding = 6; // Extra pixels for wider glyphs w += bufferWidthPadding;
// 📡 LAN badge superscript - add width for device letter if connected to dev session server // Hide in kidlisp.com embedded mode (NOAUTH_MODE) // 🔇 Disabled for now (@jeffrey) — hide the cyan A/B/C/D/E device-letter // window badge in both dev and production. Restore by uncommenting below. // const showLanBadge = devIdentity && devIdentity.host && !globalThis.NOAUTH_MODE; const showLanBadge = false; const lanBadgePadding = showLanBadge ? 10 : 0; // Space for letter + margin w += lanBadgePadding;
// ✨ Custom superscript (e.g. ".com") - add width for text const showCustomSuperscript = !!currentHUDSuperscript; const customSuperscriptPadding = showCustomSuperscript ? (currentHUDSuperscript.length * 5 + 3) : 0; w += customSuperscriptPadding;
// 📱 HUD QR code - generate cells and calculate width let hudQRSize = 0; let hudQRPadding = 0; // Debug: Log QR state on first few frames (disabled for cleaner output) // if (pieceFrameCount < 3) { // console.log("📱 HUD QR render check - frame:", pieceFrameCount, "currentHUDQR:", JSON.stringify(currentHUDQR), "currentHUDQRCells:", !!currentHUDQRCells); // } if (currentHUDQR) { // Generate QR cells if not cached if (!currentHUDQRCells) { try { currentHUDQRCells = qr(currentHUDQR, { errorCorrectLevel: ErrorCorrectLevel.L }).modules; console.log("📱 QR cells generated:", currentHUDQRCells?.length, "for URL:", currentHUDQR); } catch (e) { console.error("HUD QR generation failed:", e); currentHUDQRCells = null; } } if (currentHUDQRCells) { hudQRSize = currentHUDQRCells.length; // 1px per cell hudQRPadding = 4; // Gap between QR and text } } const hudQRTotalWidth = hudQRSize > 0 ? hudQRSize + hudQRPadding + 2 : 0; // +2 for border
// Final text dimensions: KidLisp width uses visible line metrics, height from text box let h = measuredTextHeight;
if (piece === "video") w = screen.width;
const baseLabelWidth = w + shareWidth;
// Use natural text dimensions for buffer - preserve original layout // Add QR code width to buffer if present let bufferW = baseLabelWidth + scrubExtension + editExtension + hudQRTotalWidth; let bufferH = h;
// Ensure buffer height fits QR code if present if (hudQRSize > 0 && bufferH < hudQRSize + 4) { bufferH = hudQRSize + 4; // QR size + padding }
// Ensure minimum buffer size for readability const minTextWidth = Math.max(isKidlispPiece ? textBoxWidth : textBoxWidth, 50); const minimumAllowedWidth = shareWidth + Math.max(minTextWidth, 0); const minBufferH = Math.max(hudBlockHeight, 20); // At least one line height
if (bufferW < minimumAllowedWidth + scrubExtension + editExtension) { bufferW = minimumAllowedWidth + scrubExtension + editExtension; } if (bufferH < minBufferH) { bufferH = minBufferH; }
// Fix height calculation for KidLisp pieces when wordWrap=false if (isKidlispPiece) { // When wordWrap=false, text.box doesn't calculate height correctly for multi-line text // Calculate proper height based on actual line count const properHeight = measuredTextHeight; if (properHeight > bufferH) { bufferH = properHeight; } }
const hudDescenderPadding = Math.max(2, Math.round(hudBlockHeight * 0.2)); bufferH += hudDescenderPadding;
// DEBUG: Log buffer dimensions in copy-pasteable format (reduced frequency) if (false && isKidlispPiece && currentHUDTxt && pieceFrameCount % 120 === 0) { console.log(`HUD_BUFFER: bufferW=${bufferW} bufferH=${bufferH} textBoxW=${textBounds.box.width} textBoxH=${textBounds.box.height} longestLinePx=${longestVisibleLineWidth} lines=${visibleLineCount} fontHeight=${hudBlockHeight} expectedH=${visibleLineCount * hudBlockHeight}`); console.log(`HUD_TEXT: "${cleanText.replace(/\n/g, '\\n').substring(0, 150)}"`); }
// Store actual dimensions for animation calculations currentHUDLabelMeasuredWidth = baseLabelWidth; hudAnimationState.labelWidth = bufferW; hudAnimationState.labelHeight = bufferH; h = bufferH;
// Ease the hover drift in and out (clamped dt so a stalled tab can't // leap), then keep painting until it has fully settled. { const now = performance.now(); const dt = Math.min(50, now - currentHUDHoverClock); currentHUDHoverClock = now; currentHUDHoverAmount = currentHUDHovered && !currentHUDButton?.down ? Math.min(1, currentHUDHoverAmount + dt / HUD_DRIFT_EASE_IN_MS) : Math.max(0, currentHUDHoverAmount - dt / HUD_DRIFT_EASE_OUT_MS); } if (currentHUDHovered || currentHUDHoverAmount > 0) $api.needsPaint();
label = $api.painting(bufferW, bufferH, ($) => { // Ensure label renders with clean pan state $.unpan(); // Ensure label renders without any active mask from the piece $.unmask();
// Clean rendering - no debug elements
let c; if (currentHUDTextColor) { c = num.shiftRGB(currentHUDTextColor, [255, 255, 255], 0.75); } else if (currentHUDStatusColor) { c = currentHUDStatusColor; } else { c = [255, 200, 240]; } if (piece !== "video") { let text = currentHUDTxt; if (currentHUDTxt.split(" ")[1]?.indexOf("http") !== 0) { text = currentHUDTxt?.replaceAll("~", " "); // Handle command separators: replace § with spaces for display // but keep actual newlines as newlines text = text?.replaceAll("§", " "); }
// 📱 Render QR code to the LEFT of the text if present let qrOffset = 0; if (currentHUDQR && currentHUDQRCells) { const qrCells = currentHUDQRCells; const qrSize = qrCells.length; // Position QR at the start of visible area (after share button padding) // Include scrub offset so QR moves with the label when swiping to "share" // Move left by 4px for tighter fit (up is handled by label Y offset) // Inset from the corner so frameless desktop panes (macOS // rounded corners) never clip the QR's quiet zone. const qrX = currentHUDLeftPad + currentHUDScrub + 2; const qrY = 4;
// White background (1px border around QR) $.ink(255, 255, 255).box(qrX, qrY, qrSize + 2, qrSize + 2);
// Draw QR code cells for (let y = 0; y < qrSize; y++) { for (let x = 0; x < qrSize; x++) { if (qrCells[y][x]) { $.ink(0, 0, 0).box(qrX + 1 + x, qrY + 1 + y, 1, 1); } } }
qrOffset = qrSize + 10; // QR width + border + breathing room }
const baseX = currentHUDLeftPad + qrOffset; // ✨ Rollover: tint toward the press green and let each character // drift on its own spring (hudDriftX/Y) — the "I'm alive" cue a // TextButton gives before a press, loosened up per letter. const hoverEase = hudDriftEase(currentHUDHoverAmount); // Keep text fixed while scrubbing left; only right-scrub shifts label content. const hudTextX = baseX + HUD_LABEL_TEXT_MARGIN + Math.max(0, currentHUDScrub); const hudTextY = 4; // Aligned with the QR (corner-radius safe) const typefaceNameForWrite = selectedHudFont; const hasColorCodes = textContainsColorCodes(text); const baseTextColor = hoverEase > 0 ? num.shiftRGB( graph.findColor(currentHUDTextColor || "white"), [0, 255, 0], 0.4 * hoverEase, ) : currentHUDTextColor || "white";
// 🎨 Character wrapping for KidLisp HUD prompts // Wrap based on screen width minus padding (6px total: 2px margin + 4px padding) // Subtract the left margin since text starts at x=HUD_LABEL_TEXT_MARGIN const wrapBounds = isKidlispPiece ? ($api.screen.width - 6 - HUD_LABEL_TEXT_MARGIN) : undefined;
// Drift places glyphs itself, so it takes any label that fits on // one line — including kidlisp-flagged ones (a piece named after // a kidlisp word, like `line`, gets wrapBounds too). A label that // really wraps keeps the tint and lets text.box break its lines. const fitsOneLine = !text?.includes("\n") && (selectedLayout?.wrappedLines?.length ?? 1) <= 1; if (hoverEase > 0 && (wrapBounds === undefined || fitsOneLine)) { drawHudLabelDrift($, text, { x: hudTextX, y: hudTextY, typefaceName: typefaceNameForWrite, typeface: selectedTypeface, textColor: baseTextColor, lineStep: hudBlockHeight + 1, t: currentHUDHoverClock, ease: hoverEase, }); } else { drawHudLabelText($, text, { x: hudTextX, y: hudTextY, typefaceName: typefaceNameForWrite, textColor: baseTextColor, shadowColor: "black", shadowOffsetX: 1, shadowOffsetY: 1, preserveColors: hasColorCodes, bounds: wrapBounds, // wordWrap defaults to true when bounds is set, enabling character wrapping }); }
// ✨ Custom Superscript (e.g. ".com") - draw after the text in cyan if (showCustomSuperscript) { const firstLine = text?.split('\n')[0] || text; const firstLineWidth = cachedAPI.text.width( stripColorCodes(firstLine), selectedTypeface );
const superscriptX = hudTextX + firstLineWidth + 2; // Baseline-align the compact suffix with the larger HUD label. const suffixBlockHeight = 8; // MatrixChunky8 const superscriptY = hudTextY + Math.max( 0, (currentHUDLabelBlockHeight || 10) - suffixBlockHeight, );
// Special handling for ".com" - color the "." based on connection status if (currentHUDSuperscript === ".com") { const dotWidth = cachedAPI.text.width(".", "MatrixChunky8");
// Drop shadow (1px down/right) matching the HUD label shadow $.ink("black"); $.write(".", { x: superscriptX, y: superscriptY }, undefined, undefined, false, "MatrixChunky8"); $.write("com", { x: superscriptX + dotWidth + 1, y: superscriptY + 1 }, undefined, undefined, false, "MatrixChunky8");
// Color dot based on socket connection: green if connected, red if not if (socket?.connected) { $.ink(0, 255, 100); // Green for connected } else { $.ink(255, 100, 100); // Red/salmon for disconnected } $.write(".", { x: superscriptX - 1, y: superscriptY - 1 }, undefined, undefined, false, "MatrixChunky8");
// Render "com" in bright cyan $.ink(0, 220, 255); // Bright cyan $.write("com", { x: superscriptX + dotWidth, y: superscriptY }, undefined, undefined, false, "MatrixChunky8"); } else { // Default rendering for other superscripts (with drop shadow) $.ink("black"); $.write(currentHUDSuperscript, { x: superscriptX + 1, y: superscriptY + 1 }, undefined, undefined, false, "MatrixChunky8"); $.ink(0, 220, 255); // Bright cyan $.write(currentHUDSuperscript, { x: superscriptX, y: superscriptY }, undefined, undefined, false, "MatrixChunky8"); } }
// 📡 LAN Badge Superscript - draw device letter after the text (+ custom superscript offset) if (showLanBadge) { const deviceLetter = devIdentity.letter || (devIdentity.name ? devIdentity.name.charAt(0).toUpperCase() : null) || String.fromCharCode(65 + (devIdentity.connectionIndex || 0)); // A, B, C...
// Get the first line of text for positioning the superscript const firstLine = text?.split('\n')[0] || text; const firstLineWidth = cachedAPI.text.width( stripColorCodes(firstLine), selectedTypeface );
// Position superscript to the right of first line (after custom superscript if present) const superscriptX = hudTextX + firstLineWidth + 2 + customSuperscriptPadding; const superscriptY = 0; // Top of the line (superscript position)
// Draw letter in cyan using MatrixChunky8 $.ink(0, 220, 255); // Bright cyan matching original badge $.write(deviceLetter, { x: superscriptX, y: superscriptY }, undefined, undefined, false, "MatrixChunky8"); }
// 🔗 Draw "share" text in the LEFT reveal area when scrubbing if (currentHUDScrub > 0) { // Slide "share" in from the left as the scrub progresses. const shareTextX = currentHUDLeftPad + currentHUDScrub - shareWidth + HUD_LABEL_TEXT_MARGIN; const shareTextY = 0;
drawHudLabelText($, "share", { x: shareTextX, y: shareTextY, typefaceName: undefined, // Use default font, not MatrixChunky8 textColor: [0, 255, 255], shadowColor: "black", preserveColors: false, }); }
// ⬅️ Draw backspace/edit reveal on right when scrubbing left if (currentHUDScrub < 0) { const scrubMagnitude = Math.abs(currentHUDScrub); const editThreshold = Math.max(currentHUDEditWidth || 0, getHudEditScrubThreshold()); const editProgress = Math.min(1, scrubMagnitude / editThreshold); const lastLineWidth = Math.max( 1, wrappedLastLineWidth || currentHUDTextBoxWidth || (stripColorCodes(text || "").length * hudBlockWidth), ); const caretWidth = Math.max(2, Math.round(hudBlockWidth * 0.9)); const caretHeight = Math.max(hudBlockHeight, 6); const caretX = Math.min( Math.max(hudTextX + lastLineWidth + 2, 0), Math.max(bufferW - caretWidth - 2, 0), ); const unclampedCaretY = Math.max(0, hudTextY + wrappedLastLineY); const caretY = Math.min(unclampedCaretY, Math.max(bufferH - caretHeight - 1, 0)); const filledCols = Math.max(1, Math.round(caretWidth * editProgress)); const editColor = editProgress >= 1 ? [255, 170, 210] : [255, 107, 157];
// Draw per-column with shadows and a fly-in offset from the right. for (let col = 0; col < filledCols; col++) { const t = filledCols > 1 ? col / (filledCols - 1) : 1; const flyIn = Math.round((1 - t) * (caretWidth - filledCols)); const cx = caretX + col + flyIn; $.ink(0, 0, 0, 180).box(cx + 1, caretY + 1, 1, caretHeight); $.ink(...editColor).box(cx, caretY, 1, caretHeight); } } } else { $.ink(0).line(1, 1, 1, h - 1); $.ink(c).line(0, 0, 0, h - 2); } });
if (piece === "video") currentHUDOffset = { x: 0, y: 6 }; if (!currentHUDOffset) currentHUDOffset = { x: defo, y: defo };
// Apply animation effects to HUD label position if (hudAnimationState.animating) { const currentTime = performance.now(); const elapsed = currentTime - hudAnimationState.startTime; const progress = Math.min(elapsed / hudAnimationState.duration, 1.0);
// Easing function for smooth macOS-style animation (ease-out) const easeOut = 1 - Math.pow(1 - progress, 3);
// Use actual calculated dimensions from when HUD was last drawn const hudWidth = hudAnimationState.labelWidth || 120; // fallback to default const hudHeight = hudAnimationState.labelHeight || 40; // fallback to default const qrSize = hudAnimationState.qrSize || 80;
// Calculate slide distances: ensure full content slides off-screen // Add extra padding (20px) to guarantee complete disappearance // For very large labels, cap the slide distance to keep animation smooth const maxSlideDistance = 400; // Maximum distance to slide for smooth animation const hudSlideX = Math.max(-(hudWidth + 20), -maxSlideDistance); // Cap slide distance const hudSlideY = Math.max(-(hudHeight + 20), -maxSlideDistance); // Cap slide distance const qrSlideX = qrSize + 20; // Slide right by full size + padding const qrSlideY = qrSize + 20; // Slide down by full size + padding
if (hudAnimationState.visible) { // Animating IN: fade from 0 to 1, slide from respective corners to position hudAnimationState.opacity = easeOut; // HUD slides in from top-left corner hudAnimationState.slideOffset = { x: (1 - easeOut) * hudSlideX, // Slide in from left using actual width y: (1 - easeOut) * hudSlideY // Slide in from top using actual height }; // QR slides in from bottom-right corner hudAnimationState.qrSlideOffset = { x: (1 - easeOut) * qrSlideX, // Slide in from right using actual size y: (1 - easeOut) * qrSlideY // Slide in from bottom using actual size }; } else { // Animating OUT: fade from 1 to 0, slide to respective corners hudAnimationState.opacity = 1 - easeOut; // HUD slides out to top-left corner hudAnimationState.slideOffset = { x: easeOut * hudSlideX, // Slide out to left using actual width y: easeOut * hudSlideY // Slide out to top using actual height }; // QR slides out to bottom-right corner hudAnimationState.qrSlideOffset = { x: easeOut * qrSlideX, // Slide out to right using actual size y: easeOut * qrSlideY // Slide out to bottom using actual size }; }
// End animation when progress reaches 1.0 if (progress >= 1.0) { hudAnimationState.animating = false;
// Ensure final values are exact if (hudAnimationState.visible) { hudAnimationState.opacity = 1.0; hudAnimationState.slideOffset = { x: 0, y: 0 }; hudAnimationState.qrSlideOffset = { x: 0, y: 0 }; } else { hudAnimationState.opacity = 0.0; hudAnimationState.slideOffset = { x: hudSlideX, y: hudSlideY }; hudAnimationState.qrSlideOffset = { x: qrSlideX, y: qrSlideY }; } } }
currentHUDButton = currentHUDButton || new $api.ui.Button({ x: 0, y: 0, w: currentHUDLabelMeasuredWidth, // FIXED: Don't add currentHUDOffset.x to button width h: h, // Use just the calculated height without extra y-offset });
// Update button position to match label position (with slide offset) const finalX = currentHUDOffset.x + hudAnimationState.slideOffset.x - currentHUDLeftPad; const finalY = currentHUDOffset.y + hudAnimationState.slideOffset.y; // Match hitbox to full rendered HUD label buffer (including scrub reveal areas). const hitboxWidth = Math.max( 1, Math.round(hudAnimationState.labelWidth || bufferW || currentHUDLabelMeasuredWidth || 1), ); const hitboxHeight = Math.max( 1, Math.round(hudAnimationState.labelHeight || bufferH || currentHUDTextHeight || currentHUDTextBoxHeight || h || 1), ); currentHUDButton.box.x = finalX; currentHUDButton.box.y = finalY; currentHUDButton.box.w = hitboxWidth; currentHUDButton.box.h = Math.max(1, Math.round(hitboxHeight));
// Mark HUD button to bypass the global HUD active check (it checks itself) currentHUDButton.noEdgeDetection = true; // Prevent HUD button from being activated by dragging from other buttons currentHUDButton.noRolloverActivation = true;
// Disable button when HUD is not visible (hidden with Tab key) currentHUDButton.disabled = !hudAnimationState.visible && !hudAnimationState.animating;
// $commonApi.hud.currentLabel = { // text: currentHUDTxt, // btn: currentHUDButton, // }; } } // End of skipHudRender check - close the if (!skipHudRender) block
// Return frame data back to the main thread. let sendData = { width: screen.width, height: screen.height };
// Tack on the merry progress bar at the TOP if a pipeline is running if ($api.system.merry && $api.system.merry.running) { const merry = $api.system.merry;
// Calculate progress based on elapsed time (use UTC-synced clock if available) const now = $commonApi.clock?.time?.()?.getTime?.() || Date.now(); if (merry.startTime && merry.currentPieceStart) { const totalElapsed = (now - merry.startTime) / 1000; // in seconds const pieceElapsed = (now - merry.currentPieceStart) / 1000; merry.progress = Math.min(1, totalElapsed / merry.totalDuration); merry.pieceProgress = Math.min(1, pieceElapsed / merry.pipeline[merry.currentIndex].duration); }
const mainScreenWidth = screen.width;
// Create merry progress bar painting with SEGMENTED colored blocks per piece const merryProgressBarPainting = $api.painting(mainScreenWidth, 1, ($) => { $.unmask(); // Ensure progress bar renders without piece mask const animFrame = Number($api.paintCount || 0n);
// Check for transition flash (use UTC-synced time) const flashNow = $commonApi.clock?.time?.()?.getTime?.() || Date.now(); let flashIntensity = 0; if (merry.transitionFlash && merry.transitionFlash.active) { const elapsed = flashNow - merry.transitionFlash.startTime; if (elapsed < merry.transitionFlash.duration) { // Ease out the flash (starts bright, fades quickly) const progress = elapsed / merry.transitionFlash.duration; flashIntensity = 1 - Math.pow(progress, 2); // Quadratic ease out } else { // Flash complete merry.transitionFlash.active = false; flashIntensity = 0; } }
// Fill with black backdrop (or white during flash) if (flashIntensity > 0) { const bgBrightness = Math.floor(255 * flashIntensity); $.ink(bgBrightness, bgBrightness, bgBrightness, 255).box(0, 0, mainScreenWidth, 1); } else { $.ink(0, 0, 0, 255).box(0, 0, mainScreenWidth, 1); }
// Define colors for each piece (cycle through palette) const pieceColors = [ { r: 100, g: 255, b: 100 }, // Bright green { r: 100, g: 200, b: 255 }, // Cyan { r: 255, g: 200, b: 100 }, // Orange { r: 255, g: 100, b: 200 }, // Pink { r: 200, g: 100, b: 255 }, // Purple { r: 255, g: 255, b: 100 }, // Yellow ];
// Calculate pixel width for each piece based on duration let xOffset = 0; merry.pipeline.forEach((piece, index) => { const pieceWidthRatio = piece.duration / merry.totalDuration; const pieceWidth = Math.floor(mainScreenWidth * pieceWidthRatio);
// Determine if this piece is completed, current, or upcoming const isCompleted = index < merry.currentIndex; const isCurrent = index === merry.currentIndex; const isUpcoming = index > merry.currentIndex;
// Get color for this piece const color = pieceColors[index % pieceColors.length];
// Draw this piece's segment for (let x = xOffset; x < xOffset + pieceWidth && x < mainScreenWidth; x++) { let baseR, baseG, baseB, alpha;
if (isCompleted) { // Completed pieces: Fade out progressively (darker as we move away from them) const completedAge = merry.currentIndex - index; // How many pieces ago const fadeFactor = Math.max(0.1, 1 - (completedAge * 0.2)); // Fade by 20% per step baseR = Math.floor(color.r * fadeFactor * 0.25); // Start at 25% brightness baseG = Math.floor(color.g * fadeFactor * 0.25); baseB = Math.floor(color.b * fadeFactor * 0.25); alpha = 255; } else if (isCurrent) { // Current piece: Show progress within this segment const pieceProgress = merry.pieceProgress || 0; const localX = x - xOffset; const progressPoint = Math.floor(pieceWidth * pieceProgress);
if (localX <= progressPoint) { // Filled part of current piece - bright with pulsing const pulse = Math.sin(animFrame * 0.3) * 0.2 + 0.8; baseR = Math.floor(color.r * pulse); baseG = Math.floor(color.g * pulse); baseB = Math.floor(color.b * pulse); alpha = 255;
// Leader pixel - extra bright white if (localX === progressPoint) { const leaderPulse = Math.sin(animFrame * 0.6) * 0.3 + 0.7; baseR = Math.floor(255 * leaderPulse); baseG = Math.floor(255 * leaderPulse); baseB = Math.floor(255 * leaderPulse); } } else { // Unfilled part of current piece - dim preview baseR = Math.floor(color.r * 0.3); baseG = Math.floor(color.g * 0.3); baseB = Math.floor(color.b * 0.3); alpha = 255; } } else if (isUpcoming) { // Upcoming pieces: Warm up as turn approaches const stepsUntil = index - merry.currentIndex; // How many pieces away const nextIsThis = stepsUntil === 1;
if (nextIsThis) { // Next piece warms up based on current piece progress const warmup = merry.pieceProgress || 0; // 0 to 1 const baseBrightness = 0.15; // Starting dim const targetBrightness = 0.5; // Warm up to this const brightness = baseBrightness + (targetBrightness - baseBrightness) * warmup;
// Add warm orange/yellow glow as it heats up const warmGlow = warmup * 0.3; baseR = Math.floor(color.r * brightness + 255 * warmGlow); baseG = Math.floor(color.g * brightness + 200 * warmGlow); baseB = Math.floor(color.b * brightness + 100 * warmGlow * 0.5); baseR = Math.min(255, baseR); baseG = Math.min(255, baseG); baseB = Math.min(255, baseB); alpha = 255; } else { // Further away pieces: Very dim, getting dimmer with distance const distanceFade = Math.max(0.05, 1 - (stepsUntil - 1) * 0.15); baseR = Math.floor(color.r * 0.15 * distanceFade); baseG = Math.floor(color.g * 0.15 * distanceFade); baseB = Math.floor(color.b * 0.15 * distanceFade); alpha = 255; } }
// Fade zone overlay: blend toward next piece color at end of each segment if (merry.fadeDuration > 0 && merry.pipeline.length > 1) { const fadeZonePixels = Math.max(2, Math.floor(pieceWidth * merry.fadeDuration / piece.duration)); const pixelOffset = x - xOffset; if (pixelOffset >= pieceWidth - fadeZonePixels) { const t = (pixelOffset - (pieceWidth - fadeZonePixels)) / Math.max(1, fadeZonePixels - 1); const nextIdx = (index + 1) % merry.pipeline.length; const nc = pieceColors[nextIdx % pieceColors.length]; const maxCh = Math.max(1, color.r, color.g, color.b); const bright = Math.max(baseR, baseG, baseB) / maxCh; const nR = Math.floor(nc.r * bright); const nG = Math.floor(nc.g * bright); const nB = Math.floor(nc.b * bright); baseR = Math.min(255, Math.floor(baseR * (1 - t) + nR * t)); baseG = Math.min(255, Math.floor(baseG * (1 - t) + nG * t)); baseB = Math.min(255, Math.floor(baseB * (1 - t) + nB * t)); } }
// Apply transition flash boost to current piece if (isCurrent && flashIntensity > 0) { const boost = 1 + flashIntensity * 0.8; // Up to 80% brighter baseR = Math.min(255, Math.floor(baseR * boost)); baseG = Math.min(255, Math.floor(baseG * boost)); baseB = Math.min(255, Math.floor(baseB * boost)); }
$.ink(baseR, baseG, baseB, alpha).box(x, 0, 1, 1); }
xOffset += pieceWidth; }); });
if (merryProgressBarPainting && merryProgressBarPainting.pixels && merryProgressBarPainting.pixels.length > 0) { sendData.merryProgressBar = { x: 0, y: 0, // Position at TOP of screen (1px tall) img: { width: merryProgressBarPainting.width, height: merryProgressBarPainting.height, pixels: merryProgressBarPainting.pixels } };
// 🎄⏰ Add UTC time overlay for sync debugging const utcDate = $commonApi.clock?.time?.() || new Date(); const utcTimeStr = utcDate.toISOString().split('T')[1].split('.')[0]; // HH:MM:SS const utcMs = utcDate.getMilliseconds().toString().padStart(3, '0'); const cyclePos = ((now % (merry.totalDuration * 1000)) / 1000).toFixed(2); sendData.merryUTC = { time: `${utcTimeStr}.${utcMs}`, cycle: cyclePos, piece: merry.pipeline[merry.currentIndex]?.piece || '?', index: merry.currentIndex + 1, total: merry.pipeline.length }; } }
// 🎬 Demoplay text card overlay if ($commonApi.system.demoplay?.card) { const card = $commonApi.system.demoplay.card; const elapsed = Date.now() - card.startTime; const totalDur = card.duration; const fadeIn = card.fadeIn || 500; const fadeOut = card.fadeOut || 500;
// Calculate opacity with fade in/out let opacity = 1; if (elapsed < fadeIn) { opacity = elapsed / fadeIn; } else if (elapsed > totalDur - fadeOut) { opacity = Math.max(0, (totalDur - elapsed) / fadeOut); } card.opacity = opacity;
const sw = screen.width; const sh = screen.height; const cardPainting = $api.painting(sw, sh, ($) => { $.unmask(); // Semi-transparent background $.ink(0, 0, 0, Math.floor(opacity * 180)); $.box(0, 0, sw, sh); // White text, centered $.ink(255, 255, 255, Math.floor(opacity * 255)); const lines = card.text.split("\n"); const lineHeight = 10; const startY = Math.floor((sh - lines.length * lineHeight) / 2); lines.forEach((line, i) => { $.write(line, { center: "x", y: startY + i * lineHeight }); }); });
if (cardPainting?.pixels?.length > 0) { sendData.demoplayCard = { x: 0, y: 0, img: { width: cardPainting.width, height: cardPainting.height, pixels: cardPainting.pixels, }, }; } } else if (sendData.demoplayCard) { delete sendData.demoplayCard; }
// Tack on the tape progress bar pixel buffer if necessary. if (!$api.rec.cleanMode && ($api.rec.tapeProgress || ($api.rec.recording && $api.rec.tapeTimerDuration))) { const progress = $api.rec.tapeProgress || 0;
// Determine if we should show progress bar at all const isFrameBased = $api.rec.tapeFrameMode && $api.rec.tapeFrameTarget > 0; const frameCount = $api.rec.tapeFrameTarget || 1;
// Hide progress bar for ALL frame-based recordings if (isFrameBased) { // Skip creating progress bar entirely for frame recordings } else { // IMPORTANT: Access the main screen buffer OUTSIDE the painting context // because inside painting(), $api.screen refers to the painting's own buffer, not the main screen const mainScreenPixels = screen.pixels; // This is the actual frame content const mainScreenWidth = screen.width; const mainScreenHeight = screen.height;
const isShortRecording = isFrameBased ? frameCount <= 120 : ($api.rec.tapeTimerDuration || 0) <= 1;
let currentProgressWidth; if (isFrameBased) { // For frame-based recording, create discrete segments const framesPassed = Math.floor(progress * frameCount); const segmentWidth = Math.max(1, Math.floor(mainScreenWidth / frameCount)); currentProgressWidth = framesPassed * segmentWidth; } else { // For time-based recording, use smooth progress currentProgressWidth = Math.floor(mainScreenWidth * progress); }
// Create tape progress bar painting with VHS-style red glow const tapeProgressBarPainting = $api.painting(mainScreenWidth, 2, ($) => { $.unmask(); // Ensure tape progress bar renders without piece mask // Animation frame for VHS effects - increased speed for more vibes const animFrame = Number($api.paintCount || 0n);
// Helper function to sample pixel color from above the progress bar const sampleColorFromAbove = (x) => { // Skip sampling during early frames for performance (first ~5 frames) const frameCount = Number($api.paintCount || 0n); if (frameCount < 5 || !mainScreenPixels || mainScreenPixels.length === 0) { return { r: 0, g: 0, b: 0 }; // Return black during startup }
// Sample from a few pixels above the progress bar position const sampleY = Math.max(0, screen.height - 10); // Sample 10px above progress bar const pixelIndex = (sampleY * screen.width + x) * 4;
if (pixelIndex >= 0 && pixelIndex < mainScreenPixels.length - 3) { return { r: mainScreenPixels[pixelIndex], g: mainScreenPixels[pixelIndex + 1], b: mainScreenPixels[pixelIndex + 2] }; } // Fallback to black if can't sample return { r: 0, g: 0, b: 0 }; };
// Special color override for first and last frames const isFirstFrame = progress <= 0.01; // First 1% of progress const isLastFrame = progress >= 0.99; // Last 1% of progress
// Calculate smooth alpha fade - progress bar fades from 25% to 75% for longer clean content viewing // Skip fade animation for short recordings let progressBarAlpha = 1.0; // Default to fully visible
if (!isShortRecording) { if (progress >= 0.20 && progress <= 0.30) { // Fade out from 20% to 30% (10% fade-out period) progressBarAlpha = 1.0 - ((progress - 0.20) / 0.10); } else if (progress > 0.30 && progress < 0.70) { // Fully hidden from 30% to 70% (40% hidden period) progressBarAlpha = 0.0; } else if (progress >= 0.70 && progress <= 0.80) { // Fade in from 70% to 80% (10% fade-in period) progressBarAlpha = (progress - 0.70) / 0.10; } }
// Fill entire bar with black backdrop using fade alpha $.ink(0, 0, 0, Math.floor(progressBarAlpha * 255)).box(0, 0, mainScreenWidth, 2);
if (isFrameBased) { // Draw segmented progress for frame-based recording const segmentWidth = Math.max(1, Math.floor(mainScreenWidth / frameCount)); const framesPassed = Math.floor(progress * frameCount); const currentProgressWidth = framesPassed * segmentWidth;
// Draw VHS-style progress bar pixel by pixel (same as time-based but with discrete frame spacing and no leader pixel) for (let x = 0; x < mainScreenWidth; x++) { let baseR, baseG, baseB;
if (x < currentProgressWidth) { // FILLED AREA - VHS red with analog glow and scan lines
if (isFirstFrame) { // FIRST FRAME - Fully green across entire bar baseR = 0; baseG = 255; baseB = 0; } else if (isLastFrame) { // LAST FRAME - Fully red across entire bar baseR = 255; baseG = 0; baseB = 0; } else { // NORMAL FRAMES - Enhanced color sampling from buffer above const sampledColor = sampleColorFromAbove(x);
// Sample additional colors for more variety const sampledColor2 = sampleColorFromAbove(Math.max(0, x - 3)); const sampledColor3 = sampleColorFromAbove(Math.min(mainScreenWidth - 1, x + 3));
// Mix multiple sampled colors for richer palette const mixedR = Math.floor((sampledColor.r + sampledColor2.r + sampledColor3.r) / 3); const mixedG = Math.floor((sampledColor.g + sampledColor2.g + sampledColor3.g) / 3); const mixedB = Math.floor((sampledColor.b + sampledColor2.b + sampledColor3.b) / 3); const mixedColor = { r: mixedR, g: mixedG, b: mixedB };
// Base VHS red intensity - reduced to let more color through let redIntensity = 200; // Reduced from 255 for more color mixing
// Use shared VHS effects calculation const effects = calculateVHSEffects(x, animFrame);
// Combine all VHS effects with brighter base redIntensity = Math.floor(redIntensity * effects.scanLine * effects.analogGlow * effects.tracking * effects.secondaryGlow);
// Create VHS red color const vhsR = Math.max(180, Math.min(255, redIntensity)); // Reduced min from 200 const vhsG = Math.floor(vhsR * 0.05); // Very minimal green const vhsB = Math.floor(vhsR * 0.02); // Very minimal blue
// Use shared blending function (55% sampled, 45% VHS red for more color influence) const blended = blendColorWithVHS(mixedColor, vhsR, vhsG, vhsB, 0.55); baseR = blended.r; baseG = blended.g; baseB = blended.b; }
// Use proper alpha blending for filled area (no special leader pixel treatment for frame-based) $.ink(baseR, baseG, baseB, Math.floor(progressBarAlpha * 255)).box(x, 0, 1, 2); } else { // UNFILLED AREA - Keep black background (already filled above) // No need to redraw black pixels, they're already set } } } else { // Draw VHS-style progress bar pixel by pixel (original time-based logic) for (let x = 0; x < mainScreenWidth; x++) { let baseR, baseG, baseB;
// Leading edge pixel - bright glowing leader const isLeaderPixel = x === currentProgressWidth - 1 && currentProgressWidth > 0;
if (x < currentProgressWidth) { // FILLED AREA - VHS red with analog glow and scan lines
if (isFirstFrame) { // FIRST FRAME - Fully green across entire bar baseR = 0; baseG = 255; baseB = 0; } else if (isLastFrame) { // LAST FRAME - Fully red across entire bar baseR = 255; baseG = 0; baseB = 0; } else { // NORMAL FRAMES - Enhanced color sampling from buffer above const sampledColor = sampleColorFromAbove(x);
// Sample additional colors for more variety const sampledColor2 = sampleColorFromAbove(Math.max(0, x - 3)); const sampledColor3 = sampleColorFromAbove(Math.min(mainScreenWidth - 1, x + 3));
// Mix multiple sampled colors for richer palette const mixedR = Math.floor((sampledColor.r + sampledColor2.r + sampledColor3.r) / 3); const mixedG = Math.floor((sampledColor.g + sampledColor2.g + sampledColor3.g) / 3); const mixedB = Math.floor((sampledColor.b + sampledColor2.b + sampledColor3.b) / 3); const mixedColor = { r: mixedR, g: mixedG, b: mixedB };
// Base VHS red intensity - reduced to let more color through let redIntensity = 200; // Reduced from 255 for more color mixing
// Use shared VHS effects calculation const effects = calculateVHSEffects(x, animFrame);
// Combine all VHS effects with brighter base redIntensity = Math.floor(redIntensity * effects.scanLine * effects.analogGlow * effects.tracking * effects.secondaryGlow);
// Create VHS red color const vhsR = Math.max(180, Math.min(255, redIntensity)); // Reduced min from 200 const vhsG = Math.floor(vhsR * 0.05); // Very minimal green const vhsB = Math.floor(vhsR * 0.02); // Very minimal blue
// Use shared blending function (55% sampled, 45% VHS red for more color influence) const blended = blendColorWithVHS(mixedColor, vhsR, vhsG, vhsB, 0.55); baseR = blended.r; baseG = blended.g; baseB = blended.b; }
// Special leader pixel treatment if (isLeaderPixel) { if (isFirstFrame) { // First frame - bright green leader baseR = 0; baseG = 255; baseB = 0; } else if (isLastFrame) { // Last frame - bright red leader baseR = 255; baseG = 0; baseB = 0; } else { // Check if we're in the fade period (20%-80%) to enable special blinking const isInFadePeriod = progress >= 0.20 && progress <= 0.80;
if (isInFadePeriod && !isShortRecording) { // During fade period - cycle through yellow, lime, and other colors for visibility const colorCycle = Math.floor(animFrame * 0.3) % 4; // Slower color cycling
switch (colorCycle) { case 0: baseR = 255; baseG = 255; baseB = 0; // Yellow break; case 1: baseR = 0; baseG = 255; baseB = 0; // Lime break; case 2: baseR = 255; baseG = 128; baseB = 0; // Orange break; case 3: baseR = 0; baseG = 255; baseB = 255; // Cyan break; } } else { // Normal behavior outside fade period - super bright white-hot leader with pulsing const leaderPulse = Math.sin(animFrame * 0.6) * 0.2 + 0.8;
baseR = 255; baseG = Math.floor(255 * leaderPulse); // Bright white-hot leader baseB = Math.floor(255 * leaderPulse); } } }
// Apply alpha fade to final colors, with beacon-like leader pixel let finalAlpha = progressBarAlpha; if (isLeaderPixel) { if (progress >= 0.20 && progress <= 0.80 && !isShortRecording) { // During fade period - beacon-like signal marker (strong but not full opacity) finalAlpha = 0.8; // Bright beacon for progress indication } else { // Outside fade period - still visible beacon finalAlpha = Math.min(progressBarAlpha, 0.9); // Strong beacon at 90% opacity } }
// Use proper alpha blending for filled area $.ink(baseR, baseG, baseB, Math.floor(finalAlpha * 255)).box(x, 0, 1, 2); } else { // UNFILLED AREA - Keep black background (already filled above) // No need to redraw black pixels, they're already set } } } });
// Ensure the painting was created successfully before adding to sendData if (tapeProgressBarPainting && tapeProgressBarPainting.pixels && tapeProgressBarPainting.pixels.length > 0) { // Structure the data to match what bios.mjs expects (same as label format) sendData.tapeProgressBar = { x: 0, y: screen.height - 2, // Position at bottom of screen (2px tall) img: { width: tapeProgressBarPainting.width, height: tapeProgressBarPainting.height, pixels: tapeProgressBarPainting.pixels } }; } else { console.warn("🎬 Tape progress bar painting FAILED to create:", { painting: !!tapeProgressBarPainting, pixels: !!tapeProgressBarPainting?.pixels, pixelsLength: tapeProgressBarPainting?.pixels?.length }); } } // Close the else block for frame count >= 60 }
maybeLeave();
// TODO: Write this up to the data in `painting`. sendData.TwoD = { code: twoDCommands }; if (overlay2D) sendData.overlay = overlay2D; // 🪟 vector UI display-list → uiCtx
// Attach a label buffer if necessary. // Hide label when QR is in fullscreen mode or in device mode (device.html has its own overlay) if (label && !hudAnimationState.qrFullscreen && !DEVICE_MODE) { const finalX = currentHUDOffset.x + hudAnimationState.slideOffset.x - currentHUDLeftPad; // Move label up by 4px when QR is present for tighter fit const qrYOffset = currentHUDQR ? -4 : 0; const finalY = currentHUDOffset.y + hudAnimationState.slideOffset.y + qrYOffset;
sendData.label = { x: finalX, y: finalY, opacity: hudAnimationState.opacity, img: (({ width, height, pixels }) => ({ width, height, pixels }))( label, ), };
// DEBUG: Add hitbox visualization overlay if (globalThis.debugHudHitbox && currentHUDButton) { const hitboxWidth = currentHUDButton.box.w; const hitboxHeight = currentHUDButton.box.h; const blinkFrame = Number($api.paintCount || 0n); const blinkOn = (blinkFrame % 30) < 15; const outerAlpha = blinkOn ? 200 : 80; const innerAlpha = blinkOn ? 120 : 40;
const hitboxOverlay = $api.painting(hitboxWidth, hitboxHeight, ($) => { $.unpan(); $.unmask(); // Ensure hitbox overlay renders without piece mask // Draw a blinking green border to show the hitbox $.ink(0, 255, 0, outerAlpha).box(0, 0, hitboxWidth, hitboxHeight, "outline"); if (hitboxWidth > 2 && hitboxHeight > 2) { $.ink(0, 255, 0, innerAlpha).box(1, 1, hitboxWidth - 2, hitboxHeight - 2, "outline"); } });
sendData.hitboxDebug = { x: currentHUDButton.box.x, y: currentHUDButton.box.y, opacity: hudAnimationState.opacity, img: (({ width, height, pixels }) => ({ width, height, pixels }))( hitboxOverlay, ), }; } }
// Generate QR code overlay for KidLisp pieces let qrOverlay;
// Skip QR/HUD overlay in device mode - device.kidlisp.com has its own DOM overlay // DEVICE_MODE is set from bios init message (can't use location.search in worker) const isDeviceModeQR = DEVICE_MODE;
// Clear QR cache if caching is disabled to prevent memory buildup if (isQROverlayCacheDisabled() && qrOverlayCache.size > 0) { qrOverlayCache.clear(); }
// Detect if this is a KidLisp piece const sourceCode = currentText || currentHUDTxt; // Use plain currentText first, then fall back to HUD text
// Use centralized KidLisp detection for QR code generation // But exclude proper .lisp files which should not show source HUD const isInlineKidlispPiece = (currentPath && lisp.isKidlispSource(currentPath) && !currentPath.endsWith('.lisp')) || currentPath === "(...)" || // Detect cached KidLisp pieces by $code format (sourceCode && sourceCode.startsWith("$")) || (currentPath && currentPath.includes("/disks/$")) || // Use the centralized KidLisp detection that includes comma syntax (sourceCode && lisp.isKidlispSource(sourceCode));
// Detect if this is a clock piece with a cached code const isClockPieceWithCode = (currentPath === "clock" || currentPath?.endsWith("/clock") || currentPath?.includes("/disks/clock")) && cachedClockCode;
// Debug: log clock detection (disabled - too verbose) // if (cachedClockCode) { // console.log("🎵 [QR Debug] Clock QR check:", { currentPath, cachedClockCode, isClockPieceWithCode, hideLabel, hudVisible: hudAnimationState.visible }); // }
// For clock pieces with cached code, always show the QR (don't require HUD visible) // For KidLisp pieces, require HUD to be visible/animating/fullscreen const shouldShowQRForKidLisp = (isInlineKidlispPiece && sourceCode) && (hudAnimationState.visible || hudAnimationState.animating || hudAnimationState.qrFullscreen); const shouldShowQRForClock = isClockPieceWithCode; // Always show for clock pieces
// Skip ALL pixelated HUD overlays in device mode (device.html has DOM-based overlays) if (!isDeviceModeQR && (shouldShowQRForKidLisp || shouldShowQRForClock) && !hideLabel) { // console.log("🎵 [QR Debug] Entering QR generation for clock"); try { // For clock pieces with cached code, use the cached clock code directly // For KidLisp pieces, determine cachedCode from sourceCode let cachedCode; let sigil = "$"; // Default sigil for KidLisp
if (isClockPieceWithCode) { // Clock piece: use the cached clock code with * sigil cachedCode = cachedClockCode; sigil = "*"; } else if (sourceCode.startsWith("$")) { // For $code format, the sourceCode IS the cached code (without $) cachedCode = sourceCode.substring(1).split(":")[0]; // Remove the $ prefix and any :fps colon params // console.log("🔑 [QR Debug] Using $code format, cachedCode:", cachedCode); } else { // For regular KidLisp source, check if it has been cached cachedCode = getCachedCode(sourceCode); // console.log("🔍 [QR Debug] Checked cache, cachedCode:", cachedCode);
// If not cached yet but QR fullscreen is active (shift was pressed), // trigger caching and request a repaint when complete if (!cachedCode && hudAnimationState.qrFullscreen) { // console.log("⏳ [QR Debug] Code not cached, triggering caching..."); const kidlispInstance = getGlobalKidLisp(); if (kidlispInstance) { // console.log("✅ [QR Debug] Got KidLisp instance, calling cacheKidlispSource"); // Trigger caching asynchronously kidlispInstance.cacheKidlispSource(sourceCode, $commonApi).then(() => { // Check if code was successfully cached const newCachedCode = getCachedCode(sourceCode); if (newCachedCode) { //console.log(`🔄 Code cached after shift press: $${newCachedCode}`); // Trigger repaint to show the QR code if (typeof window !== "undefined" && window.$activePaintApi?.needsPaint) { window.$activePaintApi.needsPaint(); } } }).catch(err => { console.warn("Failed to cache KidLisp code:", err); }); } } }
if (cachedCode) { // Send the cached code to main thread for tape naming (only for KidLisp) if (sigil === "$") { send({ type: "kidlisp:cached-code", content: cachedCode }); }
// Use cache key based on sigil + cached code to avoid regenerating the same QR const cacheKey = `qr_${sigil}${cachedCode}`;
// Get the font and ensure it's properly loaded before proceeding const font = typefaceCache.get("MatrixChunky8");
// Check if ALL glyphs in the label text are loaded // The label will be sigil + cachedCode (e.g., "$wipe", "*bako", etc.) // We need to verify every character's glyph is loaded to avoid pop-in const labelText = `${sigil}${cachedCode}`; let allGlyphsLoaded = false;
if (font && font.glyphs) { allGlyphsLoaded = true; const missingGlyphs = [];
for (const char of labelText) { const glyph = font.glyphs[char]; // console.log('[MatrixChunky8] Glyph check:', { char, charCode: char.charCodeAt(0), glyph: glyph ? 'exists' : 'missing', glyphType: typeof glyph }); // A real glyph has properties like dwidth, advance, or resolution // The Proxy returns null for missing glyphs const isRealGlyph = glyph && glyph !== null && !glyph.isPlaceholder && (glyph.dwidth || glyph.advance !== undefined || glyph.resolution);
if (!isRealGlyph) { allGlyphsLoaded = false; missingGlyphs.push(char); // console.log('[MatrixChunky8] Missing glyph:', char, 'charCode:', char.charCodeAt(0)); } }
// if (!allGlyphsLoaded) { // console.log('[QR] MatrixChunky8 glyphs pending:', { // labelText, // missingGlyphs: missingGlyphs.join('') || 'unknown' // }); // } }
const shouldShowQR = allGlyphsLoaded;
// Track when glyphs first become available to delay caching by one frame // This ensures the painting callback has completed rendering before we cache if (font && shouldShowQR && font.__qrGlyphsReadyFrame === undefined) { // First frame where glyphs are loaded - mark the frame number font.__qrGlyphsReadyFrame = (typeof window !== "undefined" && window.frameCount) || 0; }
// Safe to cache if glyphs were ready at least 1 frame ago (or we don't have frameCount) const currentFrame = (typeof window !== "undefined" && window.frameCount) || 0; const safeToCache = shouldShowQR && ( font?.__qrGlyphsReadyFrame === undefined || currentFrame > font.__qrGlyphsReadyFrame );
// If font exists but glyphs not loaded yet, trigger loading and request repaint if (font && !shouldShowQR) { font.__qrGlyphsReadyFrame = undefined; // Reset the ready frame tracking if (!font.__loadPromise) { ensureTypefaceLoaded(font); } if (font.__loadPromise) { font.__loadPromise.then(() => { font.__loaded = true; // Clear QR cache for this code so it regenerates with proper font qrOverlayCache.delete(cacheKey); // Trigger repaint to show QR once font is ready if (typeof window !== "undefined" && window.$activePaintApi?.needsPaint) { window.$activePaintApi.needsPaint(); } }).catch((err) => { console.warn("Failed to load MatrixChunky8 for QR:", err); font.__loaded = true; // Mark as loaded to prevent infinite waiting }); } }
// Check if this QR overlay is already cached (unless caching is disabled) const isQRCacheDisabled = isQROverlayCacheDisabled(); const hasQRCache = qrOverlayCache.has(cacheKey);
// Declare variables for QR positioning and sizing (used in both cached and fresh QR paths) let overlayWidth, overlayHeight, startX, startY;
// Render QR from cache if available, or generate fresh // Note: We render even if glyphs aren't loaded yet - they'll pop in on repaint if (!isQRCacheDisabled && hasQRCache) {
const cachedQrData = qrOverlayCache.get(cacheKey);
// Store QR dimensions in animation state for proper bounding box animations hudAnimationState.qrSize = Math.max(cachedQrData.width, cachedQrData.height);
if (hudAnimationState.qrFullscreen) { // Fullscreen mode: integer scaling only for pixel-perfect QR const originalWidth = cachedQrData.width; const originalHeight = cachedQrData.height;
// Calculate maximum integer scale that fits screen with padding const padding = Math.min(screen.width, screen.height) * 0.1; // 10% padding const maxWidth = screen.width - padding * 2; const maxHeight = screen.height - padding * 2;
const maxScaleX = Math.floor(maxWidth / originalWidth); const maxScaleY = Math.floor(maxHeight / originalHeight); const scale = Math.min(maxScaleX, maxScaleY, 8); // Cap at 8x scale
// Use integer scaling for pixel-perfect results overlayWidth = originalWidth * scale; overlayHeight = originalHeight * scale;
// Calculate text dimensions for canvas sizing const codeText = `${sigil}${cachedCode}`; const fontSize = 16; // Fixed 2x scale (8px base font * 2 = 16px) const textPadding = 8;
// Calculate actual text width using font advances for proper sizing const font = typefaceCache.get("MatrixChunky8"); const advances = font?.data?.advances || {}; let actualTextWidth = 0; for (const char of codeText) { const charWidth = advances[char] || 4; // Default 4px per char if no advance data actualTextWidth += charWidth; } // Scale up by 2x for our size: 2 scaling actualTextWidth *= 2;
const textWidth = actualTextWidth + textPadding * 2; // Add padding const textHeight = fontSize + textPadding * 2;
// Use full screen canvas to position QR code (centered) and text const canvasHeight = screen.height; const canvasWidth = screen.width;
// Center QR on screen (ensure integer coordinates) const qrX = Math.floor((screen.width - overlayWidth) / 2); const qrY = Math.floor((screen.height - overlayHeight) / 2);
// Position starts at top-left of screen since we're using full screen canvas startX = 0; startY = 0;
// Create scaled QR overlay with styled code text qrOverlay = $api.painting(canvasWidth, canvasHeight, async ($) => { $.unmask(); // Ensure QR overlay renders without piece mask // Draw scaled QR with integer scaling (centered in canvas)
for (let y = 0; y < originalHeight; y++) { for (let x = 0; x < originalWidth; x++) { const srcIndex = (y * originalWidth + x) * 4;
if (srcIndex < cachedQrData.basePixels.length) { const r = cachedQrData.basePixels[srcIndex]; const g = cachedQrData.basePixels[srcIndex + 1]; const b = cachedQrData.basePixels[srcIndex + 2];
$.ink(r, g, b); // Draw scale x scale pixel block $.box(qrX + x * scale, qrY + y * scale, scale, scale); } } }
// Add styled code text positioned at top-left corner of screen const textX = 10; // Small margin from left edge of screen const textY = 10; // Small margin from top edge of screen
// Draw black shadow $.ink("black"); $.write(codeText, { x: textX + 1, y: textY + 1, size: 2 });
// Draw white text on black background - try MatrixChunky8 first, fallback to default $.ink("white"); const matrixFont = typefaceCache.get("MatrixChunky8"); if (matrixFont && matrixFont.glyphs) { try { $.write(codeText, { x: textX, y: textY, size: 2 }, undefined, undefined, false, "MatrixChunky8"); } catch (error) { // Fallback to default font if MatrixChunky8 fails $.write(codeText, { x: textX, y: textY, size: 2 }); } } else { // Use default font if MatrixChunky8 not available $.write(codeText, { x: textX, y: textY, size: 2 }); }
// Add "TAP TO CLOSE" instruction at bottom center of screen const closeText = "TAP TO CLOSE"; const closeTextY = screen.height - 20; // 20px from bottom const closeTextX = Math.floor(screen.width / 2) - 30; // Rough center
// Draw black shadow for close text $.ink("black"); $.write(closeText, { x: closeTextX + 1, y: closeTextY + 1, size: 1 });
// Draw white close text $.ink("white"); $.write(closeText, { x: closeTextX, y: closeTextY, size: 1 }); }); } else { // Normal mode: use original positioning const margin = 4; overlayWidth = cachedQrData.width; overlayHeight = cachedQrData.height; startX = screen.width - overlayWidth - margin; startY = screen.height - overlayHeight - margin;
// Create fresh overlay for transfer from cached data qrOverlay = { width: cachedQrData.width, height: cachedQrData.height, pixels: new Uint8ClampedArray(cachedQrData.basePixels) // Fresh copy for transfer }; }
// Add QR overlay to sendData with animation effects sendData.qrOverlay = { x: startX + (hudAnimationState.qrFullscreen ? 0 : hudAnimationState.qrSlideOffset.x), y: startY + (hudAnimationState.qrFullscreen ? 0 : hudAnimationState.qrSlideOffset.y), opacity: hudAnimationState.opacity, img: qrOverlay };
// Add clickable hitbox for QR code if (hudAnimationState.qrFullscreen) { // Fullscreen QR: make entire QR tappable to go back send({ type: "button:hitbox:add", content: { label: "qr-fullscreen", box: { x: startX, y: startY, w: overlayWidth, h: overlayHeight }, message: "qr-fullscreen-tap" } }); } else { // Corner QR: make it tappable to go fullscreen if (!SOLO_MODE) { send({ type: "button:hitbox:add", content: { label: "qr-corner", box: { x: startX + hudAnimationState.qrSlideOffset.x, y: startY + hudAnimationState.qrSlideOffset.y, w: overlayWidth, h: overlayHeight }, message: "qr-corner-tap" } }); } }
} else {
// Always use prompt.ac for QR codes (even in dev/local) let url = "https://prompt.ac";
// Use the cached nanoid code with appropriate sigil prefix for a short URL // $ for KidLisp, * for clock melodies url += `/${sigil}${cachedCode}`;
// Generate QR code with medium error correction for better scannability const cells = qr(url, { errorCorrectLevel: ErrorCorrectLevel.M }).modules;
// Calculate size and position for bottom-right corner with 4px margin const margin = 4; const cellSize = 1; // 1 pixel per cell for smallest 1:1 size const qrSize = cells.length * cellSize;
// Store QR dimensions in animation state for proper bounding box animations hudAnimationState.qrSize = qrSize;
// Position in bottom-right corner let startX = screen.width - qrSize - margin - 1; // Account for shadow width const textHeight = 12; // Space for MatrixChunky8 8px font with shadow (8px + 4px padding) const totalHeight = qrSize + textHeight; let startY = screen.height - totalHeight - margin; // Move 1px closer to bottom for balanced margins
// Create QR overlay using painting API with extra space for shadow const textAreaHeight = 9; const qrOffsetY = textAreaHeight; // QR starts right after text area (no gap) const canvasHeight = qrOffsetY + qrSize + 2; // Ensure room for bottom shadow const generatedQR = $api.painting(qrSize + 1, canvasHeight, async ($) => { $.unmask(); // Ensure QR renders without piece mask // Draw QR code at offset position to make room for text above for (let y = 0; y < cells.length; y++) { for (let x = 0; x < cells.length; x++) { const isBlack = cells[y][x]; if (isBlack) { $.ink("black"); } else { $.ink("white"); } $.box(x * cellSize, (y * cellSize) + qrOffsetY, cellSize); // Add qrOffsetY to move QR down } }
// QR text style configuration const useBackdrop = true; // Set to true for backdrop style, false for shadow style
// Prepare text for rendering (using sigil variable: $ for KidLisp, * for clock) const codeToRender = `${sigil}${cachedCode}`;
// Calculate actual rendered width for mathematical centering // Get character advances from the font definition const advances = font?.data?.advances || typefaceCache.get("MatrixChunky8")?.data?.advances || {};
// Calculate text dimensions and positioning let actualTextWidth = 0; for (const char of codeToRender) { const charWidth = advances[char] || 4; actualTextWidth += charWidth; }
// Text area configuration - be more generous with sizing const textPaddingLeft = 1; // Minimal left padding const textPaddingRight = 1; // Minimal right padding for safety const textAreaWidth = actualTextWidth + textPaddingLeft + textPaddingRight; const textAreaHeight = 10; // Increased height (was 9)
// Position text area above QR code (which is now at qrOffsetY) const textAreaX = qrSize - textAreaWidth; // Still flush right const textAreaY = 0; // At the top of the canvas
// Text position within the text area (right-aligned with left padding only) const textX = textAreaX + textPaddingLeft; // Only left padding const textY = textAreaY + 1; // Vertical centering within smaller text area (was 2)
// Render text with appropriate style if (useBackdrop) { // Draw black background for text area (sized to fit text) $.ink("black"); // Black background $.box(textAreaX, textAreaY, textAreaWidth, textAreaHeight);
// Render white text (no rotation for now) $.ink("white"); // White text on black background
if (getPackMode()) { // In OBJKT mode, try MatrixChunky8 first, fall back to default if not available let matrixFont = typefaceCache.get("MatrixChunky8"); if (matrixFont) {
// Check if this is a proper Typeface instance with all methods if (!matrixFont.getGlyph || typeof matrixFont.getGlyph !== 'function') { // Create a new proper Typeface instance const newMatrixFont = new Typeface("MatrixChunky8"); await newMatrixFont.load($commonApi.net.preload); // Copy over existing glyphs data if any if (matrixFont.glyphs) { Object.assign(newMatrixFont.glyphs, matrixFont.glyphs); } matrixFont = newMatrixFont; typefaceCache.set("MatrixChunky8", matrixFont); }
// Check if glyphs actually work by testing a specific character const testGlyph = matrixFont.glyphs['$']; // MatrixChunky8 uses BDF structure: {resolution, offset, baselineOffset, advance, commands, bbx} const glyphsWorking = testGlyph && typeof testGlyph === 'object' && (testGlyph.pixels || testGlyph.commands || testGlyph.resolution);
if (!glyphsWorking) { // Create simple fallback glyphs for common characters const fallbackGlyph = { resolution: [6, 8], pixels: [ [0, 1, 1, 1, 1, 0], [1, 0, 0, 0, 0, 1], [1, 0, 1, 1, 0, 1], [1, 0, 1, 1, 0, 1], [1, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 1], [0, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0] ] };
// DO NOT replace glyphs object - it's a Proxy that loads on-demand // Just populate common characters if they're missing if (matrixFont.glyphs) { const commonChars = '$rozeabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; for (const char of commonChars) { if (!matrixFont.glyphs[char]) { matrixFont.glyphs[char] = fallbackGlyph; matrixFont[char] = fallbackGlyph; // Also set directly on font object } } } }
// Check if font has reasonable glyph data before rendering const testChar = matrixFont.glyphs && (matrixFont.glyphs['$'] || matrixFont.glyphs['A'] || matrixFont.glyphs['a']); const hasValidGlyphs = testChar && (testChar.pixels || testChar.commands || testChar.resolution); if (hasValidGlyphs) { $.write(codeToRender, { x: textX, y: textY }, undefined, undefined, false, "MatrixChunky8"); } else { // Fallback to default font if MatrixChunky8 isn't ready $.write(codeToRender, { x: textX, y: textY }); } } else { // Fallback to default font if MatrixChunky8 not available $.write(codeToRender, { x: textX, y: textY }); } } else { // Use MatrixChunky8 font in normal mode if available const matrixFont = typefaceCache.get("MatrixChunky8");
// Check if MatrixChunky8 glyphs are actually loaded and ready let useMatrixFont = false; if (matrixFont && matrixFont.glyphs) { // Check if ALL glyphs in codeToRender are loaded useMatrixFont = true; for (const char of codeToRender) { const glyph = matrixFont.glyphs[char]; const isLoaded = glyph && !glyph.isPlaceholder && (glyph.pixels || glyph.commands || glyph.resolution); if (!isLoaded) { useMatrixFont = false; break; } } }
// Only render text if MatrixChunky8 is ready (will trigger repaint when it loads) if (useMatrixFont) { $.write(codeToRender, { x: textX, y: textY }, undefined, undefined, false, "MatrixChunky8"); } // Otherwise skip rendering text - it will pop in on next repaint } } else { // Shadow style: black shadow first, then white text // Draw black shadow (1px offset) - no rotation for now $.ink("black");
if (getPackMode()) { // In OBJKT mode, try MatrixChunky8 first, fall back to default if not available const matrixFont = typefaceCache.get("MatrixChunky8"); if (matrixFont && matrixFont.glyphs) { const testChar = matrixFont.glyphs['$'] || matrixFont.glyphs['A'] || matrixFont.glyphs['a']; const hasValidGlyphs = testChar && (testChar.pixels || testChar.commands || testChar.resolution); if (hasValidGlyphs) { $.write(codeToRender, { x: textX + 1, y: textY + 1 }, undefined, undefined, false, "MatrixChunky8"); } else { $.write(codeToRender, { x: textX + 1, y: textY + 1 }); } } else { $.write(codeToRender, { x: textX + 1, y: textY + 1 }); } } else { // Check if MatrixChunky8 glyphs are loaded for shadow const matrixFont = typefaceCache.get("MatrixChunky8"); let useMatrixFont = false; if (matrixFont && matrixFont.glyphs) { const testGlyph = matrixFont.glyphs['$'] || matrixFont.glyphs['a']; useMatrixFont = !!(testGlyph && (testGlyph.pixels || testGlyph.commands || testGlyph.resolution)); }
// Only render if MatrixChunky8 is ready if (useMatrixFont) { $.write(codeToRender, { x: textX + 1, y: textY + 1 }, undefined, undefined, false, "MatrixChunky8"); } }
// Draw white text on top - no rotation for now $.ink("white"); if (getPackMode()) { // In OBJKT mode, try MatrixChunky8 first, fall back to default if not available const matrixFont = typefaceCache.get("MatrixChunky8"); if (matrixFont && matrixFont.glyphs) { const testChar = matrixFont.glyphs['$'] || matrixFont.glyphs['A'] || matrixFont.glyphs['a']; const hasValidGlyphs = testChar && (testChar.pixels || testChar.commands || testChar.resolution); if (hasValidGlyphs) { $.write(codeToRender, { x: textX, y: textY }, undefined, undefined, false, "MatrixChunky8"); } else { $.write(codeToRender, { x: textX, y: textY }); } } else { $.write(codeToRender, { x: textX, y: textY }); } } else { // Check if MatrixChunky8 glyphs are loaded for white text const matrixFont = typefaceCache.get("MatrixChunky8"); let useMatrixFont = false; if (matrixFont && matrixFont.glyphs) { const testGlyph = matrixFont.glyphs['$'] || matrixFont.glyphs['a']; useMatrixFont = !!(testGlyph && (testGlyph.pixels || testGlyph.commands || testGlyph.resolution)); }
// Only render if MatrixChunky8 is ready if (useMatrixFont) { $.write(codeToRender, { x: textX, y: textY }, undefined, undefined, false, "MatrixChunky8"); } } }
// Draw gray shadow along the right side and bottom of the entire overlay $.ink("gray", 128); // Right shadow - offset 1px down from top like a drop shadow $.box(qrSize, 1, 1, qrOffsetY + qrSize); // Bottom shadow - offset 1px from left edge like a drop shadow $.box(1, qrOffsetY + qrSize, qrSize, 1);
// Add a subtle white highlight to indicate the QR is clickable (corner only) // This creates a "button-like" appearance $.ink("white", 64); // Semi-transparent white // Top highlight line $.line(0, qrOffsetY, qrSize - 1, qrOffsetY); // Left highlight line $.line(0, qrOffsetY, 0, qrOffsetY + qrSize - 1); });
// Don't cache QR overlay if font isn't fully loaded yet (text label needs to re-render) // Cache the base QR data (not the transferable pixels) const qrData = { width: generatedQR.width, height: generatedQR.height, basePixels: new Uint8ClampedArray(generatedQR.pixels) // Keep a safe copy for caching };
// Store QR dimensions in animation state for proper bounding box animations hudAnimationState.qrSize = Math.max(qrData.width, qrData.height);
if (hudAnimationState.qrFullscreen) { // Fullscreen mode: integer scaling only for pixel-perfect QR const originalQrSize = cells.length;
// Calculate maximum integer scale that fits screen with padding const padding = Math.min(screen.width, screen.height) * 0.1; // 10% padding const maxSize = Math.min(screen.width, screen.height) - padding * 2;
const maxCellSize = Math.floor(maxSize / originalQrSize); const cellSize = Math.max(2, Math.min(maxCellSize, 12)); // Integer cell size, 2-12px const finalQrSize = originalQrSize * cellSize;
overlayWidth = finalQrSize; overlayHeight = finalQrSize;
// Calculate text dimensions for canvas sizing const codeText = `${sigil}${cachedCode}`; const fontSize = 16; // Fixed 2x scale (8px base font * 2 = 16px) const textPadding = 8;
// Calculate actual text width using font advances for proper sizing const font = typefaceCache.get("MatrixChunky8"); const advances = font?.data?.advances || {}; let actualTextWidth = 0; for (const char of codeText) { const charWidth = advances[char] || 4; // Default 4px per char if no advance data actualTextWidth += charWidth; } // Scale up by 2x for our size: 2 scaling actualTextWidth *= 2;
const textWidth = actualTextWidth + textPadding * 2; // Add padding const textHeight = fontSize + textPadding * 2;
// Use full screen canvas to position QR code (centered) and text (top-left) const canvasHeight = screen.height; const canvasWidth = screen.width;
// Position QR code in center of screen const qrX = Math.floor((screen.width - finalQrSize) / 2); const qrY = Math.floor((screen.height - finalQrSize) / 2);
// Position starts at top-left of screen since we're using full screen canvas startX = 0; startY = 0;
// Generate fullscreen canvas with centered QR and top-left text const fullscreenQR = $api.painting(canvasWidth, canvasHeight, ($) => { $.unmask(); // Ensure fullscreen QR renders without piece mask // Draw QR code centered on screen for (let y = 0; y < cells.length; y++) { for (let x = 0; x < cells.length; x++) { const isBlack = cells[y][x]; if (isBlack) { $.ink("black"); } else { $.ink("white"); } $.box(qrX + x * cellSize, qrY + y * cellSize, cellSize, cellSize); } }
// Draw text in absolute top-left corner of screen const codeText = `${sigil}${cachedCode}`; const textX = 10; // Small margin from left edge of screen const textY = 10; // Small margin from top edge of screen
// Draw black shadow offset by 1px $.ink("black"); $.write(codeText, { x: textX + 1, y: textY + 1, size: 2 }); // 2x scale for shadow
// Draw white text on top $.ink("white"); $.write(codeText, { x: textX, y: textY, size: 2 }); // 2x scale for main text
// Add "TAP TO CLOSE" instruction at bottom center of screen const closeText = "TAP TO CLOSE"; const closeTextY = screen.height - 20; // 20px from bottom const closeTextX = Math.floor(screen.width / 2) - 30; // Rough center (adjust as needed)
// Draw black shadow for close text $.ink("black"); $.write(closeText, { x: closeTextX + 1, y: closeTextY + 1, size: 1 });
// Draw white close text $.ink("white"); $.write(closeText, { x: closeTextX, y: closeTextY, size: 1 }); });
// Update qrData for fullscreen qrData.width = fullscreenQR.width; qrData.height = fullscreenQR.height; qrData.basePixels = new Uint8ClampedArray(fullscreenQR.pixels);
qrOverlay = { width: qrData.width, height: qrData.height, pixels: new Uint8ClampedArray(qrData.basePixels) }; } else { // Normal mode: use original positioning and size overlayWidth = qrData.width; overlayHeight = qrData.height; startX = screen.width - overlayWidth - margin; startY = screen.height - overlayHeight - margin; // Removed +1 to prevent label shadow overlap
// Create fresh overlay for transfer each time qrOverlay = { width: qrData.width, height: qrData.height, pixels: new Uint8ClampedArray(qrData.basePixels) // Fresh copy for transfer }; }
// Cache the QR data for this piece once glyphs are confirmed rendered // We use safeToCache to ensure the painting callback has completed if (!isQRCacheDisabled && safeToCache && !hudAnimationState.qrFullscreen) { qrOverlayCache.set(cacheKey, qrData); console.log('[QR] ✅ Cached QR with MatrixChunky8 glyphs for:', cacheKey); }
// Add QR overlay to sendData with exact position and animation effects sendData.qrOverlay = { x: startX + (hudAnimationState.qrFullscreen ? 0 : hudAnimationState.qrSlideOffset.x), y: startY + (hudAnimationState.qrFullscreen ? 0 : hudAnimationState.qrSlideOffset.y), opacity: hudAnimationState.opacity, img: qrOverlay };
// Add clickable hitbox for QR code if (hudAnimationState.qrFullscreen) { // Fullscreen QR: make entire QR tappable to go back send({ type: "button:hitbox:add", content: { label: "qr-fullscreen", box: { x: startX, y: startY, w: overlayWidth || qrData.width, h: overlayHeight || qrData.height }, message: "qr-fullscreen-tap" } }); } else { // Corner QR: make it tappable to go fullscreen if (!SOLO_MODE) { send({ type: "button:hitbox:add", content: { label: "qr-corner", box: { x: startX + hudAnimationState.qrSlideOffset.x, y: startY + hudAnimationState.qrSlideOffset.y, w: overlayWidth || qrData.width, h: overlayHeight || qrData.height }, message: "qr-corner-tap" } }); } }
} } else { // No cached code yet - QR will appear once caching is complete } } catch (err) { console.warn("Failed to generate QR overlay:", err); } }
// 👤 Author attribution overlay for KidLisp pieces (bottom-left corner) // Only show when: HUD is visible, we have an author, and it's a KidLisp piece // Skip in device mode (device.kidlisp.com has its own DOM overlay) // Note: Reuse isDeviceModeQR from earlier in the function // In device mode, completely skip author overlay creation if (isDeviceModeQR) { // Don't create author overlay - device.html has its own DOM-based display } else if (currentHUDAuthor && hudAnimationState.visible && !hudAnimationState.qrFullscreen) { try { const authorText = currentHUDAuthor; // Already formatted as @handle const hitsText = currentHUDHits ? `${currentHUDHits.toLocaleString()} hits` : null;
// Use MatrixChunky8 for compact display const font = typefaceCache.get("MatrixChunky8"); const charWidth = 4; // MatrixChunky8 char width const charHeight = 8; // MatrixChunky8 char height const padding = 2; const lineGap = 2;
// Calculate dimensions const authorWidth = authorText.length * charWidth; const hitsWidth = hitsText ? hitsText.length * charWidth : 0; const maxWidth = Math.max(authorWidth, hitsWidth); const height = hitsText ? (charHeight * 2 + lineGap + padding * 2) : (charHeight + padding * 2); const width = maxWidth + padding * 2;
// Create attribution overlay painting const authorOverlay = $api.painting(width + 1, height + 1, ($) => { $.unmask();
// Semi-transparent black background for readability $.ink(0, 0, 0, 180).box(0, 0, width, height);
// Draw author handle in pink (matching give.aesthetic.computer style) $.ink(255, 107, 157); // Pink color from give page CSS --pink if (font && font.glyphs) { $.write(authorText, { x: padding, y: padding }, undefined, undefined, false, "MatrixChunky8"); } else { $.write(authorText, { x: padding, y: padding }); }
// Draw hits count in gold if available if (hitsText) { $.ink(255, 217, 61); // Gold color from give page CSS --gold if (font && font.glyphs) { $.write(hitsText, { x: padding, y: padding + charHeight + lineGap }, undefined, undefined, false, "MatrixChunky8"); } else { $.write(hitsText, { x: padding, y: padding + charHeight + lineGap }); } } });
// Position in bottom-left corner with same margin as QR code const margin = 4; const startX = margin; const startY = screen.height - height - margin;
// Add author overlay to sendData with animation effects sendData.authorOverlay = { x: startX - hudAnimationState.qrSlideOffset.x, // Slide opposite direction from QR y: startY + hudAnimationState.qrSlideOffset.y, opacity: hudAnimationState.opacity, img: (({ width, height, pixels }) => ({ width, height, pixels }))(authorOverlay), }; } catch (err) { console.warn("Failed to generate author overlay:", err); } }
// 📦 Top-right update-ready badge — appears in any piece when // /api/version detects a new deployment. Tap to reload. // An embedded view takes the update instead of advertising it. The // message still goes — it is what arms the reload on the other side — // but it carries no picture, so nothing is drawn over the piece and // there is no green arrow waiting for a tap that cannot come. if (globalUpdateReady && autoUpdateForFrame) { sendData.updateBadge = { silent: true, auto: true }; } else if ( globalUpdateReady && !PREVIEW_OR_ICON && !DEVICE_MODE && !hudAnimationState.qrFullscreen ) { const badgeSize = 13; const badgeWidth = badgeSize * 2 + 3; const margin = 4; const startX = screen.width - badgeWidth - margin; const startY = margin;
// Pulse for attention. const tBadge = Number($api.paintCount || 0n); const pulse = 0.5 + 0.5 * Math.sin(tBadge * 0.15); const fillBoost = Math.round(40 * pulse); const outlineBoost = Math.round(60 * pulse);
const badge = $api.painting(badgeWidth, badgeSize, ($) => { $.unpan(); $.unmask(); // Filled square w/ pulsing green. $.ink(20 + fillBoost, 80 + fillBoost, 30, 230).box( 0, 0, badgeSize, badgeSize, ); $.ink(60 + outlineBoost, 180 + outlineBoost, 70, 255).box( 0, 0, badgeSize, badgeSize, "outline", ); // Up arrow ↑ in pixels. const cx = Math.floor(badgeSize / 2); $.ink(255, 255, 255, 255); $.box(cx, 3, 1, 1); // tip $.box(cx - 1, 4, 3, 1); // wing 1 $.box(cx - 2, 5, 5, 1); // wing 2 $.box(cx, 6, 1, 4); // shaft // Auto-reload toggle: check means reload after the short grace // period; X means updates wait for a manual arrow tap. const bx = badgeSize + 3; $.ink(8, 30, 12, 230).box(bx, 0, badgeSize, badgeSize); $.ink(80, 210, 90, 255).box(bx, 0, badgeSize, badgeSize, "outline"); $.ink(255, 255, 255, 255); if (globalAutoReload) { $.box(bx + 3, 6, 2, 1); $.box(bx + 4, 7, 2, 1); $.box(bx + 5, 5, 1, 2); $.box(bx + 6, 4, 1, 2); $.box(bx + 7, 3, 2, 1); } else { $.box(bx + 3, 3, 1, 1); $.box(bx + 8, 3, 1, 1); $.box(bx + 4, 4, 1, 1); $.box(bx + 7, 4, 1, 1); $.box(bx + 5, 5, 2, 2); $.box(bx + 4, 7, 1, 1); $.box(bx + 7, 7, 1, 1); $.box(bx + 3, 8, 1, 1); $.box(bx + 8, 8, 1, 1); } });
sendData.updateBadge = { x: startX, y: startY, opacity: 1, auto: globalAutoReload, // What the checkbox is actually drawing. img: (({ width, height, pixels }) => ({ width, height, pixels }))( badge, ), };
const boxKey = `${startX},${startY},${badgeWidth},${globalAutoReload}`; if (!updateBadgeBoxScreen || updateBadgeBoxScreen.key !== boxKey) { send({ type: "button:hitbox:add", content: { label: "update-corner", box: { x: startX, y: startY, w: badgeSize, h: badgeSize }, message: "update-corner-tap", }, }); send({ type: "button:hitbox:add", content: { label: "update-auto-corner", box: { x: startX + badgeSize + 3, y: startY, w: badgeSize, h: badgeSize }, message: "update-auto-corner-tap", }, }); updateBadgeBoxScreen = { x: startX, y: startY, w: badgeSize, h: badgeSize, key: boxKey, }; updateBadgeHitboxRegistered = true; } } else if (updateBadgeHitboxRegistered) { send({ type: "button:hitbox:remove", content: "update-corner" }); send({ type: "button:hitbox:remove", content: "update-auto-corner" }); updateBadgeHitboxRegistered = false; updateBadgeBoxScreen = null; }
let transferredPixels;
// Check to see if we have a dirtyBox to render from. const croppedBox = dirtyBox?.croppedBox?.(screen);
if (croppedBox?.w > 0 && croppedBox?.h > 0) { transferredPixels = dirtyBox.crop(screen); sendData.pixels = transferredPixels; sendData.dirtyBox = croppedBox; if ($commonApi.rec.presenting) { // Log dirtyBox usage during tape playback } } else if (painted === true) { // TODO: Toggling this causes a flicker in `line`... but helps prompt. 2022.01.29.13.21 // Otherwise render everything if we drew anything! transferredPixels = screen.pixels; sendData.pixels = transferredPixels; } else { // Painted is false, skip pixel transfer }
// 🩺 Sample the frame on its way to the display. This is the last point // where the worker can still see what the viewer is about to see, which // is the only place a piece that paints nothing is distinguishable from // a piece that paints — from in here they look identical. if (diagnostics.on && !screen.pixels?.buffer?.detached) { diagnostics.frame(screen.pixels, screen.width, screen.height); }
// Optional messages to send. if (painted === true) sendData.paintChanged = true; if (loading === true) sendData.loading = true;
// WebGPU state (tell main thread whether to skip CPU rendering) if ($commonApi.webgpu.enabled) { sendData.webgpuEnabled = true; // Send backend preference if specified if ($commonApi.webgpu.backend) { sendData.gpuBackend = $commonApi.webgpu.backend; } // Signal end of frame to flush WebGPU command queue send({ type: "webgpu-command", content: { type: "frame-end" } }); }
// These fields are one time `signals`. if (reframe || glazeAfterReframe) { sendData.reframe = reframe || glazeAfterReframe !== undefined; if (glazeAfterReframe) { send(glazeAfterReframe); glazeAfterReframe = undefined; } }
if (cursorCode) sendData.cursorCode = cursorCode;
// Note: transferredPixels will be undefined when sendData === {}. if (sendData.pixels) { // 🛡️ Create a copy for transfer to avoid detaching the main rendering buffer const pixelsCopy = new Uint8ClampedArray(sendData.pixels); sendData.pixels = pixelsCopy.buffer; } else { sendData.pixels = content.pixels; }
if (sendData.pixels?.byteLength === 0) sendData.pixels = undefined;
let transferredObjects = [sendData.pixels];
if (sendData.label) { // 🛡️ Create a copy for transfer to avoid detaching the label buffer const labelPixelsCopy = new Uint8ClampedArray(sendData.label.img.pixels); transferredObjects.push(labelPixelsCopy.buffer); }
if (sendData.qrOverlay) { // 🛡️ Create a copy for transfer to avoid detaching the QR overlay buffer const qrPixelsCopy = new Uint8ClampedArray(sendData.qrOverlay.img.pixels); transferredObjects.push(qrPixelsCopy.buffer); }
if (sendData.qrCornerText) { // 🛡️ Create a copy for transfer to avoid detaching the QR corner text buffer const qrCornerPixelsCopy = new Uint8ClampedArray(sendData.qrCornerText.img.pixels); transferredObjects.push(qrCornerPixelsCopy.buffer); }
if (sendData.tapeProgressBar) { // 🛡️ Create a copy for transfer to avoid detaching the tape progress bar buffer const tapePixelsCopy = new Uint8ClampedArray(sendData.tapeProgressBar.img.pixels); transferredObjects.push(tapePixelsCopy.buffer); }
if (sendData.merryProgressBar) { // 🎄 Create a copy for transfer to avoid detaching the merry progress bar buffer const merryPixelsCopy = new Uint8ClampedArray(sendData.merryProgressBar.img.pixels); transferredObjects.push(merryPixelsCopy.buffer); }
if (sendData.demoplayCard) { // 🎬 Create a copy for transfer to avoid detaching the demoplay card buffer const demoplayPixelsCopy = new Uint8ClampedArray(sendData.demoplayCard.img.pixels); transferredObjects.push(demoplayPixelsCopy.buffer); }
if (sendData.lanBadge) { // 🔗 Create a copy for transfer to avoid detaching the LAN badge buffer const lanBadgePixelsCopy = new Uint8ClampedArray(sendData.lanBadge.img.pixels); transferredObjects.push(lanBadgePixelsCopy.buffer); }
if (sendData.authorOverlay) { // 👤 Create a copy for transfer to avoid detaching the author overlay buffer const authorPixelsCopy = new Uint8ClampedArray(sendData.authorOverlay.img.pixels); transferredObjects.push(authorPixelsCopy.buffer); }
// 🎬 Add recording UI overlays (visible on screen but NOT captured in tape) const recordingUIData = $commonApi.recordingUI._getPending(); if (Object.keys(recordingUIData).length > 0) { sendData.recordingUI = {}; for (const [name, overlayData] of Object.entries(recordingUIData)) { // Create a copy of the pixels for transfer const pixelsCopy = new Uint8ClampedArray(overlayData.img.pixels); sendData.recordingUI[name] = { x: overlayData.x, y: overlayData.y, opacity: overlayData.opacity, img: { width: overlayData.img.width, height: overlayData.img.height, pixels: pixelsCopy, }, }; transferredObjects.push(pixelsCopy.buffer); } $commonApi.recordingUI._clearPending(); }
// 🖼️ HD layer — ship this frame's native-resolution Canvas2D drawing // as an ImageBitmap (transferred, so it never copies). The transfer // leaves the OffscreenCanvas blank for the piece's next paint. When an // hd piece paints a pixel-only frame (hd() gated off for a tape or // preview), drop bios's layer so the pixel view shows through. if (hdPainted && hdCanvas) { sendData.hd = { bitmap: hdCanvas.transferToImageBitmap() }; transferredObjects.push(sendData.hd.bitmap); hdPainted = false; hdVisible = true; } else if (hdVisible && painted === true) { send({ type: "hd:clear" }); hdVisible = false; }
// console.log("TO:", transferredObjects); // console.log("Sent data:", sendData);
sendData.sound = sound; // A transition can still cover the new piece after its paint has run. // Certify only frames whose final pixels reveal the current revision. sendData.aeselPreview = golTransition.active ? null : previewEvidence.frame();
// Log first render sent back to main thread if (!globalThis._firstRenderSent) { globalThis._firstRenderSent = true; diskTimings.firstRenderSent = Math.round(performance.now() - diskTimingStart); // Silent: first render sent
// Send timing data to main thread (only in dev mode) if (!getPackMode()) send({ type: "disk-timings", content: diskTimings }); }
send({ type: "render", content: sendData }, transferredObjects);
overlay2D = null; // consumed — the piece re-sets it each paint, so it never staleness-flickers sound.sounds.length = 0; // Empty the sound command buffer. sound.bubbles.length = 0; sound.farts.length = 0; sound.organics.length = 0; sound.kills.length = 0;
twoDCommands.length = 0; // Empty the 2D GPU command buffer.
// Flush the `signals` after sending. if (reframe) reframe = undefined; if (cursorCode) cursorCode = undefined; } else { // Send update (sim). maybeLeave(); // TODO: How necessary is this - does any info ever need to actually // get sent? 23.01.06.16.02
// console.log(pixels); // 🛡️ Create a copy for transfer to avoid detaching the pixels buffer const transferPixels = pixels ? new Uint8ClampedArray(pixels) : null;
send( { type: "update", content: { didntRender: true, loading, pixels: transferPixels?.buffer, width: content.width, height: content.height, sound, }, }, [transferPixels?.buffer], ); }
// Wait 8 frames of the default piece before loading the initial piece. // And also make sure the session has been queried. // console.log(sessionStarted);
// 🕷️ SPIDER MODE: Debug why piece isn't loading if (typeof window !== 'undefined' && window.acSPIDER && paintCount <= 10n) { console.log("🕷️ SPIDER: Frame check:", { paintCount: paintCount.toString(), sessionStarted, PREVIEW_OR_ICON, sandboxed: $commonApi.net.sandboxed, loadAfterPreambleExists: !!loadAfterPreamble }); }
// The preamble: frames of the default disk before the requested piece // loads. Nine of them made sense under the animated boot; with the empty // boot they were ~130 ms of nothing on screen. Two keep the display set // up and give session:started its usual chance to land first. if ( paintCount > 1n && (sessionStarted || PREVIEW_OR_ICON || $commonApi.net.sandboxed) ) { if (paintCount === 2n) { const preambleTime = Math.round(performance.now() - diskTimingStart); diskTimings.preambleComplete = preambleTime; // Silent: preamble complete } if (typeof window !== 'undefined' && window.acSPIDER) { console.log("🕷️ SPIDER: Calling loadAfterPreamble now!"); } loadAfterPreamble?.(); // Start loading after the first disk if necessary. }
// 🛡️ Safety net: if session:started was never received (auth failure, network issue, // or race condition in boot.mjs), force-proceed after ~2 seconds (120 frames at 60fps) // to prevent the system from being stuck on noise16 forever. if ( paintCount > 120n && !sessionStarted && !PREVIEW_OR_ICON && !$commonApi.net.sandboxed && loadAfterPreamble ) { console.warn("⚠️ session:started not received after 120 frames — proceeding without auth"); sessionStarted = true; // Try to recover handle from localStorage even without full auth const cachedHandle = store["handle"]; if (cachedHandle && !HANDLE) { HANDLE = "@" + cachedHandle; if (typeof window !== 'undefined') window.acHANDLE = HANDLE; if (typeof window !== 'undefined' && window.acBootCanvas?.setHandle) window.acBootCanvas.setHandle(HANDLE); send({ type: "handle", content: HANDLE }); console.log(`🔐 Recovered cached handle: ${HANDLE}`); } loadAfterPreamble?.(); }
// soundClear?.();
// ***Frame State Reset*** // Reset video transcoding / print progress.
//console.log(performance.now() - frameTime, "ms");
//performance.mark("b");
//performance.measure("a", "b"); //console.log("Frame perf:", performance.getEntriesByType("measure")[0].duration); //performance.clearMarks(); //performance.clearMeasures(); }}
// 📚 Utilities
// Get the active user's handle from the server if one exists, updating// $commonApi.handlelet HANDLE;let handleRetryInProgress = false;// TODO: Cache this in localStorage and clear it on log in and log out?// 24.05.23.22.16
async function handle(retryCount = 0) { const MAX_RETRIES = 3; const RETRY_DELAYS = [500, 1000, 2000]; // Exponential backoff
if (USER) { // Fast path: use handle already fetched by boot.mjs (via /user?withHandle=true) const bootHandle = USER.handle; // TODO: Check to see if this is in localStorage or not... const storedHandle = bootHandle || store["handle"]; // || (await store.retrieve("handle"));
// console.log("Stored handle...", storedHandle);
if (storedHandle) { const newHandle = "@" + storedHandle; if (HANDLE === newHandle) return; HANDLE = "@" + storedHandle; if (USER && !USER.handle) USER.handle = storedHandle; // Sync to user object if (typeof window !== 'undefined') window.acHANDLE = HANDLE; // Expose for UDP identity if (typeof window !== 'undefined' && window.acBootCanvas?.setHandle) window.acBootCanvas.setHandle(HANDLE); send({ type: "handle", content: HANDLE }); store["handle:received"] = true; if (bootHandle) store["handle"] = bootHandle; // Cache boot-fetched handle // console.log("Retrieved handle from store:", storedHandle); return; // Leave early if a stored handle was found. }
try { log.auth.debug("Fetching handle for:", USER.sub); const response = await fetch(`/handle?for=${USER.sub}`); if (response.status === 200) { const data = await response.json(); const newHandle = "@" + data.handle; if (newHandle === HANDLE) return; HANDLE = newHandle; if (USER && !USER.handle) USER.handle = data.handle; // Sync to user object if (typeof window !== 'undefined') window.acHANDLE = HANDLE; // Expose for UDP identity if (typeof window !== 'undefined' && window.acBootCanvas?.setHandle) window.acBootCanvas.setHandle(HANDLE); send({ type: "handle", content: HANDLE }); store["handle:received"] = true; store["handle"] = data.handle; store["handle:failed"] = false; // Clear any previous failure flag // store.persist("handle"); // Maybe this shouldn't persist. } else { const errorText = await response.text(); console.warn("🤚 Handle fetch failed:", response.status, errorText);
// Retry on 5xx server errors if (response.status >= 500 && retryCount < MAX_RETRIES && !handleRetryInProgress) { handleRetryInProgress = true; const delay = RETRY_DELAYS[retryCount] || 2000; console.log(`🔄 Retrying handle fetch in ${delay}ms (attempt ${retryCount + 1}/${MAX_RETRIES})...`); await new Promise(resolve => setTimeout(resolve, delay)); handleRetryInProgress = false; return handle(retryCount + 1); }
store["handle:failed"] = true; } } catch (error) { console.error("🤚 Handle fetch error:", error);
// Retry on network errors if (retryCount < MAX_RETRIES && !handleRetryInProgress) { handleRetryInProgress = true; const delay = RETRY_DELAYS[retryCount] || 2000; console.log(`🔄 Retrying handle fetch in ${delay}ms (attempt ${retryCount + 1}/${MAX_RETRIES})...`); await new Promise(resolve => setTimeout(resolve, delay)); handleRetryInProgress = false; return handle(retryCount + 1); }
store["handle:failed"] = true; } }}
// Tell the `bios` to pull a screenshot of the next frame.function downloadScreenshot() { send({ type: "$creenshot", content: { filename: `$creenshot-${num.timestamp()}.png`, modifiers: { scale: 6 }, }, });}
// Run the piece's "leave" function which will trigger// a new load before sending off the final frame.function maybeLeave() { // 🚪 Leave (Skips act and sim and paint...) if (leaving && leaveLoad) { try { leave({ ...painting.api, screen, ...$commonApi }); // Trigger leave. } catch (e) { console.warn("👋 Leave failure...", e); } leaveLoad(); leaveLoad = null; }}
// Debug utility for HUD hitbox visualizationglobalThis.toggleHudHitboxDebug = () => { globalThis.debugHudHitbox = !globalThis.debugHudHitbox; $commonApi.needsPaint(); // Force repaint to show/hide the debug visualization};
// Play a sound when "notice" fires.const noticeBell = (api, { tone } = { tone: 600 }) => { api.beep(tone);
noticeTimer = new gizmo.Hourglass(160, { completed: () => { notice = ""; noticeOpts = null; noticeTimer = null; $commonApi.needsPaint(); }, });};