| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367 |
- 'use strict';
- Object.defineProperty(exports, '__esModule', { value: true });
- var motionDom = require('motion-dom');
- var motionUtils = require('motion-utils');
- const clamp = (min, max, v) => {
- if (v > max)
- return max;
- if (v < min)
- return min;
- return v;
- };
- 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;
- };
- const wrap = (min, max, v) => {
- const rangeSize = max - min;
- return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
- };
- const isEasingArray = (ease) => {
- return Array.isArray(ease) && typeof ease[0] !== "number";
- };
- function getEasingForSegment(easing, i) {
- return isEasingArray(easing) ? easing[wrap(0, easing.length, i)] : easing;
- }
- /*
- 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;
- };
- 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$1(arr) {
- const offset = [0];
- fillOffset(offset, arr.length - 1);
- return offset;
- }
- const isMotionValue = (value) => Boolean(value && value.getVelocity);
- function isDOMKeyframes(keyframes) {
- return typeof keyframes === "object" && !Array.isArray(keyframes);
- }
- function resolveSubjects(subject, keyframes, scope, selectorCache) {
- if (typeof subject === "string" && isDOMKeyframes(keyframes)) {
- return motionDom.resolveElements(subject, scope, selectorCache);
- }
- else if (subject instanceof NodeList) {
- return Array.from(subject);
- }
- else if (Array.isArray(subject)) {
- return subject;
- }
- else {
- return [subject];
- }
- }
- function calculateRepeatDuration(duration, repeat, _repeatDelay) {
- return duration * (repeat + 1);
- }
- /**
- * Given a absolute or relative time definition and current/prev time state of the sequence,
- * calculate an absolute time for the next keyframes.
- */
- function calcNextTime(current, next, prev, labels) {
- if (typeof next === "number") {
- return next;
- }
- else if (next.startsWith("-") || next.startsWith("+")) {
- return Math.max(0, current + parseFloat(next));
- }
- else if (next === "<") {
- return prev;
- }
- else {
- return labels.get(next) ?? current;
- }
- }
- function eraseKeyframes(sequence, startTime, endTime) {
- for (let i = 0; i < sequence.length; i++) {
- const keyframe = sequence[i];
- if (keyframe.at > startTime && keyframe.at < endTime) {
- motionUtils.removeItem(sequence, keyframe);
- // If we remove this item we have to push the pointer back one
- i--;
- }
- }
- }
- function addKeyframes(sequence, keyframes, easing, offset, startTime, endTime) {
- /**
- * Erase every existing value between currentTime and targetTime,
- * this will essentially splice this timeline into any currently
- * defined ones.
- */
- eraseKeyframes(sequence, startTime, endTime);
- for (let i = 0; i < keyframes.length; i++) {
- sequence.push({
- value: keyframes[i],
- at: mixNumber$1(startTime, endTime, offset[i]),
- easing: getEasingForSegment(easing, i),
- });
- }
- }
- /**
- * Take an array of times that represent repeated keyframes. For instance
- * if we have original times of [0, 0.5, 1] then our repeated times will
- * be [0, 0.5, 1, 1, 1.5, 2]. Loop over the times and scale them back
- * down to a 0-1 scale.
- */
- function normalizeTimes(times, repeat) {
- for (let i = 0; i < times.length; i++) {
- times[i] = times[i] / (repeat + 1);
- }
- }
- function compareByTime(a, b) {
- if (a.at === b.at) {
- if (a.value === null)
- return 1;
- if (b.value === null)
- return -1;
- return 0;
- }
- else {
- return a.at - b.at;
- }
- }
- const defaultSegmentEasing = "easeInOut";
- const MAX_REPEAT = 20;
- function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) {
- const defaultDuration = defaultTransition.duration || 0.3;
- const animationDefinitions = new Map();
- const sequences = new Map();
- const elementCache = {};
- const timeLabels = new Map();
- let prevTime = 0;
- let currentTime = 0;
- let totalDuration = 0;
- /**
- * Build the timeline by mapping over the sequence array and converting
- * the definitions into keyframes and offsets with absolute time values.
- * These will later get converted into relative offsets in a second pass.
- */
- for (let i = 0; i < sequence.length; i++) {
- const segment = sequence[i];
- /**
- * If this is a timeline label, mark it and skip the rest of this iteration.
- */
- if (typeof segment === "string") {
- timeLabels.set(segment, currentTime);
- continue;
- }
- else if (!Array.isArray(segment)) {
- timeLabels.set(segment.name, calcNextTime(currentTime, segment.at, prevTime, timeLabels));
- continue;
- }
- let [subject, keyframes, transition = {}] = segment;
- /**
- * If a relative or absolute time value has been specified we need to resolve
- * it in relation to the currentTime.
- */
- if (transition.at !== undefined) {
- currentTime = calcNextTime(currentTime, transition.at, prevTime, timeLabels);
- }
- /**
- * Keep track of the maximum duration in this definition. This will be
- * applied to currentTime once the definition has been parsed.
- */
- let maxDuration = 0;
- const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => {
- const valueKeyframesAsList = keyframesAsList(valueKeyframes);
- const { delay = 0, times = defaultOffset$1(valueKeyframesAsList), type = "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition;
- let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
- /**
- * Resolve stagger() if defined.
- */
- const calculatedDelay = typeof delay === "function"
- ? delay(elementIndex, numSubjects)
- : delay;
- /**
- * If this animation should and can use a spring, generate a spring easing function.
- */
- const numKeyframes = valueKeyframesAsList.length;
- const createGenerator = motionDom.isGenerator(type)
- ? type
- : generators?.[type];
- if (numKeyframes <= 2 && createGenerator) {
- /**
- * As we're creating an easing function from a spring,
- * ideally we want to generate it using the real distance
- * between the two keyframes. However this isn't always
- * possible - in these situations we use 0-100.
- */
- let absoluteDelta = 100;
- if (numKeyframes === 2 &&
- isNumberKeyframesArray(valueKeyframesAsList)) {
- const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0];
- absoluteDelta = Math.abs(delta);
- }
- const springTransition = { ...remainingTransition };
- if (duration !== undefined) {
- springTransition.duration = motionUtils.secondsToMilliseconds(duration);
- }
- const springEasing = motionDom.createGeneratorEasing(springTransition, absoluteDelta, createGenerator);
- ease = springEasing.ease;
- duration = springEasing.duration;
- }
- duration ?? (duration = defaultDuration);
- const startTime = currentTime + calculatedDelay;
- /**
- * If there's only one time offset of 0, fill in a second with length 1
- */
- if (times.length === 1 && times[0] === 0) {
- times[1] = 1;
- }
- /**
- * Fill out if offset if fewer offsets than keyframes
- */
- const remainder = times.length - valueKeyframesAsList.length;
- remainder > 0 && fillOffset(times, remainder);
- /**
- * If only one value has been set, ie [1], push a null to the start of
- * the keyframe array. This will let us mark a keyframe at this point
- * that will later be hydrated with the previous value.
- */
- valueKeyframesAsList.length === 1 &&
- valueKeyframesAsList.unshift(null);
- /**
- * Handle repeat options
- */
- if (repeat) {
- motionUtils.invariant(repeat < MAX_REPEAT, "Repeat count too high, must be less than 20");
- duration = calculateRepeatDuration(duration, repeat);
- const originalKeyframes = [...valueKeyframesAsList];
- const originalTimes = [...times];
- ease = Array.isArray(ease) ? [...ease] : [ease];
- const originalEase = [...ease];
- for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
- valueKeyframesAsList.push(...originalKeyframes);
- for (let keyframeIndex = 0; keyframeIndex < originalKeyframes.length; keyframeIndex++) {
- times.push(originalTimes[keyframeIndex] + (repeatIndex + 1));
- ease.push(keyframeIndex === 0
- ? "linear"
- : getEasingForSegment(originalEase, keyframeIndex - 1));
- }
- }
- normalizeTimes(times, repeat);
- }
- const targetTime = startTime + duration;
- /**
- * Add keyframes, mapping offsets to absolute time.
- */
- addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime);
- maxDuration = Math.max(calculatedDelay + duration, maxDuration);
- totalDuration = Math.max(targetTime, totalDuration);
- };
- if (isMotionValue(subject)) {
- const subjectSequence = getSubjectSequence(subject, sequences);
- resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence));
- }
- else {
- const subjects = resolveSubjects(subject, keyframes, scope, elementCache);
- const numSubjects = subjects.length;
- /**
- * For every element in this segment, process the defined values.
- */
- for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) {
- /**
- * Cast necessary, but we know these are of this type
- */
- keyframes = keyframes;
- transition = transition;
- const thisSubject = subjects[subjectIndex];
- const subjectSequence = getSubjectSequence(thisSubject, sequences);
- for (const key in keyframes) {
- resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects);
- }
- }
- }
- prevTime = currentTime;
- currentTime += maxDuration;
- }
- /**
- * For every element and value combination create a new animation.
- */
- sequences.forEach((valueSequences, element) => {
- for (const key in valueSequences) {
- const valueSequence = valueSequences[key];
- /**
- * Arrange all the keyframes in ascending time order.
- */
- valueSequence.sort(compareByTime);
- const keyframes = [];
- const valueOffset = [];
- const valueEasing = [];
- /**
- * For each keyframe, translate absolute times into
- * relative offsets based on the total duration of the timeline.
- */
- for (let i = 0; i < valueSequence.length; i++) {
- const { at, value, easing } = valueSequence[i];
- keyframes.push(value);
- valueOffset.push(motionUtils.progress(0, totalDuration, at));
- valueEasing.push(easing || "easeOut");
- }
- /**
- * If the first keyframe doesn't land on offset: 0
- * provide one by duplicating the initial keyframe. This ensures
- * it snaps to the first keyframe when the animation starts.
- */
- if (valueOffset[0] !== 0) {
- valueOffset.unshift(0);
- keyframes.unshift(keyframes[0]);
- valueEasing.unshift(defaultSegmentEasing);
- }
- /**
- * If the last keyframe doesn't land on offset: 1
- * provide one with a null wildcard value. This will ensure it
- * stays static until the end of the animation.
- */
- if (valueOffset[valueOffset.length - 1] !== 1) {
- valueOffset.push(1);
- keyframes.push(null);
- }
- if (!animationDefinitions.has(element)) {
- animationDefinitions.set(element, {
- keyframes: {},
- transition: {},
- });
- }
- const definition = animationDefinitions.get(element);
- definition.keyframes[key] = keyframes;
- definition.transition[key] = {
- ...defaultTransition,
- duration: totalDuration,
- ease: valueEasing,
- times: valueOffset,
- ...sequenceTransition,
- };
- }
- });
- return animationDefinitions;
- }
- function getSubjectSequence(subject, sequences) {
- !sequences.has(subject) && sequences.set(subject, {});
- return sequences.get(subject);
- }
- function getValueSequence(name, sequences) {
- if (!sequences[name])
- sequences[name] = [];
- return sequences[name];
- }
- function keyframesAsList(keyframes) {
- return Array.isArray(keyframes) ? keyframes : [keyframes];
- }
- function getValueTransition(transition, key) {
- return transition && transition[key]
- ? {
- ...transition,
- ...transition[key],
- }
- : { ...transition };
- }
- const isNumber = (keyframe) => typeof keyframe === "number";
- const isNumberKeyframesArray = (keyframes) => keyframes.every(isNumber);
- const visualElementStore = new WeakMap();
- /**
- * 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 isKeyframesTarget = (v) => {
- return Array.isArray(v);
- };
- const resolveFinalValueInKeyframes = (v) => {
- // TODO maybe throw if v.length - 1 is placeholder token?
- return isKeyframesTarget(v) ? v[v.length - 1] || 0 : v;
- };
- 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;
- }
- 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);
- }
- }
- /**
- * 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];
- }
- /*
- 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;
- }
- }
- 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 (!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$1(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$1(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$1(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$1(from);
- return mixer(from, to);
- }
- 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 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 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$1(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;
- }
- }
- /**
- * 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 (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);
- }
- };
- /**
- * 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 isSVGElement(element) {
- return element instanceof SVGElement && element.tagName !== "svg";
- }
- const createAxis = () => ({ min: 0, max: 0 });
- const createBox = () => ({
- x: createAxis(),
- y: createAxis(),
- });
- 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]),
- };
- }
- const isBrowser = typeof window !== "undefined";
- // 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));
- 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;
- }
- 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}`;
- }
- }
- 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);
- }
- }
- /**
- * 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",
- ]);
- const isSVGTag = (tag) => typeof tag === "string" && tag.toLowerCase() === "svg";
- 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,
- };
- }
- }
- 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 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]);
- }
- }
- const scaleCorrectors = {};
- 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 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;
- }
- 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);
- }
- }
- /**
- * 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 },
- };
- }
- /**
- * 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 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);
- }
- }
- function isObjectKey(key, object) {
- return key in object;
- }
- class ObjectVisualElement extends VisualElement {
- constructor() {
- super(...arguments);
- this.type = "object";
- }
- readValueFromInstance(instance, key) {
- if (isObjectKey(key, instance)) {
- const value = instance[key];
- if (typeof value === "string" || typeof value === "number") {
- return value;
- }
- }
- return undefined;
- }
- getBaseTargetFromProps() {
- return undefined;
- }
- removeValueFromRenderState(key, renderState) {
- delete renderState.output[key];
- }
- measureInstanceViewportBox() {
- return createBox();
- }
- build(renderState, latestValues) {
- Object.assign(renderState.output, latestValues);
- }
- renderInstance(instance, { output }) {
- Object.assign(instance, output);
- }
- sortInstanceNodePosition() {
- return 0;
- }
- }
- function createDOMVisualElement(element) {
- const options = {
- presenceContext: null,
- props: {},
- visualState: {
- renderState: {
- transform: {},
- transformOrigin: {},
- style: {},
- vars: {},
- attrs: {},
- },
- latestValues: {},
- },
- };
- const node = isSVGElement(element)
- ? new SVGVisualElement(options)
- : new HTMLVisualElement(options);
- node.mount(element);
- visualElementStore.set(element, node);
- }
- function createObjectVisualElement(subject) {
- const options = {
- presenceContext: null,
- props: {},
- visualState: {
- renderState: {
- output: {},
- },
- latestValues: {},
- },
- };
- const node = new ObjectVisualElement(options);
- node.mount(subject);
- visualElementStore.set(subject, node);
- }
- function animateSingleValue(value, keyframes, options) {
- const motionValue = isMotionValue(value) ? value : motionDom.motionValue(value);
- motionValue.start(animateMotionValue("", motionValue, keyframes, options));
- return motionValue.animation;
- }
- function isSingleValue(subject, keyframes) {
- return (isMotionValue(subject) ||
- typeof subject === "number" ||
- (typeof subject === "string" && !isDOMKeyframes(keyframes)));
- }
- /**
- * Implementation
- */
- function animateSubject(subject, keyframes, options, scope) {
- const animations = [];
- if (isSingleValue(subject, keyframes)) {
- animations.push(animateSingleValue(subject, isDOMKeyframes(keyframes)
- ? keyframes.default || keyframes
- : keyframes, options ? options.default || options : options));
- }
- else {
- const subjects = resolveSubjects(subject, keyframes, scope);
- const numSubjects = subjects.length;
- motionUtils.invariant(Boolean(numSubjects), "No valid elements provided.");
- for (let i = 0; i < numSubjects; i++) {
- const thisSubject = subjects[i];
- const createVisualElement = thisSubject instanceof Element
- ? createDOMVisualElement
- : createObjectVisualElement;
- if (!visualElementStore.has(thisSubject)) {
- createVisualElement(thisSubject);
- }
- const visualElement = visualElementStore.get(thisSubject);
- const transition = { ...options };
- /**
- * Resolve stagger function if provided.
- */
- if ("delay" in transition &&
- typeof transition.delay === "function") {
- transition.delay = transition.delay(i, numSubjects);
- }
- animations.push(...animateTarget(visualElement, { ...keyframes, transition }, {}));
- }
- }
- return animations;
- }
- function animateSequence(sequence, options, scope) {
- const animations = [];
- const animationDefinitions = createAnimationsFromSequence(sequence, options, scope, { spring });
- animationDefinitions.forEach(({ keyframes, transition }, subject) => {
- animations.push(...animateSubject(subject, keyframes, transition));
- });
- return animations;
- }
- function isSequence(value) {
- return Array.isArray(value) && value.some(Array.isArray);
- }
- /**
- * Creates an animation function that is optionally scoped
- * to a specific element.
- */
- function createScopedAnimate(scope) {
- /**
- * Implementation
- */
- function scopedAnimate(subjectOrSequence, optionsOrKeyframes, options) {
- let animations = [];
- if (isSequence(subjectOrSequence)) {
- animations = animateSequence(subjectOrSequence, optionsOrKeyframes, scope);
- }
- else {
- animations = animateSubject(subjectOrSequence, optionsOrKeyframes, options, scope);
- }
- const animation = new motionDom.GroupAnimationWithThen(animations);
- if (scope) {
- scope.animations.push(animation);
- }
- return animation;
- }
- return scopedAnimate;
- }
- const animate = createScopedAnimate();
- function animateElements(elementOrSelector, keyframes, options, scope) {
- const elements = motionDom.resolveElements(elementOrSelector, scope);
- const numElements = elements.length;
- motionUtils.invariant(Boolean(numElements), "No valid element provided.");
- const animations = [];
- for (let i = 0; i < numElements; i++) {
- const element = elements[i];
- const elementTransition = { ...options };
- /**
- * Resolve stagger function if provided.
- */
- if (typeof elementTransition.delay === "function") {
- elementTransition.delay = elementTransition.delay(i, numElements);
- }
- for (const valueName in keyframes) {
- const valueKeyframes = keyframes[valueName];
- const valueOptions = {
- ...motionDom.getValueTransition(elementTransition, valueName),
- };
- valueOptions.duration && (valueOptions.duration = motionUtils.secondsToMilliseconds(valueOptions.duration));
- valueOptions.delay && (valueOptions.delay = motionUtils.secondsToMilliseconds(valueOptions.delay));
- animations.push(new motionDom.NativeAnimation({
- element,
- name: valueName,
- keyframes: valueKeyframes,
- transition: valueOptions,
- allowFlatten: !elementTransition.type && !elementTransition.ease,
- }));
- }
- }
- return animations;
- }
- const createScopedWaapiAnimate = (scope) => {
- function scopedAnimate(elementOrSelector, keyframes, options) {
- return new motionDom.GroupAnimationWithThen(animateElements(elementOrSelector, keyframes, options, scope));
- }
- return scopedAnimate;
- };
- const animateMini = /*@__PURE__*/ createScopedWaapiAnimate();
- function observeTimeline(update, timeline) {
- let prevProgress;
- const onFrame = () => {
- const { currentTime } = timeline;
- const percentage = currentTime === null ? 0 : currentTime.value;
- const progress = percentage / 100;
- if (prevProgress !== progress) {
- update(progress);
- }
- prevProgress = progress;
- };
- motionDom.frame.update(onFrame, true);
- return () => motionDom.cancelFrame(onFrame);
- }
- const resizeHandlers = new WeakMap();
- let observer;
- function getElementSize(target, borderBoxSize) {
- if (borderBoxSize) {
- const { inlineSize, blockSize } = borderBoxSize[0];
- return { width: inlineSize, height: blockSize };
- }
- else if (target instanceof SVGElement && "getBBox" in target) {
- return target.getBBox();
- }
- else {
- return {
- width: target.offsetWidth,
- height: target.offsetHeight,
- };
- }
- }
- function notifyTarget({ target, contentRect, borderBoxSize, }) {
- resizeHandlers.get(target)?.forEach((handler) => {
- handler({
- target,
- contentSize: contentRect,
- get size() {
- return getElementSize(target, borderBoxSize);
- },
- });
- });
- }
- function notifyAll(entries) {
- entries.forEach(notifyTarget);
- }
- function createResizeObserver() {
- if (typeof ResizeObserver === "undefined")
- return;
- observer = new ResizeObserver(notifyAll);
- }
- function resizeElement(target, handler) {
- if (!observer)
- createResizeObserver();
- const elements = motionDom.resolveElements(target);
- elements.forEach((element) => {
- let elementHandlers = resizeHandlers.get(element);
- if (!elementHandlers) {
- elementHandlers = new Set();
- resizeHandlers.set(element, elementHandlers);
- }
- elementHandlers.add(handler);
- observer?.observe(element);
- });
- return () => {
- elements.forEach((element) => {
- const elementHandlers = resizeHandlers.get(element);
- elementHandlers?.delete(handler);
- if (!elementHandlers?.size) {
- observer?.unobserve(element);
- }
- });
- };
- }
- const windowCallbacks = new Set();
- let windowResizeHandler;
- function createWindowResizeHandler() {
- windowResizeHandler = () => {
- const size = {
- width: window.innerWidth,
- height: window.innerHeight,
- };
- const info = {
- target: window,
- size,
- contentSize: size,
- };
- windowCallbacks.forEach((callback) => callback(info));
- };
- window.addEventListener("resize", windowResizeHandler);
- }
- function resizeWindow(callback) {
- windowCallbacks.add(callback);
- if (!windowResizeHandler)
- createWindowResizeHandler();
- return () => {
- windowCallbacks.delete(callback);
- if (!windowCallbacks.size && windowResizeHandler) {
- windowResizeHandler = undefined;
- }
- };
- }
- function resize(a, b) {
- return typeof a === "function" ? resizeWindow(a) : resizeElement(a, b);
- }
- /**
- * A time in milliseconds, beyond which we consider the scroll velocity to be 0.
- */
- const maxElapsed = 50;
- const createAxisInfo = () => ({
- current: 0,
- offset: [],
- progress: 0,
- scrollLength: 0,
- targetOffset: 0,
- targetLength: 0,
- containerLength: 0,
- velocity: 0,
- });
- const createScrollInfo = () => ({
- time: 0,
- x: createAxisInfo(),
- y: createAxisInfo(),
- });
- const keys = {
- x: {
- length: "Width",
- position: "Left",
- },
- y: {
- length: "Height",
- position: "Top",
- },
- };
- function updateAxisInfo(element, axisName, info, time) {
- const axis = info[axisName];
- const { length, position } = keys[axisName];
- const prev = axis.current;
- const prevTime = info.time;
- axis.current = element[`scroll${position}`];
- axis.scrollLength = element[`scroll${length}`] - element[`client${length}`];
- axis.offset.length = 0;
- axis.offset[0] = 0;
- axis.offset[1] = axis.scrollLength;
- axis.progress = motionUtils.progress(0, axis.scrollLength, axis.current);
- const elapsed = time - prevTime;
- axis.velocity =
- elapsed > maxElapsed
- ? 0
- : motionUtils.velocityPerSecond(axis.current - prev, elapsed);
- }
- function updateScrollInfo(element, info, time) {
- updateAxisInfo(element, "x", info, time);
- updateAxisInfo(element, "y", info, time);
- info.time = time;
- }
- function calcInset(element, container) {
- const inset = { x: 0, y: 0 };
- let current = element;
- while (current && current !== container) {
- if (current instanceof HTMLElement) {
- inset.x += current.offsetLeft;
- inset.y += current.offsetTop;
- current = current.offsetParent;
- }
- else if (current.tagName === "svg") {
- /**
- * This isn't an ideal approach to measuring the offset of <svg /> tags.
- * It would be preferable, given they behave like HTMLElements in most ways
- * to use offsetLeft/Top. But these don't exist on <svg />. Likewise we
- * can't use .getBBox() like most SVG elements as these provide the offset
- * relative to the SVG itself, which for <svg /> is usually 0x0.
- */
- const svgBoundingBox = current.getBoundingClientRect();
- current = current.parentElement;
- const parentBoundingBox = current.getBoundingClientRect();
- inset.x += svgBoundingBox.left - parentBoundingBox.left;
- inset.y += svgBoundingBox.top - parentBoundingBox.top;
- }
- else if (current instanceof SVGGraphicsElement) {
- const { x, y } = current.getBBox();
- inset.x += x;
- inset.y += y;
- let svg = null;
- let parent = current.parentNode;
- while (!svg) {
- if (parent.tagName === "svg") {
- svg = parent;
- }
- parent = current.parentNode;
- }
- current = svg;
- }
- else {
- break;
- }
- }
- return inset;
- }
- const namedEdges = {
- start: 0,
- center: 0.5,
- end: 1,
- };
- function resolveEdge(edge, length, inset = 0) {
- let delta = 0;
- /**
- * If we have this edge defined as a preset, replace the definition
- * with the numerical value.
- */
- if (edge in namedEdges) {
- edge = namedEdges[edge];
- }
- /**
- * Handle unit values
- */
- if (typeof edge === "string") {
- const asNumber = parseFloat(edge);
- if (edge.endsWith("px")) {
- delta = asNumber;
- }
- else if (edge.endsWith("%")) {
- edge = asNumber / 100;
- }
- else if (edge.endsWith("vw")) {
- delta = (asNumber / 100) * document.documentElement.clientWidth;
- }
- else if (edge.endsWith("vh")) {
- delta = (asNumber / 100) * document.documentElement.clientHeight;
- }
- else {
- edge = asNumber;
- }
- }
- /**
- * If the edge is defined as a number, handle as a progress value.
- */
- if (typeof edge === "number") {
- delta = length * edge;
- }
- return inset + delta;
- }
- const defaultOffset = [0, 0];
- function resolveOffset(offset, containerLength, targetLength, targetInset) {
- let offsetDefinition = Array.isArray(offset) ? offset : defaultOffset;
- let targetPoint = 0;
- let containerPoint = 0;
- if (typeof offset === "number") {
- /**
- * If we're provided offset: [0, 0.5, 1] then each number x should become
- * [x, x], so we default to the behaviour of mapping 0 => 0 of both target
- * and container etc.
- */
- offsetDefinition = [offset, offset];
- }
- else if (typeof offset === "string") {
- offset = offset.trim();
- if (offset.includes(" ")) {
- offsetDefinition = offset.split(" ");
- }
- else {
- /**
- * If we're provided a definition like "100px" then we want to apply
- * that only to the top of the target point, leaving the container at 0.
- * Whereas a named offset like "end" should be applied to both.
- */
- offsetDefinition = [offset, namedEdges[offset] ? offset : `0`];
- }
- }
- targetPoint = resolveEdge(offsetDefinition[0], targetLength, targetInset);
- containerPoint = resolveEdge(offsetDefinition[1], containerLength);
- return targetPoint - containerPoint;
- }
- const ScrollOffset = {
- Enter: [
- [0, 1],
- [1, 1],
- ],
- Exit: [
- [0, 0],
- [1, 0],
- ],
- Any: [
- [1, 0],
- [0, 1],
- ],
- All: [
- [0, 0],
- [1, 1],
- ],
- };
- const point = { x: 0, y: 0 };
- function getTargetSize(target) {
- return "getBBox" in target && target.tagName !== "svg"
- ? target.getBBox()
- : { width: target.clientWidth, height: target.clientHeight };
- }
- function resolveOffsets(container, info, options) {
- const { offset: offsetDefinition = ScrollOffset.All } = options;
- const { target = container, axis = "y" } = options;
- const lengthLabel = axis === "y" ? "height" : "width";
- const inset = target !== container ? calcInset(target, container) : point;
- /**
- * Measure the target and container. If they're the same thing then we
- * use the container's scrollWidth/Height as the target, from there
- * all other calculations can remain the same.
- */
- const targetSize = target === container
- ? { width: container.scrollWidth, height: container.scrollHeight }
- : getTargetSize(target);
- const containerSize = {
- width: container.clientWidth,
- height: container.clientHeight,
- };
- /**
- * Reset the length of the resolved offset array rather than creating a new one.
- * TODO: More reusable data structures for targetSize/containerSize would also be good.
- */
- info[axis].offset.length = 0;
- /**
- * Populate the offset array by resolving the user's offset definition into
- * a list of pixel scroll offets.
- */
- let hasChanged = !info[axis].interpolate;
- const numOffsets = offsetDefinition.length;
- for (let i = 0; i < numOffsets; i++) {
- const offset = resolveOffset(offsetDefinition[i], containerSize[lengthLabel], targetSize[lengthLabel], inset[axis]);
- if (!hasChanged && offset !== info[axis].interpolatorOffsets[i]) {
- hasChanged = true;
- }
- info[axis].offset[i] = offset;
- }
- /**
- * If the pixel scroll offsets have changed, create a new interpolator function
- * to map scroll value into a progress.
- */
- if (hasChanged) {
- info[axis].interpolate = interpolate(info[axis].offset, defaultOffset$1(offsetDefinition), { clamp: false });
- info[axis].interpolatorOffsets = [...info[axis].offset];
- }
- info[axis].progress = clamp(0, 1, info[axis].interpolate(info[axis].current));
- }
- function measure(container, target = container, info) {
- /**
- * Find inset of target within scrollable container
- */
- info.x.targetOffset = 0;
- info.y.targetOffset = 0;
- if (target !== container) {
- let node = target;
- while (node && node !== container) {
- info.x.targetOffset += node.offsetLeft;
- info.y.targetOffset += node.offsetTop;
- node = node.offsetParent;
- }
- }
- info.x.targetLength =
- target === container ? target.scrollWidth : target.clientWidth;
- info.y.targetLength =
- target === container ? target.scrollHeight : target.clientHeight;
- info.x.containerLength = container.clientWidth;
- info.y.containerLength = container.clientHeight;
- /**
- * In development mode ensure scroll containers aren't position: static as this makes
- * it difficult to measure their relative positions.
- */
- if (process.env.NODE_ENV !== "production") {
- if (container && target && target !== container) {
- motionUtils.warnOnce(getComputedStyle(container).position !== "static", "Please ensure that the container has a non-static position, like 'relative', 'fixed', or 'absolute' to ensure scroll offset is calculated correctly.");
- }
- }
- }
- function createOnScrollHandler(element, onScroll, info, options = {}) {
- return {
- measure: () => measure(element, options.target, info),
- update: (time) => {
- updateScrollInfo(element, info, time);
- if (options.offset || options.target) {
- resolveOffsets(element, info, options);
- }
- },
- notify: () => onScroll(info),
- };
- }
- const scrollListeners = new WeakMap();
- const resizeListeners = new WeakMap();
- const onScrollHandlers = new WeakMap();
- const getEventTarget = (element) => element === document.documentElement ? window : element;
- function scrollInfo(onScroll, { container = document.documentElement, ...options } = {}) {
- let containerHandlers = onScrollHandlers.get(container);
- /**
- * Get the onScroll handlers for this container.
- * If one isn't found, create a new one.
- */
- if (!containerHandlers) {
- containerHandlers = new Set();
- onScrollHandlers.set(container, containerHandlers);
- }
- /**
- * Create a new onScroll handler for the provided callback.
- */
- const info = createScrollInfo();
- const containerHandler = createOnScrollHandler(container, onScroll, info, options);
- containerHandlers.add(containerHandler);
- /**
- * Check if there's a scroll event listener for this container.
- * If not, create one.
- */
- if (!scrollListeners.has(container)) {
- const measureAll = () => {
- for (const handler of containerHandlers)
- handler.measure();
- };
- const updateAll = () => {
- for (const handler of containerHandlers) {
- handler.update(motionDom.frameData.timestamp);
- }
- };
- const notifyAll = () => {
- for (const handler of containerHandlers)
- handler.notify();
- };
- const listener = () => {
- motionDom.frame.read(measureAll, false, true);
- motionDom.frame.read(updateAll, false, true);
- motionDom.frame.update(notifyAll, false, true);
- };
- scrollListeners.set(container, listener);
- const target = getEventTarget(container);
- window.addEventListener("resize", listener, { passive: true });
- if (container !== document.documentElement) {
- resizeListeners.set(container, resize(container, listener));
- }
- target.addEventListener("scroll", listener, { passive: true });
- }
- const listener = scrollListeners.get(container);
- motionDom.frame.read(listener, false, true);
- return () => {
- motionDom.cancelFrame(listener);
- /**
- * Check if we even have any handlers for this container.
- */
- const currentHandlers = onScrollHandlers.get(container);
- if (!currentHandlers)
- return;
- currentHandlers.delete(containerHandler);
- if (currentHandlers.size)
- return;
- /**
- * If no more handlers, remove the scroll listener too.
- */
- const scrollListener = scrollListeners.get(container);
- scrollListeners.delete(container);
- if (scrollListener) {
- getEventTarget(container).removeEventListener("scroll", scrollListener);
- resizeListeners.get(container)?.();
- window.removeEventListener("resize", scrollListener);
- }
- };
- }
- function scrollTimelineFallback({ source, container, axis = "y", }) {
- // Support legacy source argument. Deprecate later.
- if (source)
- container = source;
- // ScrollTimeline records progress as a percentage CSSUnitValue
- const currentTime = { value: 0 };
- const cancel = scrollInfo((info) => {
- currentTime.value = info[axis].progress * 100;
- }, { container, axis });
- return { currentTime, cancel };
- }
- const timelineCache = new Map();
- function getTimeline({ source, container = document.documentElement, axis = "y", } = {}) {
- // Support legacy source argument. Deprecate later.
- if (source)
- container = source;
- if (!timelineCache.has(container)) {
- timelineCache.set(container, {});
- }
- const elementCache = timelineCache.get(container);
- if (!elementCache[axis]) {
- elementCache[axis] = motionDom.supportsScrollTimeline()
- ? new ScrollTimeline({ source: container, axis })
- : scrollTimelineFallback({ source: container, axis });
- }
- return elementCache[axis];
- }
- /**
- * If the onScroll function has two arguments, it's expecting
- * more specific information about the scroll from scrollInfo.
- */
- function isOnScrollWithInfo(onScroll) {
- return onScroll.length === 2;
- }
- /**
- * Currently, we only support element tracking with `scrollInfo`, though in
- * the future we can also offer ViewTimeline support.
- */
- function needsElementTracking(options) {
- return options && (options.target || options.offset);
- }
- function scrollFunction(onScroll, options) {
- if (isOnScrollWithInfo(onScroll) || needsElementTracking(options)) {
- return scrollInfo((info) => {
- onScroll(info[options.axis].progress, info);
- }, options);
- }
- else {
- return observeTimeline(onScroll, getTimeline(options));
- }
- }
- function scrollAnimation(animation, options) {
- animation.flatten();
- if (needsElementTracking(options)) {
- animation.pause();
- return scrollInfo((info) => {
- animation.time = animation.duration * info[options.axis].progress;
- }, options);
- }
- else {
- const timeline = getTimeline(options);
- if (animation.attachTimeline) {
- return animation.attachTimeline(timeline, (valueAnimation) => {
- valueAnimation.pause();
- return observeTimeline((progress) => {
- valueAnimation.time = valueAnimation.duration * progress;
- }, timeline);
- });
- }
- else {
- return motionUtils.noop;
- }
- }
- }
- function scroll(onScroll, { axis = "y", ...options } = {}) {
- const optionsWithDefaults = { axis, ...options };
- return typeof onScroll === "function"
- ? scrollFunction(onScroll, optionsWithDefaults)
- : scrollAnimation(onScroll, optionsWithDefaults);
- }
- const thresholds = {
- some: 0,
- all: 1,
- };
- function inView(elementOrSelector, onStart, { root, margin: rootMargin, amount = "some" } = {}) {
- const elements = motionDom.resolveElements(elementOrSelector);
- const activeIntersections = new WeakMap();
- const onIntersectionChange = (entries) => {
- entries.forEach((entry) => {
- const onEnd = activeIntersections.get(entry.target);
- /**
- * If there's no change to the intersection, we don't need to
- * do anything here.
- */
- if (entry.isIntersecting === Boolean(onEnd))
- return;
- if (entry.isIntersecting) {
- const newOnEnd = onStart(entry.target, entry);
- if (typeof newOnEnd === "function") {
- activeIntersections.set(entry.target, newOnEnd);
- }
- else {
- observer.unobserve(entry.target);
- }
- }
- else if (typeof onEnd === "function") {
- onEnd(entry);
- activeIntersections.delete(entry.target);
- }
- });
- };
- const observer = new IntersectionObserver(onIntersectionChange, {
- root,
- rootMargin,
- threshold: typeof amount === "number" ? amount : thresholds[amount],
- });
- elements.forEach((element) => observer.observe(element));
- return () => observer.disconnect();
- }
- function steps(numSteps, direction = "end") {
- return (progress) => {
- progress =
- direction === "end"
- ? Math.min(progress, 0.999)
- : Math.max(progress, 0.001);
- const expanded = progress * numSteps;
- const rounded = direction === "end" ? Math.floor(expanded) : Math.ceil(expanded);
- return clamp(0, 1, rounded / numSteps);
- };
- }
- function getOriginIndex(from, total) {
- if (from === "first") {
- return 0;
- }
- else {
- const lastIndex = total - 1;
- return from === "last" ? lastIndex : lastIndex / 2;
- }
- }
- function stagger(duration = 0.1, { startDelay = 0, from = 0, ease } = {}) {
- return (i, total) => {
- const fromIndex = typeof from === "number" ? from : getOriginIndex(from, total);
- const distance = Math.abs(fromIndex - i);
- let delay = duration * distance;
- if (ease) {
- const maxDelay = total * duration;
- const easingFunction = easingDefinitionToFunction(ease);
- delay = easingFunction(delay / maxDelay) * maxDelay;
- }
- return startDelay + delay;
- };
- }
- /**
- * 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);
- }
- function delayInSeconds(callback, timeout) {
- return delay(callback, motionUtils.secondsToMilliseconds(timeout));
- }
- 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);
- }
- const isCustomValueType = (v) => {
- return v && typeof v === "object" && v.mix;
- };
- const getMixer = (v) => (isCustomValueType(v) ? v.mix : undefined);
- function transform(...args) {
- const useImmediate = !Array.isArray(args[0]);
- const argOffset = useImmediate ? 0 : -1;
- const inputValue = args[0 + argOffset];
- const inputRange = args[1 + argOffset];
- const outputRange = args[2 + argOffset];
- const options = args[3 + argOffset];
- const interpolator = interpolate(inputRange, outputRange, {
- mixer: getMixer(outputRange[0]),
- ...options,
- });
- return useImmediate ? interpolator(inputValue) : interpolator;
- }
- Object.defineProperty(exports, "MotionValue", {
- enumerable: true,
- get: function () { return motionDom.MotionValue; }
- });
- Object.defineProperty(exports, "cancelFrame", {
- enumerable: true,
- get: function () { return motionDom.cancelFrame; }
- });
- Object.defineProperty(exports, "cancelSync", {
- enumerable: true,
- get: function () { return motionDom.cancelSync; }
- });
- Object.defineProperty(exports, "frame", {
- enumerable: true,
- get: function () { return motionDom.frame; }
- });
- Object.defineProperty(exports, "frameData", {
- enumerable: true,
- get: function () { return motionDom.frameData; }
- });
- Object.defineProperty(exports, "hover", {
- enumerable: true,
- get: function () { return motionDom.hover; }
- });
- Object.defineProperty(exports, "isDragActive", {
- enumerable: true,
- get: function () { return motionDom.isDragActive; }
- });
- Object.defineProperty(exports, "motionValue", {
- enumerable: true,
- get: function () { return motionDom.motionValue; }
- });
- Object.defineProperty(exports, "press", {
- enumerable: true,
- get: function () { return motionDom.press; }
- });
- Object.defineProperty(exports, "sync", {
- enumerable: true,
- get: function () { return motionDom.sync; }
- });
- Object.defineProperty(exports, "time", {
- enumerable: true,
- get: function () { return motionDom.time; }
- });
- Object.defineProperty(exports, "invariant", {
- enumerable: true,
- get: function () { return motionUtils.invariant; }
- });
- Object.defineProperty(exports, "noop", {
- enumerable: true,
- get: function () { return motionUtils.noop; }
- });
- Object.defineProperty(exports, "progress", {
- enumerable: true,
- get: function () { return motionUtils.progress; }
- });
- exports.animate = animate;
- exports.animateMini = animateMini;
- exports.anticipate = anticipate;
- exports.backIn = backIn;
- exports.backInOut = backInOut;
- exports.backOut = backOut;
- exports.circIn = circIn;
- exports.circInOut = circInOut;
- exports.circOut = circOut;
- exports.clamp = clamp;
- exports.createScopedAnimate = createScopedAnimate;
- exports.cubicBezier = cubicBezier;
- exports.delay = delayInSeconds;
- exports.distance = distance;
- exports.distance2D = distance2D;
- exports.easeIn = easeIn;
- exports.easeInOut = easeInOut;
- exports.easeOut = easeOut;
- exports.inView = inView;
- exports.inertia = inertia;
- exports.interpolate = interpolate;
- exports.keyframes = keyframes;
- exports.mirrorEasing = mirrorEasing;
- exports.mix = mix;
- exports.pipe = pipe;
- exports.reverseEasing = reverseEasing;
- exports.scroll = scroll;
- exports.scrollInfo = scrollInfo;
- exports.spring = spring;
- exports.stagger = stagger;
- exports.steps = steps;
- exports.transform = transform;
- exports.wrap = wrap;
|