| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020 |
- 'use strict';
- var motionDom = require('motion-dom');
- var motionUtils = require('motion-utils');
- var jsxRuntime = require('react/jsx-runtime');
- var React = require('react');
- const LayoutGroupContext = React.createContext({});
- /**
- * Creates a constant value over the lifecycle of a component.
- *
- * Even if `useMemo` is provided an empty array as its final argument, it doesn't offer
- * a guarantee that it won't re-run for performance reasons later on. By using `useConstant`
- * you can ensure that initialisers don't execute twice or more.
- */
- function useConstant(init) {
- const ref = React.useRef(null);
- if (ref.current === null) {
- ref.current = init();
- }
- return ref.current;
- }
- const isBrowser = typeof window !== "undefined";
- const useIsomorphicLayoutEffect = isBrowser ? React.useLayoutEffect : React.useEffect;
- /**
- * @public
- */
- const PresenceContext =
- /* @__PURE__ */ React.createContext(null);
- /**
- * @public
- */
- const MotionConfigContext = React.createContext({
- transformPagePoint: (p) => p,
- isStatic: false,
- reducedMotion: "never",
- });
- /**
- * When a component is the child of `AnimatePresence`, it can use `usePresence`
- * to access information about whether it's still present in the React tree.
- *
- * ```jsx
- * import { usePresence } from "framer-motion"
- *
- * export const Component = () => {
- * const [isPresent, safeToRemove] = usePresence()
- *
- * useEffect(() => {
- * !isPresent && setTimeout(safeToRemove, 1000)
- * }, [isPresent])
- *
- * return <div />
- * }
- * ```
- *
- * If `isPresent` is `false`, it means that a component has been removed the tree, but
- * `AnimatePresence` won't really remove it until `safeToRemove` has been called.
- *
- * @public
- */
- function usePresence(subscribe = true) {
- const context = React.useContext(PresenceContext);
- if (context === null)
- return [true, null];
- const { isPresent, onExitComplete, register } = context;
- // It's safe to call the following hooks conditionally (after an early return) because the context will always
- // either be null or non-null for the lifespan of the component.
- const id = React.useId();
- React.useEffect(() => {
- if (subscribe) {
- return register(id);
- }
- }, [subscribe]);
- const safeToRemove = React.useCallback(() => subscribe && onExitComplete && onExitComplete(id), [id, onExitComplete, subscribe]);
- return !isPresent && onExitComplete ? [false, safeToRemove] : [true];
- }
- /**
- * Similar to `usePresence`, except `useIsPresent` simply returns whether or not the component is present.
- * There is no `safeToRemove` function.
- *
- * ```jsx
- * import { useIsPresent } from "framer-motion"
- *
- * export const Component = () => {
- * const isPresent = useIsPresent()
- *
- * useEffect(() => {
- * !isPresent && console.log("I've been removed!")
- * }, [isPresent])
- *
- * return <div />
- * }
- * ```
- *
- * @public
- */
- function useIsPresent() {
- return isPresent(React.useContext(PresenceContext));
- }
- function isPresent(context) {
- return context === null ? true : context.isPresent;
- }
- /*
- Value in range from progress
- Given a lower limit and an upper limit, we return the value within
- that range as expressed by progress (usually a number from 0 to 1)
- So progress = 0.5 would change
- from -------- to
- to
- from ---- to
- E.g. from = 10, to = 20, progress = 0.5 => 15
- @param [number]: Lower limit of range
- @param [number]: Upper limit of range
- @param [number]: The progress between lower and upper limits expressed 0-1
- @return [number]: Value as calculated from progress within range (not limited within range)
- */
- const mixNumber$1 = (from, to, progress) => {
- return from + (to - from) * progress;
- };
- const SCALE_PRECISION = 0.0001;
- const SCALE_MIN = 1 - SCALE_PRECISION;
- const SCALE_MAX = 1 + SCALE_PRECISION;
- const TRANSLATE_PRECISION = 0.01;
- const TRANSLATE_MIN = 0 - TRANSLATE_PRECISION;
- const TRANSLATE_MAX = 0 + TRANSLATE_PRECISION;
- function calcLength(axis) {
- return axis.max - axis.min;
- }
- function isNear(value, target, maxDistance) {
- return Math.abs(value - target) <= maxDistance;
- }
- function calcAxisDelta(delta, source, target, origin = 0.5) {
- delta.origin = origin;
- delta.originPoint = mixNumber$1(source.min, source.max, delta.origin);
- delta.scale = calcLength(target) / calcLength(source);
- delta.translate =
- mixNumber$1(target.min, target.max, delta.origin) - delta.originPoint;
- if ((delta.scale >= SCALE_MIN && delta.scale <= SCALE_MAX) ||
- isNaN(delta.scale)) {
- delta.scale = 1.0;
- }
- if ((delta.translate >= TRANSLATE_MIN &&
- delta.translate <= TRANSLATE_MAX) ||
- isNaN(delta.translate)) {
- delta.translate = 0.0;
- }
- }
- function calcBoxDelta(delta, source, target, origin) {
- calcAxisDelta(delta.x, source.x, target.x, origin ? origin.originX : undefined);
- calcAxisDelta(delta.y, source.y, target.y, origin ? origin.originY : undefined);
- }
- function calcRelativeAxis(target, relative, parent) {
- target.min = parent.min + relative.min;
- target.max = target.min + calcLength(relative);
- }
- function calcRelativeBox(target, relative, parent) {
- calcRelativeAxis(target.x, relative.x, parent.x);
- calcRelativeAxis(target.y, relative.y, parent.y);
- }
- function calcRelativeAxisPosition(target, layout, parent) {
- target.min = layout.min - parent.min;
- target.max = target.min + calcLength(layout);
- }
- function calcRelativePosition(target, layout, parent) {
- calcRelativeAxisPosition(target.x, layout.x, parent.x);
- calcRelativeAxisPosition(target.y, layout.y, parent.y);
- }
- const isMotionValue = (value) => Boolean(value && value.getVelocity);
- const instantAnimationState = {
- current: false,
- };
- /*
- Bezier function generator
- This has been modified from Gaëtan Renaudeau's BezierEasing
- https://github.com/gre/bezier-easing/blob/master/src/index.js
- https://github.com/gre/bezier-easing/blob/master/LICENSE
-
- I've removed the newtonRaphsonIterate algo because in benchmarking it
- wasn't noticiably faster than binarySubdivision, indeed removing it
- usually improved times, depending on the curve.
- I also removed the lookup table, as for the added bundle size and loop we're
- only cutting ~4 or so subdivision iterations. I bumped the max iterations up
- to 12 to compensate and this still tended to be faster for no perceivable
- loss in accuracy.
- Usage
- const easeOut = cubicBezier(.17,.67,.83,.67);
- const x = easeOut(0.5); // returns 0.627...
- */
- // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
- const calcBezier = (t, a1, a2) => (((1.0 - 3.0 * a2 + 3.0 * a1) * t + (3.0 * a2 - 6.0 * a1)) * t + 3.0 * a1) *
- t;
- const subdivisionPrecision = 0.0000001;
- const subdivisionMaxIterations = 12;
- function binarySubdivide(x, lowerBound, upperBound, mX1, mX2) {
- let currentX;
- let currentT;
- let i = 0;
- do {
- currentT = lowerBound + (upperBound - lowerBound) / 2.0;
- currentX = calcBezier(currentT, mX1, mX2) - x;
- if (currentX > 0.0) {
- upperBound = currentT;
- }
- else {
- lowerBound = currentT;
- }
- } while (Math.abs(currentX) > subdivisionPrecision &&
- ++i < subdivisionMaxIterations);
- return currentT;
- }
- function cubicBezier(mX1, mY1, mX2, mY2) {
- // If this is a linear gradient, return linear easing
- if (mX1 === mY1 && mX2 === mY2)
- return motionUtils.noop;
- const getTForX = (aX) => binarySubdivide(aX, 0, 1, mX1, mX2);
- // If animation is at start/end, return t without easing
- return (t) => t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2);
- }
- // Accepts an easing function and returns a new one that outputs mirrored values for
- // the second half of the animation. Turns easeIn into easeInOut.
- const mirrorEasing = (easing) => (p) => p <= 0.5 ? easing(2 * p) / 2 : (2 - easing(2 * (1 - p))) / 2;
- // Accepts an easing function and returns a new one that outputs reversed values.
- // Turns easeIn into easeOut.
- const reverseEasing = (easing) => (p) => 1 - easing(1 - p);
- const backOut = /*@__PURE__*/ cubicBezier(0.33, 1.53, 0.69, 0.99);
- const backIn = /*@__PURE__*/ reverseEasing(backOut);
- const backInOut = /*@__PURE__*/ mirrorEasing(backIn);
- const anticipate = (p) => (p *= 2) < 1 ? 0.5 * backIn(p) : 0.5 * (2 - Math.pow(2, -10 * (p - 1)));
- const circIn = (p) => 1 - Math.sin(Math.acos(p));
- const circOut = reverseEasing(circIn);
- const circInOut = mirrorEasing(circIn);
- /**
- * Check if the value is a zero value string like "0px" or "0%"
- */
- const isZeroValueString = (v) => /^0[^.\s]+$/u.test(v);
- function isNone(value) {
- if (typeof value === "number") {
- return value === 0;
- }
- else if (value !== null) {
- return value === "none" || value === "0" || isZeroValueString(value);
- }
- else {
- return true;
- }
- }
- /**
- * Generate a list of every possible transform key.
- */
- const transformPropOrder = [
- "transformPerspective",
- "x",
- "y",
- "z",
- "translateX",
- "translateY",
- "translateZ",
- "scale",
- "scaleX",
- "scaleY",
- "rotate",
- "rotateX",
- "rotateY",
- "rotateZ",
- "skew",
- "skewX",
- "skewY",
- ];
- /**
- * A quick lookup for transform props.
- */
- const transformProps = new Set(transformPropOrder);
- const positionalKeys = new Set([
- "width",
- "height",
- "top",
- "left",
- "right",
- "bottom",
- ...transformPropOrder,
- ]);
- const clamp = (min, max, v) => {
- if (v > max)
- return max;
- if (v < min)
- return min;
- return v;
- };
- const number = {
- test: (v) => typeof v === "number",
- parse: parseFloat,
- transform: (v) => v,
- };
- const alpha = {
- ...number,
- transform: (v) => clamp(0, 1, v),
- };
- const scale = {
- ...number,
- default: 1,
- };
- // If this number is a decimal, make it just five decimal places
- // to avoid exponents
- const sanitize = (v) => Math.round(v * 100000) / 100000;
- const floatRegex = /-?(?:\d+(?:\.\d+)?|\.\d+)/gu;
- function isNullish(v) {
- return v == null;
- }
- const singleColorRegex = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))$/iu;
- /**
- * Returns true if the provided string is a color, ie rgba(0,0,0,0) or #000,
- * but false if a number or multiple colors
- */
- const isColorString = (type, testProp) => (v) => {
- return Boolean((typeof v === "string" &&
- singleColorRegex.test(v) &&
- v.startsWith(type)) ||
- (testProp &&
- !isNullish(v) &&
- Object.prototype.hasOwnProperty.call(v, testProp)));
- };
- const splitColor = (aName, bName, cName) => (v) => {
- if (typeof v !== "string")
- return v;
- const [a, b, c, alpha] = v.match(floatRegex);
- return {
- [aName]: parseFloat(a),
- [bName]: parseFloat(b),
- [cName]: parseFloat(c),
- alpha: alpha !== undefined ? parseFloat(alpha) : 1,
- };
- };
- const clampRgbUnit = (v) => clamp(0, 255, v);
- const rgbUnit = {
- ...number,
- transform: (v) => Math.round(clampRgbUnit(v)),
- };
- const rgba = {
- test: /*@__PURE__*/ isColorString("rgb", "red"),
- parse: /*@__PURE__*/ splitColor("red", "green", "blue"),
- transform: ({ red, green, blue, alpha: alpha$1 = 1 }) => "rgba(" +
- rgbUnit.transform(red) +
- ", " +
- rgbUnit.transform(green) +
- ", " +
- rgbUnit.transform(blue) +
- ", " +
- sanitize(alpha.transform(alpha$1)) +
- ")",
- };
- function parseHex(v) {
- let r = "";
- let g = "";
- let b = "";
- let a = "";
- // If we have 6 characters, ie #FF0000
- if (v.length > 5) {
- r = v.substring(1, 3);
- g = v.substring(3, 5);
- b = v.substring(5, 7);
- a = v.substring(7, 9);
- // Or we have 3 characters, ie #F00
- }
- else {
- r = v.substring(1, 2);
- g = v.substring(2, 3);
- b = v.substring(3, 4);
- a = v.substring(4, 5);
- r += r;
- g += g;
- b += b;
- a += a;
- }
- return {
- red: parseInt(r, 16),
- green: parseInt(g, 16),
- blue: parseInt(b, 16),
- alpha: a ? parseInt(a, 16) / 255 : 1,
- };
- }
- const hex = {
- test: /*@__PURE__*/ isColorString("#"),
- parse: parseHex,
- transform: rgba.transform,
- };
- const createUnitType = (unit) => ({
- test: (v) => typeof v === "string" && v.endsWith(unit) && v.split(" ").length === 1,
- parse: parseFloat,
- transform: (v) => `${v}${unit}`,
- });
- const degrees = /*@__PURE__*/ createUnitType("deg");
- const percent = /*@__PURE__*/ createUnitType("%");
- const px = /*@__PURE__*/ createUnitType("px");
- const vh = /*@__PURE__*/ createUnitType("vh");
- const vw = /*@__PURE__*/ createUnitType("vw");
- const progressPercentage = {
- ...percent,
- parse: (v) => percent.parse(v) / 100,
- transform: (v) => percent.transform(v * 100),
- };
- const hsla = {
- test: /*@__PURE__*/ isColorString("hsl", "hue"),
- parse: /*@__PURE__*/ splitColor("hue", "saturation", "lightness"),
- transform: ({ hue, saturation, lightness, alpha: alpha$1 = 1 }) => {
- return ("hsla(" +
- Math.round(hue) +
- ", " +
- percent.transform(sanitize(saturation)) +
- ", " +
- percent.transform(sanitize(lightness)) +
- ", " +
- sanitize(alpha.transform(alpha$1)) +
- ")");
- },
- };
- const color = {
- test: (v) => rgba.test(v) || hex.test(v) || hsla.test(v),
- parse: (v) => {
- if (rgba.test(v)) {
- return rgba.parse(v);
- }
- else if (hsla.test(v)) {
- return hsla.parse(v);
- }
- else {
- return hex.parse(v);
- }
- },
- transform: (v) => {
- return typeof v === "string"
- ? v
- : v.hasOwnProperty("red")
- ? rgba.transform(v)
- : hsla.transform(v);
- },
- };
- const colorRegex = /(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))/giu;
- function test(v) {
- return (isNaN(v) &&
- typeof v === "string" &&
- (v.match(floatRegex)?.length || 0) +
- (v.match(colorRegex)?.length || 0) >
- 0);
- }
- const NUMBER_TOKEN = "number";
- const COLOR_TOKEN = "color";
- const VAR_TOKEN = "var";
- const VAR_FUNCTION_TOKEN = "var(";
- const SPLIT_TOKEN = "${}";
- // this regex consists of the `singleCssVariableRegex|rgbHSLValueRegex|digitRegex`
- const complexRegex = /var\s*\(\s*--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)|#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\)|-?(?:\d+(?:\.\d+)?|\.\d+)/giu;
- function analyseComplexValue(value) {
- const originalValue = value.toString();
- const values = [];
- const indexes = {
- color: [],
- number: [],
- var: [],
- };
- const types = [];
- let i = 0;
- const tokenised = originalValue.replace(complexRegex, (parsedValue) => {
- if (color.test(parsedValue)) {
- indexes.color.push(i);
- types.push(COLOR_TOKEN);
- values.push(color.parse(parsedValue));
- }
- else if (parsedValue.startsWith(VAR_FUNCTION_TOKEN)) {
- indexes.var.push(i);
- types.push(VAR_TOKEN);
- values.push(parsedValue);
- }
- else {
- indexes.number.push(i);
- types.push(NUMBER_TOKEN);
- values.push(parseFloat(parsedValue));
- }
- ++i;
- return SPLIT_TOKEN;
- });
- const split = tokenised.split(SPLIT_TOKEN);
- return { values, split, indexes, types };
- }
- function parseComplexValue(v) {
- return analyseComplexValue(v).values;
- }
- function createTransformer(source) {
- const { split, types } = analyseComplexValue(source);
- const numSections = split.length;
- return (v) => {
- let output = "";
- for (let i = 0; i < numSections; i++) {
- output += split[i];
- if (v[i] !== undefined) {
- const type = types[i];
- if (type === NUMBER_TOKEN) {
- output += sanitize(v[i]);
- }
- else if (type === COLOR_TOKEN) {
- output += color.transform(v[i]);
- }
- else {
- output += v[i];
- }
- }
- }
- return output;
- };
- }
- const convertNumbersToZero = (v) => typeof v === "number" ? 0 : v;
- function getAnimatableNone$1(v) {
- const parsed = parseComplexValue(v);
- const transformer = createTransformer(v);
- return transformer(parsed.map(convertNumbersToZero));
- }
- const complex = {
- test,
- parse: parseComplexValue,
- createTransformer,
- getAnimatableNone: getAnimatableNone$1,
- };
- /**
- * Properties that should default to 1 or 100%
- */
- const maxDefaults = new Set(["brightness", "contrast", "saturate", "opacity"]);
- function applyDefaultFilter(v) {
- const [name, value] = v.slice(0, -1).split("(");
- if (name === "drop-shadow")
- return v;
- const [number] = value.match(floatRegex) || [];
- if (!number)
- return v;
- const unit = value.replace(number, "");
- let defaultValue = maxDefaults.has(name) ? 1 : 0;
- if (number !== value)
- defaultValue *= 100;
- return name + "(" + defaultValue + unit + ")";
- }
- const functionRegex = /\b([a-z-]*)\(.*?\)/gu;
- const filter = {
- ...complex,
- getAnimatableNone: (v) => {
- const functions = v.match(functionRegex);
- return functions ? functions.map(applyDefaultFilter).join(" ") : v;
- },
- };
- const browserNumberValueTypes = {
- // Border props
- borderWidth: px,
- borderTopWidth: px,
- borderRightWidth: px,
- borderBottomWidth: px,
- borderLeftWidth: px,
- borderRadius: px,
- radius: px,
- borderTopLeftRadius: px,
- borderTopRightRadius: px,
- borderBottomRightRadius: px,
- borderBottomLeftRadius: px,
- // Positioning props
- width: px,
- maxWidth: px,
- height: px,
- maxHeight: px,
- top: px,
- right: px,
- bottom: px,
- left: px,
- // Spacing props
- padding: px,
- paddingTop: px,
- paddingRight: px,
- paddingBottom: px,
- paddingLeft: px,
- margin: px,
- marginTop: px,
- marginRight: px,
- marginBottom: px,
- marginLeft: px,
- // Misc
- backgroundPositionX: px,
- backgroundPositionY: px,
- };
- const transformValueTypes = {
- rotate: degrees,
- rotateX: degrees,
- rotateY: degrees,
- rotateZ: degrees,
- scale,
- scaleX: scale,
- scaleY: scale,
- scaleZ: scale,
- skew: degrees,
- skewX: degrees,
- skewY: degrees,
- distance: px,
- translateX: px,
- translateY: px,
- translateZ: px,
- x: px,
- y: px,
- z: px,
- perspective: px,
- transformPerspective: px,
- opacity: alpha,
- originX: progressPercentage,
- originY: progressPercentage,
- originZ: px,
- };
- const int = {
- ...number,
- transform: Math.round,
- };
- const numberValueTypes = {
- ...browserNumberValueTypes,
- ...transformValueTypes,
- zIndex: int,
- size: px,
- // SVG
- fillOpacity: alpha,
- strokeOpacity: alpha,
- numOctaves: int,
- };
- /**
- * A map of default value types for common values
- */
- const defaultValueTypes = {
- ...numberValueTypes,
- // Color props
- color,
- backgroundColor: color,
- outlineColor: color,
- fill: color,
- stroke: color,
- // Border props
- borderColor: color,
- borderTopColor: color,
- borderRightColor: color,
- borderBottomColor: color,
- borderLeftColor: color,
- filter,
- WebkitFilter: filter,
- };
- /**
- * Gets the default ValueType for the provided value key
- */
- const getDefaultValueType = (key) => defaultValueTypes[key];
- function getAnimatableNone(key, value) {
- let defaultValueType = getDefaultValueType(key);
- if (defaultValueType !== filter)
- defaultValueType = complex;
- // If value is not recognised as animatable, ie "none", create an animatable version origin based on the target
- return defaultValueType.getAnimatableNone
- ? defaultValueType.getAnimatableNone(value)
- : undefined;
- }
- /**
- * If we encounter keyframes like "none" or "0" and we also have keyframes like
- * "#fff" or "200px 200px" we want to find a keyframe to serve as a template for
- * the "none" keyframes. In this case "#fff" or "200px 200px" - then these get turned into
- * zero equivalents, i.e. "#fff0" or "0px 0px".
- */
- const invalidTemplates = new Set(["auto", "none", "0"]);
- function makeNoneKeyframesAnimatable(unresolvedKeyframes, noneKeyframeIndexes, name) {
- let i = 0;
- let animatableTemplate = undefined;
- while (i < unresolvedKeyframes.length && !animatableTemplate) {
- const keyframe = unresolvedKeyframes[i];
- if (typeof keyframe === "string" &&
- !invalidTemplates.has(keyframe) &&
- analyseComplexValue(keyframe).values.length) {
- animatableTemplate = unresolvedKeyframes[i];
- }
- i++;
- }
- if (animatableTemplate && name) {
- for (const noneIndex of noneKeyframeIndexes) {
- unresolvedKeyframes[noneIndex] = getAnimatableNone(name, animatableTemplate);
- }
- }
- }
- const radToDeg = (rad) => (rad * 180) / Math.PI;
- const rotate = (v) => {
- const angle = radToDeg(Math.atan2(v[1], v[0]));
- return rebaseAngle(angle);
- };
- const matrix2dParsers = {
- x: 4,
- y: 5,
- translateX: 4,
- translateY: 5,
- scaleX: 0,
- scaleY: 3,
- scale: (v) => (Math.abs(v[0]) + Math.abs(v[3])) / 2,
- rotate,
- rotateZ: rotate,
- skewX: (v) => radToDeg(Math.atan(v[1])),
- skewY: (v) => radToDeg(Math.atan(v[2])),
- skew: (v) => (Math.abs(v[1]) + Math.abs(v[2])) / 2,
- };
- const rebaseAngle = (angle) => {
- angle = angle % 360;
- if (angle < 0)
- angle += 360;
- return angle;
- };
- const rotateZ = rotate;
- const scaleX = (v) => Math.sqrt(v[0] * v[0] + v[1] * v[1]);
- const scaleY = (v) => Math.sqrt(v[4] * v[4] + v[5] * v[5]);
- const matrix3dParsers = {
- x: 12,
- y: 13,
- z: 14,
- translateX: 12,
- translateY: 13,
- translateZ: 14,
- scaleX,
- scaleY,
- scale: (v) => (scaleX(v) + scaleY(v)) / 2,
- rotateX: (v) => rebaseAngle(radToDeg(Math.atan2(v[6], v[5]))),
- rotateY: (v) => rebaseAngle(radToDeg(Math.atan2(-v[2], v[0]))),
- rotateZ,
- rotate: rotateZ,
- skewX: (v) => radToDeg(Math.atan(v[4])),
- skewY: (v) => radToDeg(Math.atan(v[1])),
- skew: (v) => (Math.abs(v[1]) + Math.abs(v[4])) / 2,
- };
- function defaultTransformValue(name) {
- return name.includes("scale") ? 1 : 0;
- }
- function parseValueFromTransform(transform, name) {
- if (!transform || transform === "none") {
- return defaultTransformValue(name);
- }
- const matrix3dMatch = transform.match(/^matrix3d\(([-\d.e\s,]+)\)$/u);
- let parsers;
- let match;
- if (matrix3dMatch) {
- parsers = matrix3dParsers;
- match = matrix3dMatch;
- }
- else {
- const matrix2dMatch = transform.match(/^matrix\(([-\d.e\s,]+)\)$/u);
- parsers = matrix2dParsers;
- match = matrix2dMatch;
- }
- if (!match) {
- return defaultTransformValue(name);
- }
- const valueParser = parsers[name];
- const values = match[1].split(",").map(convertTransformToNumber);
- return typeof valueParser === "function"
- ? valueParser(values)
- : values[valueParser];
- }
- const readTransformValue = (instance, name) => {
- const { transform = "none" } = getComputedStyle(instance);
- return parseValueFromTransform(transform, name);
- };
- function convertTransformToNumber(value) {
- return parseFloat(value.trim());
- }
- const isNumOrPxType = (v) => v === number || v === px;
- const transformKeys = new Set(["x", "y", "z"]);
- const nonTranslationalTransformKeys = transformPropOrder.filter((key) => !transformKeys.has(key));
- function removeNonTranslationalTransform(visualElement) {
- const removedTransforms = [];
- nonTranslationalTransformKeys.forEach((key) => {
- const value = visualElement.getValue(key);
- if (value !== undefined) {
- removedTransforms.push([key, value.get()]);
- value.set(key.startsWith("scale") ? 1 : 0);
- }
- });
- return removedTransforms;
- }
- const positionalValues = {
- // Dimensions
- width: ({ x }, { paddingLeft = "0", paddingRight = "0" }) => x.max - x.min - parseFloat(paddingLeft) - parseFloat(paddingRight),
- height: ({ y }, { paddingTop = "0", paddingBottom = "0" }) => y.max - y.min - parseFloat(paddingTop) - parseFloat(paddingBottom),
- top: (_bbox, { top }) => parseFloat(top),
- left: (_bbox, { left }) => parseFloat(left),
- bottom: ({ y }, { top }) => parseFloat(top) + (y.max - y.min),
- right: ({ x }, { left }) => parseFloat(left) + (x.max - x.min),
- // Transform
- x: (_bbox, { transform }) => parseValueFromTransform(transform, "x"),
- y: (_bbox, { transform }) => parseValueFromTransform(transform, "y"),
- };
- // Alias translate longform names
- positionalValues.translateX = positionalValues.x;
- positionalValues.translateY = positionalValues.y;
- const toResolve = new Set();
- let isScheduled = false;
- let anyNeedsMeasurement = false;
- function measureAllKeyframes() {
- if (anyNeedsMeasurement) {
- const resolversToMeasure = Array.from(toResolve).filter((resolver) => resolver.needsMeasurement);
- const elementsToMeasure = new Set(resolversToMeasure.map((resolver) => resolver.element));
- const transformsToRestore = new Map();
- /**
- * Write pass
- * If we're measuring elements we want to remove bounding box-changing transforms.
- */
- elementsToMeasure.forEach((element) => {
- const removedTransforms = removeNonTranslationalTransform(element);
- if (!removedTransforms.length)
- return;
- transformsToRestore.set(element, removedTransforms);
- element.render();
- });
- // Read
- resolversToMeasure.forEach((resolver) => resolver.measureInitialState());
- // Write
- elementsToMeasure.forEach((element) => {
- element.render();
- const restore = transformsToRestore.get(element);
- if (restore) {
- restore.forEach(([key, value]) => {
- element.getValue(key)?.set(value);
- });
- }
- });
- // Read
- resolversToMeasure.forEach((resolver) => resolver.measureEndState());
- // Write
- resolversToMeasure.forEach((resolver) => {
- if (resolver.suspendedScrollY !== undefined) {
- window.scrollTo(0, resolver.suspendedScrollY);
- }
- });
- }
- anyNeedsMeasurement = false;
- isScheduled = false;
- toResolve.forEach((resolver) => resolver.complete());
- toResolve.clear();
- }
- function readAllKeyframes() {
- toResolve.forEach((resolver) => {
- resolver.readKeyframes();
- if (resolver.needsMeasurement) {
- anyNeedsMeasurement = true;
- }
- });
- }
- function flushKeyframeResolvers() {
- readAllKeyframes();
- measureAllKeyframes();
- }
- class KeyframeResolver {
- constructor(unresolvedKeyframes, onComplete, name, motionValue, element, isAsync = false) {
- /**
- * Track whether this resolver has completed. Once complete, it never
- * needs to attempt keyframe resolution again.
- */
- this.isComplete = false;
- /**
- * Track whether this resolver is async. If it is, it'll be added to the
- * resolver queue and flushed in the next frame. Resolvers that aren't going
- * to trigger read/write thrashing don't need to be async.
- */
- this.isAsync = false;
- /**
- * Track whether this resolver needs to perform a measurement
- * to resolve its keyframes.
- */
- this.needsMeasurement = false;
- /**
- * Track whether this resolver is currently scheduled to resolve
- * to allow it to be cancelled and resumed externally.
- */
- this.isScheduled = false;
- this.unresolvedKeyframes = [...unresolvedKeyframes];
- this.onComplete = onComplete;
- this.name = name;
- this.motionValue = motionValue;
- this.element = element;
- this.isAsync = isAsync;
- }
- scheduleResolve() {
- this.isScheduled = true;
- if (this.isAsync) {
- toResolve.add(this);
- if (!isScheduled) {
- isScheduled = true;
- motionDom.frame.read(readAllKeyframes);
- motionDom.frame.resolveKeyframes(measureAllKeyframes);
- }
- }
- else {
- this.readKeyframes();
- this.complete();
- }
- }
- readKeyframes() {
- const { unresolvedKeyframes, name, element, motionValue } = this;
- /**
- * If a keyframe is null, we hydrate it either by reading it from
- * the instance, or propagating from previous keyframes.
- */
- for (let i = 0; i < unresolvedKeyframes.length; i++) {
- if (unresolvedKeyframes[i] === null) {
- /**
- * If the first keyframe is null, we need to find its value by sampling the element
- */
- if (i === 0) {
- const currentValue = motionValue?.get();
- const finalKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1];
- if (currentValue !== undefined) {
- unresolvedKeyframes[0] = currentValue;
- }
- else if (element && name) {
- const valueAsRead = element.readValue(name, finalKeyframe);
- if (valueAsRead !== undefined && valueAsRead !== null) {
- unresolvedKeyframes[0] = valueAsRead;
- }
- }
- if (unresolvedKeyframes[0] === undefined) {
- unresolvedKeyframes[0] = finalKeyframe;
- }
- if (motionValue && currentValue === undefined) {
- motionValue.set(unresolvedKeyframes[0]);
- }
- }
- else {
- unresolvedKeyframes[i] = unresolvedKeyframes[i - 1];
- }
- }
- }
- }
- setFinalKeyframe() { }
- measureInitialState() { }
- renderEndStyles() { }
- measureEndState() { }
- complete() {
- this.isComplete = true;
- this.onComplete(this.unresolvedKeyframes, this.finalKeyframe);
- toResolve.delete(this);
- }
- cancel() {
- if (!this.isComplete) {
- this.isScheduled = false;
- toResolve.delete(this);
- }
- }
- resume() {
- if (!this.isComplete)
- this.scheduleResolve();
- }
- }
- /**
- * Check if value is a numerical string, ie a string that is purely a number eg "100" or "-100.1"
- */
- const isNumericalString = (v) => /^-?(?:\d+(?:\.\d+)?|\.\d+)$/u.test(v);
- const checkStringStartsWith = (token) => (key) => typeof key === "string" && key.startsWith(token);
- const isCSSVariableName =
- /*@__PURE__*/ checkStringStartsWith("--");
- const startsAsVariableToken =
- /*@__PURE__*/ checkStringStartsWith("var(--");
- const isCSSVariableToken = (value) => {
- const startsWithToken = startsAsVariableToken(value);
- if (!startsWithToken)
- return false;
- // Ensure any comments are stripped from the value as this can harm performance of the regex.
- return singleCssVariableRegex.test(value.split("/*")[0].trim());
- };
- const singleCssVariableRegex = /var\(--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)$/iu;
- /**
- * Parse Framer's special CSS variable format into a CSS token and a fallback.
- *
- * ```
- * `var(--foo, #fff)` => [`--foo`, '#fff']
- * ```
- *
- * @param current
- */
- const splitCSSVariableRegex =
- // eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive, as it can match a lot of words
- /^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u;
- function parseCSSVariable(current) {
- const match = splitCSSVariableRegex.exec(current);
- if (!match)
- return [,];
- const [, token1, token2, fallback] = match;
- return [`--${token1 ?? token2}`, fallback];
- }
- const maxDepth = 4;
- function getVariableValue(current, element, depth = 1) {
- motionUtils.invariant(depth <= maxDepth, `Max CSS variable fallback depth detected in property "${current}". This may indicate a circular fallback dependency.`);
- const [token, fallback] = parseCSSVariable(current);
- // No CSS variable detected
- if (!token)
- return;
- // Attempt to read this CSS variable off the element
- const resolved = window.getComputedStyle(element).getPropertyValue(token);
- if (resolved) {
- const trimmed = resolved.trim();
- return isNumericalString(trimmed) ? parseFloat(trimmed) : trimmed;
- }
- return isCSSVariableToken(fallback)
- ? getVariableValue(fallback, element, depth + 1)
- : fallback;
- }
- /**
- * Tests a provided value against a ValueType
- */
- const testValueType = (v) => (type) => type.test(v);
- /**
- * ValueType for "auto"
- */
- const auto = {
- test: (v) => v === "auto",
- parse: (v) => v,
- };
- /**
- * A list of value types commonly used for dimensions
- */
- const dimensionValueTypes = [number, px, percent, degrees, vw, vh, auto];
- /**
- * Tests a dimensional value against the list of dimension ValueTypes
- */
- const findDimensionValueType = (v) => dimensionValueTypes.find(testValueType(v));
- class DOMKeyframesResolver extends KeyframeResolver {
- constructor(unresolvedKeyframes, onComplete, name, motionValue, element) {
- super(unresolvedKeyframes, onComplete, name, motionValue, element, true);
- }
- readKeyframes() {
- const { unresolvedKeyframes, element, name } = this;
- if (!element || !element.current)
- return;
- super.readKeyframes();
- /**
- * If any keyframe is a CSS variable, we need to find its value by sampling the element
- */
- for (let i = 0; i < unresolvedKeyframes.length; i++) {
- let keyframe = unresolvedKeyframes[i];
- if (typeof keyframe === "string") {
- keyframe = keyframe.trim();
- if (isCSSVariableToken(keyframe)) {
- const resolved = getVariableValue(keyframe, element.current);
- if (resolved !== undefined) {
- unresolvedKeyframes[i] = resolved;
- }
- if (i === unresolvedKeyframes.length - 1) {
- this.finalKeyframe = keyframe;
- }
- }
- }
- }
- /**
- * Resolve "none" values. We do this potentially twice - once before and once after measuring keyframes.
- * This could be seen as inefficient but it's a trade-off to avoid measurements in more situations, which
- * have a far bigger performance impact.
- */
- this.resolveNoneKeyframes();
- /**
- * Check to see if unit type has changed. If so schedule jobs that will
- * temporarily set styles to the destination keyframes.
- * Skip if we have more than two keyframes or this isn't a positional value.
- * TODO: We can throw if there are multiple keyframes and the value type changes.
- */
- if (!positionalKeys.has(name) || unresolvedKeyframes.length !== 2) {
- return;
- }
- const [origin, target] = unresolvedKeyframes;
- const originType = findDimensionValueType(origin);
- const targetType = findDimensionValueType(target);
- /**
- * Either we don't recognise these value types or we can animate between them.
- */
- if (originType === targetType)
- return;
- /**
- * If both values are numbers or pixels, we can animate between them by
- * converting them to numbers.
- */
- if (isNumOrPxType(originType) && isNumOrPxType(targetType)) {
- for (let i = 0; i < unresolvedKeyframes.length; i++) {
- const value = unresolvedKeyframes[i];
- if (typeof value === "string") {
- unresolvedKeyframes[i] = parseFloat(value);
- }
- }
- }
- else {
- /**
- * Else, the only way to resolve this is by measuring the element.
- */
- this.needsMeasurement = true;
- }
- }
- resolveNoneKeyframes() {
- const { unresolvedKeyframes, name } = this;
- const noneKeyframeIndexes = [];
- for (let i = 0; i < unresolvedKeyframes.length; i++) {
- if (isNone(unresolvedKeyframes[i])) {
- noneKeyframeIndexes.push(i);
- }
- }
- if (noneKeyframeIndexes.length) {
- makeNoneKeyframesAnimatable(unresolvedKeyframes, noneKeyframeIndexes, name);
- }
- }
- measureInitialState() {
- const { element, unresolvedKeyframes, name } = this;
- if (!element || !element.current)
- return;
- if (name === "height") {
- this.suspendedScrollY = window.pageYOffset;
- }
- this.measuredOrigin = positionalValues[name](element.measureViewportBox(), window.getComputedStyle(element.current));
- unresolvedKeyframes[0] = this.measuredOrigin;
- // Set final key frame to measure after next render
- const measureKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1];
- if (measureKeyframe !== undefined) {
- element.getValue(name, measureKeyframe).jump(measureKeyframe, false);
- }
- }
- measureEndState() {
- const { element, name, unresolvedKeyframes } = this;
- if (!element || !element.current)
- return;
- const value = element.getValue(name);
- value && value.jump(this.measuredOrigin, false);
- const finalKeyframeIndex = unresolvedKeyframes.length - 1;
- const finalKeyframe = unresolvedKeyframes[finalKeyframeIndex];
- unresolvedKeyframes[finalKeyframeIndex] = positionalValues[name](element.measureViewportBox(), window.getComputedStyle(element.current));
- if (finalKeyframe !== null && this.finalKeyframe === undefined) {
- this.finalKeyframe = finalKeyframe;
- }
- // If we removed transform values, reapply them before the next render
- if (this.removedTransforms?.length) {
- this.removedTransforms.forEach(([unsetTransformName, unsetTransformValue]) => {
- element
- .getValue(unsetTransformName)
- .set(unsetTransformValue);
- });
- }
- this.resolveNoneKeyframes();
- }
- }
- /**
- * Check if a value is animatable. Examples:
- *
- * ✅: 100, "100px", "#fff"
- * ❌: "block", "url(2.jpg)"
- * @param value
- *
- * @internal
- */
- const isAnimatable = (value, name) => {
- // If the list of keys tat might be non-animatable grows, replace with Set
- if (name === "zIndex")
- return false;
- // If it's a number or a keyframes array, we can animate it. We might at some point
- // need to do a deep isAnimatable check of keyframes, or let Popmotion handle this,
- // but for now lets leave it like this for performance reasons
- if (typeof value === "number" || Array.isArray(value))
- return true;
- if (typeof value === "string" && // It's animatable if we have a string
- (complex.test(value) || value === "0") && // And it contains numbers and/or colors
- !value.startsWith("url(") // Unless it starts with "url("
- ) {
- return true;
- }
- return false;
- };
- function hasKeyframesChanged(keyframes) {
- const current = keyframes[0];
- if (keyframes.length === 1)
- return true;
- for (let i = 0; i < keyframes.length; i++) {
- if (keyframes[i] !== current)
- return true;
- }
- }
- function canAnimate(keyframes, name, type, velocity) {
- /**
- * Check if we're able to animate between the start and end keyframes,
- * and throw a warning if we're attempting to animate between one that's
- * animatable and another that isn't.
- */
- const originKeyframe = keyframes[0];
- if (originKeyframe === null)
- return false;
- /**
- * These aren't traditionally animatable but we do support them.
- * In future we could look into making this more generic or replacing
- * this function with mix() === mixImmediate
- */
- if (name === "display" || name === "visibility")
- return true;
- const targetKeyframe = keyframes[keyframes.length - 1];
- const isOriginAnimatable = isAnimatable(originKeyframe, name);
- const isTargetAnimatable = isAnimatable(targetKeyframe, name);
- motionUtils.warning(isOriginAnimatable === isTargetAnimatable, `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.`);
- // Always skip if any of these are true
- if (!isOriginAnimatable || !isTargetAnimatable) {
- return false;
- }
- return (hasKeyframesChanged(keyframes) ||
- ((type === "spring" || motionDom.isGenerator(type)) && velocity));
- }
- const isNotNull = (value) => value !== null;
- function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyframe) {
- const resolvedKeyframes = keyframes.filter(isNotNull);
- const index = repeat && repeatType !== "loop" && repeat % 2 === 1
- ? 0
- : resolvedKeyframes.length - 1;
- return !index || finalKeyframe === undefined
- ? resolvedKeyframes[index]
- : finalKeyframe;
- }
- /**
- * Maximum time allowed between an animation being created and it being
- * resolved for us to use the latter as the start time.
- *
- * This is to ensure that while we prefer to "start" an animation as soon
- * as it's triggered, we also want to avoid a visual jump if there's a big delay
- * between these two moments.
- */
- const MAX_RESOLVE_DELAY = 40;
- class BaseAnimation {
- constructor({ autoplay = true, delay = 0, type = "keyframes", repeat = 0, repeatDelay = 0, repeatType = "loop", ...options }) {
- // Track whether the animation has been stopped. Stopped animations won't restart.
- this.isStopped = false;
- this.hasAttemptedResolve = false;
- this.createdAt = motionDom.time.now();
- this.options = {
- autoplay,
- delay,
- type,
- repeat,
- repeatDelay,
- repeatType,
- ...options,
- };
- this.updateFinishedPromise();
- }
- /**
- * This method uses the createdAt and resolvedAt to calculate the
- * animation startTime. *Ideally*, we would use the createdAt time as t=0
- * as the following frame would then be the first frame of the animation in
- * progress, which would feel snappier.
- *
- * However, if there's a delay (main thread work) between the creation of
- * the animation and the first commited frame, we prefer to use resolvedAt
- * to avoid a sudden jump into the animation.
- */
- calcStartTime() {
- if (!this.resolvedAt)
- return this.createdAt;
- return this.resolvedAt - this.createdAt > MAX_RESOLVE_DELAY
- ? this.resolvedAt
- : this.createdAt;
- }
- /**
- * A getter for resolved data. If keyframes are not yet resolved, accessing
- * this.resolved will synchronously flush all pending keyframe resolvers.
- * This is a deoptimisation, but at its worst still batches read/writes.
- */
- get resolved() {
- if (!this._resolved && !this.hasAttemptedResolve) {
- flushKeyframeResolvers();
- }
- return this._resolved;
- }
- /**
- * A method to be called when the keyframes resolver completes. This method
- * will check if its possible to run the animation and, if not, skip it.
- * Otherwise, it will call initPlayback on the implementing class.
- */
- onKeyframesResolved(keyframes, finalKeyframe) {
- this.resolvedAt = motionDom.time.now();
- this.hasAttemptedResolve = true;
- const { name, type, velocity, delay, onComplete, onUpdate, isGenerator, } = this.options;
- /**
- * If we can't animate this value with the resolved keyframes
- * then we should complete it immediately.
- */
- if (!isGenerator && !canAnimate(keyframes, name, type, velocity)) {
- // Finish immediately
- if (instantAnimationState.current || !delay) {
- onUpdate &&
- onUpdate(getFinalKeyframe(keyframes, this.options, finalKeyframe));
- onComplete && onComplete();
- this.resolveFinishedPromise();
- return;
- }
- // Finish after a delay
- else {
- this.options.duration = 0;
- }
- }
- const resolvedAnimation = this.initPlayback(keyframes, finalKeyframe);
- if (resolvedAnimation === false)
- return;
- this._resolved = {
- keyframes,
- finalKeyframe,
- ...resolvedAnimation,
- };
- this.onPostResolved();
- }
- onPostResolved() { }
- /**
- * Allows the returned animation to be awaited or promise-chained. Currently
- * resolves when the animation finishes at all but in a future update could/should
- * reject if its cancels.
- */
- then(resolve, reject) {
- return this.currentFinishedPromise.then(resolve, reject);
- }
- flatten() {
- if (!this.options.allowFlatten)
- return;
- this.options.type = "keyframes";
- this.options.ease = "linear";
- }
- updateFinishedPromise() {
- this.currentFinishedPromise = new Promise((resolve) => {
- this.resolveFinishedPromise = resolve;
- });
- }
- }
- // Adapted from https://gist.github.com/mjackson/5311256
- function hueToRgb(p, q, t) {
- if (t < 0)
- t += 1;
- if (t > 1)
- t -= 1;
- if (t < 1 / 6)
- return p + (q - p) * 6 * t;
- if (t < 1 / 2)
- return q;
- if (t < 2 / 3)
- return p + (q - p) * (2 / 3 - t) * 6;
- return p;
- }
- function hslaToRgba({ hue, saturation, lightness, alpha }) {
- hue /= 360;
- saturation /= 100;
- lightness /= 100;
- let red = 0;
- let green = 0;
- let blue = 0;
- if (!saturation) {
- red = green = blue = lightness;
- }
- else {
- const q = lightness < 0.5
- ? lightness * (1 + saturation)
- : lightness + saturation - lightness * saturation;
- const p = 2 * lightness - q;
- red = hueToRgb(p, q, hue + 1 / 3);
- green = hueToRgb(p, q, hue);
- blue = hueToRgb(p, q, hue - 1 / 3);
- }
- return {
- red: Math.round(red * 255),
- green: Math.round(green * 255),
- blue: Math.round(blue * 255),
- alpha,
- };
- }
- function mixImmediate(a, b) {
- return (p) => (p > 0 ? b : a);
- }
- // Linear color space blending
- // Explained https://www.youtube.com/watch?v=LKnqECcg6Gw
- // Demonstrated http://codepen.io/osublake/pen/xGVVaN
- const mixLinearColor = (from, to, v) => {
- const fromExpo = from * from;
- const expo = v * (to * to - fromExpo) + fromExpo;
- return expo < 0 ? 0 : Math.sqrt(expo);
- };
- const colorTypes = [hex, rgba, hsla];
- const getColorType = (v) => colorTypes.find((type) => type.test(v));
- function asRGBA(color) {
- const type = getColorType(color);
- motionUtils.warning(Boolean(type), `'${color}' is not an animatable color. Use the equivalent color code instead.`);
- if (!Boolean(type))
- return false;
- let model = type.parse(color);
- if (type === hsla) {
- // TODO Remove this cast - needed since Motion's stricter typing
- model = hslaToRgba(model);
- }
- return model;
- }
- const mixColor = (from, to) => {
- const fromRGBA = asRGBA(from);
- const toRGBA = asRGBA(to);
- if (!fromRGBA || !toRGBA) {
- return mixImmediate(from, to);
- }
- const blended = { ...fromRGBA };
- return (v) => {
- blended.red = mixLinearColor(fromRGBA.red, toRGBA.red, v);
- blended.green = mixLinearColor(fromRGBA.green, toRGBA.green, v);
- blended.blue = mixLinearColor(fromRGBA.blue, toRGBA.blue, v);
- blended.alpha = mixNumber$1(fromRGBA.alpha, toRGBA.alpha, v);
- return rgba.transform(blended);
- };
- };
- /**
- * Pipe
- * Compose other transformers to run linearily
- * pipe(min(20), max(40))
- * @param {...functions} transformers
- * @return {function}
- */
- const combineFunctions = (a, b) => (v) => b(a(v));
- const pipe = (...transformers) => transformers.reduce(combineFunctions);
- const invisibleValues = new Set(["none", "hidden"]);
- /**
- * Returns a function that, when provided a progress value between 0 and 1,
- * will return the "none" or "hidden" string only when the progress is that of
- * the origin or target.
- */
- function mixVisibility(origin, target) {
- if (invisibleValues.has(origin)) {
- return (p) => (p <= 0 ? origin : target);
- }
- else {
- return (p) => (p >= 1 ? target : origin);
- }
- }
- function mixNumber(a, b) {
- return (p) => mixNumber$1(a, b, p);
- }
- function getMixer(a) {
- if (typeof a === "number") {
- return mixNumber;
- }
- else if (typeof a === "string") {
- return isCSSVariableToken(a)
- ? mixImmediate
- : color.test(a)
- ? mixColor
- : mixComplex;
- }
- else if (Array.isArray(a)) {
- return mixArray;
- }
- else if (typeof a === "object") {
- return color.test(a) ? mixColor : mixObject;
- }
- return mixImmediate;
- }
- function mixArray(a, b) {
- const output = [...a];
- const numValues = output.length;
- const blendValue = a.map((v, i) => getMixer(v)(v, b[i]));
- return (p) => {
- for (let i = 0; i < numValues; i++) {
- output[i] = blendValue[i](p);
- }
- return output;
- };
- }
- function mixObject(a, b) {
- const output = { ...a, ...b };
- const blendValue = {};
- for (const key in output) {
- if (a[key] !== undefined && b[key] !== undefined) {
- blendValue[key] = getMixer(a[key])(a[key], b[key]);
- }
- }
- return (v) => {
- for (const key in blendValue) {
- output[key] = blendValue[key](v);
- }
- return output;
- };
- }
- function matchOrder(origin, target) {
- const orderedOrigin = [];
- const pointers = { color: 0, var: 0, number: 0 };
- for (let i = 0; i < target.values.length; i++) {
- const type = target.types[i];
- const originIndex = origin.indexes[type][pointers[type]];
- const originValue = origin.values[originIndex] ?? 0;
- orderedOrigin[i] = originValue;
- pointers[type]++;
- }
- return orderedOrigin;
- }
- const mixComplex = (origin, target) => {
- const template = complex.createTransformer(target);
- const originStats = analyseComplexValue(origin);
- const targetStats = analyseComplexValue(target);
- const canInterpolate = originStats.indexes.var.length === targetStats.indexes.var.length &&
- originStats.indexes.color.length === targetStats.indexes.color.length &&
- originStats.indexes.number.length >= targetStats.indexes.number.length;
- if (canInterpolate) {
- if ((invisibleValues.has(origin) &&
- !targetStats.values.length) ||
- (invisibleValues.has(target) &&
- !originStats.values.length)) {
- return mixVisibility(origin, target);
- }
- return pipe(mixArray(matchOrder(originStats, targetStats), targetStats.values), template);
- }
- else {
- motionUtils.warning(true, `Complex values '${origin}' and '${target}' too different to mix. Ensure all colors are of the same type, and that each contains the same quantity of number and color values. Falling back to instant transition.`);
- return mixImmediate(origin, target);
- }
- };
- function mix(from, to, p) {
- if (typeof from === "number" &&
- typeof to === "number" &&
- typeof p === "number") {
- return mixNumber$1(from, to, p);
- }
- const mixer = getMixer(from);
- return mixer(from, to);
- }
- const velocitySampleDuration = 5; // ms
- function calcGeneratorVelocity(resolveValue, t, current) {
- const prevT = Math.max(t - velocitySampleDuration, 0);
- return motionUtils.velocityPerSecond(current - resolveValue(prevT), t - prevT);
- }
- const springDefaults = {
- // Default spring physics
- stiffness: 100,
- damping: 10,
- mass: 1.0,
- velocity: 0.0,
- // Default duration/bounce-based options
- duration: 800, // in ms
- bounce: 0.3,
- visualDuration: 0.3, // in seconds
- // Rest thresholds
- restSpeed: {
- granular: 0.01,
- default: 2,
- },
- restDelta: {
- granular: 0.005,
- default: 0.5,
- },
- // Limits
- minDuration: 0.01, // in seconds
- maxDuration: 10.0, // in seconds
- minDamping: 0.05,
- maxDamping: 1,
- };
- const safeMin = 0.001;
- function findSpring({ duration = springDefaults.duration, bounce = springDefaults.bounce, velocity = springDefaults.velocity, mass = springDefaults.mass, }) {
- let envelope;
- let derivative;
- motionUtils.warning(duration <= motionUtils.secondsToMilliseconds(springDefaults.maxDuration), "Spring duration must be 10 seconds or less");
- let dampingRatio = 1 - bounce;
- /**
- * Restrict dampingRatio and duration to within acceptable ranges.
- */
- dampingRatio = clamp(springDefaults.minDamping, springDefaults.maxDamping, dampingRatio);
- duration = clamp(springDefaults.minDuration, springDefaults.maxDuration, motionUtils.millisecondsToSeconds(duration));
- if (dampingRatio < 1) {
- /**
- * Underdamped spring
- */
- envelope = (undampedFreq) => {
- const exponentialDecay = undampedFreq * dampingRatio;
- const delta = exponentialDecay * duration;
- const a = exponentialDecay - velocity;
- const b = calcAngularFreq(undampedFreq, dampingRatio);
- const c = Math.exp(-delta);
- return safeMin - (a / b) * c;
- };
- derivative = (undampedFreq) => {
- const exponentialDecay = undampedFreq * dampingRatio;
- const delta = exponentialDecay * duration;
- const d = delta * velocity + velocity;
- const e = Math.pow(dampingRatio, 2) * Math.pow(undampedFreq, 2) * duration;
- const f = Math.exp(-delta);
- const g = calcAngularFreq(Math.pow(undampedFreq, 2), dampingRatio);
- const factor = -envelope(undampedFreq) + safeMin > 0 ? -1 : 1;
- return (factor * ((d - e) * f)) / g;
- };
- }
- else {
- /**
- * Critically-damped spring
- */
- envelope = (undampedFreq) => {
- const a = Math.exp(-undampedFreq * duration);
- const b = (undampedFreq - velocity) * duration + 1;
- return -safeMin + a * b;
- };
- derivative = (undampedFreq) => {
- const a = Math.exp(-undampedFreq * duration);
- const b = (velocity - undampedFreq) * (duration * duration);
- return a * b;
- };
- }
- const initialGuess = 5 / duration;
- const undampedFreq = approximateRoot(envelope, derivative, initialGuess);
- duration = motionUtils.secondsToMilliseconds(duration);
- if (isNaN(undampedFreq)) {
- return {
- stiffness: springDefaults.stiffness,
- damping: springDefaults.damping,
- duration,
- };
- }
- else {
- const stiffness = Math.pow(undampedFreq, 2) * mass;
- return {
- stiffness,
- damping: dampingRatio * 2 * Math.sqrt(mass * stiffness),
- duration,
- };
- }
- }
- const rootIterations = 12;
- function approximateRoot(envelope, derivative, initialGuess) {
- let result = initialGuess;
- for (let i = 1; i < rootIterations; i++) {
- result = result - envelope(result) / derivative(result);
- }
- return result;
- }
- function calcAngularFreq(undampedFreq, dampingRatio) {
- return undampedFreq * Math.sqrt(1 - dampingRatio * dampingRatio);
- }
- const durationKeys = ["duration", "bounce"];
- const physicsKeys = ["stiffness", "damping", "mass"];
- function isSpringType(options, keys) {
- return keys.some((key) => options[key] !== undefined);
- }
- function getSpringOptions(options) {
- let springOptions = {
- velocity: springDefaults.velocity,
- stiffness: springDefaults.stiffness,
- damping: springDefaults.damping,
- mass: springDefaults.mass,
- isResolvedFromDuration: false,
- ...options,
- };
- // stiffness/damping/mass overrides duration/bounce
- if (!isSpringType(options, physicsKeys) &&
- isSpringType(options, durationKeys)) {
- if (options.visualDuration) {
- const visualDuration = options.visualDuration;
- const root = (2 * Math.PI) / (visualDuration * 1.2);
- const stiffness = root * root;
- const damping = 2 *
- clamp(0.05, 1, 1 - (options.bounce || 0)) *
- Math.sqrt(stiffness);
- springOptions = {
- ...springOptions,
- mass: springDefaults.mass,
- stiffness,
- damping,
- };
- }
- else {
- const derived = findSpring(options);
- springOptions = {
- ...springOptions,
- ...derived,
- mass: springDefaults.mass,
- };
- springOptions.isResolvedFromDuration = true;
- }
- }
- return springOptions;
- }
- function spring(optionsOrVisualDuration = springDefaults.visualDuration, bounce = springDefaults.bounce) {
- const options = typeof optionsOrVisualDuration !== "object"
- ? {
- visualDuration: optionsOrVisualDuration,
- keyframes: [0, 1],
- bounce,
- }
- : optionsOrVisualDuration;
- let { restSpeed, restDelta } = options;
- const origin = options.keyframes[0];
- const target = options.keyframes[options.keyframes.length - 1];
- /**
- * This is the Iterator-spec return value. We ensure it's mutable rather than using a generator
- * to reduce GC during animation.
- */
- const state = { done: false, value: origin };
- const { stiffness, damping, mass, duration, velocity, isResolvedFromDuration, } = getSpringOptions({
- ...options,
- velocity: -motionUtils.millisecondsToSeconds(options.velocity || 0),
- });
- const initialVelocity = velocity || 0.0;
- const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass));
- const initialDelta = target - origin;
- const undampedAngularFreq = motionUtils.millisecondsToSeconds(Math.sqrt(stiffness / mass));
- /**
- * If we're working on a granular scale, use smaller defaults for determining
- * when the spring is finished.
- *
- * These defaults have been selected emprically based on what strikes a good
- * ratio between feeling good and finishing as soon as changes are imperceptible.
- */
- const isGranularScale = Math.abs(initialDelta) < 5;
- restSpeed || (restSpeed = isGranularScale
- ? springDefaults.restSpeed.granular
- : springDefaults.restSpeed.default);
- restDelta || (restDelta = isGranularScale
- ? springDefaults.restDelta.granular
- : springDefaults.restDelta.default);
- let resolveSpring;
- if (dampingRatio < 1) {
- const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio);
- // Underdamped spring
- resolveSpring = (t) => {
- const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
- return (target -
- envelope *
- (((initialVelocity +
- dampingRatio * undampedAngularFreq * initialDelta) /
- angularFreq) *
- Math.sin(angularFreq * t) +
- initialDelta * Math.cos(angularFreq * t)));
- };
- }
- else if (dampingRatio === 1) {
- // Critically damped spring
- resolveSpring = (t) => target -
- Math.exp(-undampedAngularFreq * t) *
- (initialDelta +
- (initialVelocity + undampedAngularFreq * initialDelta) * t);
- }
- else {
- // Overdamped spring
- const dampedAngularFreq = undampedAngularFreq * Math.sqrt(dampingRatio * dampingRatio - 1);
- resolveSpring = (t) => {
- const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
- // When performing sinh or cosh values can hit Infinity so we cap them here
- const freqForT = Math.min(dampedAngularFreq * t, 300);
- return (target -
- (envelope *
- ((initialVelocity +
- dampingRatio * undampedAngularFreq * initialDelta) *
- Math.sinh(freqForT) +
- dampedAngularFreq *
- initialDelta *
- Math.cosh(freqForT))) /
- dampedAngularFreq);
- };
- }
- const generator = {
- calculatedDuration: isResolvedFromDuration ? duration || null : null,
- next: (t) => {
- const current = resolveSpring(t);
- if (!isResolvedFromDuration) {
- let currentVelocity = 0.0;
- /**
- * We only need to calculate velocity for under-damped springs
- * as over- and critically-damped springs can't overshoot, so
- * checking only for displacement is enough.
- */
- if (dampingRatio < 1) {
- currentVelocity =
- t === 0
- ? motionUtils.secondsToMilliseconds(initialVelocity)
- : calcGeneratorVelocity(resolveSpring, t, current);
- }
- const isBelowVelocityThreshold = Math.abs(currentVelocity) <= restSpeed;
- const isBelowDisplacementThreshold = Math.abs(target - current) <= restDelta;
- state.done =
- isBelowVelocityThreshold && isBelowDisplacementThreshold;
- }
- else {
- state.done = t >= duration;
- }
- state.value = state.done ? target : current;
- return state;
- },
- toString: () => {
- const calculatedDuration = Math.min(motionDom.calcGeneratorDuration(generator), motionDom.maxGeneratorDuration);
- const easing = motionDom.generateLinearEasing((progress) => generator.next(calculatedDuration * progress).value, calculatedDuration, 30);
- return calculatedDuration + "ms " + easing;
- },
- toTransition: () => { },
- };
- return generator;
- }
- spring.applyToOptions = (options) => {
- const generatorOptions = motionDom.createGeneratorEasing(options, 100, spring);
- options.ease = motionDom.supportsLinearEasing() ? generatorOptions.ease : "easeOut";
- options.duration = motionUtils.secondsToMilliseconds(generatorOptions.duration);
- options.type = "keyframes";
- return options;
- };
- function inertia({ keyframes, velocity = 0.0, power = 0.8, timeConstant = 325, bounceDamping = 10, bounceStiffness = 500, modifyTarget, min, max, restDelta = 0.5, restSpeed, }) {
- const origin = keyframes[0];
- const state = {
- done: false,
- value: origin,
- };
- const isOutOfBounds = (v) => (min !== undefined && v < min) || (max !== undefined && v > max);
- const nearestBoundary = (v) => {
- if (min === undefined)
- return max;
- if (max === undefined)
- return min;
- return Math.abs(min - v) < Math.abs(max - v) ? min : max;
- };
- let amplitude = power * velocity;
- const ideal = origin + amplitude;
- const target = modifyTarget === undefined ? ideal : modifyTarget(ideal);
- /**
- * If the target has changed we need to re-calculate the amplitude, otherwise
- * the animation will start from the wrong position.
- */
- if (target !== ideal)
- amplitude = target - origin;
- const calcDelta = (t) => -amplitude * Math.exp(-t / timeConstant);
- const calcLatest = (t) => target + calcDelta(t);
- const applyFriction = (t) => {
- const delta = calcDelta(t);
- const latest = calcLatest(t);
- state.done = Math.abs(delta) <= restDelta;
- state.value = state.done ? target : latest;
- };
- /**
- * Ideally this would resolve for t in a stateless way, we could
- * do that by always precalculating the animation but as we know
- * this will be done anyway we can assume that spring will
- * be discovered during that.
- */
- let timeReachedBoundary;
- let spring$1;
- const checkCatchBoundary = (t) => {
- if (!isOutOfBounds(state.value))
- return;
- timeReachedBoundary = t;
- spring$1 = spring({
- keyframes: [state.value, nearestBoundary(state.value)],
- velocity: calcGeneratorVelocity(calcLatest, t, state.value), // TODO: This should be passing * 1000
- damping: bounceDamping,
- stiffness: bounceStiffness,
- restDelta,
- restSpeed,
- });
- };
- checkCatchBoundary(0);
- return {
- calculatedDuration: null,
- next: (t) => {
- /**
- * We need to resolve the friction to figure out if we need a
- * spring but we don't want to do this twice per frame. So here
- * we flag if we updated for this frame and later if we did
- * we can skip doing it again.
- */
- let hasUpdatedFrame = false;
- if (!spring$1 && timeReachedBoundary === undefined) {
- hasUpdatedFrame = true;
- applyFriction(t);
- checkCatchBoundary(t);
- }
- /**
- * If we have a spring and the provided t is beyond the moment the friction
- * animation crossed the min/max boundary, use the spring.
- */
- if (timeReachedBoundary !== undefined && t >= timeReachedBoundary) {
- return spring$1.next(t - timeReachedBoundary);
- }
- else {
- !hasUpdatedFrame && applyFriction(t);
- return state;
- }
- },
- };
- }
- const easeIn = /*@__PURE__*/ cubicBezier(0.42, 0, 1, 1);
- const easeOut = /*@__PURE__*/ cubicBezier(0, 0, 0.58, 1);
- const easeInOut = /*@__PURE__*/ cubicBezier(0.42, 0, 0.58, 1);
- const isEasingArray = (ease) => {
- return Array.isArray(ease) && typeof ease[0] !== "number";
- };
- const easingLookup = {
- linear: motionUtils.noop,
- easeIn,
- easeInOut,
- easeOut,
- circIn,
- circInOut,
- circOut,
- backIn,
- backInOut,
- backOut,
- anticipate,
- };
- const easingDefinitionToFunction = (definition) => {
- if (motionDom.isBezierDefinition(definition)) {
- // If cubic bezier definition, create bezier curve
- motionUtils.invariant(definition.length === 4, `Cubic bezier arrays must contain four numerical values.`);
- const [x1, y1, x2, y2] = definition;
- return cubicBezier(x1, y1, x2, y2);
- }
- else if (typeof definition === "string") {
- // Else lookup from table
- motionUtils.invariant(easingLookup[definition] !== undefined, `Invalid easing type '${definition}'`);
- return easingLookup[definition];
- }
- return definition;
- };
- function createMixers(output, ease, customMixer) {
- const mixers = [];
- const mixerFactory = customMixer || mix;
- const numMixers = output.length - 1;
- for (let i = 0; i < numMixers; i++) {
- let mixer = mixerFactory(output[i], output[i + 1]);
- if (ease) {
- const easingFunction = Array.isArray(ease) ? ease[i] || motionUtils.noop : ease;
- mixer = pipe(easingFunction, mixer);
- }
- mixers.push(mixer);
- }
- return mixers;
- }
- /**
- * Create a function that maps from a numerical input array to a generic output array.
- *
- * Accepts:
- * - Numbers
- * - Colors (hex, hsl, hsla, rgb, rgba)
- * - Complex (combinations of one or more numbers or strings)
- *
- * ```jsx
- * const mixColor = interpolate([0, 1], ['#fff', '#000'])
- *
- * mixColor(0.5) // 'rgba(128, 128, 128, 1)'
- * ```
- *
- * TODO Revist this approach once we've moved to data models for values,
- * probably not needed to pregenerate mixer functions.
- *
- * @public
- */
- function interpolate(input, output, { clamp: isClamp = true, ease, mixer } = {}) {
- const inputLength = input.length;
- motionUtils.invariant(inputLength === output.length, "Both input and output ranges must be the same length");
- /**
- * If we're only provided a single input, we can just make a function
- * that returns the output.
- */
- if (inputLength === 1)
- return () => output[0];
- if (inputLength === 2 && output[0] === output[1])
- return () => output[1];
- const isZeroDeltaRange = input[0] === input[1];
- // If input runs highest -> lowest, reverse both arrays
- if (input[0] > input[inputLength - 1]) {
- input = [...input].reverse();
- output = [...output].reverse();
- }
- const mixers = createMixers(output, ease, mixer);
- const numMixers = mixers.length;
- const interpolator = (v) => {
- if (isZeroDeltaRange && v < input[0])
- return output[0];
- let i = 0;
- if (numMixers > 1) {
- for (; i < input.length - 2; i++) {
- if (v < input[i + 1])
- break;
- }
- }
- const progressInRange = motionUtils.progress(input[i], input[i + 1], v);
- return mixers[i](progressInRange);
- };
- return isClamp
- ? (v) => interpolator(clamp(input[0], input[inputLength - 1], v))
- : interpolator;
- }
- function fillOffset(offset, remaining) {
- const min = offset[offset.length - 1];
- for (let i = 1; i <= remaining; i++) {
- const offsetProgress = motionUtils.progress(0, remaining, i);
- offset.push(mixNumber$1(min, 1, offsetProgress));
- }
- }
- function defaultOffset(arr) {
- const offset = [0];
- fillOffset(offset, arr.length - 1);
- return offset;
- }
- function convertOffsetToTimes(offset, duration) {
- return offset.map((o) => o * duration);
- }
- function defaultEasing(values, easing) {
- return values.map(() => easing || easeInOut).splice(0, values.length - 1);
- }
- function keyframes({ duration = 300, keyframes: keyframeValues, times, ease = "easeInOut", }) {
- /**
- * Easing functions can be externally defined as strings. Here we convert them
- * into actual functions.
- */
- const easingFunctions = isEasingArray(ease)
- ? ease.map(easingDefinitionToFunction)
- : easingDefinitionToFunction(ease);
- /**
- * This is the Iterator-spec return value. We ensure it's mutable rather than using a generator
- * to reduce GC during animation.
- */
- const state = {
- done: false,
- value: keyframeValues[0],
- };
- /**
- * Create a times array based on the provided 0-1 offsets
- */
- const absoluteTimes = convertOffsetToTimes(
- // Only use the provided offsets if they're the correct length
- // TODO Maybe we should warn here if there's a length mismatch
- times && times.length === keyframeValues.length
- ? times
- : defaultOffset(keyframeValues), duration);
- const mapTimeToKeyframe = interpolate(absoluteTimes, keyframeValues, {
- ease: Array.isArray(easingFunctions)
- ? easingFunctions
- : defaultEasing(keyframeValues, easingFunctions),
- });
- return {
- calculatedDuration: duration,
- next: (t) => {
- state.value = mapTimeToKeyframe(t);
- state.done = t >= duration;
- return state;
- },
- };
- }
- const frameloopDriver = (update) => {
- const passTimestamp = ({ timestamp }) => update(timestamp);
- return {
- start: () => motionDom.frame.update(passTimestamp, true),
- stop: () => motionDom.cancelFrame(passTimestamp),
- /**
- * If we're processing this frame we can use the
- * framelocked timestamp to keep things in sync.
- */
- now: () => (motionDom.frameData.isProcessing ? motionDom.frameData.timestamp : motionDom.time.now()),
- };
- };
- const generators = {
- decay: inertia,
- inertia,
- tween: keyframes,
- keyframes: keyframes,
- spring,
- };
- const percentToProgress = (percent) => percent / 100;
- /**
- * Animation that runs on the main thread. Designed to be WAAPI-spec in the subset of
- * features we expose publically. Mostly the compatibility is to ensure visual identity
- * between both WAAPI and main thread animations.
- */
- class MainThreadAnimation extends BaseAnimation {
- constructor(options) {
- super(options);
- /**
- * The time at which the animation was paused.
- */
- this.holdTime = null;
- /**
- * The time at which the animation was cancelled.
- */
- this.cancelTime = null;
- /**
- * The current time of the animation.
- */
- this.currentTime = 0;
- /**
- * Playback speed as a factor. 0 would be stopped, -1 reverse and 2 double speed.
- */
- this.playbackSpeed = 1;
- /**
- * The state of the animation to apply when the animation is resolved. This
- * allows calls to the public API to control the animation before it is resolved,
- * without us having to resolve it first.
- */
- this.pendingPlayState = "running";
- /**
- * The time at which the animation was started.
- */
- this.startTime = null;
- this.state = "idle";
- /**
- * This method is bound to the instance to fix a pattern where
- * animation.stop is returned as a reference from a useEffect.
- */
- this.stop = () => {
- this.resolver.cancel();
- this.isStopped = true;
- if (this.state === "idle")
- return;
- this.teardown();
- const { onStop } = this.options;
- onStop && onStop();
- };
- const { name, motionValue, element, keyframes } = this.options;
- const KeyframeResolver$1 = element?.KeyframeResolver || KeyframeResolver;
- const onResolved = (resolvedKeyframes, finalKeyframe) => this.onKeyframesResolved(resolvedKeyframes, finalKeyframe);
- this.resolver = new KeyframeResolver$1(keyframes, onResolved, name, motionValue, element);
- this.resolver.scheduleResolve();
- }
- flatten() {
- super.flatten();
- // If we've already resolved the animation, re-initialise it
- if (this._resolved) {
- Object.assign(this._resolved, this.initPlayback(this._resolved.keyframes));
- }
- }
- initPlayback(keyframes$1) {
- const { type = "keyframes", repeat = 0, repeatDelay = 0, repeatType, velocity = 0, } = this.options;
- const generatorFactory = motionDom.isGenerator(type)
- ? type
- : generators[type] || keyframes;
- /**
- * If our generator doesn't support mixing numbers, we need to replace keyframes with
- * [0, 100] and then make a function that maps that to the actual keyframes.
- *
- * 100 is chosen instead of 1 as it works nicer with spring animations.
- */
- let mapPercentToKeyframes;
- let mirroredGenerator;
- if (process.env.NODE_ENV !== "production" &&
- generatorFactory !== keyframes) {
- motionUtils.invariant(keyframes$1.length <= 2, `Only two keyframes currently supported with spring and inertia animations. Trying to animate ${keyframes$1}`);
- }
- if (generatorFactory !== keyframes &&
- typeof keyframes$1[0] !== "number") {
- mapPercentToKeyframes = pipe(percentToProgress, mix(keyframes$1[0], keyframes$1[1]));
- keyframes$1 = [0, 100];
- }
- const generator = generatorFactory({ ...this.options, keyframes: keyframes$1 });
- /**
- * If we have a mirror repeat type we need to create a second generator that outputs the
- * mirrored (not reversed) animation and later ping pong between the two generators.
- */
- if (repeatType === "mirror") {
- mirroredGenerator = generatorFactory({
- ...this.options,
- keyframes: [...keyframes$1].reverse(),
- velocity: -velocity,
- });
- }
- /**
- * If duration is undefined and we have repeat options,
- * we need to calculate a duration from the generator.
- *
- * We set it to the generator itself to cache the duration.
- * Any timeline resolver will need to have already precalculated
- * the duration by this step.
- */
- if (generator.calculatedDuration === null) {
- generator.calculatedDuration = motionDom.calcGeneratorDuration(generator);
- }
- const { calculatedDuration } = generator;
- const resolvedDuration = calculatedDuration + repeatDelay;
- const totalDuration = resolvedDuration * (repeat + 1) - repeatDelay;
- return {
- generator,
- mirroredGenerator,
- mapPercentToKeyframes,
- calculatedDuration,
- resolvedDuration,
- totalDuration,
- };
- }
- onPostResolved() {
- const { autoplay = true } = this.options;
- motionDom.activeAnimations.mainThread++;
- this.play();
- if (this.pendingPlayState === "paused" || !autoplay) {
- this.pause();
- }
- else {
- this.state = this.pendingPlayState;
- }
- }
- tick(timestamp, sample = false) {
- const { resolved } = this;
- // If the animations has failed to resolve, return the final keyframe.
- if (!resolved) {
- const { keyframes } = this.options;
- return { done: true, value: keyframes[keyframes.length - 1] };
- }
- const { finalKeyframe, generator, mirroredGenerator, mapPercentToKeyframes, keyframes, calculatedDuration, totalDuration, resolvedDuration, } = resolved;
- if (this.startTime === null)
- return generator.next(0);
- const { delay, repeat, repeatType, repeatDelay, onUpdate } = this.options;
- /**
- * requestAnimationFrame timestamps can come through as lower than
- * the startTime as set by performance.now(). Here we prevent this,
- * though in the future it could be possible to make setting startTime
- * a pending operation that gets resolved here.
- */
- if (this.speed > 0) {
- this.startTime = Math.min(this.startTime, timestamp);
- }
- else if (this.speed < 0) {
- this.startTime = Math.min(timestamp - totalDuration / this.speed, this.startTime);
- }
- // Update currentTime
- if (sample) {
- this.currentTime = timestamp;
- }
- else if (this.holdTime !== null) {
- this.currentTime = this.holdTime;
- }
- else {
- // Rounding the time because floating point arithmetic is not always accurate, e.g. 3000.367 - 1000.367 =
- // 2000.0000000000002. This is a problem when we are comparing the currentTime with the duration, for
- // example.
- this.currentTime =
- Math.round(timestamp - this.startTime) * this.speed;
- }
- // Rebase on delay
- const timeWithoutDelay = this.currentTime - delay * (this.speed >= 0 ? 1 : -1);
- const isInDelayPhase = this.speed >= 0
- ? timeWithoutDelay < 0
- : timeWithoutDelay > totalDuration;
- this.currentTime = Math.max(timeWithoutDelay, 0);
- // If this animation has finished, set the current time to the total duration.
- if (this.state === "finished" && this.holdTime === null) {
- this.currentTime = totalDuration;
- }
- let elapsed = this.currentTime;
- let frameGenerator = generator;
- if (repeat) {
- /**
- * Get the current progress (0-1) of the animation. If t is >
- * than duration we'll get values like 2.5 (midway through the
- * third iteration)
- */
- const progress = Math.min(this.currentTime, totalDuration) / resolvedDuration;
- /**
- * Get the current iteration (0 indexed). For instance the floor of
- * 2.5 is 2.
- */
- let currentIteration = Math.floor(progress);
- /**
- * Get the current progress of the iteration by taking the remainder
- * so 2.5 is 0.5 through iteration 2
- */
- let iterationProgress = progress % 1.0;
- /**
- * If iteration progress is 1 we count that as the end
- * of the previous iteration.
- */
- if (!iterationProgress && progress >= 1) {
- iterationProgress = 1;
- }
- iterationProgress === 1 && currentIteration--;
- currentIteration = Math.min(currentIteration, repeat + 1);
- /**
- * Reverse progress if we're not running in "normal" direction
- */
- const isOddIteration = Boolean(currentIteration % 2);
- if (isOddIteration) {
- if (repeatType === "reverse") {
- iterationProgress = 1 - iterationProgress;
- if (repeatDelay) {
- iterationProgress -= repeatDelay / resolvedDuration;
- }
- }
- else if (repeatType === "mirror") {
- frameGenerator = mirroredGenerator;
- }
- }
- elapsed = clamp(0, 1, iterationProgress) * resolvedDuration;
- }
- /**
- * If we're in negative time, set state as the initial keyframe.
- * This prevents delay: x, duration: 0 animations from finishing
- * instantly.
- */
- const state = isInDelayPhase
- ? { done: false, value: keyframes[0] }
- : frameGenerator.next(elapsed);
- if (mapPercentToKeyframes) {
- state.value = mapPercentToKeyframes(state.value);
- }
- let { done } = state;
- if (!isInDelayPhase && calculatedDuration !== null) {
- done =
- this.speed >= 0
- ? this.currentTime >= totalDuration
- : this.currentTime <= 0;
- }
- const isAnimationFinished = this.holdTime === null &&
- (this.state === "finished" || (this.state === "running" && done));
- if (isAnimationFinished && finalKeyframe !== undefined) {
- state.value = getFinalKeyframe(keyframes, this.options, finalKeyframe);
- }
- if (onUpdate) {
- onUpdate(state.value);
- }
- if (isAnimationFinished) {
- this.finish();
- }
- return state;
- }
- get duration() {
- const { resolved } = this;
- return resolved ? motionUtils.millisecondsToSeconds(resolved.calculatedDuration) : 0;
- }
- get time() {
- return motionUtils.millisecondsToSeconds(this.currentTime);
- }
- set time(newTime) {
- newTime = motionUtils.secondsToMilliseconds(newTime);
- this.currentTime = newTime;
- if (this.holdTime !== null || this.speed === 0) {
- this.holdTime = newTime;
- }
- else if (this.driver) {
- this.startTime = this.driver.now() - newTime / this.speed;
- }
- }
- get speed() {
- return this.playbackSpeed;
- }
- set speed(newSpeed) {
- const hasChanged = this.playbackSpeed !== newSpeed;
- this.playbackSpeed = newSpeed;
- if (hasChanged) {
- this.time = motionUtils.millisecondsToSeconds(this.currentTime);
- }
- }
- play() {
- if (!this.resolver.isScheduled) {
- this.resolver.resume();
- }
- if (!this._resolved) {
- this.pendingPlayState = "running";
- return;
- }
- if (this.isStopped)
- return;
- const { driver = frameloopDriver, onPlay, startTime } = this.options;
- if (!this.driver) {
- this.driver = driver((timestamp) => this.tick(timestamp));
- }
- onPlay && onPlay();
- const now = this.driver.now();
- if (this.holdTime !== null) {
- this.startTime = now - this.holdTime;
- }
- else if (!this.startTime) {
- this.startTime = startTime ?? this.calcStartTime();
- }
- else if (this.state === "finished") {
- this.startTime = now;
- }
- if (this.state === "finished") {
- this.updateFinishedPromise();
- }
- this.cancelTime = this.startTime;
- this.holdTime = null;
- /**
- * Set playState to running only after we've used it in
- * the previous logic.
- */
- this.state = "running";
- this.driver.start();
- }
- pause() {
- if (!this._resolved) {
- this.pendingPlayState = "paused";
- return;
- }
- this.state = "paused";
- this.holdTime = this.currentTime ?? 0;
- }
- complete() {
- if (this.state !== "running") {
- this.play();
- }
- this.pendingPlayState = this.state = "finished";
- this.holdTime = null;
- }
- finish() {
- this.teardown();
- this.state = "finished";
- const { onComplete } = this.options;
- onComplete && onComplete();
- }
- cancel() {
- if (this.cancelTime !== null) {
- this.tick(this.cancelTime);
- }
- this.teardown();
- this.updateFinishedPromise();
- }
- teardown() {
- this.state = "idle";
- this.stopDriver();
- this.resolveFinishedPromise();
- this.updateFinishedPromise();
- this.startTime = this.cancelTime = null;
- this.resolver.cancel();
- motionDom.activeAnimations.mainThread--;
- }
- stopDriver() {
- if (!this.driver)
- return;
- this.driver.stop();
- this.driver = undefined;
- }
- sample(time) {
- this.startTime = 0;
- return this.tick(time, true);
- }
- get finished() {
- return this.currentFinishedPromise;
- }
- }
- // Legacy interface
- function animateValue(options) {
- return new MainThreadAnimation(options);
- }
- /**
- * A list of values that can be hardware-accelerated.
- */
- const acceleratedValues = new Set([
- "opacity",
- "clipPath",
- "filter",
- "transform",
- // TODO: Can be accelerated but currently disabled until https://issues.chromium.org/issues/41491098 is resolved
- // or until we implement support for linear() easing.
- // "background-color"
- ]);
- const supportsWaapi = /*@__PURE__*/ motionUtils.memo(() => Object.hasOwnProperty.call(Element.prototype, "animate"));
- /**
- * 10ms is chosen here as it strikes a balance between smooth
- * results (more than one keyframe per frame at 60fps) and
- * keyframe quantity.
- */
- const sampleDelta = 10; //ms
- /**
- * Implement a practical max duration for keyframe generation
- * to prevent infinite loops
- */
- const maxDuration = 20000;
- /**
- * Check if an animation can run natively via WAAPI or requires pregenerated keyframes.
- * WAAPI doesn't support spring or function easings so we run these as JS animation before
- * handing off.
- */
- function requiresPregeneratedKeyframes(options) {
- return (motionDom.isGenerator(options.type) ||
- options.type === "spring" ||
- !motionDom.isWaapiSupportedEasing(options.ease));
- }
- function pregenerateKeyframes(keyframes, options) {
- /**
- * Create a main-thread animation to pregenerate keyframes.
- * We sample this at regular intervals to generate keyframes that we then
- * linearly interpolate between.
- */
- const sampleAnimation = new MainThreadAnimation({
- ...options,
- keyframes,
- repeat: 0,
- delay: 0,
- isGenerator: true,
- });
- let state = { done: false, value: keyframes[0] };
- const pregeneratedKeyframes = [];
- /**
- * Bail after 20 seconds of pre-generated keyframes as it's likely
- * we're heading for an infinite loop.
- */
- let t = 0;
- while (!state.done && t < maxDuration) {
- state = sampleAnimation.sample(t);
- pregeneratedKeyframes.push(state.value);
- t += sampleDelta;
- }
- return {
- times: undefined,
- keyframes: pregeneratedKeyframes,
- duration: t - sampleDelta,
- ease: "linear",
- };
- }
- const unsupportedEasingFunctions = {
- anticipate,
- backInOut,
- circInOut,
- };
- function isUnsupportedEase(key) {
- return key in unsupportedEasingFunctions;
- }
- class AcceleratedAnimation extends BaseAnimation {
- constructor(options) {
- super(options);
- const { name, motionValue, element, keyframes } = this.options;
- this.resolver = new DOMKeyframesResolver(keyframes, (resolvedKeyframes, finalKeyframe) => this.onKeyframesResolved(resolvedKeyframes, finalKeyframe), name, motionValue, element);
- this.resolver.scheduleResolve();
- }
- initPlayback(keyframes, finalKeyframe) {
- let { duration = 300, times, ease, type, motionValue, name, startTime, } = this.options;
- /**
- * If element has since been unmounted, return false to indicate
- * the animation failed to initialised.
- */
- if (!motionValue.owner || !motionValue.owner.current) {
- return false;
- }
- /**
- * If the user has provided an easing function name that isn't supported
- * by WAAPI (like "anticipate"), we need to provide the corressponding
- * function. This will later get converted to a linear() easing function.
- */
- if (typeof ease === "string" &&
- motionDom.supportsLinearEasing() &&
- isUnsupportedEase(ease)) {
- ease = unsupportedEasingFunctions[ease];
- }
- /**
- * If this animation needs pre-generated keyframes then generate.
- */
- if (requiresPregeneratedKeyframes(this.options)) {
- const { onComplete, onUpdate, motionValue, element, ...options } = this.options;
- const pregeneratedAnimation = pregenerateKeyframes(keyframes, options);
- keyframes = pregeneratedAnimation.keyframes;
- // If this is a very short animation, ensure we have
- // at least two keyframes to animate between as older browsers
- // can't animate between a single keyframe.
- if (keyframes.length === 1) {
- keyframes[1] = keyframes[0];
- }
- duration = pregeneratedAnimation.duration;
- times = pregeneratedAnimation.times;
- ease = pregeneratedAnimation.ease;
- type = "keyframes";
- }
- const animation = motionDom.startWaapiAnimation(motionValue.owner.current, name, keyframes, { ...this.options, duration, times, ease });
- // Override the browser calculated startTime with one synchronised to other JS
- // and WAAPI animations starting this event loop.
- animation.startTime = startTime ?? this.calcStartTime();
- if (this.pendingTimeline) {
- motionDom.attachTimeline(animation, this.pendingTimeline);
- this.pendingTimeline = undefined;
- }
- else {
- /**
- * Prefer the `onfinish` prop as it's more widely supported than
- * the `finished` promise.
- *
- * Here, we synchronously set the provided MotionValue to the end
- * keyframe. If we didn't, when the WAAPI animation is finished it would
- * be removed from the element which would then revert to its old styles.
- */
- animation.onfinish = () => {
- const { onComplete } = this.options;
- motionValue.set(getFinalKeyframe(keyframes, this.options, finalKeyframe));
- onComplete && onComplete();
- this.cancel();
- this.resolveFinishedPromise();
- };
- }
- return {
- animation,
- duration,
- times,
- type,
- ease,
- keyframes: keyframes,
- };
- }
- get duration() {
- const { resolved } = this;
- if (!resolved)
- return 0;
- const { duration } = resolved;
- return motionUtils.millisecondsToSeconds(duration);
- }
- get time() {
- const { resolved } = this;
- if (!resolved)
- return 0;
- const { animation } = resolved;
- return motionUtils.millisecondsToSeconds(animation.currentTime || 0);
- }
- set time(newTime) {
- const { resolved } = this;
- if (!resolved)
- return;
- const { animation } = resolved;
- animation.currentTime = motionUtils.secondsToMilliseconds(newTime);
- }
- get speed() {
- const { resolved } = this;
- if (!resolved)
- return 1;
- const { animation } = resolved;
- return animation.playbackRate;
- }
- get finished() {
- return this.resolved.animation.finished;
- }
- set speed(newSpeed) {
- const { resolved } = this;
- if (!resolved)
- return;
- const { animation } = resolved;
- animation.playbackRate = newSpeed;
- }
- get state() {
- const { resolved } = this;
- if (!resolved)
- return "idle";
- const { animation } = resolved;
- return animation.playState;
- }
- get startTime() {
- const { resolved } = this;
- if (!resolved)
- return null;
- const { animation } = resolved;
- // Coerce to number as TypeScript incorrectly types this
- // as CSSNumberish
- return animation.startTime;
- }
- /**
- * Replace the default DocumentTimeline with another AnimationTimeline.
- * Currently used for scroll animations.
- */
- attachTimeline(timeline) {
- if (!this._resolved) {
- this.pendingTimeline = timeline;
- }
- else {
- const { resolved } = this;
- if (!resolved)
- return motionUtils.noop;
- const { animation } = resolved;
- motionDom.attachTimeline(animation, timeline);
- }
- return motionUtils.noop;
- }
- play() {
- if (this.isStopped)
- return;
- const { resolved } = this;
- if (!resolved)
- return;
- const { animation } = resolved;
- if (animation.playState === "finished") {
- this.updateFinishedPromise();
- }
- animation.play();
- }
- pause() {
- const { resolved } = this;
- if (!resolved)
- return;
- const { animation } = resolved;
- animation.pause();
- }
- stop() {
- this.resolver.cancel();
- this.isStopped = true;
- if (this.state === "idle")
- return;
- this.resolveFinishedPromise();
- this.updateFinishedPromise();
- const { resolved } = this;
- if (!resolved)
- return;
- const { animation, keyframes, duration, type, ease, times } = resolved;
- if (animation.playState === "idle" ||
- animation.playState === "finished") {
- return;
- }
- /**
- * WAAPI doesn't natively have any interruption capabilities.
- *
- * Rather than read commited styles back out of the DOM, we can
- * create a renderless JS animation and sample it twice to calculate
- * its current value, "previous" value, and therefore allow
- * Motion to calculate velocity for any subsequent animation.
- */
- if (this.time) {
- const { motionValue, onUpdate, onComplete, element, ...options } = this.options;
- const sampleAnimation = new MainThreadAnimation({
- ...options,
- keyframes,
- duration,
- type,
- ease,
- times,
- isGenerator: true,
- });
- const sampleTime = motionUtils.secondsToMilliseconds(this.time);
- motionValue.setWithVelocity(sampleAnimation.sample(sampleTime - sampleDelta).value, sampleAnimation.sample(sampleTime).value, sampleDelta);
- }
- const { onStop } = this.options;
- onStop && onStop();
- this.cancel();
- }
- complete() {
- const { resolved } = this;
- if (!resolved)
- return;
- resolved.animation.finish();
- }
- cancel() {
- const { resolved } = this;
- if (!resolved)
- return;
- resolved.animation.cancel();
- }
- static supports(options) {
- const { motionValue, name, repeatDelay, repeatType, damping, type } = options;
- if (!motionValue ||
- !motionValue.owner ||
- !(motionValue.owner.current instanceof HTMLElement)) {
- return false;
- }
- const { onUpdate, transformTemplate } = motionValue.owner.getProps();
- return (supportsWaapi() &&
- name &&
- acceleratedValues.has(name) &&
- (name !== "transform" || !transformTemplate) &&
- /**
- * If we're outputting values to onUpdate then we can't use WAAPI as there's
- * no way to read the value from WAAPI every frame.
- */
- !onUpdate &&
- !repeatDelay &&
- repeatType !== "mirror" &&
- damping !== 0 &&
- type !== "inertia");
- }
- }
- const underDampedSpring = {
- type: "spring",
- stiffness: 500,
- damping: 25,
- restSpeed: 10,
- };
- const criticallyDampedSpring = (target) => ({
- type: "spring",
- stiffness: 550,
- damping: target === 0 ? 2 * Math.sqrt(550) : 30,
- restSpeed: 10,
- });
- const keyframesTransition = {
- type: "keyframes",
- duration: 0.8,
- };
- /**
- * Default easing curve is a slightly shallower version of
- * the default browser easing curve.
- */
- const ease = {
- type: "keyframes",
- ease: [0.25, 0.1, 0.35, 1],
- duration: 0.3,
- };
- const getDefaultTransition = (valueKey, { keyframes }) => {
- if (keyframes.length > 2) {
- return keyframesTransition;
- }
- else if (transformProps.has(valueKey)) {
- return valueKey.startsWith("scale")
- ? criticallyDampedSpring(keyframes[1])
- : underDampedSpring;
- }
- return ease;
- };
- /**
- * Decide whether a transition is defined on a given Transition.
- * This filters out orchestration options and returns true
- * if any options are left.
- */
- function isTransitionDefined({ when, delay: _delay, delayChildren, staggerChildren, staggerDirection, repeat, repeatType, repeatDelay, from, elapsed, ...transition }) {
- return !!Object.keys(transition).length;
- }
- const animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => {
- const valueTransition = motionDom.getValueTransition(transition, name) || {};
- /**
- * Most transition values are currently completely overwritten by value-specific
- * transitions. In the future it'd be nicer to blend these transitions. But for now
- * delay actually does inherit from the root transition if not value-specific.
- */
- const delay = valueTransition.delay || transition.delay || 0;
- /**
- * Elapsed isn't a public transition option but can be passed through from
- * optimized appear effects in milliseconds.
- */
- let { elapsed = 0 } = transition;
- elapsed = elapsed - motionUtils.secondsToMilliseconds(delay);
- let options = {
- keyframes: Array.isArray(target) ? target : [null, target],
- ease: "easeOut",
- velocity: value.getVelocity(),
- ...valueTransition,
- delay: -elapsed,
- onUpdate: (v) => {
- value.set(v);
- valueTransition.onUpdate && valueTransition.onUpdate(v);
- },
- onComplete: () => {
- onComplete();
- valueTransition.onComplete && valueTransition.onComplete();
- },
- name,
- motionValue: value,
- element: isHandoff ? undefined : element,
- };
- /**
- * If there's no transition defined for this value, we can generate
- * unique transition settings for this value.
- */
- if (!isTransitionDefined(valueTransition)) {
- options = {
- ...options,
- ...getDefaultTransition(name, options),
- };
- }
- /**
- * Both WAAPI and our internal animation functions use durations
- * as defined by milliseconds, while our external API defines them
- * as seconds.
- */
- if (options.duration) {
- options.duration = motionUtils.secondsToMilliseconds(options.duration);
- }
- if (options.repeatDelay) {
- options.repeatDelay = motionUtils.secondsToMilliseconds(options.repeatDelay);
- }
- if (options.from !== undefined) {
- options.keyframes[0] = options.from;
- }
- let shouldSkip = false;
- if (options.type === false ||
- (options.duration === 0 && !options.repeatDelay)) {
- options.duration = 0;
- if (options.delay === 0) {
- shouldSkip = true;
- }
- }
- if (instantAnimationState.current ||
- motionUtils.MotionGlobalConfig.skipAnimations) {
- shouldSkip = true;
- options.duration = 0;
- options.delay = 0;
- }
- /**
- * If the transition type or easing has been explicitly set by the user
- * then we don't want to allow flattening the animation.
- */
- options.allowFlatten = !valueTransition.type && !valueTransition.ease;
- /**
- * If we can or must skip creating the animation, and apply only
- * the final keyframe, do so. We also check once keyframes are resolved but
- * this early check prevents the need to create an animation at all.
- */
- if (shouldSkip && !isHandoff && value.get() !== undefined) {
- const finalKeyframe = getFinalKeyframe(options.keyframes, valueTransition);
- if (finalKeyframe !== undefined) {
- motionDom.frame.update(() => {
- options.onUpdate(finalKeyframe);
- options.onComplete();
- });
- // We still want to return some animation controls here rather
- // than returning undefined
- return new motionDom.GroupAnimationWithThen([]);
- }
- }
- /**
- * Animate via WAAPI if possible. If this is a handoff animation, the optimised animation will be running via
- * WAAPI. Therefore, this animation must be JS to ensure it runs "under" the
- * optimised animation.
- */
- if (!isHandoff && AcceleratedAnimation.supports(options)) {
- return new AcceleratedAnimation(options);
- }
- else {
- return new MainThreadAnimation(options);
- }
- };
- function animateSingleValue(value, keyframes, options) {
- const motionValue = isMotionValue(value) ? value : motionDom.motionValue(value);
- motionValue.start(animateMotionValue("", motionValue, keyframes, options));
- return motionValue.animation;
- }
- /**
- * Convert camelCase to dash-case properties.
- */
- const camelToDash = (str) => str.replace(/([a-z])([A-Z])/gu, "$1-$2").toLowerCase();
- const optimizedAppearDataId = "framerAppearId";
- const optimizedAppearDataAttribute = "data-" + camelToDash(optimizedAppearDataId);
- function getOptimisedAppearId(visualElement) {
- return visualElement.props[optimizedAppearDataAttribute];
- }
- function isSVGElement(element) {
- return element instanceof SVGElement && element.tagName !== "svg";
- }
- const compareByDepth = (a, b) => a.depth - b.depth;
- class FlatTree {
- constructor() {
- this.children = [];
- this.isDirty = false;
- }
- add(child) {
- motionUtils.addUniqueItem(this.children, child);
- this.isDirty = true;
- }
- remove(child) {
- motionUtils.removeItem(this.children, child);
- this.isDirty = true;
- }
- forEach(callback) {
- this.isDirty && this.children.sort(compareByDepth);
- this.isDirty = false;
- this.children.forEach(callback);
- }
- }
- /**
- * Timeout defined in ms
- */
- function delay(callback, timeout) {
- const start = motionDom.time.now();
- const checkElapsed = ({ timestamp }) => {
- const elapsed = timestamp - start;
- if (elapsed >= timeout) {
- motionDom.cancelFrame(checkElapsed);
- callback(elapsed - timeout);
- }
- };
- motionDom.frame.read(checkElapsed, true);
- return () => motionDom.cancelFrame(checkElapsed);
- }
- const isKeyframesTarget = (v) => {
- return Array.isArray(v);
- };
- const isCustomValue = (v) => {
- return Boolean(v && typeof v === "object" && v.mix && v.toValue);
- };
- const resolveFinalValueInKeyframes = (v) => {
- // TODO maybe throw if v.length - 1 is placeholder token?
- return isKeyframesTarget(v) ? v[v.length - 1] || 0 : v;
- };
- /**
- * If the provided value is a MotionValue, this returns the actual value, otherwise just the value itself
- *
- * TODO: Remove and move to library
- */
- function resolveMotionValue(value) {
- const unwrappedValue = isMotionValue(value) ? value.get() : value;
- return isCustomValue(unwrappedValue)
- ? unwrappedValue.toValue()
- : unwrappedValue;
- }
- const borders = ["TopLeft", "TopRight", "BottomLeft", "BottomRight"];
- const numBorders = borders.length;
- const asNumber = (value) => typeof value === "string" ? parseFloat(value) : value;
- const isPx = (value) => typeof value === "number" || px.test(value);
- function mixValues(target, follow, lead, progress, shouldCrossfadeOpacity, isOnlyMember) {
- if (shouldCrossfadeOpacity) {
- target.opacity = mixNumber$1(0, lead.opacity ?? 1, easeCrossfadeIn(progress));
- target.opacityExit = mixNumber$1(follow.opacity ?? 1, 0, easeCrossfadeOut(progress));
- }
- else if (isOnlyMember) {
- target.opacity = mixNumber$1(follow.opacity ?? 1, lead.opacity ?? 1, progress);
- }
- /**
- * Mix border radius
- */
- for (let i = 0; i < numBorders; i++) {
- const borderLabel = `border${borders[i]}Radius`;
- let followRadius = getRadius(follow, borderLabel);
- let leadRadius = getRadius(lead, borderLabel);
- if (followRadius === undefined && leadRadius === undefined)
- continue;
- followRadius || (followRadius = 0);
- leadRadius || (leadRadius = 0);
- const canMix = followRadius === 0 ||
- leadRadius === 0 ||
- isPx(followRadius) === isPx(leadRadius);
- if (canMix) {
- target[borderLabel] = Math.max(mixNumber$1(asNumber(followRadius), asNumber(leadRadius), progress), 0);
- if (percent.test(leadRadius) || percent.test(followRadius)) {
- target[borderLabel] += "%";
- }
- }
- else {
- target[borderLabel] = leadRadius;
- }
- }
- /**
- * Mix rotation
- */
- if (follow.rotate || lead.rotate) {
- target.rotate = mixNumber$1(follow.rotate || 0, lead.rotate || 0, progress);
- }
- }
- function getRadius(values, radiusName) {
- return values[radiusName] !== undefined
- ? values[radiusName]
- : values.borderRadius;
- }
- // /**
- // * We only want to mix the background color if there's a follow element
- // * that we're not crossfading opacity between. For instance with switch
- // * AnimateSharedLayout animations, this helps the illusion of a continuous
- // * element being animated but also cuts down on the number of paints triggered
- // * for elements where opacity is doing that work for us.
- // */
- // if (
- // !hasFollowElement &&
- // latestLeadValues.backgroundColor &&
- // latestFollowValues.backgroundColor
- // ) {
- // /**
- // * This isn't ideal performance-wise as mixColor is creating a new function every frame.
- // * We could probably create a mixer that runs at the start of the animation but
- // * the idea behind the crossfader is that it runs dynamically between two potentially
- // * changing targets (ie opacity or borderRadius may be animating independently via variants)
- // */
- // leadState.backgroundColor = followState.backgroundColor = mixColor(
- // latestFollowValues.backgroundColor as string,
- // latestLeadValues.backgroundColor as string
- // )(p)
- // }
- const easeCrossfadeIn = /*@__PURE__*/ compress(0, 0.5, circOut);
- const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, motionUtils.noop);
- function compress(min, max, easing) {
- return (p) => {
- // Could replace ifs with clamp
- if (p < min)
- return 0;
- if (p > max)
- return 1;
- return easing(motionUtils.progress(min, max, p));
- };
- }
- /**
- * Reset an axis to the provided origin box.
- *
- * This is a mutative operation.
- */
- function copyAxisInto(axis, originAxis) {
- axis.min = originAxis.min;
- axis.max = originAxis.max;
- }
- /**
- * Reset a box to the provided origin box.
- *
- * This is a mutative operation.
- */
- function copyBoxInto(box, originBox) {
- copyAxisInto(box.x, originBox.x);
- copyAxisInto(box.y, originBox.y);
- }
- /**
- * Reset a delta to the provided origin box.
- *
- * This is a mutative operation.
- */
- function copyAxisDeltaInto(delta, originDelta) {
- delta.translate = originDelta.translate;
- delta.scale = originDelta.scale;
- delta.originPoint = originDelta.originPoint;
- delta.origin = originDelta.origin;
- }
- function isIdentityScale(scale) {
- return scale === undefined || scale === 1;
- }
- function hasScale({ scale, scaleX, scaleY }) {
- return (!isIdentityScale(scale) ||
- !isIdentityScale(scaleX) ||
- !isIdentityScale(scaleY));
- }
- function hasTransform(values) {
- return (hasScale(values) ||
- has2DTranslate(values) ||
- values.z ||
- values.rotate ||
- values.rotateX ||
- values.rotateY ||
- values.skewX ||
- values.skewY);
- }
- function has2DTranslate(values) {
- return is2DTranslate(values.x) || is2DTranslate(values.y);
- }
- function is2DTranslate(value) {
- return value && value !== "0%";
- }
- /**
- * Scales a point based on a factor and an originPoint
- */
- function scalePoint(point, scale, originPoint) {
- const distanceFromOrigin = point - originPoint;
- const scaled = scale * distanceFromOrigin;
- return originPoint + scaled;
- }
- /**
- * Applies a translate/scale delta to a point
- */
- function applyPointDelta(point, translate, scale, originPoint, boxScale) {
- if (boxScale !== undefined) {
- point = scalePoint(point, boxScale, originPoint);
- }
- return scalePoint(point, scale, originPoint) + translate;
- }
- /**
- * Applies a translate/scale delta to an axis
- */
- function applyAxisDelta(axis, translate = 0, scale = 1, originPoint, boxScale) {
- axis.min = applyPointDelta(axis.min, translate, scale, originPoint, boxScale);
- axis.max = applyPointDelta(axis.max, translate, scale, originPoint, boxScale);
- }
- /**
- * Applies a translate/scale delta to a box
- */
- function applyBoxDelta(box, { x, y }) {
- applyAxisDelta(box.x, x.translate, x.scale, x.originPoint);
- applyAxisDelta(box.y, y.translate, y.scale, y.originPoint);
- }
- const TREE_SCALE_SNAP_MIN = 0.999999999999;
- const TREE_SCALE_SNAP_MAX = 1.0000000000001;
- /**
- * Apply a tree of deltas to a box. We do this to calculate the effect of all the transforms
- * in a tree upon our box before then calculating how to project it into our desired viewport-relative box
- *
- * This is the final nested loop within updateLayoutDelta for future refactoring
- */
- function applyTreeDeltas(box, treeScale, treePath, isSharedTransition = false) {
- const treeLength = treePath.length;
- if (!treeLength)
- return;
- // Reset the treeScale
- treeScale.x = treeScale.y = 1;
- let node;
- let delta;
- for (let i = 0; i < treeLength; i++) {
- node = treePath[i];
- delta = node.projectionDelta;
- /**
- * TODO: Prefer to remove this, but currently we have motion components with
- * display: contents in Framer.
- */
- const { visualElement } = node.options;
- if (visualElement &&
- visualElement.props.style &&
- visualElement.props.style.display === "contents") {
- continue;
- }
- if (isSharedTransition &&
- node.options.layoutScroll &&
- node.scroll &&
- node !== node.root) {
- transformBox(box, {
- x: -node.scroll.offset.x,
- y: -node.scroll.offset.y,
- });
- }
- if (delta) {
- // Incoporate each ancestor's scale into a culmulative treeScale for this component
- treeScale.x *= delta.x.scale;
- treeScale.y *= delta.y.scale;
- // Apply each ancestor's calculated delta into this component's recorded layout box
- applyBoxDelta(box, delta);
- }
- if (isSharedTransition && hasTransform(node.latestValues)) {
- transformBox(box, node.latestValues);
- }
- }
- /**
- * Snap tree scale back to 1 if it's within a non-perceivable threshold.
- * This will help reduce useless scales getting rendered.
- */
- if (treeScale.x < TREE_SCALE_SNAP_MAX &&
- treeScale.x > TREE_SCALE_SNAP_MIN) {
- treeScale.x = 1.0;
- }
- if (treeScale.y < TREE_SCALE_SNAP_MAX &&
- treeScale.y > TREE_SCALE_SNAP_MIN) {
- treeScale.y = 1.0;
- }
- }
- function translateAxis(axis, distance) {
- axis.min = axis.min + distance;
- axis.max = axis.max + distance;
- }
- /**
- * Apply a transform to an axis from the latest resolved motion values.
- * This function basically acts as a bridge between a flat motion value map
- * and applyAxisDelta
- */
- function transformAxis(axis, axisTranslate, axisScale, boxScale, axisOrigin = 0.5) {
- const originPoint = mixNumber$1(axis.min, axis.max, axisOrigin);
- // Apply the axis delta to the final axis
- applyAxisDelta(axis, axisTranslate, axisScale, originPoint, boxScale);
- }
- /**
- * Apply a transform to a box from the latest resolved motion values.
- */
- function transformBox(box, transform) {
- transformAxis(box.x, transform.x, transform.scaleX, transform.scale, transform.originX);
- transformAxis(box.y, transform.y, transform.scaleY, transform.scale, transform.originY);
- }
- /**
- * Remove a delta from a point. This is essentially the steps of applyPointDelta in reverse
- */
- function removePointDelta(point, translate, scale, originPoint, boxScale) {
- point -= translate;
- point = scalePoint(point, 1 / scale, originPoint);
- if (boxScale !== undefined) {
- point = scalePoint(point, 1 / boxScale, originPoint);
- }
- return point;
- }
- /**
- * Remove a delta from an axis. This is essentially the steps of applyAxisDelta in reverse
- */
- function removeAxisDelta(axis, translate = 0, scale = 1, origin = 0.5, boxScale, originAxis = axis, sourceAxis = axis) {
- if (percent.test(translate)) {
- translate = parseFloat(translate);
- const relativeProgress = mixNumber$1(sourceAxis.min, sourceAxis.max, translate / 100);
- translate = relativeProgress - sourceAxis.min;
- }
- if (typeof translate !== "number")
- return;
- let originPoint = mixNumber$1(originAxis.min, originAxis.max, origin);
- if (axis === originAxis)
- originPoint -= translate;
- axis.min = removePointDelta(axis.min, translate, scale, originPoint, boxScale);
- axis.max = removePointDelta(axis.max, translate, scale, originPoint, boxScale);
- }
- /**
- * Remove a transforms from an axis. This is essentially the steps of applyAxisTransforms in reverse
- * and acts as a bridge between motion values and removeAxisDelta
- */
- function removeAxisTransforms(axis, transforms, [key, scaleKey, originKey], origin, sourceAxis) {
- removeAxisDelta(axis, transforms[key], transforms[scaleKey], transforms[originKey], transforms.scale, origin, sourceAxis);
- }
- /**
- * The names of the motion values we want to apply as translation, scale and origin.
- */
- const xKeys = ["x", "scaleX", "originX"];
- const yKeys = ["y", "scaleY", "originY"];
- /**
- * Remove a transforms from an box. This is essentially the steps of applyAxisBox in reverse
- * and acts as a bridge between motion values and removeAxisDelta
- */
- function removeBoxTransforms(box, transforms, originBox, sourceBox) {
- removeAxisTransforms(box.x, transforms, xKeys, originBox ? originBox.x : undefined, sourceBox ? sourceBox.x : undefined);
- removeAxisTransforms(box.y, transforms, yKeys, originBox ? originBox.y : undefined, sourceBox ? sourceBox.y : undefined);
- }
- const createAxisDelta = () => ({
- translate: 0,
- scale: 1,
- origin: 0,
- originPoint: 0,
- });
- const createDelta = () => ({
- x: createAxisDelta(),
- y: createAxisDelta(),
- });
- const createAxis = () => ({ min: 0, max: 0 });
- const createBox = () => ({
- x: createAxis(),
- y: createAxis(),
- });
- function isAxisDeltaZero(delta) {
- return delta.translate === 0 && delta.scale === 1;
- }
- function isDeltaZero(delta) {
- return isAxisDeltaZero(delta.x) && isAxisDeltaZero(delta.y);
- }
- function axisEquals(a, b) {
- return a.min === b.min && a.max === b.max;
- }
- function boxEquals(a, b) {
- return axisEquals(a.x, b.x) && axisEquals(a.y, b.y);
- }
- function axisEqualsRounded(a, b) {
- return (Math.round(a.min) === Math.round(b.min) &&
- Math.round(a.max) === Math.round(b.max));
- }
- function boxEqualsRounded(a, b) {
- return axisEqualsRounded(a.x, b.x) && axisEqualsRounded(a.y, b.y);
- }
- function aspectRatio(box) {
- return calcLength(box.x) / calcLength(box.y);
- }
- function axisDeltaEquals(a, b) {
- return (a.translate === b.translate &&
- a.scale === b.scale &&
- a.originPoint === b.originPoint);
- }
- class NodeStack {
- constructor() {
- this.members = [];
- }
- add(node) {
- motionUtils.addUniqueItem(this.members, node);
- node.scheduleRender();
- }
- remove(node) {
- motionUtils.removeItem(this.members, node);
- if (node === this.prevLead) {
- this.prevLead = undefined;
- }
- if (node === this.lead) {
- const prevLead = this.members[this.members.length - 1];
- if (prevLead) {
- this.promote(prevLead);
- }
- }
- }
- relegate(node) {
- const indexOfNode = this.members.findIndex((member) => node === member);
- if (indexOfNode === 0)
- return false;
- /**
- * Find the next projection node that is present
- */
- let prevLead;
- for (let i = indexOfNode; i >= 0; i--) {
- const member = this.members[i];
- if (member.isPresent !== false) {
- prevLead = member;
- break;
- }
- }
- if (prevLead) {
- this.promote(prevLead);
- return true;
- }
- else {
- return false;
- }
- }
- promote(node, preserveFollowOpacity) {
- const prevLead = this.lead;
- if (node === prevLead)
- return;
- this.prevLead = prevLead;
- this.lead = node;
- node.show();
- if (prevLead) {
- prevLead.instance && prevLead.scheduleRender();
- node.scheduleRender();
- node.resumeFrom = prevLead;
- if (preserveFollowOpacity) {
- node.resumeFrom.preserveOpacity = true;
- }
- if (prevLead.snapshot) {
- node.snapshot = prevLead.snapshot;
- node.snapshot.latestValues =
- prevLead.animationValues || prevLead.latestValues;
- }
- if (node.root && node.root.isUpdating) {
- node.isLayoutDirty = true;
- }
- const { crossfade } = node.options;
- if (crossfade === false) {
- prevLead.hide();
- }
- /**
- * TODO:
- * - Test border radius when previous node was deleted
- * - boxShadow mixing
- * - Shared between element A in scrolled container and element B (scroll stays the same or changes)
- * - Shared between element A in transformed container and element B (transform stays the same or changes)
- * - Shared between element A in scrolled page and element B (scroll stays the same or changes)
- * ---
- * - Crossfade opacity of root nodes
- * - layoutId changes after animation
- * - layoutId changes mid animation
- */
- }
- }
- exitAnimationComplete() {
- this.members.forEach((node) => {
- const { options, resumingFrom } = node;
- options.onExitComplete && options.onExitComplete();
- if (resumingFrom) {
- resumingFrom.options.onExitComplete &&
- resumingFrom.options.onExitComplete();
- }
- });
- }
- scheduleRender() {
- this.members.forEach((node) => {
- node.instance && node.scheduleRender(false);
- });
- }
- /**
- * Clear any leads that have been removed this render to prevent them from being
- * used in future animations and to prevent memory leaks
- */
- removeLeadSnapshot() {
- if (this.lead && this.lead.snapshot) {
- this.lead.snapshot = undefined;
- }
- }
- }
- const scaleCorrectors = {};
- function addScaleCorrector(correctors) {
- for (const key in correctors) {
- scaleCorrectors[key] = correctors[key];
- if (isCSSVariableName(key)) {
- scaleCorrectors[key].isCSSVariable = true;
- }
- }
- }
- function buildProjectionTransform(delta, treeScale, latestTransform) {
- let transform = "";
- /**
- * The translations we use to calculate are always relative to the viewport coordinate space.
- * But when we apply scales, we also scale the coordinate space of an element and its children.
- * For instance if we have a treeScale (the culmination of all parent scales) of 0.5 and we need
- * to move an element 100 pixels, we actually need to move it 200 in within that scaled space.
- */
- const xTranslate = delta.x.translate / treeScale.x;
- const yTranslate = delta.y.translate / treeScale.y;
- const zTranslate = latestTransform?.z || 0;
- if (xTranslate || yTranslate || zTranslate) {
- transform = `translate3d(${xTranslate}px, ${yTranslate}px, ${zTranslate}px) `;
- }
- /**
- * Apply scale correction for the tree transform.
- * This will apply scale to the screen-orientated axes.
- */
- if (treeScale.x !== 1 || treeScale.y !== 1) {
- transform += `scale(${1 / treeScale.x}, ${1 / treeScale.y}) `;
- }
- if (latestTransform) {
- const { transformPerspective, rotate, rotateX, rotateY, skewX, skewY } = latestTransform;
- if (transformPerspective)
- transform = `perspective(${transformPerspective}px) ${transform}`;
- if (rotate)
- transform += `rotate(${rotate}deg) `;
- if (rotateX)
- transform += `rotateX(${rotateX}deg) `;
- if (rotateY)
- transform += `rotateY(${rotateY}deg) `;
- if (skewX)
- transform += `skewX(${skewX}deg) `;
- if (skewY)
- transform += `skewY(${skewY}deg) `;
- }
- /**
- * Apply scale to match the size of the element to the size we want it.
- * This will apply scale to the element-orientated axes.
- */
- const elementScaleX = delta.x.scale * treeScale.x;
- const elementScaleY = delta.y.scale * treeScale.y;
- if (elementScaleX !== 1 || elementScaleY !== 1) {
- transform += `scale(${elementScaleX}, ${elementScaleY})`;
- }
- return transform || "none";
- }
- function eachAxis(callback) {
- return [callback("x"), callback("y")];
- }
- /**
- * This should only ever be modified on the client otherwise it'll
- * persist through server requests. If we need instanced states we
- * could lazy-init via root.
- */
- const globalProjectionState = {
- /**
- * Global flag as to whether the tree has animated since the last time
- * we resized the window
- */
- hasAnimatedSinceResize: true,
- /**
- * We set this to true once, on the first update. Any nodes added to the tree beyond that
- * update will be given a `data-projection-id` attribute.
- */
- hasEverUpdated: false,
- };
- const metrics = {
- nodes: 0,
- calculatedTargetDeltas: 0,
- calculatedProjections: 0,
- };
- const transformAxes = ["", "X", "Y", "Z"];
- const hiddenVisibility = { visibility: "hidden" };
- /**
- * We use 1000 as the animation target as 0-1000 maps better to pixels than 0-1
- * which has a noticeable difference in spring animations
- */
- const animationTarget = 1000;
- let id$1 = 0;
- function resetDistortingTransform(key, visualElement, values, sharedAnimationValues) {
- const { latestValues } = visualElement;
- // Record the distorting transform and then temporarily set it to 0
- if (latestValues[key]) {
- values[key] = latestValues[key];
- visualElement.setStaticValue(key, 0);
- if (sharedAnimationValues) {
- sharedAnimationValues[key] = 0;
- }
- }
- }
- function cancelTreeOptimisedTransformAnimations(projectionNode) {
- projectionNode.hasCheckedOptimisedAppear = true;
- if (projectionNode.root === projectionNode)
- return;
- const { visualElement } = projectionNode.options;
- if (!visualElement)
- return;
- const appearId = getOptimisedAppearId(visualElement);
- if (window.MotionHasOptimisedAnimation(appearId, "transform")) {
- const { layout, layoutId } = projectionNode.options;
- window.MotionCancelOptimisedAnimation(appearId, "transform", motionDom.frame, !(layout || layoutId));
- }
- const { parent } = projectionNode;
- if (parent && !parent.hasCheckedOptimisedAppear) {
- cancelTreeOptimisedTransformAnimations(parent);
- }
- }
- function createProjectionNode$1({ attachResizeListener, defaultParent, measureScroll, checkIsScrollRoot, resetTransform, }) {
- return class ProjectionNode {
- constructor(latestValues = {}, parent = defaultParent?.()) {
- /**
- * A unique ID generated for every projection node.
- */
- this.id = id$1++;
- /**
- * An id that represents a unique session instigated by startUpdate.
- */
- this.animationId = 0;
- /**
- * A Set containing all this component's children. This is used to iterate
- * through the children.
- *
- * TODO: This could be faster to iterate as a flat array stored on the root node.
- */
- this.children = new Set();
- /**
- * Options for the node. We use this to configure what kind of layout animations
- * we should perform (if any).
- */
- this.options = {};
- /**
- * We use this to detect when its safe to shut down part of a projection tree.
- * We have to keep projecting children for scale correction and relative projection
- * until all their parents stop performing layout animations.
- */
- this.isTreeAnimating = false;
- this.isAnimationBlocked = false;
- /**
- * Flag to true if we think this layout has been changed. We can't always know this,
- * currently we set it to true every time a component renders, or if it has a layoutDependency
- * if that has changed between renders. Additionally, components can be grouped by LayoutGroup
- * and if one node is dirtied, they all are.
- */
- this.isLayoutDirty = false;
- /**
- * Flag to true if we think the projection calculations for this node needs
- * recalculating as a result of an updated transform or layout animation.
- */
- this.isProjectionDirty = false;
- /**
- * Flag to true if the layout *or* transform has changed. This then gets propagated
- * throughout the projection tree, forcing any element below to recalculate on the next frame.
- */
- this.isSharedProjectionDirty = false;
- /**
- * Flag transform dirty. This gets propagated throughout the whole tree but is only
- * respected by shared nodes.
- */
- this.isTransformDirty = false;
- /**
- * Block layout updates for instant layout transitions throughout the tree.
- */
- this.updateManuallyBlocked = false;
- this.updateBlockedByResize = false;
- /**
- * Set to true between the start of the first `willUpdate` call and the end of the `didUpdate`
- * call.
- */
- this.isUpdating = false;
- /**
- * If this is an SVG element we currently disable projection transforms
- */
- this.isSVG = false;
- /**
- * Flag to true (during promotion) if a node doing an instant layout transition needs to reset
- * its projection styles.
- */
- this.needsReset = false;
- /**
- * Flags whether this node should have its transform reset prior to measuring.
- */
- this.shouldResetTransform = false;
- /**
- * Store whether this node has been checked for optimised appear animations. As
- * effects fire bottom-up, and we want to look up the tree for appear animations,
- * this makes sure we only check each path once, stopping at nodes that
- * have already been checked.
- */
- this.hasCheckedOptimisedAppear = false;
- /**
- * An object representing the calculated contextual/accumulated/tree scale.
- * This will be used to scale calculcated projection transforms, as these are
- * calculated in screen-space but need to be scaled for elements to layoutly
- * make it to their calculated destinations.
- *
- * TODO: Lazy-init
- */
- this.treeScale = { x: 1, y: 1 };
- /**
- *
- */
- this.eventHandlers = new Map();
- this.hasTreeAnimated = false;
- // Note: Currently only running on root node
- this.updateScheduled = false;
- this.scheduleUpdate = () => this.update();
- this.projectionUpdateScheduled = false;
- this.checkUpdateFailed = () => {
- if (this.isUpdating) {
- this.isUpdating = false;
- this.clearAllSnapshots();
- }
- };
- /**
- * This is a multi-step process as shared nodes might be of different depths. Nodes
- * are sorted by depth order, so we need to resolve the entire tree before moving to
- * the next step.
- */
- this.updateProjection = () => {
- this.projectionUpdateScheduled = false;
- /**
- * Reset debug counts. Manually resetting rather than creating a new
- * object each frame.
- */
- if (motionDom.statsBuffer.value) {
- metrics.nodes =
- metrics.calculatedTargetDeltas =
- metrics.calculatedProjections =
- 0;
- }
- this.nodes.forEach(propagateDirtyNodes);
- this.nodes.forEach(resolveTargetDelta);
- this.nodes.forEach(calcProjection);
- this.nodes.forEach(cleanDirtyNodes);
- if (motionDom.statsBuffer.addProjectionMetrics) {
- motionDom.statsBuffer.addProjectionMetrics(metrics);
- }
- };
- /**
- * Frame calculations
- */
- this.resolvedRelativeTargetAt = 0.0;
- this.hasProjected = false;
- this.isVisible = true;
- this.animationProgress = 0;
- /**
- * Shared layout
- */
- // TODO Only running on root node
- this.sharedNodes = new Map();
- this.latestValues = latestValues;
- this.root = parent ? parent.root || parent : this;
- this.path = parent ? [...parent.path, parent] : [];
- this.parent = parent;
- this.depth = parent ? parent.depth + 1 : 0;
- for (let i = 0; i < this.path.length; i++) {
- this.path[i].shouldResetTransform = true;
- }
- if (this.root === this)
- this.nodes = new FlatTree();
- }
- addEventListener(name, handler) {
- if (!this.eventHandlers.has(name)) {
- this.eventHandlers.set(name, new motionUtils.SubscriptionManager());
- }
- return this.eventHandlers.get(name).add(handler);
- }
- notifyListeners(name, ...args) {
- const subscriptionManager = this.eventHandlers.get(name);
- subscriptionManager && subscriptionManager.notify(...args);
- }
- hasListeners(name) {
- return this.eventHandlers.has(name);
- }
- /**
- * Lifecycles
- */
- mount(instance, isLayoutDirty = this.root.hasTreeAnimated) {
- if (this.instance)
- return;
- this.isSVG = isSVGElement(instance);
- this.instance = instance;
- const { layoutId, layout, visualElement } = this.options;
- if (visualElement && !visualElement.current) {
- visualElement.mount(instance);
- }
- this.root.nodes.add(this);
- this.parent && this.parent.children.add(this);
- if (isLayoutDirty && (layout || layoutId)) {
- this.isLayoutDirty = true;
- }
- if (attachResizeListener) {
- let cancelDelay;
- const resizeUnblockUpdate = () => (this.root.updateBlockedByResize = false);
- attachResizeListener(instance, () => {
- this.root.updateBlockedByResize = true;
- cancelDelay && cancelDelay();
- cancelDelay = delay(resizeUnblockUpdate, 250);
- if (globalProjectionState.hasAnimatedSinceResize) {
- globalProjectionState.hasAnimatedSinceResize = false;
- this.nodes.forEach(finishAnimation);
- }
- });
- }
- if (layoutId) {
- this.root.registerSharedNode(layoutId, this);
- }
- // Only register the handler if it requires layout animation
- if (this.options.animate !== false &&
- visualElement &&
- (layoutId || layout)) {
- this.addEventListener("didUpdate", ({ delta, hasLayoutChanged, hasRelativeLayoutChanged, layout: newLayout, }) => {
- if (this.isTreeAnimationBlocked()) {
- this.target = undefined;
- this.relativeTarget = undefined;
- return;
- }
- // TODO: Check here if an animation exists
- const layoutTransition = this.options.transition ||
- visualElement.getDefaultTransition() ||
- defaultLayoutTransition;
- const { onLayoutAnimationStart, onLayoutAnimationComplete, } = visualElement.getProps();
- /**
- * The target layout of the element might stay the same,
- * but its position relative to its parent has changed.
- */
- const hasTargetChanged = !this.targetLayout ||
- !boxEqualsRounded(this.targetLayout, newLayout);
- /*
- * Note: Disabled to fix relative animations always triggering new
- * layout animations. If this causes further issues, we can try
- * a different approach to detecting relative target changes.
- */
- // || hasRelativeLayoutChanged
- /**
- * If the layout hasn't seemed to have changed, it might be that the
- * element is visually in the same place in the document but its position
- * relative to its parent has indeed changed. So here we check for that.
- */
- const hasOnlyRelativeTargetChanged = !hasLayoutChanged && hasRelativeLayoutChanged;
- if (this.options.layoutRoot ||
- this.resumeFrom ||
- hasOnlyRelativeTargetChanged ||
- (hasLayoutChanged &&
- (hasTargetChanged || !this.currentAnimation))) {
- if (this.resumeFrom) {
- this.resumingFrom = this.resumeFrom;
- this.resumingFrom.resumingFrom = undefined;
- }
- this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged);
- const animationOptions = {
- ...motionDom.getValueTransition(layoutTransition, "layout"),
- onPlay: onLayoutAnimationStart,
- onComplete: onLayoutAnimationComplete,
- };
- if (visualElement.shouldReduceMotion ||
- this.options.layoutRoot) {
- animationOptions.delay = 0;
- animationOptions.type = false;
- }
- this.startAnimation(animationOptions);
- }
- else {
- /**
- * If the layout hasn't changed and we have an animation that hasn't started yet,
- * finish it immediately. Otherwise it will be animating from a location
- * that was probably never commited to screen and look like a jumpy box.
- */
- if (!hasLayoutChanged) {
- finishAnimation(this);
- }
- if (this.isLead() && this.options.onExitComplete) {
- this.options.onExitComplete();
- }
- }
- this.targetLayout = newLayout;
- });
- }
- }
- unmount() {
- this.options.layoutId && this.willUpdate();
- this.root.nodes.remove(this);
- const stack = this.getStack();
- stack && stack.remove(this);
- this.parent && this.parent.children.delete(this);
- this.instance = undefined;
- motionDom.cancelFrame(this.updateProjection);
- }
- // only on the root
- blockUpdate() {
- this.updateManuallyBlocked = true;
- }
- unblockUpdate() {
- this.updateManuallyBlocked = false;
- }
- isUpdateBlocked() {
- return this.updateManuallyBlocked || this.updateBlockedByResize;
- }
- isTreeAnimationBlocked() {
- return (this.isAnimationBlocked ||
- (this.parent && this.parent.isTreeAnimationBlocked()) ||
- false);
- }
- // Note: currently only running on root node
- startUpdate() {
- if (this.isUpdateBlocked())
- return;
- this.isUpdating = true;
- this.nodes && this.nodes.forEach(resetSkewAndRotation);
- this.animationId++;
- }
- getTransformTemplate() {
- const { visualElement } = this.options;
- return visualElement && visualElement.getProps().transformTemplate;
- }
- willUpdate(shouldNotifyListeners = true) {
- this.root.hasTreeAnimated = true;
- if (this.root.isUpdateBlocked()) {
- this.options.onExitComplete && this.options.onExitComplete();
- return;
- }
- /**
- * If we're running optimised appear animations then these must be
- * cancelled before measuring the DOM. This is so we can measure
- * the true layout of the element rather than the WAAPI animation
- * which will be unaffected by the resetSkewAndRotate step.
- *
- * Note: This is a DOM write. Worst case scenario is this is sandwiched
- * between other snapshot reads which will cause unnecessary style recalculations.
- * This has to happen here though, as we don't yet know which nodes will need
- * snapshots in startUpdate(), but we only want to cancel optimised animations
- * if a layout animation measurement is actually going to be affected by them.
- */
- if (window.MotionCancelOptimisedAnimation &&
- !this.hasCheckedOptimisedAppear) {
- cancelTreeOptimisedTransformAnimations(this);
- }
- !this.root.isUpdating && this.root.startUpdate();
- if (this.isLayoutDirty)
- return;
- this.isLayoutDirty = true;
- for (let i = 0; i < this.path.length; i++) {
- const node = this.path[i];
- node.shouldResetTransform = true;
- node.updateScroll("snapshot");
- if (node.options.layoutRoot) {
- node.willUpdate(false);
- }
- }
- const { layoutId, layout } = this.options;
- if (layoutId === undefined && !layout)
- return;
- const transformTemplate = this.getTransformTemplate();
- this.prevTransformTemplateValue = transformTemplate
- ? transformTemplate(this.latestValues, "")
- : undefined;
- this.updateSnapshot();
- shouldNotifyListeners && this.notifyListeners("willUpdate");
- }
- update() {
- this.updateScheduled = false;
- const updateWasBlocked = this.isUpdateBlocked();
- // When doing an instant transition, we skip the layout update,
- // but should still clean up the measurements so that the next
- // snapshot could be taken correctly.
- if (updateWasBlocked) {
- this.unblockUpdate();
- this.clearAllSnapshots();
- this.nodes.forEach(clearMeasurements);
- return;
- }
- if (!this.isUpdating) {
- this.nodes.forEach(clearIsLayoutDirty);
- }
- this.isUpdating = false;
- /**
- * Write
- */
- this.nodes.forEach(resetTransformStyle);
- /**
- * Read ==================
- */
- // Update layout measurements of updated children
- this.nodes.forEach(updateLayout);
- /**
- * Write
- */
- // Notify listeners that the layout is updated
- this.nodes.forEach(notifyLayoutUpdate);
- this.clearAllSnapshots();
- /**
- * Manually flush any pending updates. Ideally
- * we could leave this to the following requestAnimationFrame but this seems
- * to leave a flash of incorrectly styled content.
- */
- const now = motionDom.time.now();
- motionDom.frameData.delta = clamp(0, 1000 / 60, now - motionDom.frameData.timestamp);
- motionDom.frameData.timestamp = now;
- motionDom.frameData.isProcessing = true;
- motionDom.frameSteps.update.process(motionDom.frameData);
- motionDom.frameSteps.preRender.process(motionDom.frameData);
- motionDom.frameSteps.render.process(motionDom.frameData);
- motionDom.frameData.isProcessing = false;
- }
- didUpdate() {
- if (!this.updateScheduled) {
- this.updateScheduled = true;
- motionDom.microtask.read(this.scheduleUpdate);
- }
- }
- clearAllSnapshots() {
- this.nodes.forEach(clearSnapshot);
- this.sharedNodes.forEach(removeLeadSnapshots);
- }
- scheduleUpdateProjection() {
- if (!this.projectionUpdateScheduled) {
- this.projectionUpdateScheduled = true;
- motionDom.frame.preRender(this.updateProjection, false, true);
- }
- }
- scheduleCheckAfterUnmount() {
- /**
- * If the unmounting node is in a layoutGroup and did trigger a willUpdate,
- * we manually call didUpdate to give a chance to the siblings to animate.
- * Otherwise, cleanup all snapshots to prevents future nodes from reusing them.
- */
- motionDom.frame.postRender(() => {
- if (this.isLayoutDirty) {
- this.root.didUpdate();
- }
- else {
- this.root.checkUpdateFailed();
- }
- });
- }
- /**
- * Update measurements
- */
- updateSnapshot() {
- if (this.snapshot || !this.instance)
- return;
- this.snapshot = this.measure();
- if (this.snapshot &&
- !calcLength(this.snapshot.measuredBox.x) &&
- !calcLength(this.snapshot.measuredBox.y)) {
- this.snapshot = undefined;
- }
- }
- updateLayout() {
- if (!this.instance)
- return;
- // TODO: Incorporate into a forwarded scroll offset
- this.updateScroll();
- if (!(this.options.alwaysMeasureLayout && this.isLead()) &&
- !this.isLayoutDirty) {
- return;
- }
- /**
- * When a node is mounted, it simply resumes from the prevLead's
- * snapshot instead of taking a new one, but the ancestors scroll
- * might have updated while the prevLead is unmounted. We need to
- * update the scroll again to make sure the layout we measure is
- * up to date.
- */
- if (this.resumeFrom && !this.resumeFrom.instance) {
- for (let i = 0; i < this.path.length; i++) {
- const node = this.path[i];
- node.updateScroll();
- }
- }
- const prevLayout = this.layout;
- this.layout = this.measure(false);
- this.layoutCorrected = createBox();
- this.isLayoutDirty = false;
- this.projectionDelta = undefined;
- this.notifyListeners("measure", this.layout.layoutBox);
- const { visualElement } = this.options;
- visualElement &&
- visualElement.notify("LayoutMeasure", this.layout.layoutBox, prevLayout ? prevLayout.layoutBox : undefined);
- }
- updateScroll(phase = "measure") {
- let needsMeasurement = Boolean(this.options.layoutScroll && this.instance);
- if (this.scroll &&
- this.scroll.animationId === this.root.animationId &&
- this.scroll.phase === phase) {
- needsMeasurement = false;
- }
- if (needsMeasurement) {
- const isRoot = checkIsScrollRoot(this.instance);
- this.scroll = {
- animationId: this.root.animationId,
- phase,
- isRoot,
- offset: measureScroll(this.instance),
- wasRoot: this.scroll ? this.scroll.isRoot : isRoot,
- };
- }
- }
- resetTransform() {
- if (!resetTransform)
- return;
- const isResetRequested = this.isLayoutDirty ||
- this.shouldResetTransform ||
- this.options.alwaysMeasureLayout;
- const hasProjection = this.projectionDelta && !isDeltaZero(this.projectionDelta);
- const transformTemplate = this.getTransformTemplate();
- const transformTemplateValue = transformTemplate
- ? transformTemplate(this.latestValues, "")
- : undefined;
- const transformTemplateHasChanged = transformTemplateValue !== this.prevTransformTemplateValue;
- if (isResetRequested &&
- (hasProjection ||
- hasTransform(this.latestValues) ||
- transformTemplateHasChanged)) {
- resetTransform(this.instance, transformTemplateValue);
- this.shouldResetTransform = false;
- this.scheduleRender();
- }
- }
- measure(removeTransform = true) {
- const pageBox = this.measurePageBox();
- let layoutBox = this.removeElementScroll(pageBox);
- /**
- * Measurements taken during the pre-render stage
- * still have transforms applied so we remove them
- * via calculation.
- */
- if (removeTransform) {
- layoutBox = this.removeTransform(layoutBox);
- }
- roundBox(layoutBox);
- return {
- animationId: this.root.animationId,
- measuredBox: pageBox,
- layoutBox,
- latestValues: {},
- source: this.id,
- };
- }
- measurePageBox() {
- const { visualElement } = this.options;
- if (!visualElement)
- return createBox();
- const box = visualElement.measureViewportBox();
- const wasInScrollRoot = this.scroll?.wasRoot || this.path.some(checkNodeWasScrollRoot);
- if (!wasInScrollRoot) {
- // Remove viewport scroll to give page-relative coordinates
- const { scroll } = this.root;
- if (scroll) {
- translateAxis(box.x, scroll.offset.x);
- translateAxis(box.y, scroll.offset.y);
- }
- }
- return box;
- }
- removeElementScroll(box) {
- const boxWithoutScroll = createBox();
- copyBoxInto(boxWithoutScroll, box);
- if (this.scroll?.wasRoot) {
- return boxWithoutScroll;
- }
- /**
- * Performance TODO: Keep a cumulative scroll offset down the tree
- * rather than loop back up the path.
- */
- for (let i = 0; i < this.path.length; i++) {
- const node = this.path[i];
- const { scroll, options } = node;
- if (node !== this.root && scroll && options.layoutScroll) {
- /**
- * If this is a new scroll root, we want to remove all previous scrolls
- * from the viewport box.
- */
- if (scroll.wasRoot) {
- copyBoxInto(boxWithoutScroll, box);
- }
- translateAxis(boxWithoutScroll.x, scroll.offset.x);
- translateAxis(boxWithoutScroll.y, scroll.offset.y);
- }
- }
- return boxWithoutScroll;
- }
- applyTransform(box, transformOnly = false) {
- const withTransforms = createBox();
- copyBoxInto(withTransforms, box);
- for (let i = 0; i < this.path.length; i++) {
- const node = this.path[i];
- if (!transformOnly &&
- node.options.layoutScroll &&
- node.scroll &&
- node !== node.root) {
- transformBox(withTransforms, {
- x: -node.scroll.offset.x,
- y: -node.scroll.offset.y,
- });
- }
- if (!hasTransform(node.latestValues))
- continue;
- transformBox(withTransforms, node.latestValues);
- }
- if (hasTransform(this.latestValues)) {
- transformBox(withTransforms, this.latestValues);
- }
- return withTransforms;
- }
- removeTransform(box) {
- const boxWithoutTransform = createBox();
- copyBoxInto(boxWithoutTransform, box);
- for (let i = 0; i < this.path.length; i++) {
- const node = this.path[i];
- if (!node.instance)
- continue;
- if (!hasTransform(node.latestValues))
- continue;
- hasScale(node.latestValues) && node.updateSnapshot();
- const sourceBox = createBox();
- const nodeBox = node.measurePageBox();
- copyBoxInto(sourceBox, nodeBox);
- removeBoxTransforms(boxWithoutTransform, node.latestValues, node.snapshot ? node.snapshot.layoutBox : undefined, sourceBox);
- }
- if (hasTransform(this.latestValues)) {
- removeBoxTransforms(boxWithoutTransform, this.latestValues);
- }
- return boxWithoutTransform;
- }
- setTargetDelta(delta) {
- this.targetDelta = delta;
- this.root.scheduleUpdateProjection();
- this.isProjectionDirty = true;
- }
- setOptions(options) {
- this.options = {
- ...this.options,
- ...options,
- crossfade: options.crossfade !== undefined ? options.crossfade : true,
- };
- }
- clearMeasurements() {
- this.scroll = undefined;
- this.layout = undefined;
- this.snapshot = undefined;
- this.prevTransformTemplateValue = undefined;
- this.targetDelta = undefined;
- this.target = undefined;
- this.isLayoutDirty = false;
- }
- forceRelativeParentToResolveTarget() {
- if (!this.relativeParent)
- return;
- /**
- * If the parent target isn't up-to-date, force it to update.
- * This is an unfortunate de-optimisation as it means any updating relative
- * projection will cause all the relative parents to recalculate back
- * up the tree.
- */
- if (this.relativeParent.resolvedRelativeTargetAt !==
- motionDom.frameData.timestamp) {
- this.relativeParent.resolveTargetDelta(true);
- }
- }
- resolveTargetDelta(forceRecalculation = false) {
- /**
- * Once the dirty status of nodes has been spread through the tree, we also
- * need to check if we have a shared node of a different depth that has itself
- * been dirtied.
- */
- const lead = this.getLead();
- this.isProjectionDirty || (this.isProjectionDirty = lead.isProjectionDirty);
- this.isTransformDirty || (this.isTransformDirty = lead.isTransformDirty);
- this.isSharedProjectionDirty || (this.isSharedProjectionDirty = lead.isSharedProjectionDirty);
- const isShared = Boolean(this.resumingFrom) || this !== lead;
- /**
- * We don't use transform for this step of processing so we don't
- * need to check whether any nodes have changed transform.
- */
- const canSkip = !(forceRecalculation ||
- (isShared && this.isSharedProjectionDirty) ||
- this.isProjectionDirty ||
- this.parent?.isProjectionDirty ||
- this.attemptToResolveRelativeTarget ||
- this.root.updateBlockedByResize);
- if (canSkip)
- return;
- const { layout, layoutId } = this.options;
- /**
- * If we have no layout, we can't perform projection, so early return
- */
- if (!this.layout || !(layout || layoutId))
- return;
- this.resolvedRelativeTargetAt = motionDom.frameData.timestamp;
- /**
- * If we don't have a targetDelta but do have a layout, we can attempt to resolve
- * a relativeParent. This will allow a component to perform scale correction
- * even if no animation has started.
- */
- if (!this.targetDelta && !this.relativeTarget) {
- const relativeParent = this.getClosestProjectingParent();
- if (relativeParent &&
- relativeParent.layout &&
- this.animationProgress !== 1) {
- this.relativeParent = relativeParent;
- this.forceRelativeParentToResolveTarget();
- this.relativeTarget = createBox();
- this.relativeTargetOrigin = createBox();
- calcRelativePosition(this.relativeTargetOrigin, this.layout.layoutBox, relativeParent.layout.layoutBox);
- copyBoxInto(this.relativeTarget, this.relativeTargetOrigin);
- }
- else {
- this.relativeParent = this.relativeTarget = undefined;
- }
- }
- /**
- * If we have no relative target or no target delta our target isn't valid
- * for this frame.
- */
- if (!this.relativeTarget && !this.targetDelta)
- return;
- /**
- * Lazy-init target data structure
- */
- if (!this.target) {
- this.target = createBox();
- this.targetWithTransforms = createBox();
- }
- /**
- * If we've got a relative box for this component, resolve it into a target relative to the parent.
- */
- if (this.relativeTarget &&
- this.relativeTargetOrigin &&
- this.relativeParent &&
- this.relativeParent.target) {
- this.forceRelativeParentToResolveTarget();
- calcRelativeBox(this.target, this.relativeTarget, this.relativeParent.target);
- /**
- * If we've only got a targetDelta, resolve it into a target
- */
- }
- else if (this.targetDelta) {
- if (Boolean(this.resumingFrom)) {
- // TODO: This is creating a new object every frame
- this.target = this.applyTransform(this.layout.layoutBox);
- }
- else {
- copyBoxInto(this.target, this.layout.layoutBox);
- }
- applyBoxDelta(this.target, this.targetDelta);
- }
- else {
- /**
- * If no target, use own layout as target
- */
- copyBoxInto(this.target, this.layout.layoutBox);
- }
- /**
- * If we've been told to attempt to resolve a relative target, do so.
- */
- if (this.attemptToResolveRelativeTarget) {
- this.attemptToResolveRelativeTarget = false;
- const relativeParent = this.getClosestProjectingParent();
- if (relativeParent &&
- Boolean(relativeParent.resumingFrom) ===
- Boolean(this.resumingFrom) &&
- !relativeParent.options.layoutScroll &&
- relativeParent.target &&
- this.animationProgress !== 1) {
- this.relativeParent = relativeParent;
- this.forceRelativeParentToResolveTarget();
- this.relativeTarget = createBox();
- this.relativeTargetOrigin = createBox();
- calcRelativePosition(this.relativeTargetOrigin, this.target, relativeParent.target);
- copyBoxInto(this.relativeTarget, this.relativeTargetOrigin);
- }
- else {
- this.relativeParent = this.relativeTarget = undefined;
- }
- }
- /**
- * Increase debug counter for resolved target deltas
- */
- if (motionDom.statsBuffer.value) {
- metrics.calculatedTargetDeltas++;
- }
- }
- getClosestProjectingParent() {
- if (!this.parent ||
- hasScale(this.parent.latestValues) ||
- has2DTranslate(this.parent.latestValues)) {
- return undefined;
- }
- if (this.parent.isProjecting()) {
- return this.parent;
- }
- else {
- return this.parent.getClosestProjectingParent();
- }
- }
- isProjecting() {
- return Boolean((this.relativeTarget ||
- this.targetDelta ||
- this.options.layoutRoot) &&
- this.layout);
- }
- calcProjection() {
- const lead = this.getLead();
- const isShared = Boolean(this.resumingFrom) || this !== lead;
- let canSkip = true;
- /**
- * If this is a normal layout animation and neither this node nor its nearest projecting
- * is dirty then we can't skip.
- */
- if (this.isProjectionDirty || this.parent?.isProjectionDirty) {
- canSkip = false;
- }
- /**
- * If this is a shared layout animation and this node's shared projection is dirty then
- * we can't skip.
- */
- if (isShared &&
- (this.isSharedProjectionDirty || this.isTransformDirty)) {
- canSkip = false;
- }
- /**
- * If we have resolved the target this frame we must recalculate the
- * projection to ensure it visually represents the internal calculations.
- */
- if (this.resolvedRelativeTargetAt === motionDom.frameData.timestamp) {
- canSkip = false;
- }
- if (canSkip)
- return;
- const { layout, layoutId } = this.options;
- /**
- * If this section of the tree isn't animating we can
- * delete our target sources for the following frame.
- */
- this.isTreeAnimating = Boolean((this.parent && this.parent.isTreeAnimating) ||
- this.currentAnimation ||
- this.pendingAnimation);
- if (!this.isTreeAnimating) {
- this.targetDelta = this.relativeTarget = undefined;
- }
- if (!this.layout || !(layout || layoutId))
- return;
- /**
- * Reset the corrected box with the latest values from box, as we're then going
- * to perform mutative operations on it.
- */
- copyBoxInto(this.layoutCorrected, this.layout.layoutBox);
- /**
- * Record previous tree scales before updating.
- */
- const prevTreeScaleX = this.treeScale.x;
- const prevTreeScaleY = this.treeScale.y;
- /**
- * Apply all the parent deltas to this box to produce the corrected box. This
- * is the layout box, as it will appear on screen as a result of the transforms of its parents.
- */
- applyTreeDeltas(this.layoutCorrected, this.treeScale, this.path, isShared);
- /**
- * If this layer needs to perform scale correction but doesn't have a target,
- * use the layout as the target.
- */
- if (lead.layout &&
- !lead.target &&
- (this.treeScale.x !== 1 || this.treeScale.y !== 1)) {
- lead.target = lead.layout.layoutBox;
- lead.targetWithTransforms = createBox();
- }
- const { target } = lead;
- if (!target) {
- /**
- * If we don't have a target to project into, but we were previously
- * projecting, we want to remove the stored transform and schedule
- * a render to ensure the elements reflect the removed transform.
- */
- if (this.prevProjectionDelta) {
- this.createProjectionDeltas();
- this.scheduleRender();
- }
- return;
- }
- if (!this.projectionDelta || !this.prevProjectionDelta) {
- this.createProjectionDeltas();
- }
- else {
- copyAxisDeltaInto(this.prevProjectionDelta.x, this.projectionDelta.x);
- copyAxisDeltaInto(this.prevProjectionDelta.y, this.projectionDelta.y);
- }
- /**
- * Update the delta between the corrected box and the target box before user-set transforms were applied.
- * This will allow us to calculate the corrected borderRadius and boxShadow to compensate
- * for our layout reprojection, but still allow them to be scaled correctly by the user.
- * It might be that to simplify this we may want to accept that user-set scale is also corrected
- * and we wouldn't have to keep and calc both deltas, OR we could support a user setting
- * to allow people to choose whether these styles are corrected based on just the
- * layout reprojection or the final bounding box.
- */
- calcBoxDelta(this.projectionDelta, this.layoutCorrected, target, this.latestValues);
- if (this.treeScale.x !== prevTreeScaleX ||
- this.treeScale.y !== prevTreeScaleY ||
- !axisDeltaEquals(this.projectionDelta.x, this.prevProjectionDelta.x) ||
- !axisDeltaEquals(this.projectionDelta.y, this.prevProjectionDelta.y)) {
- this.hasProjected = true;
- this.scheduleRender();
- this.notifyListeners("projectionUpdate", target);
- }
- /**
- * Increase debug counter for recalculated projections
- */
- if (motionDom.statsBuffer.value) {
- metrics.calculatedProjections++;
- }
- }
- hide() {
- this.isVisible = false;
- // TODO: Schedule render
- }
- show() {
- this.isVisible = true;
- // TODO: Schedule render
- }
- scheduleRender(notifyAll = true) {
- this.options.visualElement?.scheduleRender();
- if (notifyAll) {
- const stack = this.getStack();
- stack && stack.scheduleRender();
- }
- if (this.resumingFrom && !this.resumingFrom.instance) {
- this.resumingFrom = undefined;
- }
- }
- createProjectionDeltas() {
- this.prevProjectionDelta = createDelta();
- this.projectionDelta = createDelta();
- this.projectionDeltaWithTransform = createDelta();
- }
- setAnimationOrigin(delta, hasOnlyRelativeTargetChanged = false) {
- const snapshot = this.snapshot;
- const snapshotLatestValues = snapshot
- ? snapshot.latestValues
- : {};
- const mixedValues = { ...this.latestValues };
- const targetDelta = createDelta();
- if (!this.relativeParent ||
- !this.relativeParent.options.layoutRoot) {
- this.relativeTarget = this.relativeTargetOrigin = undefined;
- }
- this.attemptToResolveRelativeTarget = !hasOnlyRelativeTargetChanged;
- const relativeLayout = createBox();
- const snapshotSource = snapshot ? snapshot.source : undefined;
- const layoutSource = this.layout ? this.layout.source : undefined;
- const isSharedLayoutAnimation = snapshotSource !== layoutSource;
- const stack = this.getStack();
- const isOnlyMember = !stack || stack.members.length <= 1;
- const shouldCrossfadeOpacity = Boolean(isSharedLayoutAnimation &&
- !isOnlyMember &&
- this.options.crossfade === true &&
- !this.path.some(hasOpacityCrossfade));
- this.animationProgress = 0;
- let prevRelativeTarget;
- this.mixTargetDelta = (latest) => {
- const progress = latest / 1000;
- mixAxisDelta(targetDelta.x, delta.x, progress);
- mixAxisDelta(targetDelta.y, delta.y, progress);
- this.setTargetDelta(targetDelta);
- if (this.relativeTarget &&
- this.relativeTargetOrigin &&
- this.layout &&
- this.relativeParent &&
- this.relativeParent.layout) {
- calcRelativePosition(relativeLayout, this.layout.layoutBox, this.relativeParent.layout.layoutBox);
- mixBox(this.relativeTarget, this.relativeTargetOrigin, relativeLayout, progress);
- /**
- * If this is an unchanged relative target we can consider the
- * projection not dirty.
- */
- if (prevRelativeTarget &&
- boxEquals(this.relativeTarget, prevRelativeTarget)) {
- this.isProjectionDirty = false;
- }
- if (!prevRelativeTarget)
- prevRelativeTarget = createBox();
- copyBoxInto(prevRelativeTarget, this.relativeTarget);
- }
- if (isSharedLayoutAnimation) {
- this.animationValues = mixedValues;
- mixValues(mixedValues, snapshotLatestValues, this.latestValues, progress, shouldCrossfadeOpacity, isOnlyMember);
- }
- this.root.scheduleUpdateProjection();
- this.scheduleRender();
- this.animationProgress = progress;
- };
- this.mixTargetDelta(this.options.layoutRoot ? 1000 : 0);
- }
- startAnimation(options) {
- this.notifyListeners("animationStart");
- this.currentAnimation && this.currentAnimation.stop();
- if (this.resumingFrom && this.resumingFrom.currentAnimation) {
- this.resumingFrom.currentAnimation.stop();
- }
- if (this.pendingAnimation) {
- motionDom.cancelFrame(this.pendingAnimation);
- this.pendingAnimation = undefined;
- }
- /**
- * Start the animation in the next frame to have a frame with progress 0,
- * where the target is the same as when the animation started, so we can
- * calculate the relative positions correctly for instant transitions.
- */
- this.pendingAnimation = motionDom.frame.update(() => {
- globalProjectionState.hasAnimatedSinceResize = true;
- motionDom.activeAnimations.layout++;
- this.currentAnimation = animateSingleValue(0, animationTarget, {
- ...options,
- onUpdate: (latest) => {
- this.mixTargetDelta(latest);
- options.onUpdate && options.onUpdate(latest);
- },
- onStop: () => {
- motionDom.activeAnimations.layout--;
- },
- onComplete: () => {
- motionDom.activeAnimations.layout--;
- options.onComplete && options.onComplete();
- this.completeAnimation();
- },
- });
- if (this.resumingFrom) {
- this.resumingFrom.currentAnimation = this.currentAnimation;
- }
- this.pendingAnimation = undefined;
- });
- }
- completeAnimation() {
- if (this.resumingFrom) {
- this.resumingFrom.currentAnimation = undefined;
- this.resumingFrom.preserveOpacity = undefined;
- }
- const stack = this.getStack();
- stack && stack.exitAnimationComplete();
- this.resumingFrom =
- this.currentAnimation =
- this.animationValues =
- undefined;
- this.notifyListeners("animationComplete");
- }
- finishAnimation() {
- if (this.currentAnimation) {
- this.mixTargetDelta && this.mixTargetDelta(animationTarget);
- this.currentAnimation.stop();
- }
- this.completeAnimation();
- }
- applyTransformsToTarget() {
- const lead = this.getLead();
- let { targetWithTransforms, target, layout, latestValues } = lead;
- if (!targetWithTransforms || !target || !layout)
- return;
- /**
- * If we're only animating position, and this element isn't the lead element,
- * then instead of projecting into the lead box we instead want to calculate
- * a new target that aligns the two boxes but maintains the layout shape.
- */
- if (this !== lead &&
- this.layout &&
- layout &&
- shouldAnimatePositionOnly(this.options.animationType, this.layout.layoutBox, layout.layoutBox)) {
- target = this.target || createBox();
- const xLength = calcLength(this.layout.layoutBox.x);
- target.x.min = lead.target.x.min;
- target.x.max = target.x.min + xLength;
- const yLength = calcLength(this.layout.layoutBox.y);
- target.y.min = lead.target.y.min;
- target.y.max = target.y.min + yLength;
- }
- copyBoxInto(targetWithTransforms, target);
- /**
- * Apply the latest user-set transforms to the targetBox to produce the targetBoxFinal.
- * This is the final box that we will then project into by calculating a transform delta and
- * applying it to the corrected box.
- */
- transformBox(targetWithTransforms, latestValues);
- /**
- * Update the delta between the corrected box and the final target box, after
- * user-set transforms are applied to it. This will be used by the renderer to
- * create a transform style that will reproject the element from its layout layout
- * into the desired bounding box.
- */
- calcBoxDelta(this.projectionDeltaWithTransform, this.layoutCorrected, targetWithTransforms, latestValues);
- }
- registerSharedNode(layoutId, node) {
- if (!this.sharedNodes.has(layoutId)) {
- this.sharedNodes.set(layoutId, new NodeStack());
- }
- const stack = this.sharedNodes.get(layoutId);
- stack.add(node);
- const config = node.options.initialPromotionConfig;
- node.promote({
- transition: config ? config.transition : undefined,
- preserveFollowOpacity: config && config.shouldPreserveFollowOpacity
- ? config.shouldPreserveFollowOpacity(node)
- : undefined,
- });
- }
- isLead() {
- const stack = this.getStack();
- return stack ? stack.lead === this : true;
- }
- getLead() {
- const { layoutId } = this.options;
- return layoutId ? this.getStack()?.lead || this : this;
- }
- getPrevLead() {
- const { layoutId } = this.options;
- return layoutId ? this.getStack()?.prevLead : undefined;
- }
- getStack() {
- const { layoutId } = this.options;
- if (layoutId)
- return this.root.sharedNodes.get(layoutId);
- }
- promote({ needsReset, transition, preserveFollowOpacity, } = {}) {
- const stack = this.getStack();
- if (stack)
- stack.promote(this, preserveFollowOpacity);
- if (needsReset) {
- this.projectionDelta = undefined;
- this.needsReset = true;
- }
- if (transition)
- this.setOptions({ transition });
- }
- relegate() {
- const stack = this.getStack();
- if (stack) {
- return stack.relegate(this);
- }
- else {
- return false;
- }
- }
- resetSkewAndRotation() {
- const { visualElement } = this.options;
- if (!visualElement)
- return;
- // If there's no detected skew or rotation values, we can early return without a forced render.
- let hasDistortingTransform = false;
- /**
- * An unrolled check for rotation values. Most elements don't have any rotation and
- * skipping the nested loop and new object creation is 50% faster.
- */
- const { latestValues } = visualElement;
- if (latestValues.z ||
- latestValues.rotate ||
- latestValues.rotateX ||
- latestValues.rotateY ||
- latestValues.rotateZ ||
- latestValues.skewX ||
- latestValues.skewY) {
- hasDistortingTransform = true;
- }
- // If there's no distorting values, we don't need to do any more.
- if (!hasDistortingTransform)
- return;
- const resetValues = {};
- if (latestValues.z) {
- resetDistortingTransform("z", visualElement, resetValues, this.animationValues);
- }
- // Check the skew and rotate value of all axes and reset to 0
- for (let i = 0; i < transformAxes.length; i++) {
- resetDistortingTransform(`rotate${transformAxes[i]}`, visualElement, resetValues, this.animationValues);
- resetDistortingTransform(`skew${transformAxes[i]}`, visualElement, resetValues, this.animationValues);
- }
- // Force a render of this element to apply the transform with all skews and rotations
- // set to 0.
- visualElement.render();
- // Put back all the values we reset
- for (const key in resetValues) {
- visualElement.setStaticValue(key, resetValues[key]);
- if (this.animationValues) {
- this.animationValues[key] = resetValues[key];
- }
- }
- // Schedule a render for the next frame. This ensures we won't visually
- // see the element with the reset rotate value applied.
- visualElement.scheduleRender();
- }
- getProjectionStyles(styleProp) {
- if (!this.instance || this.isSVG)
- return undefined;
- if (!this.isVisible) {
- return hiddenVisibility;
- }
- const styles = {
- visibility: "",
- };
- const transformTemplate = this.getTransformTemplate();
- if (this.needsReset) {
- this.needsReset = false;
- styles.opacity = "";
- styles.pointerEvents =
- resolveMotionValue(styleProp?.pointerEvents) || "";
- styles.transform = transformTemplate
- ? transformTemplate(this.latestValues, "")
- : "none";
- return styles;
- }
- const lead = this.getLead();
- if (!this.projectionDelta || !this.layout || !lead.target) {
- const emptyStyles = {};
- if (this.options.layoutId) {
- emptyStyles.opacity =
- this.latestValues.opacity !== undefined
- ? this.latestValues.opacity
- : 1;
- emptyStyles.pointerEvents =
- resolveMotionValue(styleProp?.pointerEvents) || "";
- }
- if (this.hasProjected && !hasTransform(this.latestValues)) {
- emptyStyles.transform = transformTemplate
- ? transformTemplate({}, "")
- : "none";
- this.hasProjected = false;
- }
- return emptyStyles;
- }
- const valuesToRender = lead.animationValues || lead.latestValues;
- this.applyTransformsToTarget();
- styles.transform = buildProjectionTransform(this.projectionDeltaWithTransform, this.treeScale, valuesToRender);
- if (transformTemplate) {
- styles.transform = transformTemplate(valuesToRender, styles.transform);
- }
- const { x, y } = this.projectionDelta;
- styles.transformOrigin = `${x.origin * 100}% ${y.origin * 100}% 0`;
- if (lead.animationValues) {
- /**
- * If the lead component is animating, assign this either the entering/leaving
- * opacity
- */
- styles.opacity =
- lead === this
- ? valuesToRender.opacity ??
- this.latestValues.opacity ??
- 1
- : this.preserveOpacity
- ? this.latestValues.opacity
- : valuesToRender.opacityExit;
- }
- else {
- /**
- * Or we're not animating at all, set the lead component to its layout
- * opacity and other components to hidden.
- */
- styles.opacity =
- lead === this
- ? valuesToRender.opacity !== undefined
- ? valuesToRender.opacity
- : ""
- : valuesToRender.opacityExit !== undefined
- ? valuesToRender.opacityExit
- : 0;
- }
- /**
- * Apply scale correction
- */
- for (const key in scaleCorrectors) {
- if (valuesToRender[key] === undefined)
- continue;
- const { correct, applyTo, isCSSVariable } = scaleCorrectors[key];
- /**
- * Only apply scale correction to the value if we have an
- * active projection transform. Otherwise these values become
- * vulnerable to distortion if the element changes size without
- * a corresponding layout animation.
- */
- const corrected = styles.transform === "none"
- ? valuesToRender[key]
- : correct(valuesToRender[key], lead);
- if (applyTo) {
- const num = applyTo.length;
- for (let i = 0; i < num; i++) {
- styles[applyTo[i]] = corrected;
- }
- }
- else {
- // If this is a CSS variable, set it directly on the instance.
- // Replacing this function from creating styles to setting them
- // would be a good place to remove per frame object creation
- if (isCSSVariable) {
- this.options.visualElement.renderState.vars[key] = corrected;
- }
- else {
- styles[key] = corrected;
- }
- }
- }
- /**
- * Disable pointer events on follow components. This is to ensure
- * that if a follow component covers a lead component it doesn't block
- * pointer events on the lead.
- */
- if (this.options.layoutId) {
- styles.pointerEvents =
- lead === this
- ? resolveMotionValue(styleProp?.pointerEvents) || ""
- : "none";
- }
- return styles;
- }
- clearSnapshot() {
- this.resumeFrom = this.snapshot = undefined;
- }
- // Only run on root
- resetTree() {
- this.root.nodes.forEach((node) => node.currentAnimation?.stop());
- this.root.nodes.forEach(clearMeasurements);
- this.root.sharedNodes.clear();
- }
- };
- }
- function updateLayout(node) {
- node.updateLayout();
- }
- function notifyLayoutUpdate(node) {
- const snapshot = node.resumeFrom?.snapshot || node.snapshot;
- if (node.isLead() &&
- node.layout &&
- snapshot &&
- node.hasListeners("didUpdate")) {
- const { layoutBox: layout, measuredBox: measuredLayout } = node.layout;
- const { animationType } = node.options;
- const isShared = snapshot.source !== node.layout.source;
- // TODO Maybe we want to also resize the layout snapshot so we don't trigger
- // animations for instance if layout="size" and an element has only changed position
- if (animationType === "size") {
- eachAxis((axis) => {
- const axisSnapshot = isShared
- ? snapshot.measuredBox[axis]
- : snapshot.layoutBox[axis];
- const length = calcLength(axisSnapshot);
- axisSnapshot.min = layout[axis].min;
- axisSnapshot.max = axisSnapshot.min + length;
- });
- }
- else if (shouldAnimatePositionOnly(animationType, snapshot.layoutBox, layout)) {
- eachAxis((axis) => {
- const axisSnapshot = isShared
- ? snapshot.measuredBox[axis]
- : snapshot.layoutBox[axis];
- const length = calcLength(layout[axis]);
- axisSnapshot.max = axisSnapshot.min + length;
- /**
- * Ensure relative target gets resized and rerendererd
- */
- if (node.relativeTarget && !node.currentAnimation) {
- node.isProjectionDirty = true;
- node.relativeTarget[axis].max =
- node.relativeTarget[axis].min + length;
- }
- });
- }
- const layoutDelta = createDelta();
- calcBoxDelta(layoutDelta, layout, snapshot.layoutBox);
- const visualDelta = createDelta();
- if (isShared) {
- calcBoxDelta(visualDelta, node.applyTransform(measuredLayout, true), snapshot.measuredBox);
- }
- else {
- calcBoxDelta(visualDelta, layout, snapshot.layoutBox);
- }
- const hasLayoutChanged = !isDeltaZero(layoutDelta);
- let hasRelativeLayoutChanged = false;
- if (!node.resumeFrom) {
- const relativeParent = node.getClosestProjectingParent();
- /**
- * If the relativeParent is itself resuming from a different element then
- * the relative snapshot is not relavent
- */
- if (relativeParent && !relativeParent.resumeFrom) {
- const { snapshot: parentSnapshot, layout: parentLayout } = relativeParent;
- if (parentSnapshot && parentLayout) {
- const relativeSnapshot = createBox();
- calcRelativePosition(relativeSnapshot, snapshot.layoutBox, parentSnapshot.layoutBox);
- const relativeLayout = createBox();
- calcRelativePosition(relativeLayout, layout, parentLayout.layoutBox);
- if (!boxEqualsRounded(relativeSnapshot, relativeLayout)) {
- hasRelativeLayoutChanged = true;
- }
- if (relativeParent.options.layoutRoot) {
- node.relativeTarget = relativeLayout;
- node.relativeTargetOrigin = relativeSnapshot;
- node.relativeParent = relativeParent;
- }
- }
- }
- }
- node.notifyListeners("didUpdate", {
- layout,
- snapshot,
- delta: visualDelta,
- layoutDelta,
- hasLayoutChanged,
- hasRelativeLayoutChanged,
- });
- }
- else if (node.isLead()) {
- const { onExitComplete } = node.options;
- onExitComplete && onExitComplete();
- }
- /**
- * Clearing transition
- * TODO: Investigate why this transition is being passed in as {type: false } from Framer
- * and why we need it at all
- */
- node.options.transition = undefined;
- }
- function propagateDirtyNodes(node) {
- /**
- * Increase debug counter for nodes encountered this frame
- */
- if (motionDom.statsBuffer.value) {
- metrics.nodes++;
- }
- if (!node.parent)
- return;
- /**
- * If this node isn't projecting, propagate isProjectionDirty. It will have
- * no performance impact but it will allow the next child that *is* projecting
- * but *isn't* dirty to just check its parent to see if *any* ancestor needs
- * correcting.
- */
- if (!node.isProjecting()) {
- node.isProjectionDirty = node.parent.isProjectionDirty;
- }
- /**
- * Propagate isSharedProjectionDirty and isTransformDirty
- * throughout the whole tree. A future revision can take another look at
- * this but for safety we still recalcualte shared nodes.
- */
- node.isSharedProjectionDirty || (node.isSharedProjectionDirty = Boolean(node.isProjectionDirty ||
- node.parent.isProjectionDirty ||
- node.parent.isSharedProjectionDirty));
- node.isTransformDirty || (node.isTransformDirty = node.parent.isTransformDirty);
- }
- function cleanDirtyNodes(node) {
- node.isProjectionDirty =
- node.isSharedProjectionDirty =
- node.isTransformDirty =
- false;
- }
- function clearSnapshot(node) {
- node.clearSnapshot();
- }
- function clearMeasurements(node) {
- node.clearMeasurements();
- }
- function clearIsLayoutDirty(node) {
- node.isLayoutDirty = false;
- }
- function resetTransformStyle(node) {
- const { visualElement } = node.options;
- if (visualElement && visualElement.getProps().onBeforeLayoutMeasure) {
- visualElement.notify("BeforeLayoutMeasure");
- }
- node.resetTransform();
- }
- function finishAnimation(node) {
- node.finishAnimation();
- node.targetDelta = node.relativeTarget = node.target = undefined;
- node.isProjectionDirty = true;
- }
- function resolveTargetDelta(node) {
- node.resolveTargetDelta();
- }
- function calcProjection(node) {
- node.calcProjection();
- }
- function resetSkewAndRotation(node) {
- node.resetSkewAndRotation();
- }
- function removeLeadSnapshots(stack) {
- stack.removeLeadSnapshot();
- }
- function mixAxisDelta(output, delta, p) {
- output.translate = mixNumber$1(delta.translate, 0, p);
- output.scale = mixNumber$1(delta.scale, 1, p);
- output.origin = delta.origin;
- output.originPoint = delta.originPoint;
- }
- function mixAxis(output, from, to, p) {
- output.min = mixNumber$1(from.min, to.min, p);
- output.max = mixNumber$1(from.max, to.max, p);
- }
- function mixBox(output, from, to, p) {
- mixAxis(output.x, from.x, to.x, p);
- mixAxis(output.y, from.y, to.y, p);
- }
- function hasOpacityCrossfade(node) {
- return (node.animationValues && node.animationValues.opacityExit !== undefined);
- }
- const defaultLayoutTransition = {
- duration: 0.45,
- ease: [0.4, 0, 0.1, 1],
- };
- const userAgentContains = (string) => typeof navigator !== "undefined" &&
- navigator.userAgent &&
- navigator.userAgent.toLowerCase().includes(string);
- /**
- * Measured bounding boxes must be rounded in Safari and
- * left untouched in Chrome, otherwise non-integer layouts within scaled-up elements
- * can appear to jump.
- */
- const roundPoint = userAgentContains("applewebkit/") && !userAgentContains("chrome/")
- ? Math.round
- : motionUtils.noop;
- function roundAxis(axis) {
- // Round to the nearest .5 pixels to support subpixel layouts
- axis.min = roundPoint(axis.min);
- axis.max = roundPoint(axis.max);
- }
- function roundBox(box) {
- roundAxis(box.x);
- roundAxis(box.y);
- }
- function shouldAnimatePositionOnly(animationType, snapshot, layout) {
- return (animationType === "position" ||
- (animationType === "preserve-aspect" &&
- !isNear(aspectRatio(snapshot), aspectRatio(layout), 0.2)));
- }
- function checkNodeWasScrollRoot(node) {
- return node !== node.root && node.scroll?.wasRoot;
- }
- function addDomEvent(target, eventName, handler, options = { passive: true }) {
- target.addEventListener(eventName, handler, options);
- return () => target.removeEventListener(eventName, handler);
- }
- const DocumentProjectionNode = createProjectionNode$1({
- attachResizeListener: (ref, notify) => addDomEvent(ref, "resize", notify),
- measureScroll: () => ({
- x: document.documentElement.scrollLeft || document.body.scrollLeft,
- y: document.documentElement.scrollTop || document.body.scrollTop,
- }),
- checkIsScrollRoot: () => true,
- });
- const rootProjectionNode = {
- current: undefined,
- };
- const HTMLProjectionNode = createProjectionNode$1({
- measureScroll: (instance) => ({
- x: instance.scrollLeft,
- y: instance.scrollTop,
- }),
- defaultParent: () => {
- if (!rootProjectionNode.current) {
- const documentNode = new DocumentProjectionNode({});
- documentNode.mount(window);
- documentNode.setOptions({ layoutScroll: true });
- rootProjectionNode.current = documentNode;
- }
- return rootProjectionNode.current;
- },
- resetTransform: (instance, value) => {
- instance.style.transform = value !== undefined ? value : "none";
- },
- checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
- });
- function pixelsToPercent(pixels, axis) {
- if (axis.max === axis.min)
- return 0;
- return (pixels / (axis.max - axis.min)) * 100;
- }
- /**
- * We always correct borderRadius as a percentage rather than pixels to reduce paints.
- * For example, if you are projecting a box that is 100px wide with a 10px borderRadius
- * into a box that is 200px wide with a 20px borderRadius, that is actually a 10%
- * borderRadius in both states. If we animate between the two in pixels that will trigger
- * a paint each time. If we animate between the two in percentage we'll avoid a paint.
- */
- const correctBorderRadius = {
- correct: (latest, node) => {
- if (!node.target)
- return latest;
- /**
- * If latest is a string, if it's a percentage we can return immediately as it's
- * going to be stretched appropriately. Otherwise, if it's a pixel, convert it to a number.
- */
- if (typeof latest === "string") {
- if (px.test(latest)) {
- latest = parseFloat(latest);
- }
- else {
- return latest;
- }
- }
- /**
- * If latest is a number, it's a pixel value. We use the current viewportBox to calculate that
- * pixel value as a percentage of each axis
- */
- const x = pixelsToPercent(latest, node.target.x);
- const y = pixelsToPercent(latest, node.target.y);
- return `${x}% ${y}%`;
- },
- };
- const correctBoxShadow = {
- correct: (latest, { treeScale, projectionDelta }) => {
- const original = latest;
- const shadow = complex.parse(latest);
- // TODO: Doesn't support multiple shadows
- if (shadow.length > 5)
- return original;
- const template = complex.createTransformer(latest);
- const offset = typeof shadow[0] !== "number" ? 1 : 0;
- // Calculate the overall context scale
- const xScale = projectionDelta.x.scale * treeScale.x;
- const yScale = projectionDelta.y.scale * treeScale.y;
- shadow[0 + offset] /= xScale;
- shadow[1 + offset] /= yScale;
- /**
- * Ideally we'd correct x and y scales individually, but because blur and
- * spread apply to both we have to take a scale average and apply that instead.
- * We could potentially improve the outcome of this by incorporating the ratio between
- * the two scales.
- */
- const averageScale = mixNumber$1(xScale, yScale, 0.5);
- // Blur
- if (typeof shadow[2 + offset] === "number")
- shadow[2 + offset] /= averageScale;
- // Spread
- if (typeof shadow[3 + offset] === "number")
- shadow[3 + offset] /= averageScale;
- return template(shadow);
- },
- };
- /**
- * Bounding boxes tend to be defined as top, left, right, bottom. For various operations
- * it's easier to consider each axis individually. This function returns a bounding box
- * as a map of single-axis min/max values.
- */
- function convertBoundingBoxToBox({ top, left, right, bottom, }) {
- return {
- x: { min: left, max: right },
- y: { min: top, max: bottom },
- };
- }
- function convertBoxToBoundingBox({ x, y }) {
- return { top: y.min, right: x.max, bottom: y.max, left: x.min };
- }
- /**
- * Applies a TransformPoint function to a bounding box. TransformPoint is usually a function
- * provided by Framer to allow measured points to be corrected for device scaling. This is used
- * when measuring DOM elements and DOM event points.
- */
- function transformBoxPoints(point, transformPoint) {
- if (!transformPoint)
- return point;
- const topLeft = transformPoint({ x: point.left, y: point.top });
- const bottomRight = transformPoint({ x: point.right, y: point.bottom });
- return {
- top: topLeft.y,
- left: topLeft.x,
- bottom: bottomRight.y,
- right: bottomRight.x,
- };
- }
- function measureViewportBox(instance, transformPoint) {
- return convertBoundingBoxToBox(transformBoxPoints(instance.getBoundingClientRect(), transformPoint));
- }
- function measurePageBox(element, rootProjectionNode, transformPagePoint) {
- const viewportBox = measureViewportBox(element, transformPagePoint);
- const { scroll } = rootProjectionNode;
- if (scroll) {
- translateAxis(viewportBox.x, scroll.offset.x);
- translateAxis(viewportBox.y, scroll.offset.y);
- }
- return viewportBox;
- }
- const featureProps = {
- animation: [
- "animate",
- "variants",
- "whileHover",
- "whileTap",
- "exit",
- "whileInView",
- "whileFocus",
- "whileDrag",
- ],
- exit: ["exit"],
- drag: ["drag", "dragControls"],
- focus: ["whileFocus"],
- hover: ["whileHover", "onHoverStart", "onHoverEnd"],
- tap: ["whileTap", "onTap", "onTapStart", "onTapCancel"],
- pan: ["onPan", "onPanStart", "onPanSessionStart", "onPanEnd"],
- inView: ["whileInView", "onViewportEnter", "onViewportLeave"],
- layout: ["layout", "layoutId"],
- };
- const featureDefinitions = {};
- for (const key in featureProps) {
- featureDefinitions[key] = {
- isEnabled: (props) => featureProps[key].some((name) => !!props[name]),
- };
- }
- // Does this device prefer reduced motion? Returns `null` server-side.
- const prefersReducedMotion = { current: null };
- const hasReducedMotionListener = { current: false };
- function initPrefersReducedMotion() {
- hasReducedMotionListener.current = true;
- if (!isBrowser)
- return;
- if (window.matchMedia) {
- const motionMediaQuery = window.matchMedia("(prefers-reduced-motion)");
- const setReducedMotionPreferences = () => (prefersReducedMotion.current = motionMediaQuery.matches);
- motionMediaQuery.addListener(setReducedMotionPreferences);
- setReducedMotionPreferences();
- }
- else {
- prefersReducedMotion.current = false;
- }
- }
- /**
- * A list of all ValueTypes
- */
- const valueTypes = [...dimensionValueTypes, color, complex];
- /**
- * Tests a value against the list of ValueTypes
- */
- const findValueType = (v) => valueTypes.find(testValueType(v));
- const visualElementStore = new WeakMap();
- function isAnimationControls(v) {
- return (v !== null &&
- typeof v === "object" &&
- typeof v.start === "function");
- }
- /**
- * Decides if the supplied variable is variant label
- */
- function isVariantLabel(v) {
- return typeof v === "string" || Array.isArray(v);
- }
- const variantPriorityOrder = [
- "animate",
- "whileInView",
- "whileFocus",
- "whileHover",
- "whileTap",
- "whileDrag",
- "exit",
- ];
- const variantProps = ["initial", ...variantPriorityOrder];
- function isControllingVariants(props) {
- return (isAnimationControls(props.animate) ||
- variantProps.some((name) => isVariantLabel(props[name])));
- }
- function isVariantNode(props) {
- return Boolean(isControllingVariants(props) || props.variants);
- }
- function updateMotionValuesFromProps(element, next, prev) {
- for (const key in next) {
- const nextValue = next[key];
- const prevValue = prev[key];
- if (isMotionValue(nextValue)) {
- /**
- * If this is a motion value found in props or style, we want to add it
- * to our visual element's motion value map.
- */
- element.addValue(key, nextValue);
- /**
- * Check the version of the incoming motion value with this version
- * and warn against mismatches.
- */
- if (process.env.NODE_ENV === "development") {
- motionUtils.warnOnce(nextValue.version === "12.7.3", `Attempting to mix Motion versions ${nextValue.version} with 12.7.3 may not work as expected.`);
- }
- }
- else if (isMotionValue(prevValue)) {
- /**
- * If we're swapping from a motion value to a static value,
- * create a new motion value from that
- */
- element.addValue(key, motionDom.motionValue(nextValue, { owner: element }));
- }
- else if (prevValue !== nextValue) {
- /**
- * If this is a flat value that has changed, update the motion value
- * or create one if it doesn't exist. We only want to do this if we're
- * not handling the value with our animation state.
- */
- if (element.hasValue(key)) {
- const existingValue = element.getValue(key);
- if (existingValue.liveStyle === true) {
- existingValue.jump(nextValue);
- }
- else if (!existingValue.hasAnimated) {
- existingValue.set(nextValue);
- }
- }
- else {
- const latestValue = element.getStaticValue(key);
- element.addValue(key, motionDom.motionValue(latestValue !== undefined ? latestValue : nextValue, { owner: element }));
- }
- }
- }
- // Handle removed values
- for (const key in prev) {
- if (next[key] === undefined)
- element.removeValue(key);
- }
- return next;
- }
- function getValueState(visualElement) {
- const state = [{}, {}];
- visualElement?.values.forEach((value, key) => {
- state[0][key] = value.get();
- state[1][key] = value.getVelocity();
- });
- return state;
- }
- function resolveVariantFromProps(props, definition, custom, visualElement) {
- /**
- * If the variant definition is a function, resolve.
- */
- if (typeof definition === "function") {
- const [current, velocity] = getValueState(visualElement);
- definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
- }
- /**
- * If the variant definition is a variant label, or
- * the function returned a variant label, resolve.
- */
- if (typeof definition === "string") {
- definition = props.variants && props.variants[definition];
- }
- /**
- * At this point we've resolved both functions and variant labels,
- * but the resolved variant label might itself have been a function.
- * If so, resolve. This can only have returned a valid target object.
- */
- if (typeof definition === "function") {
- const [current, velocity] = getValueState(visualElement);
- definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
- }
- return definition;
- }
- const propEventHandlers = [
- "AnimationStart",
- "AnimationComplete",
- "Update",
- "BeforeLayoutMeasure",
- "LayoutMeasure",
- "LayoutAnimationStart",
- "LayoutAnimationComplete",
- ];
- /**
- * A VisualElement is an imperative abstraction around UI elements such as
- * HTMLElement, SVGElement, Three.Object3D etc.
- */
- class VisualElement {
- /**
- * This method takes React props and returns found MotionValues. For example, HTML
- * MotionValues will be found within the style prop, whereas for Three.js within attribute arrays.
- *
- * This isn't an abstract method as it needs calling in the constructor, but it is
- * intended to be one.
- */
- scrapeMotionValuesFromProps(_props, _prevProps, _visualElement) {
- return {};
- }
- constructor({ parent, props, presenceContext, reducedMotionConfig, blockInitialAnimation, visualState, }, options = {}) {
- /**
- * A reference to the current underlying Instance, e.g. a HTMLElement
- * or Three.Mesh etc.
- */
- this.current = null;
- /**
- * A set containing references to this VisualElement's children.
- */
- this.children = new Set();
- /**
- * Determine what role this visual element should take in the variant tree.
- */
- this.isVariantNode = false;
- this.isControllingVariants = false;
- /**
- * Decides whether this VisualElement should animate in reduced motion
- * mode.
- *
- * TODO: This is currently set on every individual VisualElement but feels
- * like it could be set globally.
- */
- this.shouldReduceMotion = null;
- /**
- * A map of all motion values attached to this visual element. Motion
- * values are source of truth for any given animated value. A motion
- * value might be provided externally by the component via props.
- */
- this.values = new Map();
- this.KeyframeResolver = KeyframeResolver;
- /**
- * Cleanup functions for active features (hover/tap/exit etc)
- */
- this.features = {};
- /**
- * A map of every subscription that binds the provided or generated
- * motion values onChange listeners to this visual element.
- */
- this.valueSubscriptions = new Map();
- /**
- * A reference to the previously-provided motion values as returned
- * from scrapeMotionValuesFromProps. We use the keys in here to determine
- * if any motion values need to be removed after props are updated.
- */
- this.prevMotionValues = {};
- /**
- * An object containing a SubscriptionManager for each active event.
- */
- this.events = {};
- /**
- * An object containing an unsubscribe function for each prop event subscription.
- * For example, every "Update" event can have multiple subscribers via
- * VisualElement.on(), but only one of those can be defined via the onUpdate prop.
- */
- this.propEventSubscriptions = {};
- this.notifyUpdate = () => this.notify("Update", this.latestValues);
- this.render = () => {
- if (!this.current)
- return;
- this.triggerBuild();
- this.renderInstance(this.current, this.renderState, this.props.style, this.projection);
- };
- this.renderScheduledAt = 0.0;
- this.scheduleRender = () => {
- const now = motionDom.time.now();
- if (this.renderScheduledAt < now) {
- this.renderScheduledAt = now;
- motionDom.frame.render(this.render, false, true);
- }
- };
- const { latestValues, renderState, onUpdate } = visualState;
- this.onUpdate = onUpdate;
- this.latestValues = latestValues;
- this.baseTarget = { ...latestValues };
- this.initialValues = props.initial ? { ...latestValues } : {};
- this.renderState = renderState;
- this.parent = parent;
- this.props = props;
- this.presenceContext = presenceContext;
- this.depth = parent ? parent.depth + 1 : 0;
- this.reducedMotionConfig = reducedMotionConfig;
- this.options = options;
- this.blockInitialAnimation = Boolean(blockInitialAnimation);
- this.isControllingVariants = isControllingVariants(props);
- this.isVariantNode = isVariantNode(props);
- if (this.isVariantNode) {
- this.variantChildren = new Set();
- }
- this.manuallyAnimateOnMount = Boolean(parent && parent.current);
- /**
- * Any motion values that are provided to the element when created
- * aren't yet bound to the element, as this would technically be impure.
- * However, we iterate through the motion values and set them to the
- * initial values for this component.
- *
- * TODO: This is impure and we should look at changing this to run on mount.
- * Doing so will break some tests but this isn't necessarily a breaking change,
- * more a reflection of the test.
- */
- const { willChange, ...initialMotionValues } = this.scrapeMotionValuesFromProps(props, {}, this);
- for (const key in initialMotionValues) {
- const value = initialMotionValues[key];
- if (latestValues[key] !== undefined && isMotionValue(value)) {
- value.set(latestValues[key], false);
- }
- }
- }
- mount(instance) {
- this.current = instance;
- visualElementStore.set(instance, this);
- if (this.projection && !this.projection.instance) {
- this.projection.mount(instance);
- }
- if (this.parent && this.isVariantNode && !this.isControllingVariants) {
- this.removeFromVariantTree = this.parent.addVariantChild(this);
- }
- this.values.forEach((value, key) => this.bindToMotionValue(key, value));
- if (!hasReducedMotionListener.current) {
- initPrefersReducedMotion();
- }
- this.shouldReduceMotion =
- this.reducedMotionConfig === "never"
- ? false
- : this.reducedMotionConfig === "always"
- ? true
- : prefersReducedMotion.current;
- if (process.env.NODE_ENV !== "production") {
- motionUtils.warnOnce(this.shouldReduceMotion !== true, "You have Reduced Motion enabled on your device. Animations may not appear as expected.");
- }
- if (this.parent)
- this.parent.children.add(this);
- this.update(this.props, this.presenceContext);
- }
- unmount() {
- this.projection && this.projection.unmount();
- motionDom.cancelFrame(this.notifyUpdate);
- motionDom.cancelFrame(this.render);
- this.valueSubscriptions.forEach((remove) => remove());
- this.valueSubscriptions.clear();
- this.removeFromVariantTree && this.removeFromVariantTree();
- this.parent && this.parent.children.delete(this);
- for (const key in this.events) {
- this.events[key].clear();
- }
- for (const key in this.features) {
- const feature = this.features[key];
- if (feature) {
- feature.unmount();
- feature.isMounted = false;
- }
- }
- this.current = null;
- }
- bindToMotionValue(key, value) {
- if (this.valueSubscriptions.has(key)) {
- this.valueSubscriptions.get(key)();
- }
- const valueIsTransform = transformProps.has(key);
- if (valueIsTransform && this.onBindTransform) {
- this.onBindTransform();
- }
- const removeOnChange = value.on("change", (latestValue) => {
- this.latestValues[key] = latestValue;
- this.props.onUpdate && motionDom.frame.preRender(this.notifyUpdate);
- if (valueIsTransform && this.projection) {
- this.projection.isTransformDirty = true;
- }
- });
- const removeOnRenderRequest = value.on("renderRequest", this.scheduleRender);
- let removeSyncCheck;
- if (window.MotionCheckAppearSync) {
- removeSyncCheck = window.MotionCheckAppearSync(this, key, value);
- }
- this.valueSubscriptions.set(key, () => {
- removeOnChange();
- removeOnRenderRequest();
- if (removeSyncCheck)
- removeSyncCheck();
- if (value.owner)
- value.stop();
- });
- }
- sortNodePosition(other) {
- /**
- * If these nodes aren't even of the same type we can't compare their depth.
- */
- if (!this.current ||
- !this.sortInstanceNodePosition ||
- this.type !== other.type) {
- return 0;
- }
- return this.sortInstanceNodePosition(this.current, other.current);
- }
- updateFeatures() {
- let key = "animation";
- for (key in featureDefinitions) {
- const featureDefinition = featureDefinitions[key];
- if (!featureDefinition)
- continue;
- const { isEnabled, Feature: FeatureConstructor } = featureDefinition;
- /**
- * If this feature is enabled but not active, make a new instance.
- */
- if (!this.features[key] &&
- FeatureConstructor &&
- isEnabled(this.props)) {
- this.features[key] = new FeatureConstructor(this);
- }
- /**
- * If we have a feature, mount or update it.
- */
- if (this.features[key]) {
- const feature = this.features[key];
- if (feature.isMounted) {
- feature.update();
- }
- else {
- feature.mount();
- feature.isMounted = true;
- }
- }
- }
- }
- triggerBuild() {
- this.build(this.renderState, this.latestValues, this.props);
- }
- /**
- * Measure the current viewport box with or without transforms.
- * Only measures axis-aligned boxes, rotate and skew must be manually
- * removed with a re-render to work.
- */
- measureViewportBox() {
- return this.current
- ? this.measureInstanceViewportBox(this.current, this.props)
- : createBox();
- }
- getStaticValue(key) {
- return this.latestValues[key];
- }
- setStaticValue(key, value) {
- this.latestValues[key] = value;
- }
- /**
- * Update the provided props. Ensure any newly-added motion values are
- * added to our map, old ones removed, and listeners updated.
- */
- update(props, presenceContext) {
- if (props.transformTemplate || this.props.transformTemplate) {
- this.scheduleRender();
- }
- this.prevProps = this.props;
- this.props = props;
- this.prevPresenceContext = this.presenceContext;
- this.presenceContext = presenceContext;
- /**
- * Update prop event handlers ie onAnimationStart, onAnimationComplete
- */
- for (let i = 0; i < propEventHandlers.length; i++) {
- const key = propEventHandlers[i];
- if (this.propEventSubscriptions[key]) {
- this.propEventSubscriptions[key]();
- delete this.propEventSubscriptions[key];
- }
- const listenerName = ("on" + key);
- const listener = props[listenerName];
- if (listener) {
- this.propEventSubscriptions[key] = this.on(key, listener);
- }
- }
- this.prevMotionValues = updateMotionValuesFromProps(this, this.scrapeMotionValuesFromProps(props, this.prevProps, this), this.prevMotionValues);
- if (this.handleChildMotionValue) {
- this.handleChildMotionValue();
- }
- this.onUpdate && this.onUpdate(this);
- }
- getProps() {
- return this.props;
- }
- /**
- * Returns the variant definition with a given name.
- */
- getVariant(name) {
- return this.props.variants ? this.props.variants[name] : undefined;
- }
- /**
- * Returns the defined default transition on this component.
- */
- getDefaultTransition() {
- return this.props.transition;
- }
- getTransformPagePoint() {
- return this.props.transformPagePoint;
- }
- getClosestVariantNode() {
- return this.isVariantNode
- ? this
- : this.parent
- ? this.parent.getClosestVariantNode()
- : undefined;
- }
- /**
- * Add a child visual element to our set of children.
- */
- addVariantChild(child) {
- const closestVariantNode = this.getClosestVariantNode();
- if (closestVariantNode) {
- closestVariantNode.variantChildren &&
- closestVariantNode.variantChildren.add(child);
- return () => closestVariantNode.variantChildren.delete(child);
- }
- }
- /**
- * Add a motion value and bind it to this visual element.
- */
- addValue(key, value) {
- // Remove existing value if it exists
- const existingValue = this.values.get(key);
- if (value !== existingValue) {
- if (existingValue)
- this.removeValue(key);
- this.bindToMotionValue(key, value);
- this.values.set(key, value);
- this.latestValues[key] = value.get();
- }
- }
- /**
- * Remove a motion value and unbind any active subscriptions.
- */
- removeValue(key) {
- this.values.delete(key);
- const unsubscribe = this.valueSubscriptions.get(key);
- if (unsubscribe) {
- unsubscribe();
- this.valueSubscriptions.delete(key);
- }
- delete this.latestValues[key];
- this.removeValueFromRenderState(key, this.renderState);
- }
- /**
- * Check whether we have a motion value for this key
- */
- hasValue(key) {
- return this.values.has(key);
- }
- getValue(key, defaultValue) {
- if (this.props.values && this.props.values[key]) {
- return this.props.values[key];
- }
- let value = this.values.get(key);
- if (value === undefined && defaultValue !== undefined) {
- value = motionDom.motionValue(defaultValue === null ? undefined : defaultValue, { owner: this });
- this.addValue(key, value);
- }
- return value;
- }
- /**
- * If we're trying to animate to a previously unencountered value,
- * we need to check for it in our state and as a last resort read it
- * directly from the instance (which might have performance implications).
- */
- readValue(key, target) {
- let value = this.latestValues[key] !== undefined || !this.current
- ? this.latestValues[key]
- : this.getBaseTargetFromProps(this.props, key) ??
- this.readValueFromInstance(this.current, key, this.options);
- if (value !== undefined && value !== null) {
- if (typeof value === "string" &&
- (isNumericalString(value) || isZeroValueString(value))) {
- // If this is a number read as a string, ie "0" or "200", convert it to a number
- value = parseFloat(value);
- }
- else if (!findValueType(value) && complex.test(target)) {
- value = getAnimatableNone(key, target);
- }
- this.setBaseTarget(key, isMotionValue(value) ? value.get() : value);
- }
- return isMotionValue(value) ? value.get() : value;
- }
- /**
- * Set the base target to later animate back to. This is currently
- * only hydrated on creation and when we first read a value.
- */
- setBaseTarget(key, value) {
- this.baseTarget[key] = value;
- }
- /**
- * Find the base target for a value thats been removed from all animation
- * props.
- */
- getBaseTarget(key) {
- const { initial } = this.props;
- let valueFromInitial;
- if (typeof initial === "string" || typeof initial === "object") {
- const variant = resolveVariantFromProps(this.props, initial, this.presenceContext?.custom);
- if (variant) {
- valueFromInitial = variant[key];
- }
- }
- /**
- * If this value still exists in the current initial variant, read that.
- */
- if (initial && valueFromInitial !== undefined) {
- return valueFromInitial;
- }
- /**
- * Alternatively, if this VisualElement config has defined a getBaseTarget
- * so we can read the value from an alternative source, try that.
- */
- const target = this.getBaseTargetFromProps(this.props, key);
- if (target !== undefined && !isMotionValue(target))
- return target;
- /**
- * If the value was initially defined on initial, but it doesn't any more,
- * return undefined. Otherwise return the value as initially read from the DOM.
- */
- return this.initialValues[key] !== undefined &&
- valueFromInitial === undefined
- ? undefined
- : this.baseTarget[key];
- }
- on(eventName, callback) {
- if (!this.events[eventName]) {
- this.events[eventName] = new motionUtils.SubscriptionManager();
- }
- return this.events[eventName].add(callback);
- }
- notify(eventName, ...args) {
- if (this.events[eventName]) {
- this.events[eventName].notify(...args);
- }
- }
- }
- class DOMVisualElement extends VisualElement {
- constructor() {
- super(...arguments);
- this.KeyframeResolver = DOMKeyframesResolver;
- }
- sortInstanceNodePosition(a, b) {
- /**
- * compareDocumentPosition returns a bitmask, by using the bitwise &
- * we're returning true if 2 in that bitmask is set to true. 2 is set
- * to true if b preceeds a.
- */
- return a.compareDocumentPosition(b) & 2 ? 1 : -1;
- }
- getBaseTargetFromProps(props, key) {
- return props.style
- ? props.style[key]
- : undefined;
- }
- removeValueFromRenderState(key, { vars, style }) {
- delete vars[key];
- delete style[key];
- }
- handleChildMotionValue() {
- if (this.childSubscription) {
- this.childSubscription();
- delete this.childSubscription;
- }
- const { children } = this.props;
- if (isMotionValue(children)) {
- this.childSubscription = children.on("change", (latest) => {
- if (this.current) {
- this.current.textContent = `${latest}`;
- }
- });
- }
- }
- }
- /**
- * Provided a value and a ValueType, returns the value as that value type.
- */
- const getValueAsType = (value, type) => {
- return type && typeof value === "number"
- ? type.transform(value)
- : value;
- };
- const translateAlias = {
- x: "translateX",
- y: "translateY",
- z: "translateZ",
- transformPerspective: "perspective",
- };
- const numTransforms = transformPropOrder.length;
- /**
- * Build a CSS transform style from individual x/y/scale etc properties.
- *
- * This outputs with a default order of transforms/scales/rotations, this can be customised by
- * providing a transformTemplate function.
- */
- function buildTransform(latestValues, transform, transformTemplate) {
- // The transform string we're going to build into.
- let transformString = "";
- let transformIsDefault = true;
- /**
- * Loop over all possible transforms in order, adding the ones that
- * are present to the transform string.
- */
- for (let i = 0; i < numTransforms; i++) {
- const key = transformPropOrder[i];
- const value = latestValues[key];
- if (value === undefined)
- continue;
- let valueIsDefault = true;
- if (typeof value === "number") {
- valueIsDefault = value === (key.startsWith("scale") ? 1 : 0);
- }
- else {
- valueIsDefault = parseFloat(value) === 0;
- }
- if (!valueIsDefault || transformTemplate) {
- const valueAsType = getValueAsType(value, numberValueTypes[key]);
- if (!valueIsDefault) {
- transformIsDefault = false;
- const transformName = translateAlias[key] || key;
- transformString += `${transformName}(${valueAsType}) `;
- }
- if (transformTemplate) {
- transform[key] = valueAsType;
- }
- }
- }
- transformString = transformString.trim();
- // If we have a custom `transform` template, pass our transform values and
- // generated transformString to that before returning
- if (transformTemplate) {
- transformString = transformTemplate(transform, transformIsDefault ? "" : transformString);
- }
- else if (transformIsDefault) {
- transformString = "none";
- }
- return transformString;
- }
- function buildHTMLStyles(state, latestValues, transformTemplate) {
- const { style, vars, transformOrigin } = state;
- // Track whether we encounter any transform or transformOrigin values.
- let hasTransform = false;
- let hasTransformOrigin = false;
- /**
- * Loop over all our latest animated values and decide whether to handle them
- * as a style or CSS variable.
- *
- * Transforms and transform origins are kept separately for further processing.
- */
- for (const key in latestValues) {
- const value = latestValues[key];
- if (transformProps.has(key)) {
- // If this is a transform, flag to enable further transform processing
- hasTransform = true;
- continue;
- }
- else if (isCSSVariableName(key)) {
- vars[key] = value;
- continue;
- }
- else {
- // Convert the value to its default value type, ie 0 -> "0px"
- const valueAsType = getValueAsType(value, numberValueTypes[key]);
- if (key.startsWith("origin")) {
- // If this is a transform origin, flag and enable further transform-origin processing
- hasTransformOrigin = true;
- transformOrigin[key] =
- valueAsType;
- }
- else {
- style[key] = valueAsType;
- }
- }
- }
- if (!latestValues.transform) {
- if (hasTransform || transformTemplate) {
- style.transform = buildTransform(latestValues, state.transform, transformTemplate);
- }
- else if (style.transform) {
- /**
- * If we have previously created a transform but currently don't have any,
- * reset transform style to none.
- */
- style.transform = "none";
- }
- }
- /**
- * Build a transformOrigin style. Uses the same defaults as the browser for
- * undefined origins.
- */
- if (hasTransformOrigin) {
- const { originX = "50%", originY = "50%", originZ = 0, } = transformOrigin;
- style.transformOrigin = `${originX} ${originY} ${originZ}`;
- }
- }
- function renderHTML(element, { style, vars }, styleProp, projection) {
- Object.assign(element.style, style, projection && projection.getProjectionStyles(styleProp));
- // Loop over any CSS variables and assign those.
- for (const key in vars) {
- element.style.setProperty(key, vars[key]);
- }
- }
- function isForcedMotionValue(key, { layout, layoutId }) {
- return (transformProps.has(key) ||
- key.startsWith("origin") ||
- ((layout || layoutId !== undefined) &&
- (!!scaleCorrectors[key] || key === "opacity")));
- }
- function scrapeMotionValuesFromProps$1(props, prevProps, visualElement) {
- const { style } = props;
- const newValues = {};
- for (const key in style) {
- if (isMotionValue(style[key]) ||
- (prevProps.style &&
- isMotionValue(prevProps.style[key])) ||
- isForcedMotionValue(key, props) ||
- visualElement?.getValue(key)?.liveStyle !== undefined) {
- newValues[key] = style[key];
- }
- }
- return newValues;
- }
- function getComputedStyle$1(element) {
- return window.getComputedStyle(element);
- }
- class HTMLVisualElement extends DOMVisualElement {
- constructor() {
- super(...arguments);
- this.type = "html";
- this.renderInstance = renderHTML;
- }
- readValueFromInstance(instance, key) {
- if (transformProps.has(key)) {
- return readTransformValue(instance, key);
- }
- else {
- const computedStyle = getComputedStyle$1(instance);
- const value = (isCSSVariableName(key)
- ? computedStyle.getPropertyValue(key)
- : computedStyle[key]) || 0;
- return typeof value === "string" ? value.trim() : value;
- }
- }
- measureInstanceViewportBox(instance, { transformPagePoint }) {
- return measureViewportBox(instance, transformPagePoint);
- }
- build(renderState, latestValues, props) {
- buildHTMLStyles(renderState, latestValues, props.transformTemplate);
- }
- scrapeMotionValuesFromProps(props, prevProps, visualElement) {
- return scrapeMotionValuesFromProps$1(props, prevProps, visualElement);
- }
- }
- const LazyContext = React.createContext({ strict: false });
- function loadFeatures(features) {
- for (const key in features) {
- featureDefinitions[key] = {
- ...featureDefinitions[key],
- ...features[key],
- };
- }
- }
- /**
- * A list of all valid MotionProps.
- *
- * @privateRemarks
- * This doesn't throw if a `MotionProp` name is missing - it should.
- */
- const validMotionProps = new Set([
- "animate",
- "exit",
- "variants",
- "initial",
- "style",
- "values",
- "variants",
- "transition",
- "transformTemplate",
- "custom",
- "inherit",
- "onBeforeLayoutMeasure",
- "onAnimationStart",
- "onAnimationComplete",
- "onUpdate",
- "onDragStart",
- "onDrag",
- "onDragEnd",
- "onMeasureDragConstraints",
- "onDirectionLock",
- "onDragTransitionEnd",
- "_dragX",
- "_dragY",
- "onHoverStart",
- "onHoverEnd",
- "onViewportEnter",
- "onViewportLeave",
- "globalTapTarget",
- "ignoreStrict",
- "viewport",
- ]);
- /**
- * Check whether a prop name is a valid `MotionProp` key.
- *
- * @param key - Name of the property to check
- * @returns `true` is key is a valid `MotionProp`.
- *
- * @public
- */
- function isValidMotionProp(key) {
- return (key.startsWith("while") ||
- (key.startsWith("drag") && key !== "draggable") ||
- key.startsWith("layout") ||
- key.startsWith("onTap") ||
- key.startsWith("onPan") ||
- key.startsWith("onLayout") ||
- validMotionProps.has(key));
- }
- let shouldForward = (key) => !isValidMotionProp(key);
- function loadExternalIsValidProp(isValidProp) {
- if (!isValidProp)
- return;
- // Explicitly filter our events
- shouldForward = (key) => key.startsWith("on") ? !isValidMotionProp(key) : isValidProp(key);
- }
- /**
- * Emotion and Styled Components both allow users to pass through arbitrary props to their components
- * to dynamically generate CSS. They both use the `@emotion/is-prop-valid` package to determine which
- * of these should be passed to the underlying DOM node.
- *
- * However, when styling a Motion component `styled(motion.div)`, both packages pass through *all* props
- * as it's seen as an arbitrary component rather than a DOM node. Motion only allows arbitrary props
- * passed through the `custom` prop so it doesn't *need* the payload or computational overhead of
- * `@emotion/is-prop-valid`, however to fix this problem we need to use it.
- *
- * By making it an optionalDependency we can offer this functionality only in the situations where it's
- * actually required.
- */
- try {
- /**
- * We attempt to import this package but require won't be defined in esm environments, in that case
- * isPropValid will have to be provided via `MotionContext`. In a 6.0.0 this should probably be removed
- * in favour of explicit injection.
- */
- loadExternalIsValidProp(require("@emotion/is-prop-valid").default);
- }
- catch {
- // We don't need to actually do anything here - the fallback is the existing `isPropValid`.
- }
- function filterProps(props, isDom, forwardMotionProps) {
- const filteredProps = {};
- for (const key in props) {
- /**
- * values is considered a valid prop by Emotion, so if it's present
- * this will be rendered out to the DOM unless explicitly filtered.
- *
- * We check the type as it could be used with the `feColorMatrix`
- * element, which we support.
- */
- if (key === "values" && typeof props.values === "object")
- continue;
- if (shouldForward(key) ||
- (forwardMotionProps === true && isValidMotionProp(key)) ||
- (!isDom && !isValidMotionProp(key)) ||
- // If trying to use native HTML drag events, forward drag listeners
- (props["draggable"] &&
- key.startsWith("onDrag"))) {
- filteredProps[key] =
- props[key];
- }
- }
- return filteredProps;
- }
- function resolveVariant(visualElement, definition, custom) {
- const props = visualElement.getProps();
- return resolveVariantFromProps(props, definition, custom !== undefined ? custom : props.custom, visualElement);
- }
- /**
- * Set VisualElement's MotionValue, creating a new MotionValue for it if
- * it doesn't exist.
- */
- function setMotionValue(visualElement, key, value) {
- if (visualElement.hasValue(key)) {
- visualElement.getValue(key).set(value);
- }
- else {
- visualElement.addValue(key, motionDom.motionValue(value));
- }
- }
- function setTarget(visualElement, definition) {
- const resolved = resolveVariant(visualElement, definition);
- let { transitionEnd = {}, transition = {}, ...target } = resolved || {};
- target = { ...target, ...transitionEnd };
- for (const key in target) {
- const value = resolveFinalValueInKeyframes(target[key]);
- setMotionValue(visualElement, key, value);
- }
- }
- function isWillChangeMotionValue(value) {
- return Boolean(isMotionValue(value) && value.add);
- }
- function addValueToWillChange(visualElement, key) {
- const willChange = visualElement.getValue("willChange");
- /**
- * It could be that a user has set willChange to a regular MotionValue,
- * in which case we can't add the value to it.
- */
- if (isWillChangeMotionValue(willChange)) {
- return willChange.add(key);
- }
- else if (!willChange && motionUtils.MotionGlobalConfig.WillChange) {
- const newWillChange = new motionUtils.MotionGlobalConfig.WillChange("auto");
- visualElement.addValue("willChange", newWillChange);
- newWillChange.add(key);
- }
- }
- /**
- * Decide whether we should block this animation. Previously, we achieved this
- * just by checking whether the key was listed in protectedKeys, but this
- * posed problems if an animation was triggered by afterChildren and protectedKeys
- * had been set to true in the meantime.
- */
- function shouldBlockAnimation({ protectedKeys, needsAnimating }, key) {
- const shouldBlock = protectedKeys.hasOwnProperty(key) && needsAnimating[key] !== true;
- needsAnimating[key] = false;
- return shouldBlock;
- }
- function animateTarget(visualElement, targetAndTransition, { delay = 0, transitionOverride, type } = {}) {
- let { transition = visualElement.getDefaultTransition(), transitionEnd, ...target } = targetAndTransition;
- if (transitionOverride)
- transition = transitionOverride;
- const animations = [];
- const animationTypeState = type &&
- visualElement.animationState &&
- visualElement.animationState.getState()[type];
- for (const key in target) {
- const value = visualElement.getValue(key, visualElement.latestValues[key] ?? null);
- const valueTarget = target[key];
- if (valueTarget === undefined ||
- (animationTypeState &&
- shouldBlockAnimation(animationTypeState, key))) {
- continue;
- }
- const valueTransition = {
- delay,
- ...motionDom.getValueTransition(transition || {}, key),
- };
- /**
- * If this is the first time a value is being animated, check
- * to see if we're handling off from an existing animation.
- */
- let isHandoff = false;
- if (window.MotionHandoffAnimation) {
- const appearId = getOptimisedAppearId(visualElement);
- if (appearId) {
- const startTime = window.MotionHandoffAnimation(appearId, key, motionDom.frame);
- if (startTime !== null) {
- valueTransition.startTime = startTime;
- isHandoff = true;
- }
- }
- }
- addValueToWillChange(visualElement, key);
- value.start(animateMotionValue(key, value, valueTarget, visualElement.shouldReduceMotion && positionalKeys.has(key)
- ? { type: false }
- : valueTransition, visualElement, isHandoff));
- const animation = value.animation;
- if (animation) {
- animations.push(animation);
- }
- }
- if (transitionEnd) {
- Promise.all(animations).then(() => {
- motionDom.frame.update(() => {
- transitionEnd && setTarget(visualElement, transitionEnd);
- });
- });
- }
- return animations;
- }
- function animateVariant(visualElement, variant, options = {}) {
- const resolved = resolveVariant(visualElement, variant, options.type === "exit"
- ? visualElement.presenceContext?.custom
- : undefined);
- let { transition = visualElement.getDefaultTransition() || {} } = resolved || {};
- if (options.transitionOverride) {
- transition = options.transitionOverride;
- }
- /**
- * If we have a variant, create a callback that runs it as an animation.
- * Otherwise, we resolve a Promise immediately for a composable no-op.
- */
- const getAnimation = resolved
- ? () => Promise.all(animateTarget(visualElement, resolved, options))
- : () => Promise.resolve();
- /**
- * If we have children, create a callback that runs all their animations.
- * Otherwise, we resolve a Promise immediately for a composable no-op.
- */
- const getChildAnimations = visualElement.variantChildren && visualElement.variantChildren.size
- ? (forwardDelay = 0) => {
- const { delayChildren = 0, staggerChildren, staggerDirection, } = transition;
- return animateChildren(visualElement, variant, delayChildren + forwardDelay, staggerChildren, staggerDirection, options);
- }
- : () => Promise.resolve();
- /**
- * If the transition explicitly defines a "when" option, we need to resolve either
- * this animation or all children animations before playing the other.
- */
- const { when } = transition;
- if (when) {
- const [first, last] = when === "beforeChildren"
- ? [getAnimation, getChildAnimations]
- : [getChildAnimations, getAnimation];
- return first().then(() => last());
- }
- else {
- return Promise.all([getAnimation(), getChildAnimations(options.delay)]);
- }
- }
- function animateChildren(visualElement, variant, delayChildren = 0, staggerChildren = 0, staggerDirection = 1, options) {
- const animations = [];
- const maxStaggerDuration = (visualElement.variantChildren.size - 1) * staggerChildren;
- const generateStaggerDuration = staggerDirection === 1
- ? (i = 0) => i * staggerChildren
- : (i = 0) => maxStaggerDuration - i * staggerChildren;
- Array.from(visualElement.variantChildren)
- .sort(sortByTreeOrder)
- .forEach((child, i) => {
- child.notify("AnimationStart", variant);
- animations.push(animateVariant(child, variant, {
- ...options,
- delay: delayChildren + generateStaggerDuration(i),
- }).then(() => child.notify("AnimationComplete", variant)));
- });
- return Promise.all(animations);
- }
- function sortByTreeOrder(a, b) {
- return a.sortNodePosition(b);
- }
- function animateVisualElement(visualElement, definition, options = {}) {
- visualElement.notify("AnimationStart", definition);
- let animation;
- if (Array.isArray(definition)) {
- const animations = definition.map((variant) => animateVariant(visualElement, variant, options));
- animation = Promise.all(animations);
- }
- else if (typeof definition === "string") {
- animation = animateVariant(visualElement, definition, options);
- }
- else {
- const resolvedDefinition = typeof definition === "function"
- ? resolveVariant(visualElement, definition, options.custom)
- : definition;
- animation = Promise.all(animateTarget(visualElement, resolvedDefinition, options));
- }
- return animation.then(() => {
- visualElement.notify("AnimationComplete", definition);
- });
- }
- function shallowCompare(next, prev) {
- if (!Array.isArray(prev))
- return false;
- const prevLength = prev.length;
- if (prevLength !== next.length)
- return false;
- for (let i = 0; i < prevLength; i++) {
- if (prev[i] !== next[i])
- return false;
- }
- return true;
- }
- const numVariantProps = variantProps.length;
- function getVariantContext(visualElement) {
- if (!visualElement)
- return undefined;
- if (!visualElement.isControllingVariants) {
- const context = visualElement.parent
- ? getVariantContext(visualElement.parent) || {}
- : {};
- if (visualElement.props.initial !== undefined) {
- context.initial = visualElement.props.initial;
- }
- return context;
- }
- const context = {};
- for (let i = 0; i < numVariantProps; i++) {
- const name = variantProps[i];
- const prop = visualElement.props[name];
- if (isVariantLabel(prop) || prop === false) {
- context[name] = prop;
- }
- }
- return context;
- }
- const reversePriorityOrder = [...variantPriorityOrder].reverse();
- const numAnimationTypes = variantPriorityOrder.length;
- function animateList(visualElement) {
- return (animations) => Promise.all(animations.map(({ animation, options }) => animateVisualElement(visualElement, animation, options)));
- }
- function createAnimationState(visualElement) {
- let animate = animateList(visualElement);
- let state = createState();
- let isInitialRender = true;
- /**
- * This function will be used to reduce the animation definitions for
- * each active animation type into an object of resolved values for it.
- */
- const buildResolvedTypeValues = (type) => (acc, definition) => {
- const resolved = resolveVariant(visualElement, definition, type === "exit"
- ? visualElement.presenceContext?.custom
- : undefined);
- if (resolved) {
- const { transition, transitionEnd, ...target } = resolved;
- acc = { ...acc, ...target, ...transitionEnd };
- }
- return acc;
- };
- /**
- * This just allows us to inject mocked animation functions
- * @internal
- */
- function setAnimateFunction(makeAnimator) {
- animate = makeAnimator(visualElement);
- }
- /**
- * When we receive new props, we need to:
- * 1. Create a list of protected keys for each type. This is a directory of
- * value keys that are currently being "handled" by types of a higher priority
- * so that whenever an animation is played of a given type, these values are
- * protected from being animated.
- * 2. Determine if an animation type needs animating.
- * 3. Determine if any values have been removed from a type and figure out
- * what to animate those to.
- */
- function animateChanges(changedActiveType) {
- const { props } = visualElement;
- const context = getVariantContext(visualElement.parent) || {};
- /**
- * A list of animations that we'll build into as we iterate through the animation
- * types. This will get executed at the end of the function.
- */
- const animations = [];
- /**
- * Keep track of which values have been removed. Then, as we hit lower priority
- * animation types, we can check if they contain removed values and animate to that.
- */
- const removedKeys = new Set();
- /**
- * A dictionary of all encountered keys. This is an object to let us build into and
- * copy it without iteration. Each time we hit an animation type we set its protected
- * keys - the keys its not allowed to animate - to the latest version of this object.
- */
- let encounteredKeys = {};
- /**
- * If a variant has been removed at a given index, and this component is controlling
- * variant animations, we want to ensure lower-priority variants are forced to animate.
- */
- let removedVariantIndex = Infinity;
- /**
- * Iterate through all animation types in reverse priority order. For each, we want to
- * detect which values it's handling and whether or not they've changed (and therefore
- * need to be animated). If any values have been removed, we want to detect those in
- * lower priority props and flag for animation.
- */
- for (let i = 0; i < numAnimationTypes; i++) {
- const type = reversePriorityOrder[i];
- const typeState = state[type];
- const prop = props[type] !== undefined
- ? props[type]
- : context[type];
- const propIsVariant = isVariantLabel(prop);
- /**
- * If this type has *just* changed isActive status, set activeDelta
- * to that status. Otherwise set to null.
- */
- const activeDelta = type === changedActiveType ? typeState.isActive : null;
- if (activeDelta === false)
- removedVariantIndex = i;
- /**
- * If this prop is an inherited variant, rather than been set directly on the
- * component itself, we want to make sure we allow the parent to trigger animations.
- *
- * TODO: Can probably change this to a !isControllingVariants check
- */
- let isInherited = prop === context[type] &&
- prop !== props[type] &&
- propIsVariant;
- /**
- *
- */
- if (isInherited &&
- isInitialRender &&
- visualElement.manuallyAnimateOnMount) {
- isInherited = false;
- }
- /**
- * Set all encountered keys so far as the protected keys for this type. This will
- * be any key that has been animated or otherwise handled by active, higher-priortiy types.
- */
- typeState.protectedKeys = { ...encounteredKeys };
- // Check if we can skip analysing this prop early
- if (
- // If it isn't active and hasn't *just* been set as inactive
- (!typeState.isActive && activeDelta === null) ||
- // If we didn't and don't have any defined prop for this animation type
- (!prop && !typeState.prevProp) ||
- // Or if the prop doesn't define an animation
- isAnimationControls(prop) ||
- typeof prop === "boolean") {
- continue;
- }
- /**
- * As we go look through the values defined on this type, if we detect
- * a changed value or a value that was removed in a higher priority, we set
- * this to true and add this prop to the animation list.
- */
- const variantDidChange = checkVariantsDidChange(typeState.prevProp, prop);
- let shouldAnimateType = variantDidChange ||
- // If we're making this variant active, we want to always make it active
- (type === changedActiveType &&
- typeState.isActive &&
- !isInherited &&
- propIsVariant) ||
- // If we removed a higher-priority variant (i is in reverse order)
- (i > removedVariantIndex && propIsVariant);
- let handledRemovedValues = false;
- /**
- * As animations can be set as variant lists, variants or target objects, we
- * coerce everything to an array if it isn't one already
- */
- const definitionList = Array.isArray(prop) ? prop : [prop];
- /**
- * Build an object of all the resolved values. We'll use this in the subsequent
- * animateChanges calls to determine whether a value has changed.
- */
- let resolvedValues = definitionList.reduce(buildResolvedTypeValues(type), {});
- if (activeDelta === false)
- resolvedValues = {};
- /**
- * Now we need to loop through all the keys in the prev prop and this prop,
- * and decide:
- * 1. If the value has changed, and needs animating
- * 2. If it has been removed, and needs adding to the removedKeys set
- * 3. If it has been removed in a higher priority type and needs animating
- * 4. If it hasn't been removed in a higher priority but hasn't changed, and
- * needs adding to the type's protectedKeys list.
- */
- const { prevResolvedValues = {} } = typeState;
- const allKeys = {
- ...prevResolvedValues,
- ...resolvedValues,
- };
- const markToAnimate = (key) => {
- shouldAnimateType = true;
- if (removedKeys.has(key)) {
- handledRemovedValues = true;
- removedKeys.delete(key);
- }
- typeState.needsAnimating[key] = true;
- const motionValue = visualElement.getValue(key);
- if (motionValue)
- motionValue.liveStyle = false;
- };
- for (const key in allKeys) {
- const next = resolvedValues[key];
- const prev = prevResolvedValues[key];
- // If we've already handled this we can just skip ahead
- if (encounteredKeys.hasOwnProperty(key))
- continue;
- /**
- * If the value has changed, we probably want to animate it.
- */
- let valueHasChanged = false;
- if (isKeyframesTarget(next) && isKeyframesTarget(prev)) {
- valueHasChanged = !shallowCompare(next, prev);
- }
- else {
- valueHasChanged = next !== prev;
- }
- if (valueHasChanged) {
- if (next !== undefined && next !== null) {
- // If next is defined and doesn't equal prev, it needs animating
- markToAnimate(key);
- }
- else {
- // If it's undefined, it's been removed.
- removedKeys.add(key);
- }
- }
- else if (next !== undefined && removedKeys.has(key)) {
- /**
- * If next hasn't changed and it isn't undefined, we want to check if it's
- * been removed by a higher priority
- */
- markToAnimate(key);
- }
- else {
- /**
- * If it hasn't changed, we add it to the list of protected values
- * to ensure it doesn't get animated.
- */
- typeState.protectedKeys[key] = true;
- }
- }
- /**
- * Update the typeState so next time animateChanges is called we can compare the
- * latest prop and resolvedValues to these.
- */
- typeState.prevProp = prop;
- typeState.prevResolvedValues = resolvedValues;
- /**
- *
- */
- if (typeState.isActive) {
- encounteredKeys = { ...encounteredKeys, ...resolvedValues };
- }
- if (isInitialRender && visualElement.blockInitialAnimation) {
- shouldAnimateType = false;
- }
- /**
- * If this is an inherited prop we want to skip this animation
- * unless the inherited variants haven't changed on this render.
- */
- const willAnimateViaParent = isInherited && variantDidChange;
- const needsAnimating = !willAnimateViaParent || handledRemovedValues;
- if (shouldAnimateType && needsAnimating) {
- animations.push(...definitionList.map((animation) => ({
- animation: animation,
- options: { type },
- })));
- }
- }
- /**
- * If there are some removed value that haven't been dealt with,
- * we need to create a new animation that falls back either to the value
- * defined in the style prop, or the last read value.
- */
- if (removedKeys.size) {
- const fallbackAnimation = {};
- /**
- * If the initial prop contains a transition we can use that, otherwise
- * allow the animation function to use the visual element's default.
- */
- if (typeof props.initial !== "boolean") {
- const initialTransition = resolveVariant(visualElement, Array.isArray(props.initial)
- ? props.initial[0]
- : props.initial);
- if (initialTransition && initialTransition.transition) {
- fallbackAnimation.transition = initialTransition.transition;
- }
- }
- removedKeys.forEach((key) => {
- const fallbackTarget = visualElement.getBaseTarget(key);
- const motionValue = visualElement.getValue(key);
- if (motionValue)
- motionValue.liveStyle = true;
- // @ts-expect-error - @mattgperry to figure if we should do something here
- fallbackAnimation[key] = fallbackTarget ?? null;
- });
- animations.push({ animation: fallbackAnimation });
- }
- let shouldAnimate = Boolean(animations.length);
- if (isInitialRender &&
- (props.initial === false || props.initial === props.animate) &&
- !visualElement.manuallyAnimateOnMount) {
- shouldAnimate = false;
- }
- isInitialRender = false;
- return shouldAnimate ? animate(animations) : Promise.resolve();
- }
- /**
- * Change whether a certain animation type is active.
- */
- function setActive(type, isActive) {
- // If the active state hasn't changed, we can safely do nothing here
- if (state[type].isActive === isActive)
- return Promise.resolve();
- // Propagate active change to children
- visualElement.variantChildren?.forEach((child) => child.animationState?.setActive(type, isActive));
- state[type].isActive = isActive;
- const animations = animateChanges(type);
- for (const key in state) {
- state[key].protectedKeys = {};
- }
- return animations;
- }
- return {
- animateChanges,
- setActive,
- setAnimateFunction,
- getState: () => state,
- reset: () => {
- state = createState();
- isInitialRender = true;
- },
- };
- }
- function checkVariantsDidChange(prev, next) {
- if (typeof next === "string") {
- return next !== prev;
- }
- else if (Array.isArray(next)) {
- return !shallowCompare(next, prev);
- }
- return false;
- }
- function createTypeState(isActive = false) {
- return {
- isActive,
- protectedKeys: {},
- needsAnimating: {},
- prevResolvedValues: {},
- };
- }
- function createState() {
- return {
- animate: createTypeState(true),
- whileInView: createTypeState(),
- whileHover: createTypeState(),
- whileTap: createTypeState(),
- whileDrag: createTypeState(),
- whileFocus: createTypeState(),
- exit: createTypeState(),
- };
- }
- class Feature {
- constructor(node) {
- this.isMounted = false;
- this.node = node;
- }
- update() { }
- }
- class AnimationFeature extends Feature {
- /**
- * We dynamically generate the AnimationState manager as it contains a reference
- * to the underlying animation library. We only want to load that if we load this,
- * so people can optionally code split it out using the `m` component.
- */
- constructor(node) {
- super(node);
- node.animationState || (node.animationState = createAnimationState(node));
- }
- updateAnimationControlsSubscription() {
- const { animate } = this.node.getProps();
- if (isAnimationControls(animate)) {
- this.unmountControls = animate.subscribe(this.node);
- }
- }
- /**
- * Subscribe any provided AnimationControls to the component's VisualElement
- */
- mount() {
- this.updateAnimationControlsSubscription();
- }
- update() {
- const { animate } = this.node.getProps();
- const { animate: prevAnimate } = this.node.prevProps || {};
- if (animate !== prevAnimate) {
- this.updateAnimationControlsSubscription();
- }
- }
- unmount() {
- this.node.animationState.reset();
- this.unmountControls?.();
- }
- }
- let id = 0;
- class ExitAnimationFeature extends Feature {
- constructor() {
- super(...arguments);
- this.id = id++;
- }
- update() {
- if (!this.node.presenceContext)
- return;
- const { isPresent, onExitComplete } = this.node.presenceContext;
- const { isPresent: prevIsPresent } = this.node.prevPresenceContext || {};
- if (!this.node.animationState || isPresent === prevIsPresent) {
- return;
- }
- const exitAnimation = this.node.animationState.setActive("exit", !isPresent);
- if (onExitComplete && !isPresent) {
- exitAnimation.then(() => {
- onExitComplete(this.id);
- });
- }
- }
- mount() {
- const { register, onExitComplete } = this.node.presenceContext || {};
- if (onExitComplete) {
- onExitComplete(this.id);
- }
- if (register) {
- this.unmount = register(this.id);
- }
- }
- unmount() { }
- }
- const animations = {
- animation: {
- Feature: AnimationFeature,
- },
- exit: {
- Feature: ExitAnimationFeature,
- },
- };
- function extractEventInfo(event) {
- return {
- point: {
- x: event.pageX,
- y: event.pageY,
- },
- };
- }
- const addPointerInfo = (handler) => {
- return (event) => motionDom.isPrimaryPointer(event) && handler(event, extractEventInfo(event));
- };
- function addPointerEvent(target, eventName, handler, options) {
- return addDomEvent(target, eventName, addPointerInfo(handler), options);
- }
- // Fixes https://github.com/motiondivision/motion/issues/2270
- const getContextWindow = ({ current }) => {
- return current ? current.ownerDocument.defaultView : null;
- };
- function isRefObject(ref) {
- return (ref &&
- typeof ref === "object" &&
- Object.prototype.hasOwnProperty.call(ref, "current"));
- }
- const distance = (a, b) => Math.abs(a - b);
- function distance2D(a, b) {
- // Multi-dimensional
- const xDelta = distance(a.x, b.x);
- const yDelta = distance(a.y, b.y);
- return Math.sqrt(xDelta ** 2 + yDelta ** 2);
- }
- /**
- * @internal
- */
- class PanSession {
- constructor(event, handlers, { transformPagePoint, contextWindow, dragSnapToOrigin = false, } = {}) {
- /**
- * @internal
- */
- this.startEvent = null;
- /**
- * @internal
- */
- this.lastMoveEvent = null;
- /**
- * @internal
- */
- this.lastMoveEventInfo = null;
- /**
- * @internal
- */
- this.handlers = {};
- /**
- * @internal
- */
- this.contextWindow = window;
- this.updatePoint = () => {
- if (!(this.lastMoveEvent && this.lastMoveEventInfo))
- return;
- const info = getPanInfo(this.lastMoveEventInfo, this.history);
- const isPanStarted = this.startEvent !== null;
- // Only start panning if the offset is larger than 3 pixels. If we make it
- // any larger than this we'll want to reset the pointer history
- // on the first update to avoid visual snapping to the cursoe.
- const isDistancePastThreshold = distance2D(info.offset, { x: 0, y: 0 }) >= 3;
- if (!isPanStarted && !isDistancePastThreshold)
- return;
- const { point } = info;
- const { timestamp } = motionDom.frameData;
- this.history.push({ ...point, timestamp });
- const { onStart, onMove } = this.handlers;
- if (!isPanStarted) {
- onStart && onStart(this.lastMoveEvent, info);
- this.startEvent = this.lastMoveEvent;
- }
- onMove && onMove(this.lastMoveEvent, info);
- };
- this.handlePointerMove = (event, info) => {
- this.lastMoveEvent = event;
- this.lastMoveEventInfo = transformPoint(info, this.transformPagePoint);
- // Throttle mouse move event to once per frame
- motionDom.frame.update(this.updatePoint, true);
- };
- this.handlePointerUp = (event, info) => {
- this.end();
- const { onEnd, onSessionEnd, resumeAnimation } = this.handlers;
- if (this.dragSnapToOrigin)
- resumeAnimation && resumeAnimation();
- if (!(this.lastMoveEvent && this.lastMoveEventInfo))
- return;
- const panInfo = getPanInfo(event.type === "pointercancel"
- ? this.lastMoveEventInfo
- : transformPoint(info, this.transformPagePoint), this.history);
- if (this.startEvent && onEnd) {
- onEnd(event, panInfo);
- }
- onSessionEnd && onSessionEnd(event, panInfo);
- };
- // If we have more than one touch, don't start detecting this gesture
- if (!motionDom.isPrimaryPointer(event))
- return;
- this.dragSnapToOrigin = dragSnapToOrigin;
- this.handlers = handlers;
- this.transformPagePoint = transformPagePoint;
- this.contextWindow = contextWindow || window;
- const info = extractEventInfo(event);
- const initialInfo = transformPoint(info, this.transformPagePoint);
- const { point } = initialInfo;
- const { timestamp } = motionDom.frameData;
- this.history = [{ ...point, timestamp }];
- const { onSessionStart } = handlers;
- onSessionStart &&
- onSessionStart(event, getPanInfo(initialInfo, this.history));
- this.removeListeners = pipe(addPointerEvent(this.contextWindow, "pointermove", this.handlePointerMove), addPointerEvent(this.contextWindow, "pointerup", this.handlePointerUp), addPointerEvent(this.contextWindow, "pointercancel", this.handlePointerUp));
- }
- updateHandlers(handlers) {
- this.handlers = handlers;
- }
- end() {
- this.removeListeners && this.removeListeners();
- motionDom.cancelFrame(this.updatePoint);
- }
- }
- function transformPoint(info, transformPagePoint) {
- return transformPagePoint ? { point: transformPagePoint(info.point) } : info;
- }
- function subtractPoint(a, b) {
- return { x: a.x - b.x, y: a.y - b.y };
- }
- function getPanInfo({ point }, history) {
- return {
- point,
- delta: subtractPoint(point, lastDevicePoint(history)),
- offset: subtractPoint(point, startDevicePoint(history)),
- velocity: getVelocity(history, 0.1),
- };
- }
- function startDevicePoint(history) {
- return history[0];
- }
- function lastDevicePoint(history) {
- return history[history.length - 1];
- }
- function getVelocity(history, timeDelta) {
- if (history.length < 2) {
- return { x: 0, y: 0 };
- }
- let i = history.length - 1;
- let timestampedPoint = null;
- const lastPoint = lastDevicePoint(history);
- while (i >= 0) {
- timestampedPoint = history[i];
- if (lastPoint.timestamp - timestampedPoint.timestamp >
- motionUtils.secondsToMilliseconds(timeDelta)) {
- break;
- }
- i--;
- }
- if (!timestampedPoint) {
- return { x: 0, y: 0 };
- }
- const time = motionUtils.millisecondsToSeconds(lastPoint.timestamp - timestampedPoint.timestamp);
- if (time === 0) {
- return { x: 0, y: 0 };
- }
- const currentVelocity = {
- x: (lastPoint.x - timestampedPoint.x) / time,
- y: (lastPoint.y - timestampedPoint.y) / time,
- };
- if (currentVelocity.x === Infinity) {
- currentVelocity.x = 0;
- }
- if (currentVelocity.y === Infinity) {
- currentVelocity.y = 0;
- }
- return currentVelocity;
- }
- /**
- * Apply constraints to a point. These constraints are both physical along an
- * axis, and an elastic factor that determines how much to constrain the point
- * by if it does lie outside the defined parameters.
- */
- function applyConstraints(point, { min, max }, elastic) {
- if (min !== undefined && point < min) {
- // If we have a min point defined, and this is outside of that, constrain
- point = elastic
- ? mixNumber$1(min, point, elastic.min)
- : Math.max(point, min);
- }
- else if (max !== undefined && point > max) {
- // If we have a max point defined, and this is outside of that, constrain
- point = elastic
- ? mixNumber$1(max, point, elastic.max)
- : Math.min(point, max);
- }
- return point;
- }
- /**
- * Calculate constraints in terms of the viewport when defined relatively to the
- * measured axis. This is measured from the nearest edge, so a max constraint of 200
- * on an axis with a max value of 300 would return a constraint of 500 - axis length
- */
- function calcRelativeAxisConstraints(axis, min, max) {
- return {
- min: min !== undefined ? axis.min + min : undefined,
- max: max !== undefined
- ? axis.max + max - (axis.max - axis.min)
- : undefined,
- };
- }
- /**
- * Calculate constraints in terms of the viewport when
- * defined relatively to the measured bounding box.
- */
- function calcRelativeConstraints(layoutBox, { top, left, bottom, right }) {
- return {
- x: calcRelativeAxisConstraints(layoutBox.x, left, right),
- y: calcRelativeAxisConstraints(layoutBox.y, top, bottom),
- };
- }
- /**
- * Calculate viewport constraints when defined as another viewport-relative axis
- */
- function calcViewportAxisConstraints(layoutAxis, constraintsAxis) {
- let min = constraintsAxis.min - layoutAxis.min;
- let max = constraintsAxis.max - layoutAxis.max;
- // If the constraints axis is actually smaller than the layout axis then we can
- // flip the constraints
- if (constraintsAxis.max - constraintsAxis.min <
- layoutAxis.max - layoutAxis.min) {
- [min, max] = [max, min];
- }
- return { min, max };
- }
- /**
- * Calculate viewport constraints when defined as another viewport-relative box
- */
- function calcViewportConstraints(layoutBox, constraintsBox) {
- return {
- x: calcViewportAxisConstraints(layoutBox.x, constraintsBox.x),
- y: calcViewportAxisConstraints(layoutBox.y, constraintsBox.y),
- };
- }
- /**
- * Calculate a transform origin relative to the source axis, between 0-1, that results
- * in an asthetically pleasing scale/transform needed to project from source to target.
- */
- function calcOrigin$1(source, target) {
- let origin = 0.5;
- const sourceLength = calcLength(source);
- const targetLength = calcLength(target);
- if (targetLength > sourceLength) {
- origin = motionUtils.progress(target.min, target.max - sourceLength, source.min);
- }
- else if (sourceLength > targetLength) {
- origin = motionUtils.progress(source.min, source.max - targetLength, target.min);
- }
- return clamp(0, 1, origin);
- }
- /**
- * Rebase the calculated viewport constraints relative to the layout.min point.
- */
- function rebaseAxisConstraints(layout, constraints) {
- const relativeConstraints = {};
- if (constraints.min !== undefined) {
- relativeConstraints.min = constraints.min - layout.min;
- }
- if (constraints.max !== undefined) {
- relativeConstraints.max = constraints.max - layout.min;
- }
- return relativeConstraints;
- }
- const defaultElastic = 0.35;
- /**
- * Accepts a dragElastic prop and returns resolved elastic values for each axis.
- */
- function resolveDragElastic(dragElastic = defaultElastic) {
- if (dragElastic === false) {
- dragElastic = 0;
- }
- else if (dragElastic === true) {
- dragElastic = defaultElastic;
- }
- return {
- x: resolveAxisElastic(dragElastic, "left", "right"),
- y: resolveAxisElastic(dragElastic, "top", "bottom"),
- };
- }
- function resolveAxisElastic(dragElastic, minLabel, maxLabel) {
- return {
- min: resolvePointElastic(dragElastic, minLabel),
- max: resolvePointElastic(dragElastic, maxLabel),
- };
- }
- function resolvePointElastic(dragElastic, label) {
- return typeof dragElastic === "number"
- ? dragElastic
- : dragElastic[label] || 0;
- }
- const elementDragControls = new WeakMap();
- /**
- *
- */
- // let latestPointerEvent: PointerEvent
- class VisualElementDragControls {
- constructor(visualElement) {
- this.openDragLock = null;
- this.isDragging = false;
- this.currentDirection = null;
- this.originPoint = { x: 0, y: 0 };
- /**
- * The permitted boundaries of travel, in pixels.
- */
- this.constraints = false;
- this.hasMutatedConstraints = false;
- /**
- * The per-axis resolved elastic values.
- */
- this.elastic = createBox();
- this.visualElement = visualElement;
- }
- start(originEvent, { snapToCursor = false } = {}) {
- /**
- * Don't start dragging if this component is exiting
- */
- const { presenceContext } = this.visualElement;
- if (presenceContext && presenceContext.isPresent === false)
- return;
- const onSessionStart = (event) => {
- const { dragSnapToOrigin } = this.getProps();
- // Stop or pause any animations on both axis values immediately. This allows the user to throw and catch
- // the component.
- dragSnapToOrigin ? this.pauseAnimation() : this.stopAnimation();
- if (snapToCursor) {
- this.snapToCursor(extractEventInfo(event).point);
- }
- };
- const onStart = (event, info) => {
- // Attempt to grab the global drag gesture lock - maybe make this part of PanSession
- const { drag, dragPropagation, onDragStart } = this.getProps();
- if (drag && !dragPropagation) {
- if (this.openDragLock)
- this.openDragLock();
- this.openDragLock = motionDom.setDragLock(drag);
- // If we don 't have the lock, don't start dragging
- if (!this.openDragLock)
- return;
- }
- this.isDragging = true;
- this.currentDirection = null;
- this.resolveConstraints();
- if (this.visualElement.projection) {
- this.visualElement.projection.isAnimationBlocked = true;
- this.visualElement.projection.target = undefined;
- }
- /**
- * Record gesture origin
- */
- eachAxis((axis) => {
- let current = this.getAxisMotionValue(axis).get() || 0;
- /**
- * If the MotionValue is a percentage value convert to px
- */
- if (percent.test(current)) {
- const { projection } = this.visualElement;
- if (projection && projection.layout) {
- const measuredAxis = projection.layout.layoutBox[axis];
- if (measuredAxis) {
- const length = calcLength(measuredAxis);
- current = length * (parseFloat(current) / 100);
- }
- }
- }
- this.originPoint[axis] = current;
- });
- // Fire onDragStart event
- if (onDragStart) {
- motionDom.frame.postRender(() => onDragStart(event, info));
- }
- addValueToWillChange(this.visualElement, "transform");
- const { animationState } = this.visualElement;
- animationState && animationState.setActive("whileDrag", true);
- };
- const onMove = (event, info) => {
- // latestPointerEvent = event
- const { dragPropagation, dragDirectionLock, onDirectionLock, onDrag, } = this.getProps();
- // If we didn't successfully receive the gesture lock, early return.
- if (!dragPropagation && !this.openDragLock)
- return;
- const { offset } = info;
- // Attempt to detect drag direction if directionLock is true
- if (dragDirectionLock && this.currentDirection === null) {
- this.currentDirection = getCurrentDirection(offset);
- // If we've successfully set a direction, notify listener
- if (this.currentDirection !== null) {
- onDirectionLock && onDirectionLock(this.currentDirection);
- }
- return;
- }
- // Update each point with the latest position
- this.updateAxis("x", info.point, offset);
- this.updateAxis("y", info.point, offset);
- /**
- * Ideally we would leave the renderer to fire naturally at the end of
- * this frame but if the element is about to change layout as the result
- * of a re-render we want to ensure the browser can read the latest
- * bounding box to ensure the pointer and element don't fall out of sync.
- */
- this.visualElement.render();
- /**
- * This must fire after the render call as it might trigger a state
- * change which itself might trigger a layout update.
- */
- onDrag && onDrag(event, info);
- };
- const onSessionEnd = (event, info) => this.stop(event, info);
- const resumeAnimation = () => eachAxis((axis) => this.getAnimationState(axis) === "paused" &&
- this.getAxisMotionValue(axis).animation?.play());
- const { dragSnapToOrigin } = this.getProps();
- this.panSession = new PanSession(originEvent, {
- onSessionStart,
- onStart,
- onMove,
- onSessionEnd,
- resumeAnimation,
- }, {
- transformPagePoint: this.visualElement.getTransformPagePoint(),
- dragSnapToOrigin,
- contextWindow: getContextWindow(this.visualElement),
- });
- }
- stop(event, info) {
- const isDragging = this.isDragging;
- this.cancel();
- if (!isDragging)
- return;
- const { velocity } = info;
- this.startAnimation(velocity);
- const { onDragEnd } = this.getProps();
- if (onDragEnd) {
- motionDom.frame.postRender(() => onDragEnd(event, info));
- }
- }
- cancel() {
- this.isDragging = false;
- const { projection, animationState } = this.visualElement;
- if (projection) {
- projection.isAnimationBlocked = false;
- }
- this.panSession && this.panSession.end();
- this.panSession = undefined;
- const { dragPropagation } = this.getProps();
- if (!dragPropagation && this.openDragLock) {
- this.openDragLock();
- this.openDragLock = null;
- }
- animationState && animationState.setActive("whileDrag", false);
- }
- updateAxis(axis, _point, offset) {
- const { drag } = this.getProps();
- // If we're not dragging this axis, do an early return.
- if (!offset || !shouldDrag(axis, drag, this.currentDirection))
- return;
- const axisValue = this.getAxisMotionValue(axis);
- let next = this.originPoint[axis] + offset[axis];
- // Apply constraints
- if (this.constraints && this.constraints[axis]) {
- next = applyConstraints(next, this.constraints[axis], this.elastic[axis]);
- }
- axisValue.set(next);
- }
- resolveConstraints() {
- const { dragConstraints, dragElastic } = this.getProps();
- const layout = this.visualElement.projection &&
- !this.visualElement.projection.layout
- ? this.visualElement.projection.measure(false)
- : this.visualElement.projection?.layout;
- const prevConstraints = this.constraints;
- if (dragConstraints && isRefObject(dragConstraints)) {
- if (!this.constraints) {
- this.constraints = this.resolveRefConstraints();
- }
- }
- else {
- if (dragConstraints && layout) {
- this.constraints = calcRelativeConstraints(layout.layoutBox, dragConstraints);
- }
- else {
- this.constraints = false;
- }
- }
- this.elastic = resolveDragElastic(dragElastic);
- /**
- * If we're outputting to external MotionValues, we want to rebase the measured constraints
- * from viewport-relative to component-relative.
- */
- if (prevConstraints !== this.constraints &&
- layout &&
- this.constraints &&
- !this.hasMutatedConstraints) {
- eachAxis((axis) => {
- if (this.constraints !== false &&
- this.getAxisMotionValue(axis)) {
- this.constraints[axis] = rebaseAxisConstraints(layout.layoutBox[axis], this.constraints[axis]);
- }
- });
- }
- }
- resolveRefConstraints() {
- const { dragConstraints: constraints, onMeasureDragConstraints } = this.getProps();
- if (!constraints || !isRefObject(constraints))
- return false;
- const constraintsElement = constraints.current;
- motionUtils.invariant(constraintsElement !== null, "If `dragConstraints` is set as a React ref, that ref must be passed to another component's `ref` prop.");
- const { projection } = this.visualElement;
- // TODO
- if (!projection || !projection.layout)
- return false;
- const constraintsBox = measurePageBox(constraintsElement, projection.root, this.visualElement.getTransformPagePoint());
- let measuredConstraints = calcViewportConstraints(projection.layout.layoutBox, constraintsBox);
- /**
- * If there's an onMeasureDragConstraints listener we call it and
- * if different constraints are returned, set constraints to that
- */
- if (onMeasureDragConstraints) {
- const userConstraints = onMeasureDragConstraints(convertBoxToBoundingBox(measuredConstraints));
- this.hasMutatedConstraints = !!userConstraints;
- if (userConstraints) {
- measuredConstraints = convertBoundingBoxToBox(userConstraints);
- }
- }
- return measuredConstraints;
- }
- startAnimation(velocity) {
- const { drag, dragMomentum, dragElastic, dragTransition, dragSnapToOrigin, onDragTransitionEnd, } = this.getProps();
- const constraints = this.constraints || {};
- const momentumAnimations = eachAxis((axis) => {
- if (!shouldDrag(axis, drag, this.currentDirection)) {
- return;
- }
- let transition = (constraints && constraints[axis]) || {};
- if (dragSnapToOrigin)
- transition = { min: 0, max: 0 };
- /**
- * Overdamp the boundary spring if `dragElastic` is disabled. There's still a frame
- * of spring animations so we should look into adding a disable spring option to `inertia`.
- * We could do something here where we affect the `bounceStiffness` and `bounceDamping`
- * using the value of `dragElastic`.
- */
- const bounceStiffness = dragElastic ? 200 : 1000000;
- const bounceDamping = dragElastic ? 40 : 10000000;
- const inertia = {
- type: "inertia",
- velocity: dragMomentum ? velocity[axis] : 0,
- bounceStiffness,
- bounceDamping,
- timeConstant: 750,
- restDelta: 1,
- restSpeed: 10,
- ...dragTransition,
- ...transition,
- };
- // If we're not animating on an externally-provided `MotionValue` we can use the
- // component's animation controls which will handle interactions with whileHover (etc),
- // otherwise we just have to animate the `MotionValue` itself.
- return this.startAxisValueAnimation(axis, inertia);
- });
- // Run all animations and then resolve the new drag constraints.
- return Promise.all(momentumAnimations).then(onDragTransitionEnd);
- }
- startAxisValueAnimation(axis, transition) {
- const axisValue = this.getAxisMotionValue(axis);
- addValueToWillChange(this.visualElement, axis);
- return axisValue.start(animateMotionValue(axis, axisValue, 0, transition, this.visualElement, false));
- }
- stopAnimation() {
- eachAxis((axis) => this.getAxisMotionValue(axis).stop());
- }
- pauseAnimation() {
- eachAxis((axis) => this.getAxisMotionValue(axis).animation?.pause());
- }
- getAnimationState(axis) {
- return this.getAxisMotionValue(axis).animation?.state;
- }
- /**
- * Drag works differently depending on which props are provided.
- *
- * - If _dragX and _dragY are provided, we output the gesture delta directly to those motion values.
- * - Otherwise, we apply the delta to the x/y motion values.
- */
- getAxisMotionValue(axis) {
- const dragKey = `_drag${axis.toUpperCase()}`;
- const props = this.visualElement.getProps();
- const externalMotionValue = props[dragKey];
- return externalMotionValue
- ? externalMotionValue
- : this.visualElement.getValue(axis, (props.initial
- ? props.initial[axis]
- : undefined) || 0);
- }
- snapToCursor(point) {
- eachAxis((axis) => {
- const { drag } = this.getProps();
- // If we're not dragging this axis, do an early return.
- if (!shouldDrag(axis, drag, this.currentDirection))
- return;
- const { projection } = this.visualElement;
- const axisValue = this.getAxisMotionValue(axis);
- if (projection && projection.layout) {
- const { min, max } = projection.layout.layoutBox[axis];
- axisValue.set(point[axis] - mixNumber$1(min, max, 0.5));
- }
- });
- }
- /**
- * When the viewport resizes we want to check if the measured constraints
- * have changed and, if so, reposition the element within those new constraints
- * relative to where it was before the resize.
- */
- scalePositionWithinConstraints() {
- if (!this.visualElement.current)
- return;
- const { drag, dragConstraints } = this.getProps();
- const { projection } = this.visualElement;
- if (!isRefObject(dragConstraints) || !projection || !this.constraints)
- return;
- /**
- * Stop current animations as there can be visual glitching if we try to do
- * this mid-animation
- */
- this.stopAnimation();
- /**
- * Record the relative position of the dragged element relative to the
- * constraints box and save as a progress value.
- */
- const boxProgress = { x: 0, y: 0 };
- eachAxis((axis) => {
- const axisValue = this.getAxisMotionValue(axis);
- if (axisValue && this.constraints !== false) {
- const latest = axisValue.get();
- boxProgress[axis] = calcOrigin$1({ min: latest, max: latest }, this.constraints[axis]);
- }
- });
- /**
- * Update the layout of this element and resolve the latest drag constraints
- */
- const { transformTemplate } = this.visualElement.getProps();
- this.visualElement.current.style.transform = transformTemplate
- ? transformTemplate({}, "")
- : "none";
- projection.root && projection.root.updateScroll();
- projection.updateLayout();
- this.resolveConstraints();
- /**
- * For each axis, calculate the current progress of the layout axis
- * within the new constraints.
- */
- eachAxis((axis) => {
- if (!shouldDrag(axis, drag, null))
- return;
- /**
- * Calculate a new transform based on the previous box progress
- */
- const axisValue = this.getAxisMotionValue(axis);
- const { min, max } = this.constraints[axis];
- axisValue.set(mixNumber$1(min, max, boxProgress[axis]));
- });
- }
- addListeners() {
- if (!this.visualElement.current)
- return;
- elementDragControls.set(this.visualElement, this);
- const element = this.visualElement.current;
- /**
- * Attach a pointerdown event listener on this DOM element to initiate drag tracking.
- */
- const stopPointerListener = addPointerEvent(element, "pointerdown", (event) => {
- const { drag, dragListener = true } = this.getProps();
- drag && dragListener && this.start(event);
- });
- const measureDragConstraints = () => {
- const { dragConstraints } = this.getProps();
- if (isRefObject(dragConstraints) && dragConstraints.current) {
- this.constraints = this.resolveRefConstraints();
- }
- };
- const { projection } = this.visualElement;
- const stopMeasureLayoutListener = projection.addEventListener("measure", measureDragConstraints);
- if (projection && !projection.layout) {
- projection.root && projection.root.updateScroll();
- projection.updateLayout();
- }
- motionDom.frame.read(measureDragConstraints);
- /**
- * Attach a window resize listener to scale the draggable target within its defined
- * constraints as the window resizes.
- */
- const stopResizeListener = addDomEvent(window, "resize", () => this.scalePositionWithinConstraints());
- /**
- * If the element's layout changes, calculate the delta and apply that to
- * the drag gesture's origin point.
- */
- const stopLayoutUpdateListener = projection.addEventListener("didUpdate", (({ delta, hasLayoutChanged }) => {
- if (this.isDragging && hasLayoutChanged) {
- eachAxis((axis) => {
- const motionValue = this.getAxisMotionValue(axis);
- if (!motionValue)
- return;
- this.originPoint[axis] += delta[axis].translate;
- motionValue.set(motionValue.get() + delta[axis].translate);
- });
- this.visualElement.render();
- }
- }));
- return () => {
- stopResizeListener();
- stopPointerListener();
- stopMeasureLayoutListener();
- stopLayoutUpdateListener && stopLayoutUpdateListener();
- };
- }
- getProps() {
- const props = this.visualElement.getProps();
- const { drag = false, dragDirectionLock = false, dragPropagation = false, dragConstraints = false, dragElastic = defaultElastic, dragMomentum = true, } = props;
- return {
- ...props,
- drag,
- dragDirectionLock,
- dragPropagation,
- dragConstraints,
- dragElastic,
- dragMomentum,
- };
- }
- }
- function shouldDrag(direction, drag, currentDirection) {
- return ((drag === true || drag === direction) &&
- (currentDirection === null || currentDirection === direction));
- }
- /**
- * Based on an x/y offset determine the current drag direction. If both axis' offsets are lower
- * than the provided threshold, return `null`.
- *
- * @param offset - The x/y offset from origin.
- * @param lockThreshold - (Optional) - the minimum absolute offset before we can determine a drag direction.
- */
- function getCurrentDirection(offset, lockThreshold = 10) {
- let direction = null;
- if (Math.abs(offset.y) > lockThreshold) {
- direction = "y";
- }
- else if (Math.abs(offset.x) > lockThreshold) {
- direction = "x";
- }
- return direction;
- }
- class DragGesture extends Feature {
- constructor(node) {
- super(node);
- this.removeGroupControls = motionUtils.noop;
- this.removeListeners = motionUtils.noop;
- this.controls = new VisualElementDragControls(node);
- }
- mount() {
- // If we've been provided a DragControls for manual control over the drag gesture,
- // subscribe this component to it on mount.
- const { dragControls } = this.node.getProps();
- if (dragControls) {
- this.removeGroupControls = dragControls.subscribe(this.controls);
- }
- this.removeListeners = this.controls.addListeners() || motionUtils.noop;
- }
- unmount() {
- this.removeGroupControls();
- this.removeListeners();
- }
- }
- const asyncHandler = (handler) => (event, info) => {
- if (handler) {
- motionDom.frame.postRender(() => handler(event, info));
- }
- };
- class PanGesture extends Feature {
- constructor() {
- super(...arguments);
- this.removePointerDownListener = motionUtils.noop;
- }
- onPointerDown(pointerDownEvent) {
- this.session = new PanSession(pointerDownEvent, this.createPanHandlers(), {
- transformPagePoint: this.node.getTransformPagePoint(),
- contextWindow: getContextWindow(this.node),
- });
- }
- createPanHandlers() {
- const { onPanSessionStart, onPanStart, onPan, onPanEnd } = this.node.getProps();
- return {
- onSessionStart: asyncHandler(onPanSessionStart),
- onStart: asyncHandler(onPanStart),
- onMove: onPan,
- onEnd: (event, info) => {
- delete this.session;
- if (onPanEnd) {
- motionDom.frame.postRender(() => onPanEnd(event, info));
- }
- },
- };
- }
- mount() {
- this.removePointerDownListener = addPointerEvent(this.node.current, "pointerdown", (event) => this.onPointerDown(event));
- }
- update() {
- this.session && this.session.updateHandlers(this.createPanHandlers());
- }
- unmount() {
- this.removePointerDownListener();
- this.session && this.session.end();
- }
- }
- /**
- * Internal, exported only for usage in Framer
- */
- const SwitchLayoutGroupContext = React.createContext({});
- class MeasureLayoutWithContext extends React.Component {
- /**
- * This only mounts projection nodes for components that
- * need measuring, we might want to do it for all components
- * in order to incorporate transforms
- */
- componentDidMount() {
- const { visualElement, layoutGroup, switchLayoutGroup, layoutId } = this.props;
- const { projection } = visualElement;
- addScaleCorrector(defaultScaleCorrectors);
- if (projection) {
- if (layoutGroup.group)
- layoutGroup.group.add(projection);
- if (switchLayoutGroup && switchLayoutGroup.register && layoutId) {
- switchLayoutGroup.register(projection);
- }
- projection.root.didUpdate();
- projection.addEventListener("animationComplete", () => {
- this.safeToRemove();
- });
- projection.setOptions({
- ...projection.options,
- onExitComplete: () => this.safeToRemove(),
- });
- }
- globalProjectionState.hasEverUpdated = true;
- }
- getSnapshotBeforeUpdate(prevProps) {
- const { layoutDependency, visualElement, drag, isPresent } = this.props;
- const projection = visualElement.projection;
- if (!projection)
- return null;
- /**
- * TODO: We use this data in relegate to determine whether to
- * promote a previous element. There's no guarantee its presence data
- * will have updated by this point - if a bug like this arises it will
- * have to be that we markForRelegation and then find a new lead some other way,
- * perhaps in didUpdate
- */
- projection.isPresent = isPresent;
- if (drag ||
- prevProps.layoutDependency !== layoutDependency ||
- layoutDependency === undefined ||
- prevProps.isPresent !== isPresent) {
- projection.willUpdate();
- }
- else {
- this.safeToRemove();
- }
- if (prevProps.isPresent !== isPresent) {
- if (isPresent) {
- projection.promote();
- }
- else if (!projection.relegate()) {
- /**
- * If there's another stack member taking over from this one,
- * it's in charge of the exit animation and therefore should
- * be in charge of the safe to remove. Otherwise we call it here.
- */
- motionDom.frame.postRender(() => {
- const stack = projection.getStack();
- if (!stack || !stack.members.length) {
- this.safeToRemove();
- }
- });
- }
- }
- return null;
- }
- componentDidUpdate() {
- const { projection } = this.props.visualElement;
- if (projection) {
- projection.root.didUpdate();
- motionDom.microtask.postRender(() => {
- if (!projection.currentAnimation && projection.isLead()) {
- this.safeToRemove();
- }
- });
- }
- }
- componentWillUnmount() {
- const { visualElement, layoutGroup, switchLayoutGroup: promoteContext, } = this.props;
- const { projection } = visualElement;
- if (projection) {
- projection.scheduleCheckAfterUnmount();
- if (layoutGroup && layoutGroup.group)
- layoutGroup.group.remove(projection);
- if (promoteContext && promoteContext.deregister)
- promoteContext.deregister(projection);
- }
- }
- safeToRemove() {
- const { safeToRemove } = this.props;
- safeToRemove && safeToRemove();
- }
- render() {
- return null;
- }
- }
- function MeasureLayout(props) {
- const [isPresent, safeToRemove] = usePresence();
- const layoutGroup = React.useContext(LayoutGroupContext);
- return (jsxRuntime.jsx(MeasureLayoutWithContext, { ...props, layoutGroup: layoutGroup, switchLayoutGroup: React.useContext(SwitchLayoutGroupContext), isPresent: isPresent, safeToRemove: safeToRemove }));
- }
- const defaultScaleCorrectors = {
- borderRadius: {
- ...correctBorderRadius,
- applyTo: [
- "borderTopLeftRadius",
- "borderTopRightRadius",
- "borderBottomLeftRadius",
- "borderBottomRightRadius",
- ],
- },
- borderTopLeftRadius: correctBorderRadius,
- borderTopRightRadius: correctBorderRadius,
- borderBottomLeftRadius: correctBorderRadius,
- borderBottomRightRadius: correctBorderRadius,
- boxShadow: correctBoxShadow,
- };
- const drag = {
- pan: {
- Feature: PanGesture,
- },
- drag: {
- Feature: DragGesture,
- ProjectionNode: HTMLProjectionNode,
- MeasureLayout,
- },
- };
- function handleHoverEvent(node, event, lifecycle) {
- const { props } = node;
- if (node.animationState && props.whileHover) {
- node.animationState.setActive("whileHover", lifecycle === "Start");
- }
- const eventName = ("onHover" + lifecycle);
- const callback = props[eventName];
- if (callback) {
- motionDom.frame.postRender(() => callback(event, extractEventInfo(event)));
- }
- }
- class HoverGesture extends Feature {
- mount() {
- const { current } = this.node;
- if (!current)
- return;
- this.unmount = motionDom.hover(current, (_element, startEvent) => {
- handleHoverEvent(this.node, startEvent, "Start");
- return (endEvent) => handleHoverEvent(this.node, endEvent, "End");
- });
- }
- unmount() { }
- }
- class FocusGesture extends Feature {
- constructor() {
- super(...arguments);
- this.isActive = false;
- }
- onFocus() {
- let isFocusVisible = false;
- /**
- * If this element doesn't match focus-visible then don't
- * apply whileHover. But, if matches throws that focus-visible
- * is not a valid selector then in that browser outline styles will be applied
- * to the element by default and we want to match that behaviour with whileFocus.
- */
- try {
- isFocusVisible = this.node.current.matches(":focus-visible");
- }
- catch (e) {
- isFocusVisible = true;
- }
- if (!isFocusVisible || !this.node.animationState)
- return;
- this.node.animationState.setActive("whileFocus", true);
- this.isActive = true;
- }
- onBlur() {
- if (!this.isActive || !this.node.animationState)
- return;
- this.node.animationState.setActive("whileFocus", false);
- this.isActive = false;
- }
- mount() {
- this.unmount = pipe(addDomEvent(this.node.current, "focus", () => this.onFocus()), addDomEvent(this.node.current, "blur", () => this.onBlur()));
- }
- unmount() { }
- }
- function handlePressEvent(node, event, lifecycle) {
- const { props } = node;
- if (node.current instanceof HTMLButtonElement && node.current.disabled) {
- return;
- }
- if (node.animationState && props.whileTap) {
- node.animationState.setActive("whileTap", lifecycle === "Start");
- }
- const eventName = ("onTap" + (lifecycle === "End" ? "" : lifecycle));
- const callback = props[eventName];
- if (callback) {
- motionDom.frame.postRender(() => callback(event, extractEventInfo(event)));
- }
- }
- class PressGesture extends Feature {
- mount() {
- const { current } = this.node;
- if (!current)
- return;
- this.unmount = motionDom.press(current, (_element, startEvent) => {
- handlePressEvent(this.node, startEvent, "Start");
- return (endEvent, { success }) => handlePressEvent(this.node, endEvent, success ? "End" : "Cancel");
- }, { useGlobalTarget: this.node.props.globalTapTarget });
- }
- unmount() { }
- }
- /**
- * Map an IntersectionHandler callback to an element. We only ever make one handler for one
- * element, so even though these handlers might all be triggered by different
- * observers, we can keep them in the same map.
- */
- const observerCallbacks = new WeakMap();
- /**
- * Multiple observers can be created for multiple element/document roots. Each with
- * different settings. So here we store dictionaries of observers to each root,
- * using serialised settings (threshold/margin) as lookup keys.
- */
- const observers = new WeakMap();
- const fireObserverCallback = (entry) => {
- const callback = observerCallbacks.get(entry.target);
- callback && callback(entry);
- };
- const fireAllObserverCallbacks = (entries) => {
- entries.forEach(fireObserverCallback);
- };
- function initIntersectionObserver({ root, ...options }) {
- const lookupRoot = root || document;
- /**
- * If we don't have an observer lookup map for this root, create one.
- */
- if (!observers.has(lookupRoot)) {
- observers.set(lookupRoot, {});
- }
- const rootObservers = observers.get(lookupRoot);
- const key = JSON.stringify(options);
- /**
- * If we don't have an observer for this combination of root and settings,
- * create one.
- */
- if (!rootObservers[key]) {
- rootObservers[key] = new IntersectionObserver(fireAllObserverCallbacks, { root, ...options });
- }
- return rootObservers[key];
- }
- function observeIntersection(element, options, callback) {
- const rootInteresectionObserver = initIntersectionObserver(options);
- observerCallbacks.set(element, callback);
- rootInteresectionObserver.observe(element);
- return () => {
- observerCallbacks.delete(element);
- rootInteresectionObserver.unobserve(element);
- };
- }
- const thresholdNames = {
- some: 0,
- all: 1,
- };
- class InViewFeature extends Feature {
- constructor() {
- super(...arguments);
- this.hasEnteredView = false;
- this.isInView = false;
- }
- startObserver() {
- this.unmount();
- const { viewport = {} } = this.node.getProps();
- const { root, margin: rootMargin, amount = "some", once } = viewport;
- const options = {
- root: root ? root.current : undefined,
- rootMargin,
- threshold: typeof amount === "number" ? amount : thresholdNames[amount],
- };
- const onIntersectionUpdate = (entry) => {
- const { isIntersecting } = entry;
- /**
- * If there's been no change in the viewport state, early return.
- */
- if (this.isInView === isIntersecting)
- return;
- this.isInView = isIntersecting;
- /**
- * Handle hasEnteredView. If this is only meant to run once, and
- * element isn't visible, early return. Otherwise set hasEnteredView to true.
- */
- if (once && !isIntersecting && this.hasEnteredView) {
- return;
- }
- else if (isIntersecting) {
- this.hasEnteredView = true;
- }
- if (this.node.animationState) {
- this.node.animationState.setActive("whileInView", isIntersecting);
- }
- /**
- * Use the latest committed props rather than the ones in scope
- * when this observer is created
- */
- const { onViewportEnter, onViewportLeave } = this.node.getProps();
- const callback = isIntersecting ? onViewportEnter : onViewportLeave;
- callback && callback(entry);
- };
- return observeIntersection(this.node.current, options, onIntersectionUpdate);
- }
- mount() {
- this.startObserver();
- }
- update() {
- if (typeof IntersectionObserver === "undefined")
- return;
- const { props, prevProps } = this.node;
- const hasOptionsChanged = ["amount", "margin", "root"].some(hasViewportOptionChanged(props, prevProps));
- if (hasOptionsChanged) {
- this.startObserver();
- }
- }
- unmount() { }
- }
- function hasViewportOptionChanged({ viewport = {} }, { viewport: prevViewport = {} } = {}) {
- return (name) => viewport[name] !== prevViewport[name];
- }
- const gestureAnimations = {
- inView: {
- Feature: InViewFeature,
- },
- tap: {
- Feature: PressGesture,
- },
- focus: {
- Feature: FocusGesture,
- },
- hover: {
- Feature: HoverGesture,
- },
- };
- const layout = {
- layout: {
- ProjectionNode: HTMLProjectionNode,
- MeasureLayout,
- },
- };
- const MotionContext = /* @__PURE__ */ React.createContext({});
- function getCurrentTreeVariants(props, context) {
- if (isControllingVariants(props)) {
- const { initial, animate } = props;
- return {
- initial: initial === false || isVariantLabel(initial)
- ? initial
- : undefined,
- animate: isVariantLabel(animate) ? animate : undefined,
- };
- }
- return props.inherit !== false ? context : {};
- }
- function useCreateMotionContext(props) {
- const { initial, animate } = getCurrentTreeVariants(props, React.useContext(MotionContext));
- return React.useMemo(() => ({ initial, animate }), [variantLabelsAsDependency(initial), variantLabelsAsDependency(animate)]);
- }
- function variantLabelsAsDependency(prop) {
- return Array.isArray(prop) ? prop.join(" ") : prop;
- }
- const motionComponentSymbol = Symbol.for("motionComponentSymbol");
- /**
- * Creates a ref function that, when called, hydrates the provided
- * external ref and VisualElement.
- */
- function useMotionRef(visualState, visualElement, externalRef) {
- return React.useCallback((instance) => {
- if (instance) {
- visualState.onMount && visualState.onMount(instance);
- }
- if (visualElement) {
- if (instance) {
- visualElement.mount(instance);
- }
- else {
- visualElement.unmount();
- }
- }
- if (externalRef) {
- if (typeof externalRef === "function") {
- externalRef(instance);
- }
- else if (isRefObject(externalRef)) {
- externalRef.current = instance;
- }
- }
- },
- /**
- * Only pass a new ref callback to React if we've received a visual element
- * factory. Otherwise we'll be mounting/remounting every time externalRef
- * or other dependencies change.
- */
- [visualElement]);
- }
- function useVisualElement(Component, visualState, props, createVisualElement, ProjectionNodeConstructor) {
- const { visualElement: parent } = React.useContext(MotionContext);
- const lazyContext = React.useContext(LazyContext);
- const presenceContext = React.useContext(PresenceContext);
- const reducedMotionConfig = React.useContext(MotionConfigContext).reducedMotion;
- const visualElementRef = React.useRef(null);
- /**
- * If we haven't preloaded a renderer, check to see if we have one lazy-loaded
- */
- createVisualElement = createVisualElement || lazyContext.renderer;
- if (!visualElementRef.current && createVisualElement) {
- visualElementRef.current = createVisualElement(Component, {
- visualState,
- parent,
- props,
- presenceContext,
- blockInitialAnimation: presenceContext
- ? presenceContext.initial === false
- : false,
- reducedMotionConfig,
- });
- }
- const visualElement = visualElementRef.current;
- /**
- * Load Motion gesture and animation features. These are rendered as renderless
- * components so each feature can optionally make use of React lifecycle methods.
- */
- const initialLayoutGroupConfig = React.useContext(SwitchLayoutGroupContext);
- if (visualElement &&
- !visualElement.projection &&
- ProjectionNodeConstructor &&
- (visualElement.type === "html" || visualElement.type === "svg")) {
- createProjectionNode(visualElementRef.current, props, ProjectionNodeConstructor, initialLayoutGroupConfig);
- }
- const isMounted = React.useRef(false);
- React.useInsertionEffect(() => {
- /**
- * Check the component has already mounted before calling
- * `update` unnecessarily. This ensures we skip the initial update.
- */
- if (visualElement && isMounted.current) {
- visualElement.update(props, presenceContext);
- }
- });
- /**
- * Cache this value as we want to know whether HandoffAppearAnimations
- * was present on initial render - it will be deleted after this.
- */
- const optimisedAppearId = props[optimizedAppearDataAttribute];
- const wantsHandoff = React.useRef(Boolean(optimisedAppearId) &&
- !window.MotionHandoffIsComplete?.(optimisedAppearId) &&
- window.MotionHasOptimisedAnimation?.(optimisedAppearId));
- useIsomorphicLayoutEffect(() => {
- if (!visualElement)
- return;
- isMounted.current = true;
- window.MotionIsMounted = true;
- visualElement.updateFeatures();
- motionDom.microtask.render(visualElement.render);
- /**
- * Ideally this function would always run in a useEffect.
- *
- * However, if we have optimised appear animations to handoff from,
- * it needs to happen synchronously to ensure there's no flash of
- * incorrect styles in the event of a hydration error.
- *
- * So if we detect a situtation where optimised appear animations
- * are running, we use useLayoutEffect to trigger animations.
- */
- if (wantsHandoff.current && visualElement.animationState) {
- visualElement.animationState.animateChanges();
- }
- });
- React.useEffect(() => {
- if (!visualElement)
- return;
- if (!wantsHandoff.current && visualElement.animationState) {
- visualElement.animationState.animateChanges();
- }
- if (wantsHandoff.current) {
- // This ensures all future calls to animateChanges() in this component will run in useEffect
- queueMicrotask(() => {
- window.MotionHandoffMarkAsComplete?.(optimisedAppearId);
- });
- wantsHandoff.current = false;
- }
- });
- return visualElement;
- }
- function createProjectionNode(visualElement, props, ProjectionNodeConstructor, initialPromotionConfig) {
- const { layoutId, layout, drag, dragConstraints, layoutScroll, layoutRoot, layoutCrossfade, } = props;
- visualElement.projection = new ProjectionNodeConstructor(visualElement.latestValues, props["data-framer-portal-id"]
- ? undefined
- : getClosestProjectingNode(visualElement.parent));
- visualElement.projection.setOptions({
- layoutId,
- layout,
- alwaysMeasureLayout: Boolean(drag) || (dragConstraints && isRefObject(dragConstraints)),
- visualElement,
- /**
- * TODO: Update options in an effect. This could be tricky as it'll be too late
- * to update by the time layout animations run.
- * We also need to fix this safeToRemove by linking it up to the one returned by usePresence,
- * ensuring it gets called if there's no potential layout animations.
- *
- */
- animationType: typeof layout === "string" ? layout : "both",
- initialPromotionConfig,
- crossfade: layoutCrossfade,
- layoutScroll,
- layoutRoot,
- });
- }
- function getClosestProjectingNode(visualElement) {
- if (!visualElement)
- return undefined;
- return visualElement.options.allowProjection !== false
- ? visualElement.projection
- : getClosestProjectingNode(visualElement.parent);
- }
- /**
- * Create a `motion` component.
- *
- * This function accepts a Component argument, which can be either a string (ie "div"
- * for `motion.div`), or an actual React component.
- *
- * Alongside this is a config option which provides a way of rendering the provided
- * component "offline", or outside the React render cycle.
- */
- function createRendererMotionComponent({ preloadedFeatures, createVisualElement, useRender, useVisualState, Component, }) {
- preloadedFeatures && loadFeatures(preloadedFeatures);
- function MotionComponent(props, externalRef) {
- /**
- * If we need to measure the element we load this functionality in a
- * separate class component in order to gain access to getSnapshotBeforeUpdate.
- */
- let MeasureLayout;
- const configAndProps = {
- ...React.useContext(MotionConfigContext),
- ...props,
- layoutId: useLayoutId(props),
- };
- const { isStatic } = configAndProps;
- const context = useCreateMotionContext(props);
- const visualState = useVisualState(props, isStatic);
- if (!isStatic && isBrowser) {
- useStrictMode(configAndProps, preloadedFeatures);
- const layoutProjection = getProjectionFunctionality(configAndProps);
- MeasureLayout = layoutProjection.MeasureLayout;
- /**
- * Create a VisualElement for this component. A VisualElement provides a common
- * interface to renderer-specific APIs (ie DOM/Three.js etc) as well as
- * providing a way of rendering to these APIs outside of the React render loop
- * for more performant animations and interactions
- */
- context.visualElement = useVisualElement(Component, visualState, configAndProps, createVisualElement, layoutProjection.ProjectionNode);
- }
- /**
- * The mount order and hierarchy is specific to ensure our element ref
- * is hydrated by the time features fire their effects.
- */
- return (jsxRuntime.jsxs(MotionContext.Provider, { value: context, children: [MeasureLayout && context.visualElement ? (jsxRuntime.jsx(MeasureLayout, { visualElement: context.visualElement, ...configAndProps })) : null, useRender(Component, props, useMotionRef(visualState, context.visualElement, externalRef), visualState, isStatic, context.visualElement)] }));
- }
- MotionComponent.displayName = `motion.${typeof Component === "string"
- ? Component
- : `create(${Component.displayName ?? Component.name ?? ""})`}`;
- const ForwardRefMotionComponent = React.forwardRef(MotionComponent);
- ForwardRefMotionComponent[motionComponentSymbol] = Component;
- return ForwardRefMotionComponent;
- }
- function useLayoutId({ layoutId }) {
- const layoutGroupId = React.useContext(LayoutGroupContext).id;
- return layoutGroupId && layoutId !== undefined
- ? layoutGroupId + "-" + layoutId
- : layoutId;
- }
- function useStrictMode(configAndProps, preloadedFeatures) {
- const isStrict = React.useContext(LazyContext).strict;
- /**
- * If we're in development mode, check to make sure we're not rendering a motion component
- * as a child of LazyMotion, as this will break the file-size benefits of using it.
- */
- if (process.env.NODE_ENV !== "production" &&
- preloadedFeatures &&
- isStrict) {
- const strictMessage = "You have rendered a `motion` component within a `LazyMotion` component. This will break tree shaking. Import and render a `m` component instead.";
- configAndProps.ignoreStrict
- ? motionUtils.warning(false, strictMessage)
- : motionUtils.invariant(false, strictMessage);
- }
- }
- function getProjectionFunctionality(props) {
- const { drag, layout } = featureDefinitions;
- if (!drag && !layout)
- return {};
- const combined = { ...drag, ...layout };
- return {
- MeasureLayout: drag?.isEnabled(props) || layout?.isEnabled(props)
- ? combined.MeasureLayout
- : undefined,
- ProjectionNode: combined.ProjectionNode,
- };
- }
- const createHtmlRenderState = () => ({
- style: {},
- transform: {},
- transformOrigin: {},
- vars: {},
- });
- function copyRawValuesOnly(target, source, props) {
- for (const key in source) {
- if (!isMotionValue(source[key]) && !isForcedMotionValue(key, props)) {
- target[key] = source[key];
- }
- }
- }
- function useInitialMotionValues({ transformTemplate }, visualState) {
- return React.useMemo(() => {
- const state = createHtmlRenderState();
- buildHTMLStyles(state, visualState, transformTemplate);
- return Object.assign({}, state.vars, state.style);
- }, [visualState]);
- }
- function useStyle(props, visualState) {
- const styleProp = props.style || {};
- const style = {};
- /**
- * Copy non-Motion Values straight into style
- */
- copyRawValuesOnly(style, styleProp, props);
- Object.assign(style, useInitialMotionValues(props, visualState));
- return style;
- }
- function useHTMLProps(props, visualState) {
- // The `any` isn't ideal but it is the type of createElement props argument
- const htmlProps = {};
- const style = useStyle(props, visualState);
- if (props.drag && props.dragListener !== false) {
- // Disable the ghost element when a user drags
- htmlProps.draggable = false;
- // Disable text selection
- style.userSelect =
- style.WebkitUserSelect =
- style.WebkitTouchCallout =
- "none";
- // Disable scrolling on the draggable direction
- style.touchAction =
- props.drag === true
- ? "none"
- : `pan-${props.drag === "x" ? "y" : "x"}`;
- }
- if (props.tabIndex === undefined &&
- (props.onTap || props.onTapStart || props.whileTap)) {
- htmlProps.tabIndex = 0;
- }
- htmlProps.style = style;
- return htmlProps;
- }
- /**
- * We keep these listed separately as we use the lowercase tag names as part
- * of the runtime bundle to detect SVG components
- */
- const lowercaseSVGElements = [
- "animate",
- "circle",
- "defs",
- "desc",
- "ellipse",
- "g",
- "image",
- "line",
- "filter",
- "marker",
- "mask",
- "metadata",
- "path",
- "pattern",
- "polygon",
- "polyline",
- "rect",
- "stop",
- "switch",
- "symbol",
- "svg",
- "text",
- "tspan",
- "use",
- "view",
- ];
- function isSVGComponent(Component) {
- if (
- /**
- * If it's not a string, it's a custom React component. Currently we only support
- * HTML custom React components.
- */
- typeof Component !== "string" ||
- /**
- * If it contains a dash, the element is a custom HTML webcomponent.
- */
- Component.includes("-")) {
- return false;
- }
- else if (
- /**
- * If it's in our list of lowercase SVG tags, it's an SVG component
- */
- lowercaseSVGElements.indexOf(Component) > -1 ||
- /**
- * If it contains a capital letter, it's an SVG component
- */
- /[A-Z]/u.test(Component)) {
- return true;
- }
- return false;
- }
- const dashKeys = {
- offset: "stroke-dashoffset",
- array: "stroke-dasharray",
- };
- const camelKeys = {
- offset: "strokeDashoffset",
- array: "strokeDasharray",
- };
- /**
- * Build SVG path properties. Uses the path's measured length to convert
- * our custom pathLength, pathSpacing and pathOffset into stroke-dashoffset
- * and stroke-dasharray attributes.
- *
- * This function is mutative to reduce per-frame GC.
- */
- function buildSVGPath(attrs, length, spacing = 1, offset = 0, useDashCase = true) {
- // Normalise path length by setting SVG attribute pathLength to 1
- attrs.pathLength = 1;
- // We use dash case when setting attributes directly to the DOM node and camel case
- // when defining props on a React component.
- const keys = useDashCase ? dashKeys : camelKeys;
- // Build the dash offset
- attrs[keys.offset] = px.transform(-offset);
- // Build the dash array
- const pathLength = px.transform(length);
- const pathSpacing = px.transform(spacing);
- attrs[keys.array] = `${pathLength} ${pathSpacing}`;
- }
- function calcOrigin(origin, offset, size) {
- return typeof origin === "string"
- ? origin
- : px.transform(offset + size * origin);
- }
- /**
- * The SVG transform origin defaults are different to CSS and is less intuitive,
- * so we use the measured dimensions of the SVG to reconcile these.
- */
- function calcSVGTransformOrigin(dimensions, originX, originY) {
- const pxOriginX = calcOrigin(originX, dimensions.x, dimensions.width);
- const pxOriginY = calcOrigin(originY, dimensions.y, dimensions.height);
- return `${pxOriginX} ${pxOriginY}`;
- }
- /**
- * Build SVG visual attrbutes, like cx and style.transform
- */
- function buildSVGAttrs(state, { attrX, attrY, attrScale, originX, originY, pathLength, pathSpacing = 1, pathOffset = 0,
- // This is object creation, which we try to avoid per-frame.
- ...latest }, isSVGTag, transformTemplate) {
- buildHTMLStyles(state, latest, transformTemplate);
- /**
- * For svg tags we just want to make sure viewBox is animatable and treat all the styles
- * as normal HTML tags.
- */
- if (isSVGTag) {
- if (state.style.viewBox) {
- state.attrs.viewBox = state.style.viewBox;
- }
- return;
- }
- state.attrs = state.style;
- state.style = {};
- const { attrs, style, dimensions } = state;
- /**
- * However, we apply transforms as CSS transforms. So if we detect a transform we take it from attrs
- * and copy it into style.
- */
- if (attrs.transform) {
- if (dimensions)
- style.transform = attrs.transform;
- delete attrs.transform;
- }
- // Parse transformOrigin
- if (dimensions &&
- (originX !== undefined || originY !== undefined || style.transform)) {
- style.transformOrigin = calcSVGTransformOrigin(dimensions, originX !== undefined ? originX : 0.5, originY !== undefined ? originY : 0.5);
- }
- // Render attrX/attrY/attrScale as attributes
- if (attrX !== undefined)
- attrs.x = attrX;
- if (attrY !== undefined)
- attrs.y = attrY;
- if (attrScale !== undefined)
- attrs.scale = attrScale;
- // Build SVG path if one has been defined
- if (pathLength !== undefined) {
- buildSVGPath(attrs, pathLength, pathSpacing, pathOffset, false);
- }
- }
- const createSvgRenderState = () => ({
- ...createHtmlRenderState(),
- attrs: {},
- });
- const isSVGTag = (tag) => typeof tag === "string" && tag.toLowerCase() === "svg";
- function useSVGProps(props, visualState, _isStatic, Component) {
- const visualProps = React.useMemo(() => {
- const state = createSvgRenderState();
- buildSVGAttrs(state, visualState, isSVGTag(Component), props.transformTemplate);
- return {
- ...state.attrs,
- style: { ...state.style },
- };
- }, [visualState]);
- if (props.style) {
- const rawStyles = {};
- copyRawValuesOnly(rawStyles, props.style, props);
- visualProps.style = { ...rawStyles, ...visualProps.style };
- }
- return visualProps;
- }
- function createUseRender(forwardMotionProps = false) {
- const useRender = (Component, props, ref, { latestValues }, isStatic) => {
- const useVisualProps = isSVGComponent(Component)
- ? useSVGProps
- : useHTMLProps;
- const visualProps = useVisualProps(props, latestValues, isStatic, Component);
- const filteredProps = filterProps(props, typeof Component === "string", forwardMotionProps);
- const elementProps = Component !== React.Fragment
- ? { ...filteredProps, ...visualProps, ref }
- : {};
- /**
- * If component has been handed a motion value as its child,
- * memoise its initial value and render that. Subsequent updates
- * will be handled by the onChange handler
- */
- const { children } = props;
- const renderedChildren = React.useMemo(() => (isMotionValue(children) ? children.get() : children), [children]);
- return React.createElement(Component, {
- ...elementProps,
- children: renderedChildren,
- });
- };
- return useRender;
- }
- function makeState({ scrapeMotionValuesFromProps, createRenderState, onUpdate, }, props, context, presenceContext) {
- const state = {
- latestValues: makeLatestValues(props, context, presenceContext, scrapeMotionValuesFromProps),
- renderState: createRenderState(),
- };
- if (onUpdate) {
- /**
- * onMount works without the VisualElement because it could be
- * called before the VisualElement payload has been hydrated.
- * (e.g. if someone is using m components <m.circle />)
- */
- state.onMount = (instance) => onUpdate({ props, current: instance, ...state });
- state.onUpdate = (visualElement) => onUpdate(visualElement);
- }
- return state;
- }
- const makeUseVisualState = (config) => (props, isStatic) => {
- const context = React.useContext(MotionContext);
- const presenceContext = React.useContext(PresenceContext);
- const make = () => makeState(config, props, context, presenceContext);
- return isStatic ? make() : useConstant(make);
- };
- function makeLatestValues(props, context, presenceContext, scrapeMotionValues) {
- const values = {};
- const motionValues = scrapeMotionValues(props, {});
- for (const key in motionValues) {
- values[key] = resolveMotionValue(motionValues[key]);
- }
- let { initial, animate } = props;
- const isControllingVariants$1 = isControllingVariants(props);
- const isVariantNode$1 = isVariantNode(props);
- if (context &&
- isVariantNode$1 &&
- !isControllingVariants$1 &&
- props.inherit !== false) {
- if (initial === undefined)
- initial = context.initial;
- if (animate === undefined)
- animate = context.animate;
- }
- let isInitialAnimationBlocked = presenceContext
- ? presenceContext.initial === false
- : false;
- isInitialAnimationBlocked = isInitialAnimationBlocked || initial === false;
- const variantToSet = isInitialAnimationBlocked ? animate : initial;
- if (variantToSet &&
- typeof variantToSet !== "boolean" &&
- !isAnimationControls(variantToSet)) {
- const list = Array.isArray(variantToSet) ? variantToSet : [variantToSet];
- for (let i = 0; i < list.length; i++) {
- const resolved = resolveVariantFromProps(props, list[i]);
- if (resolved) {
- const { transitionEnd, transition, ...target } = resolved;
- for (const key in target) {
- let valueTarget = target[key];
- if (Array.isArray(valueTarget)) {
- /**
- * Take final keyframe if the initial animation is blocked because
- * we want to initialise at the end of that blocked animation.
- */
- const index = isInitialAnimationBlocked
- ? valueTarget.length - 1
- : 0;
- valueTarget = valueTarget[index];
- }
- if (valueTarget !== null) {
- values[key] = valueTarget;
- }
- }
- for (const key in transitionEnd) {
- values[key] = transitionEnd[key];
- }
- }
- }
- }
- return values;
- }
- const htmlMotionConfig = {
- useVisualState: makeUseVisualState({
- scrapeMotionValuesFromProps: scrapeMotionValuesFromProps$1,
- createRenderState: createHtmlRenderState,
- }),
- };
- function updateSVGDimensions(instance, renderState) {
- try {
- renderState.dimensions =
- typeof instance.getBBox === "function"
- ? instance.getBBox()
- : instance.getBoundingClientRect();
- }
- catch (e) {
- // Most likely trying to measure an unrendered element under Firefox
- renderState.dimensions = {
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- };
- }
- }
- /**
- * A set of attribute names that are always read/written as camel case.
- */
- const camelCaseAttributes = new Set([
- "baseFrequency",
- "diffuseConstant",
- "kernelMatrix",
- "kernelUnitLength",
- "keySplines",
- "keyTimes",
- "limitingConeAngle",
- "markerHeight",
- "markerWidth",
- "numOctaves",
- "targetX",
- "targetY",
- "surfaceScale",
- "specularConstant",
- "specularExponent",
- "stdDeviation",
- "tableValues",
- "viewBox",
- "gradientTransform",
- "pathLength",
- "startOffset",
- "textLength",
- "lengthAdjust",
- ]);
- function renderSVG(element, renderState, _styleProp, projection) {
- renderHTML(element, renderState, undefined, projection);
- for (const key in renderState.attrs) {
- element.setAttribute(!camelCaseAttributes.has(key) ? camelToDash(key) : key, renderState.attrs[key]);
- }
- }
- function scrapeMotionValuesFromProps(props, prevProps, visualElement) {
- const newValues = scrapeMotionValuesFromProps$1(props, prevProps, visualElement);
- for (const key in props) {
- if (isMotionValue(props[key]) ||
- isMotionValue(prevProps[key])) {
- const targetKey = transformPropOrder.indexOf(key) !== -1
- ? "attr" + key.charAt(0).toUpperCase() + key.substring(1)
- : key;
- newValues[targetKey] = props[key];
- }
- }
- return newValues;
- }
- const layoutProps = ["x", "y", "width", "height", "cx", "cy", "r"];
- const svgMotionConfig = {
- useVisualState: makeUseVisualState({
- scrapeMotionValuesFromProps: scrapeMotionValuesFromProps,
- createRenderState: createSvgRenderState,
- onUpdate: ({ props, prevProps, current, renderState, latestValues, }) => {
- if (!current)
- return;
- let hasTransform = !!props.drag;
- if (!hasTransform) {
- for (const key in latestValues) {
- if (transformProps.has(key)) {
- hasTransform = true;
- break;
- }
- }
- }
- if (!hasTransform)
- return;
- let needsMeasure = !prevProps;
- if (prevProps) {
- /**
- * Check the layout props for changes, if any are found we need to
- * measure the element again.
- */
- for (let i = 0; i < layoutProps.length; i++) {
- const key = layoutProps[i];
- if (props[key] !==
- prevProps[key]) {
- needsMeasure = true;
- }
- }
- }
- if (!needsMeasure)
- return;
- motionDom.frame.read(() => {
- updateSVGDimensions(current, renderState);
- motionDom.frame.render(() => {
- buildSVGAttrs(renderState, latestValues, isSVGTag(current.tagName), props.transformTemplate);
- renderSVG(current, renderState);
- });
- });
- },
- }),
- };
- function createMotionComponentFactory(preloadedFeatures, createVisualElement) {
- return function createMotionComponent(Component, { forwardMotionProps } = { forwardMotionProps: false }) {
- const baseConfig = isSVGComponent(Component)
- ? svgMotionConfig
- : htmlMotionConfig;
- const config = {
- ...baseConfig,
- preloadedFeatures,
- useRender: createUseRender(forwardMotionProps),
- createVisualElement,
- Component,
- };
- return createRendererMotionComponent(config);
- };
- }
- class SVGVisualElement extends DOMVisualElement {
- constructor() {
- super(...arguments);
- this.type = "svg";
- this.isSVGTag = false;
- this.measureInstanceViewportBox = createBox;
- this.updateDimensions = () => {
- if (this.current && !this.renderState.dimensions) {
- updateSVGDimensions(this.current, this.renderState);
- }
- };
- }
- getBaseTargetFromProps(props, key) {
- return props[key];
- }
- readValueFromInstance(instance, key) {
- if (transformProps.has(key)) {
- const defaultType = getDefaultValueType(key);
- return defaultType ? defaultType.default || 0 : 0;
- }
- key = !camelCaseAttributes.has(key) ? camelToDash(key) : key;
- return instance.getAttribute(key);
- }
- scrapeMotionValuesFromProps(props, prevProps, visualElement) {
- return scrapeMotionValuesFromProps(props, prevProps, visualElement);
- }
- onBindTransform() {
- if (this.current && !this.renderState.dimensions) {
- motionDom.frame.postRender(this.updateDimensions);
- }
- }
- build(renderState, latestValues, props) {
- buildSVGAttrs(renderState, latestValues, this.isSVGTag, props.transformTemplate);
- }
- renderInstance(instance, renderState, styleProp, projection) {
- renderSVG(instance, renderState, styleProp, projection);
- }
- mount(instance) {
- this.isSVGTag = isSVGTag(instance.tagName);
- super.mount(instance);
- }
- }
- const createDomVisualElement = (Component, options) => {
- return isSVGComponent(Component)
- ? new SVGVisualElement(options)
- : new HTMLVisualElement(options, {
- allowProjection: Component !== React.Fragment,
- });
- };
- const createMotionComponent = /*@__PURE__*/ createMotionComponentFactory({
- ...animations,
- ...gestureAnimations,
- ...drag,
- ...layout,
- }, createDomVisualElement);
- exports.AcceleratedAnimation = AcceleratedAnimation;
- exports.FlatTree = FlatTree;
- exports.HTMLVisualElement = HTMLVisualElement;
- exports.LayoutGroupContext = LayoutGroupContext;
- exports.LazyContext = LazyContext;
- exports.MotionConfigContext = MotionConfigContext;
- exports.MotionContext = MotionContext;
- exports.PresenceContext = PresenceContext;
- exports.SVGVisualElement = SVGVisualElement;
- exports.SwitchLayoutGroupContext = SwitchLayoutGroupContext;
- exports.VisualElement = VisualElement;
- exports.acceleratedValues = acceleratedValues;
- exports.addDomEvent = addDomEvent;
- exports.addPointerEvent = addPointerEvent;
- exports.addPointerInfo = addPointerInfo;
- exports.addScaleCorrector = addScaleCorrector;
- exports.animateSingleValue = animateSingleValue;
- exports.animateTarget = animateTarget;
- exports.animateValue = animateValue;
- exports.animateVisualElement = animateVisualElement;
- exports.animations = animations;
- exports.anticipate = anticipate;
- exports.backIn = backIn;
- exports.backInOut = backInOut;
- exports.backOut = backOut;
- exports.buildTransform = buildTransform;
- exports.calcLength = calcLength;
- exports.camelToDash = camelToDash;
- exports.circIn = circIn;
- exports.circInOut = circInOut;
- exports.circOut = circOut;
- exports.clamp = clamp;
- exports.color = color;
- exports.complex = complex;
- exports.createBox = createBox;
- exports.createDomVisualElement = createDomVisualElement;
- exports.createMotionComponent = createMotionComponent;
- exports.createMotionComponentFactory = createMotionComponentFactory;
- exports.createRendererMotionComponent = createRendererMotionComponent;
- exports.cubicBezier = cubicBezier;
- exports.defaultOffset = defaultOffset;
- exports.delay = delay;
- exports.distance = distance;
- exports.distance2D = distance2D;
- exports.drag = drag;
- exports.easeIn = easeIn;
- exports.easeInOut = easeInOut;
- exports.easeOut = easeOut;
- exports.easingDefinitionToFunction = easingDefinitionToFunction;
- exports.fillOffset = fillOffset;
- exports.filterProps = filterProps;
- exports.findSpring = findSpring;
- exports.gestureAnimations = gestureAnimations;
- exports.getOptimisedAppearId = getOptimisedAppearId;
- exports.hasReducedMotionListener = hasReducedMotionListener;
- exports.inertia = inertia;
- exports.initPrefersReducedMotion = initPrefersReducedMotion;
- exports.instantAnimationState = instantAnimationState;
- exports.interpolate = interpolate;
- exports.isBrowser = isBrowser;
- exports.isEasingArray = isEasingArray;
- exports.isMotionValue = isMotionValue;
- exports.isSVGElement = isSVGElement;
- exports.isValidMotionProp = isValidMotionProp;
- exports.keyframes = keyframes;
- exports.layout = layout;
- exports.loadExternalIsValidProp = loadExternalIsValidProp;
- exports.loadFeatures = loadFeatures;
- exports.makeUseVisualState = makeUseVisualState;
- exports.mirrorEasing = mirrorEasing;
- exports.mix = mix;
- exports.mixNumber = mixNumber$1;
- exports.motionComponentSymbol = motionComponentSymbol;
- exports.optimizedAppearDataAttribute = optimizedAppearDataAttribute;
- exports.optimizedAppearDataId = optimizedAppearDataId;
- exports.pipe = pipe;
- exports.prefersReducedMotion = prefersReducedMotion;
- exports.px = px;
- exports.resolveMotionValue = resolveMotionValue;
- exports.reverseEasing = reverseEasing;
- exports.rootProjectionNode = rootProjectionNode;
- exports.setTarget = setTarget;
- exports.spring = spring;
- exports.transformProps = transformProps;
- exports.useConstant = useConstant;
- exports.useIsPresent = useIsPresent;
- exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect;
- exports.usePresence = usePresence;
- exports.visualElementStore = visualElementStore;
|