motion-dom.dev.js 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746
  1. (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('motion-utils')) :
  3. typeof define === 'function' && define.amd ? define(['exports', 'motion-utils'], factory) :
  4. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MotionDom = {}, global.MotionUtils));
  5. })(this, (function (exports, motionUtils) { 'use strict';
  6. const supportsScrollTimeline = /* @__PURE__ */ motionUtils.memo(() => window.ScrollTimeline !== undefined);
  7. class GroupAnimation {
  8. constructor(animations) {
  9. // Bound to accomodate common `return animation.stop` pattern
  10. this.stop = () => this.runAll("stop");
  11. this.animations = animations.filter(Boolean);
  12. }
  13. get finished() {
  14. return Promise.all(this.animations.map((animation) => animation.finished));
  15. }
  16. /**
  17. * TODO: Filter out cancelled or stopped animations before returning
  18. */
  19. getAll(propName) {
  20. return this.animations[0][propName];
  21. }
  22. setAll(propName, newValue) {
  23. for (let i = 0; i < this.animations.length; i++) {
  24. this.animations[i][propName] = newValue;
  25. }
  26. }
  27. attachTimeline(timeline, fallback) {
  28. const subscriptions = this.animations.map((animation) => {
  29. if (supportsScrollTimeline() && animation.attachTimeline) {
  30. return animation.attachTimeline(timeline);
  31. }
  32. else if (typeof fallback === "function") {
  33. return fallback(animation);
  34. }
  35. });
  36. return () => {
  37. subscriptions.forEach((cancel, i) => {
  38. cancel && cancel();
  39. this.animations[i].stop();
  40. });
  41. };
  42. }
  43. get time() {
  44. return this.getAll("time");
  45. }
  46. set time(time) {
  47. this.setAll("time", time);
  48. }
  49. get speed() {
  50. return this.getAll("speed");
  51. }
  52. set speed(speed) {
  53. this.setAll("speed", speed);
  54. }
  55. get startTime() {
  56. return this.getAll("startTime");
  57. }
  58. get duration() {
  59. let max = 0;
  60. for (let i = 0; i < this.animations.length; i++) {
  61. max = Math.max(max, this.animations[i].duration);
  62. }
  63. return max;
  64. }
  65. runAll(methodName) {
  66. this.animations.forEach((controls) => controls[methodName]());
  67. }
  68. flatten() {
  69. this.runAll("flatten");
  70. }
  71. play() {
  72. this.runAll("play");
  73. }
  74. pause() {
  75. this.runAll("pause");
  76. }
  77. cancel() {
  78. this.runAll("cancel");
  79. }
  80. complete() {
  81. this.runAll("complete");
  82. }
  83. }
  84. class GroupAnimationWithThen extends GroupAnimation {
  85. then(onResolve, _onReject) {
  86. return this.finished.finally(onResolve).then(() => { });
  87. }
  88. }
  89. const isCSSVar = (name) => name.startsWith("--");
  90. const style$1 = {
  91. set: (element, name, value) => {
  92. isCSSVar(name)
  93. ? element.style.setProperty(name, value)
  94. : (element.style[name] = value);
  95. },
  96. get: (element, name) => {
  97. return isCSSVar(name)
  98. ? element.style.getPropertyValue(name)
  99. : element.style[name];
  100. },
  101. };
  102. const isNotNull = (value) => value !== null;
  103. function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyframe) {
  104. const resolvedKeyframes = keyframes.filter(isNotNull);
  105. const index = repeat && repeatType !== "loop" && repeat % 2 === 1
  106. ? 0
  107. : resolvedKeyframes.length - 1;
  108. return !index || finalKeyframe === undefined
  109. ? resolvedKeyframes[index]
  110. : finalKeyframe;
  111. }
  112. const supportsPartialKeyframes = /*@__PURE__*/ motionUtils.memo(() => {
  113. try {
  114. document.createElement("div").animate({ opacity: [1] });
  115. }
  116. catch (e) {
  117. return false;
  118. }
  119. return true;
  120. });
  121. const pxValues = new Set([
  122. // Border props
  123. "borderWidth",
  124. "borderTopWidth",
  125. "borderRightWidth",
  126. "borderBottomWidth",
  127. "borderLeftWidth",
  128. "borderRadius",
  129. "radius",
  130. "borderTopLeftRadius",
  131. "borderTopRightRadius",
  132. "borderBottomRightRadius",
  133. "borderBottomLeftRadius",
  134. // Positioning props
  135. "width",
  136. "maxWidth",
  137. "height",
  138. "maxHeight",
  139. "top",
  140. "right",
  141. "bottom",
  142. "left",
  143. // Spacing props
  144. "padding",
  145. "paddingTop",
  146. "paddingRight",
  147. "paddingBottom",
  148. "paddingLeft",
  149. "margin",
  150. "marginTop",
  151. "marginRight",
  152. "marginBottom",
  153. "marginLeft",
  154. // Misc
  155. "backgroundPositionX",
  156. "backgroundPositionY",
  157. ]);
  158. function hydrateKeyframes(element, name, keyframes, pseudoElement) {
  159. if (!Array.isArray(keyframes)) {
  160. keyframes = [keyframes];
  161. }
  162. for (let i = 0; i < keyframes.length; i++) {
  163. if (keyframes[i] === null) {
  164. keyframes[i] =
  165. i === 0 && !pseudoElement
  166. ? style$1.get(element, name)
  167. : keyframes[i - 1];
  168. }
  169. if (typeof keyframes[i] === "number" && pxValues.has(name)) {
  170. keyframes[i] = keyframes[i] + "px";
  171. }
  172. }
  173. if (!pseudoElement && !supportsPartialKeyframes() && keyframes.length < 2) {
  174. keyframes.unshift(style$1.get(element, name));
  175. }
  176. return keyframes;
  177. }
  178. const activeAnimations = {
  179. layout: 0,
  180. mainThread: 0,
  181. waapi: 0,
  182. };
  183. const statsBuffer = {
  184. value: null,
  185. addProjectionMetrics: null,
  186. };
  187. const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
  188. /**
  189. * Add the ability for test suites to manually set support flags
  190. * to better test more environments.
  191. */
  192. const supportsFlags = {};
  193. function memoSupports(callback, supportsFlag) {
  194. const memoized = motionUtils.memo(callback);
  195. return () => supportsFlags[supportsFlag] ?? memoized();
  196. }
  197. const supportsLinearEasing = /*@__PURE__*/ memoSupports(() => {
  198. try {
  199. document
  200. .createElement("div")
  201. .animate({ opacity: 0 }, { easing: "linear(0, 1)" });
  202. }
  203. catch (e) {
  204. return false;
  205. }
  206. return true;
  207. }, "linearEasing");
  208. const generateLinearEasing = (easing, duration, // as milliseconds
  209. resolution = 10 // as milliseconds
  210. ) => {
  211. let points = "";
  212. const numPoints = Math.max(Math.round(duration / resolution), 2);
  213. for (let i = 0; i < numPoints; i++) {
  214. points += easing(i / (numPoints - 1)) + ", ";
  215. }
  216. return `linear(${points.substring(0, points.length - 2)})`;
  217. };
  218. const cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`;
  219. const supportedWaapiEasing = {
  220. linear: "linear",
  221. ease: "ease",
  222. easeIn: "ease-in",
  223. easeOut: "ease-out",
  224. easeInOut: "ease-in-out",
  225. circIn: /*@__PURE__*/ cubicBezierAsString([0, 0.65, 0.55, 1]),
  226. circOut: /*@__PURE__*/ cubicBezierAsString([0.55, 0, 1, 0.45]),
  227. backIn: /*@__PURE__*/ cubicBezierAsString([0.31, 0.01, 0.66, -0.59]),
  228. backOut: /*@__PURE__*/ cubicBezierAsString([0.33, 1.53, 0.69, 0.99]),
  229. };
  230. function mapEasingToNativeEasing(easing, duration) {
  231. if (!easing) {
  232. return undefined;
  233. }
  234. else if (typeof easing === "function" && supportsLinearEasing()) {
  235. return generateLinearEasing(easing, duration);
  236. }
  237. else if (isBezierDefinition(easing)) {
  238. return cubicBezierAsString(easing);
  239. }
  240. else if (Array.isArray(easing)) {
  241. return easing.map((segmentEasing) => mapEasingToNativeEasing(segmentEasing, duration) ||
  242. supportedWaapiEasing.easeOut);
  243. }
  244. else {
  245. return supportedWaapiEasing[easing];
  246. }
  247. }
  248. function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duration = 300, repeat = 0, repeatType = "loop", ease = "easeInOut", times, } = {}, pseudoElement = undefined) {
  249. const keyframeOptions = {
  250. [valueName]: keyframes,
  251. };
  252. if (times)
  253. keyframeOptions.offset = times;
  254. const easing = mapEasingToNativeEasing(ease, duration);
  255. /**
  256. * If this is an easing array, apply to keyframes, not animation as a whole
  257. */
  258. if (Array.isArray(easing))
  259. keyframeOptions.easing = easing;
  260. if (statsBuffer.value) {
  261. activeAnimations.waapi++;
  262. }
  263. const animation = element.animate(keyframeOptions, {
  264. delay,
  265. duration,
  266. easing: !Array.isArray(easing) ? easing : "linear",
  267. fill: "both",
  268. iterations: repeat + 1,
  269. direction: repeatType === "reverse" ? "alternate" : "normal",
  270. pseudoElement,
  271. });
  272. if (statsBuffer.value) {
  273. animation.finished.finally(() => {
  274. activeAnimations.waapi--;
  275. });
  276. }
  277. return animation;
  278. }
  279. function isGenerator(type) {
  280. return typeof type === "function" && "applyToOptions" in type;
  281. }
  282. function applyGeneratorOptions({ type, ...options }) {
  283. if (isGenerator(type)) {
  284. return type.applyToOptions(options);
  285. }
  286. else {
  287. options.duration ?? (options.duration = 300);
  288. options.ease ?? (options.ease = "easeOut");
  289. }
  290. return options;
  291. }
  292. const animationMaps = new WeakMap();
  293. const animationMapKey = (name, pseudoElement) => `${name}:${pseudoElement}`;
  294. function getAnimationMap(element) {
  295. const map = animationMaps.get(element) || new Map();
  296. animationMaps.set(element, map);
  297. return map;
  298. }
  299. /**
  300. * NativeAnimation implements AnimationPlaybackControls for the browser's Web Animations API.
  301. */
  302. class NativeAnimation {
  303. constructor(options) {
  304. /**
  305. * If we already have an animation, we don't need to instantiate one
  306. * and can just use this as a controls interface.
  307. */
  308. if ("animation" in options) {
  309. this.animation = options.animation;
  310. return;
  311. }
  312. const { element, name, keyframes: unresolvedKeyframes, pseudoElement, allowFlatten = false, } = options;
  313. let { transition } = options;
  314. this.isPseudoElement = Boolean(pseudoElement);
  315. this.allowFlatten = allowFlatten;
  316. /**
  317. * Stop any existing animations on the element before reading existing keyframes.
  318. *
  319. * TODO: Check for VisualElement before using animation state. This is a fallback
  320. * for mini animate(). Do this when implementing NativeAnimationExtended.
  321. */
  322. const animationMap = getAnimationMap(element);
  323. const key = animationMapKey(name, pseudoElement || "");
  324. const currentAnimation = animationMap.get(key);
  325. currentAnimation && currentAnimation.stop();
  326. /**
  327. * TODO: If these keyframes aren't correctly hydrated then we want to throw
  328. * run an instant animation.
  329. */
  330. const keyframes = hydrateKeyframes(element, name, unresolvedKeyframes, pseudoElement);
  331. motionUtils.invariant(typeof transition.type !== "string", `animateMini doesn't support "type" as a string. Did you mean to import { spring } from "motion"?`);
  332. transition = applyGeneratorOptions(transition);
  333. this.animation = startWaapiAnimation(element, name, keyframes, transition, pseudoElement);
  334. if (transition.autoplay === false) {
  335. this.animation.pause();
  336. }
  337. this.removeAnimation = () => animationMap.delete(key);
  338. this.animation.onfinish = () => {
  339. if (!pseudoElement) {
  340. style$1.set(element, name, getFinalKeyframe(keyframes, transition));
  341. this.cancel();
  342. }
  343. };
  344. /**
  345. * TODO: Check for VisualElement before using animation state.
  346. */
  347. animationMap.set(key, this);
  348. }
  349. play() {
  350. this.animation.play();
  351. }
  352. pause() {
  353. this.animation.pause();
  354. }
  355. complete() {
  356. this.animation.finish();
  357. }
  358. cancel() {
  359. try {
  360. this.animation.cancel();
  361. }
  362. catch (e) { }
  363. this.removeAnimation();
  364. }
  365. stop() {
  366. const { state } = this;
  367. if (state === "idle" || state === "finished") {
  368. return;
  369. }
  370. this.commitStyles();
  371. this.cancel();
  372. }
  373. /**
  374. * WAAPI doesn't natively have any interruption capabilities.
  375. *
  376. * In this method, we commit styles back to the DOM before cancelling
  377. * the animation.
  378. *
  379. * This is designed to be overridden by NativeAnimationExtended, which
  380. * will create a renderless JS animation and sample it twice to calculate
  381. * its current value, "previous" value, and therefore allow
  382. * Motion to also correctly calculate velocity for any subsequent animation
  383. * while deferring the commit until the next animation frame.
  384. */
  385. commitStyles() {
  386. if (!this.isPseudoElement) {
  387. this.animation.commitStyles?.();
  388. }
  389. }
  390. get duration() {
  391. const duration = this.animation.effect?.getComputedTiming().duration || 0;
  392. return motionUtils.millisecondsToSeconds(Number(duration));
  393. }
  394. get time() {
  395. return motionUtils.millisecondsToSeconds(Number(this.animation.currentTime) || 0);
  396. }
  397. set time(newTime) {
  398. this.animation.currentTime = motionUtils.secondsToMilliseconds(newTime);
  399. }
  400. /**
  401. * The playback speed of the animation.
  402. * 1 = normal speed, 2 = double speed, 0.5 = half speed.
  403. */
  404. get speed() {
  405. return this.animation.playbackRate;
  406. }
  407. set speed(newSpeed) {
  408. this.animation.playbackRate = newSpeed;
  409. }
  410. get state() {
  411. return this.animation.playState;
  412. }
  413. get startTime() {
  414. return Number(this.animation.startTime);
  415. }
  416. get finished() {
  417. return this.animation.finished;
  418. }
  419. flatten() {
  420. if (this.allowFlatten) {
  421. this.animation.effect?.updateTiming({ easing: "linear" });
  422. }
  423. }
  424. /**
  425. * Attaches a timeline to the animation, for instance the `ScrollTimeline`.
  426. */
  427. attachTimeline(timeline) {
  428. this.animation.timeline = timeline;
  429. this.animation.onfinish = null;
  430. return motionUtils.noop;
  431. }
  432. /**
  433. * Allows the animation to be awaited.
  434. *
  435. * @deprecated Use `finished` instead.
  436. */
  437. then(onResolve, onReject) {
  438. return this.finished.then(onResolve).catch(onReject);
  439. }
  440. }
  441. function getValueTransition(transition, key) {
  442. return (transition?.[key] ??
  443. transition?.["default"] ??
  444. transition);
  445. }
  446. /**
  447. * Implement a practical max duration for keyframe generation
  448. * to prevent infinite loops
  449. */
  450. const maxGeneratorDuration = 20000;
  451. function calcGeneratorDuration(generator) {
  452. let duration = 0;
  453. const timeStep = 50;
  454. let state = generator.next(duration);
  455. while (!state.done && duration < maxGeneratorDuration) {
  456. duration += timeStep;
  457. state = generator.next(duration);
  458. }
  459. return duration >= maxGeneratorDuration ? Infinity : duration;
  460. }
  461. /**
  462. * Create a progress => progress easing function from a generator.
  463. */
  464. function createGeneratorEasing(options, scale = 100, createGenerator) {
  465. const generator = createGenerator({ ...options, keyframes: [0, scale] });
  466. const duration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration);
  467. return {
  468. type: "keyframes",
  469. ease: (progress) => {
  470. return generator.next(duration * progress).value / scale;
  471. },
  472. duration: motionUtils.millisecondsToSeconds(duration),
  473. };
  474. }
  475. function isWaapiSupportedEasing(easing) {
  476. return Boolean((typeof easing === "function" && supportsLinearEasing()) ||
  477. !easing ||
  478. (typeof easing === "string" &&
  479. (easing in supportedWaapiEasing || supportsLinearEasing())) ||
  480. isBezierDefinition(easing) ||
  481. (Array.isArray(easing) && easing.every(isWaapiSupportedEasing)));
  482. }
  483. function attachTimeline(animation, timeline) {
  484. animation.timeline = timeline;
  485. animation.onfinish = null;
  486. }
  487. const stepsOrder = [
  488. "read", // Read
  489. "resolveKeyframes", // Write/Read/Write/Read
  490. "update", // Compute
  491. "preRender", // Compute
  492. "render", // Write
  493. "postRender", // Compute
  494. ];
  495. function createRenderStep(runNextFrame, stepName) {
  496. /**
  497. * We create and reuse two queues, one to queue jobs for the current frame
  498. * and one for the next. We reuse to avoid triggering GC after x frames.
  499. */
  500. let thisFrame = new Set();
  501. let nextFrame = new Set();
  502. /**
  503. * Track whether we're currently processing jobs in this step. This way
  504. * we can decide whether to schedule new jobs for this frame or next.
  505. */
  506. let isProcessing = false;
  507. let flushNextFrame = false;
  508. /**
  509. * A set of processes which were marked keepAlive when scheduled.
  510. */
  511. const toKeepAlive = new WeakSet();
  512. let latestFrameData = {
  513. delta: 0.0,
  514. timestamp: 0.0,
  515. isProcessing: false,
  516. };
  517. let numCalls = 0;
  518. function triggerCallback(callback) {
  519. if (toKeepAlive.has(callback)) {
  520. step.schedule(callback);
  521. runNextFrame();
  522. }
  523. numCalls++;
  524. callback(latestFrameData);
  525. }
  526. const step = {
  527. /**
  528. * Schedule a process to run on the next frame.
  529. */
  530. schedule: (callback, keepAlive = false, immediate = false) => {
  531. const addToCurrentFrame = immediate && isProcessing;
  532. const queue = addToCurrentFrame ? thisFrame : nextFrame;
  533. if (keepAlive)
  534. toKeepAlive.add(callback);
  535. if (!queue.has(callback))
  536. queue.add(callback);
  537. return callback;
  538. },
  539. /**
  540. * Cancel the provided callback from running on the next frame.
  541. */
  542. cancel: (callback) => {
  543. nextFrame.delete(callback);
  544. toKeepAlive.delete(callback);
  545. },
  546. /**
  547. * Execute all schedule callbacks.
  548. */
  549. process: (frameData) => {
  550. latestFrameData = frameData;
  551. /**
  552. * If we're already processing we've probably been triggered by a flushSync
  553. * inside an existing process. Instead of executing, mark flushNextFrame
  554. * as true and ensure we flush the following frame at the end of this one.
  555. */
  556. if (isProcessing) {
  557. flushNextFrame = true;
  558. return;
  559. }
  560. isProcessing = true;
  561. [thisFrame, nextFrame] = [nextFrame, thisFrame];
  562. // Execute this frame
  563. thisFrame.forEach(triggerCallback);
  564. /**
  565. * If we're recording stats then
  566. */
  567. if (stepName && statsBuffer.value) {
  568. statsBuffer.value.frameloop[stepName].push(numCalls);
  569. }
  570. numCalls = 0;
  571. // Clear the frame so no callbacks remain. This is to avoid
  572. // memory leaks should this render step not run for a while.
  573. thisFrame.clear();
  574. isProcessing = false;
  575. if (flushNextFrame) {
  576. flushNextFrame = false;
  577. step.process(frameData);
  578. }
  579. },
  580. };
  581. return step;
  582. }
  583. const maxElapsed = 40;
  584. function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
  585. let runNextFrame = false;
  586. let useDefaultElapsed = true;
  587. const state = {
  588. delta: 0.0,
  589. timestamp: 0.0,
  590. isProcessing: false,
  591. };
  592. const flagRunNextFrame = () => (runNextFrame = true);
  593. const steps = stepsOrder.reduce((acc, key) => {
  594. acc[key] = createRenderStep(flagRunNextFrame, allowKeepAlive ? key : undefined);
  595. return acc;
  596. }, {});
  597. const { read, resolveKeyframes, update, preRender, render, postRender } = steps;
  598. const processBatch = () => {
  599. const timestamp = motionUtils.MotionGlobalConfig.useManualTiming
  600. ? state.timestamp
  601. : performance.now();
  602. runNextFrame = false;
  603. if (!motionUtils.MotionGlobalConfig.useManualTiming) {
  604. state.delta = useDefaultElapsed
  605. ? 1000 / 60
  606. : Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1);
  607. }
  608. state.timestamp = timestamp;
  609. state.isProcessing = true;
  610. // Unrolled render loop for better per-frame performance
  611. read.process(state);
  612. resolveKeyframes.process(state);
  613. update.process(state);
  614. preRender.process(state);
  615. render.process(state);
  616. postRender.process(state);
  617. state.isProcessing = false;
  618. if (runNextFrame && allowKeepAlive) {
  619. useDefaultElapsed = false;
  620. scheduleNextBatch(processBatch);
  621. }
  622. };
  623. const wake = () => {
  624. runNextFrame = true;
  625. useDefaultElapsed = true;
  626. if (!state.isProcessing) {
  627. scheduleNextBatch(processBatch);
  628. }
  629. };
  630. const schedule = stepsOrder.reduce((acc, key) => {
  631. const step = steps[key];
  632. acc[key] = (process, keepAlive = false, immediate = false) => {
  633. if (!runNextFrame)
  634. wake();
  635. return step.schedule(process, keepAlive, immediate);
  636. };
  637. return acc;
  638. }, {});
  639. const cancel = (process) => {
  640. for (let i = 0; i < stepsOrder.length; i++) {
  641. steps[stepsOrder[i]].cancel(process);
  642. }
  643. };
  644. return { schedule, cancel, state, steps };
  645. }
  646. const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : motionUtils.noop, true);
  647. const { schedule: microtask, cancel: cancelMicrotask } =
  648. /* @__PURE__ */ createRenderBatcher(queueMicrotask, false);
  649. let now;
  650. function clearTime() {
  651. now = undefined;
  652. }
  653. /**
  654. * An eventloop-synchronous alternative to performance.now().
  655. *
  656. * Ensures that time measurements remain consistent within a synchronous context.
  657. * Usually calling performance.now() twice within the same synchronous context
  658. * will return different values which isn't useful for animations when we're usually
  659. * trying to sync animations to the same frame.
  660. */
  661. const time = {
  662. now: () => {
  663. if (now === undefined) {
  664. time.set(frameData.isProcessing || motionUtils.MotionGlobalConfig.useManualTiming
  665. ? frameData.timestamp
  666. : performance.now());
  667. }
  668. return now;
  669. },
  670. set: (newTime) => {
  671. now = newTime;
  672. queueMicrotask(clearTime);
  673. },
  674. };
  675. const isDragging = {
  676. x: false,
  677. y: false,
  678. };
  679. function isDragActive() {
  680. return isDragging.x || isDragging.y;
  681. }
  682. function setDragLock(axis) {
  683. if (axis === "x" || axis === "y") {
  684. if (isDragging[axis]) {
  685. return null;
  686. }
  687. else {
  688. isDragging[axis] = true;
  689. return () => {
  690. isDragging[axis] = false;
  691. };
  692. }
  693. }
  694. else {
  695. if (isDragging.x || isDragging.y) {
  696. return null;
  697. }
  698. else {
  699. isDragging.x = isDragging.y = true;
  700. return () => {
  701. isDragging.x = isDragging.y = false;
  702. };
  703. }
  704. }
  705. }
  706. function resolveElements(elementOrSelector, scope, selectorCache) {
  707. if (elementOrSelector instanceof EventTarget) {
  708. return [elementOrSelector];
  709. }
  710. else if (typeof elementOrSelector === "string") {
  711. let root = document;
  712. if (scope) {
  713. root = scope.current;
  714. }
  715. const elements = selectorCache?.[elementOrSelector] ??
  716. root.querySelectorAll(elementOrSelector);
  717. return elements ? Array.from(elements) : [];
  718. }
  719. return Array.from(elementOrSelector);
  720. }
  721. function setupGesture(elementOrSelector, options) {
  722. const elements = resolveElements(elementOrSelector);
  723. const gestureAbortController = new AbortController();
  724. const eventOptions = {
  725. passive: true,
  726. ...options,
  727. signal: gestureAbortController.signal,
  728. };
  729. const cancel = () => gestureAbortController.abort();
  730. return [elements, eventOptions, cancel];
  731. }
  732. function isValidHover(event) {
  733. return !(event.pointerType === "touch" || isDragActive());
  734. }
  735. /**
  736. * Create a hover gesture. hover() is different to .addEventListener("pointerenter")
  737. * in that it has an easier syntax, filters out polyfilled touch events, interoperates
  738. * with drag gestures, and automatically removes the "pointerennd" event listener when the hover ends.
  739. *
  740. * @public
  741. */
  742. function hover(elementOrSelector, onHoverStart, options = {}) {
  743. const [elements, eventOptions, cancel] = setupGesture(elementOrSelector, options);
  744. const onPointerEnter = (enterEvent) => {
  745. if (!isValidHover(enterEvent))
  746. return;
  747. const { target } = enterEvent;
  748. const onHoverEnd = onHoverStart(target, enterEvent);
  749. if (typeof onHoverEnd !== "function" || !target)
  750. return;
  751. const onPointerLeave = (leaveEvent) => {
  752. if (!isValidHover(leaveEvent))
  753. return;
  754. onHoverEnd(leaveEvent);
  755. target.removeEventListener("pointerleave", onPointerLeave);
  756. };
  757. target.addEventListener("pointerleave", onPointerLeave, eventOptions);
  758. };
  759. elements.forEach((element) => {
  760. element.addEventListener("pointerenter", onPointerEnter, eventOptions);
  761. });
  762. return cancel;
  763. }
  764. /**
  765. * Recursively traverse up the tree to check whether the provided child node
  766. * is the parent or a descendant of it.
  767. *
  768. * @param parent - Element to find
  769. * @param child - Element to test against parent
  770. */
  771. const isNodeOrChild = (parent, child) => {
  772. if (!child) {
  773. return false;
  774. }
  775. else if (parent === child) {
  776. return true;
  777. }
  778. else {
  779. return isNodeOrChild(parent, child.parentElement);
  780. }
  781. };
  782. const isPrimaryPointer = (event) => {
  783. if (event.pointerType === "mouse") {
  784. return typeof event.button !== "number" || event.button <= 0;
  785. }
  786. else {
  787. /**
  788. * isPrimary is true for all mice buttons, whereas every touch point
  789. * is regarded as its own input. So subsequent concurrent touch points
  790. * will be false.
  791. *
  792. * Specifically match against false here as incomplete versions of
  793. * PointerEvents in very old browser might have it set as undefined.
  794. */
  795. return event.isPrimary !== false;
  796. }
  797. };
  798. const focusableElements = new Set([
  799. "BUTTON",
  800. "INPUT",
  801. "SELECT",
  802. "TEXTAREA",
  803. "A",
  804. ]);
  805. function isElementKeyboardAccessible(element) {
  806. return (focusableElements.has(element.tagName) ||
  807. element.tabIndex !== -1);
  808. }
  809. const isPressing = new WeakSet();
  810. /**
  811. * Filter out events that are not "Enter" keys.
  812. */
  813. function filterEvents(callback) {
  814. return (event) => {
  815. if (event.key !== "Enter")
  816. return;
  817. callback(event);
  818. };
  819. }
  820. function firePointerEvent(target, type) {
  821. target.dispatchEvent(new PointerEvent("pointer" + type, { isPrimary: true, bubbles: true }));
  822. }
  823. const enableKeyboardPress = (focusEvent, eventOptions) => {
  824. const element = focusEvent.currentTarget;
  825. if (!element)
  826. return;
  827. const handleKeydown = filterEvents(() => {
  828. if (isPressing.has(element))
  829. return;
  830. firePointerEvent(element, "down");
  831. const handleKeyup = filterEvents(() => {
  832. firePointerEvent(element, "up");
  833. });
  834. const handleBlur = () => firePointerEvent(element, "cancel");
  835. element.addEventListener("keyup", handleKeyup, eventOptions);
  836. element.addEventListener("blur", handleBlur, eventOptions);
  837. });
  838. element.addEventListener("keydown", handleKeydown, eventOptions);
  839. /**
  840. * Add an event listener that fires on blur to remove the keydown events.
  841. */
  842. element.addEventListener("blur", () => element.removeEventListener("keydown", handleKeydown), eventOptions);
  843. };
  844. /**
  845. * Filter out events that are not primary pointer events, or are triggering
  846. * while a Motion gesture is active.
  847. */
  848. function isValidPressEvent(event) {
  849. return isPrimaryPointer(event) && !isDragActive();
  850. }
  851. /**
  852. * Create a press gesture.
  853. *
  854. * Press is different to `"pointerdown"`, `"pointerup"` in that it
  855. * automatically filters out secondary pointer events like right
  856. * click and multitouch.
  857. *
  858. * It also adds accessibility support for keyboards, where
  859. * an element with a press gesture will receive focus and
  860. * trigger on Enter `"keydown"` and `"keyup"` events.
  861. *
  862. * This is different to a browser's `"click"` event, which does
  863. * respond to keyboards but only for the `"click"` itself, rather
  864. * than the press start and end/cancel. The element also needs
  865. * to be focusable for this to work, whereas a press gesture will
  866. * make an element focusable by default.
  867. *
  868. * @public
  869. */
  870. function press(targetOrSelector, onPressStart, options = {}) {
  871. const [targets, eventOptions, cancelEvents] = setupGesture(targetOrSelector, options);
  872. const startPress = (startEvent) => {
  873. const target = startEvent.currentTarget;
  874. if (!isValidPressEvent(startEvent) || isPressing.has(target))
  875. return;
  876. isPressing.add(target);
  877. const onPressEnd = onPressStart(target, startEvent);
  878. const onPointerEnd = (endEvent, success) => {
  879. window.removeEventListener("pointerup", onPointerUp);
  880. window.removeEventListener("pointercancel", onPointerCancel);
  881. if (!isValidPressEvent(endEvent) || !isPressing.has(target)) {
  882. return;
  883. }
  884. isPressing.delete(target);
  885. if (typeof onPressEnd === "function") {
  886. onPressEnd(endEvent, { success });
  887. }
  888. };
  889. const onPointerUp = (upEvent) => {
  890. onPointerEnd(upEvent, target === window ||
  891. target === document ||
  892. options.useGlobalTarget ||
  893. isNodeOrChild(target, upEvent.target));
  894. };
  895. const onPointerCancel = (cancelEvent) => {
  896. onPointerEnd(cancelEvent, false);
  897. };
  898. window.addEventListener("pointerup", onPointerUp, eventOptions);
  899. window.addEventListener("pointercancel", onPointerCancel, eventOptions);
  900. };
  901. targets.forEach((target) => {
  902. const pointerDownTarget = options.useGlobalTarget ? window : target;
  903. pointerDownTarget.addEventListener("pointerdown", startPress, eventOptions);
  904. if (target instanceof HTMLElement) {
  905. target.addEventListener("focus", (event) => enableKeyboardPress(event, eventOptions));
  906. if (!isElementKeyboardAccessible(target) &&
  907. !target.hasAttribute("tabindex")) {
  908. target.tabIndex = 0;
  909. }
  910. }
  911. });
  912. return cancelEvents;
  913. }
  914. function record() {
  915. const { value } = statsBuffer;
  916. if (value === null) {
  917. cancelFrame(record);
  918. return;
  919. }
  920. value.frameloop.rate.push(frameData.delta);
  921. value.animations.mainThread.push(activeAnimations.mainThread);
  922. value.animations.waapi.push(activeAnimations.waapi);
  923. value.animations.layout.push(activeAnimations.layout);
  924. }
  925. function mean(values) {
  926. return values.reduce((acc, value) => acc + value, 0) / values.length;
  927. }
  928. function summarise(values, calcAverage = mean) {
  929. if (values.length === 0) {
  930. return {
  931. min: 0,
  932. max: 0,
  933. avg: 0,
  934. };
  935. }
  936. return {
  937. min: Math.min(...values),
  938. max: Math.max(...values),
  939. avg: calcAverage(values),
  940. };
  941. }
  942. const msToFps = (ms) => Math.round(1000 / ms);
  943. function clearStatsBuffer() {
  944. statsBuffer.value = null;
  945. statsBuffer.addProjectionMetrics = null;
  946. }
  947. function reportStats() {
  948. const { value } = statsBuffer;
  949. if (!value) {
  950. throw new Error("Stats are not being measured");
  951. }
  952. clearStatsBuffer();
  953. cancelFrame(record);
  954. const summary = {
  955. frameloop: {
  956. rate: summarise(value.frameloop.rate),
  957. read: summarise(value.frameloop.read),
  958. resolveKeyframes: summarise(value.frameloop.resolveKeyframes),
  959. update: summarise(value.frameloop.update),
  960. preRender: summarise(value.frameloop.preRender),
  961. render: summarise(value.frameloop.render),
  962. postRender: summarise(value.frameloop.postRender),
  963. },
  964. animations: {
  965. mainThread: summarise(value.animations.mainThread),
  966. waapi: summarise(value.animations.waapi),
  967. layout: summarise(value.animations.layout),
  968. },
  969. layoutProjection: {
  970. nodes: summarise(value.layoutProjection.nodes),
  971. calculatedTargetDeltas: summarise(value.layoutProjection.calculatedTargetDeltas),
  972. calculatedProjections: summarise(value.layoutProjection.calculatedProjections),
  973. },
  974. };
  975. /**
  976. * Convert the rate to FPS
  977. */
  978. const { rate } = summary.frameloop;
  979. rate.min = msToFps(rate.min);
  980. rate.max = msToFps(rate.max);
  981. rate.avg = msToFps(rate.avg);
  982. [rate.min, rate.max] = [rate.max, rate.min];
  983. return summary;
  984. }
  985. function recordStats() {
  986. if (statsBuffer.value) {
  987. clearStatsBuffer();
  988. throw new Error("Stats are already being measured");
  989. }
  990. const newStatsBuffer = statsBuffer;
  991. newStatsBuffer.value = {
  992. frameloop: {
  993. rate: [],
  994. read: [],
  995. resolveKeyframes: [],
  996. update: [],
  997. preRender: [],
  998. render: [],
  999. postRender: [],
  1000. },
  1001. animations: {
  1002. mainThread: [],
  1003. waapi: [],
  1004. layout: [],
  1005. },
  1006. layoutProjection: {
  1007. nodes: [],
  1008. calculatedTargetDeltas: [],
  1009. calculatedProjections: [],
  1010. },
  1011. };
  1012. newStatsBuffer.addProjectionMetrics = (metrics) => {
  1013. const { layoutProjection } = newStatsBuffer.value;
  1014. layoutProjection.nodes.push(metrics.nodes);
  1015. layoutProjection.calculatedTargetDeltas.push(metrics.calculatedTargetDeltas);
  1016. layoutProjection.calculatedProjections.push(metrics.calculatedProjections);
  1017. };
  1018. frame.postRender(record, true);
  1019. return reportStats;
  1020. }
  1021. /**
  1022. * Maximum time between the value of two frames, beyond which we
  1023. * assume the velocity has since been 0.
  1024. */
  1025. const MAX_VELOCITY_DELTA = 30;
  1026. const isFloat = (value) => {
  1027. return !isNaN(parseFloat(value));
  1028. };
  1029. const collectMotionValues = {
  1030. current: undefined,
  1031. };
  1032. /**
  1033. * `MotionValue` is used to track the state and velocity of motion values.
  1034. *
  1035. * @public
  1036. */
  1037. class MotionValue {
  1038. /**
  1039. * @param init - The initiating value
  1040. * @param config - Optional configuration options
  1041. *
  1042. * - `transformer`: A function to transform incoming values with.
  1043. */
  1044. constructor(init, options = {}) {
  1045. /**
  1046. * This will be replaced by the build step with the latest version number.
  1047. * When MotionValues are provided to motion components, warn if versions are mixed.
  1048. */
  1049. this.version = "12.7.3";
  1050. /**
  1051. * Tracks whether this value can output a velocity. Currently this is only true
  1052. * if the value is numerical, but we might be able to widen the scope here and support
  1053. * other value types.
  1054. *
  1055. * @internal
  1056. */
  1057. this.canTrackVelocity = null;
  1058. /**
  1059. * An object containing a SubscriptionManager for each active event.
  1060. */
  1061. this.events = {};
  1062. this.updateAndNotify = (v, render = true) => {
  1063. const currentTime = time.now();
  1064. /**
  1065. * If we're updating the value during another frame or eventloop
  1066. * than the previous frame, then the we set the previous frame value
  1067. * to current.
  1068. */
  1069. if (this.updatedAt !== currentTime) {
  1070. this.setPrevFrameValue();
  1071. }
  1072. this.prev = this.current;
  1073. this.setCurrent(v);
  1074. // Update update subscribers
  1075. if (this.current !== this.prev && this.events.change) {
  1076. this.events.change.notify(this.current);
  1077. }
  1078. // Update render subscribers
  1079. if (render && this.events.renderRequest) {
  1080. this.events.renderRequest.notify(this.current);
  1081. }
  1082. };
  1083. this.hasAnimated = false;
  1084. this.setCurrent(init);
  1085. this.owner = options.owner;
  1086. }
  1087. setCurrent(current) {
  1088. this.current = current;
  1089. this.updatedAt = time.now();
  1090. if (this.canTrackVelocity === null && current !== undefined) {
  1091. this.canTrackVelocity = isFloat(this.current);
  1092. }
  1093. }
  1094. setPrevFrameValue(prevFrameValue = this.current) {
  1095. this.prevFrameValue = prevFrameValue;
  1096. this.prevUpdatedAt = this.updatedAt;
  1097. }
  1098. /**
  1099. * Adds a function that will be notified when the `MotionValue` is updated.
  1100. *
  1101. * It returns a function that, when called, will cancel the subscription.
  1102. *
  1103. * When calling `onChange` inside a React component, it should be wrapped with the
  1104. * `useEffect` hook. As it returns an unsubscribe function, this should be returned
  1105. * from the `useEffect` function to ensure you don't add duplicate subscribers..
  1106. *
  1107. * ```jsx
  1108. * export const MyComponent = () => {
  1109. * const x = useMotionValue(0)
  1110. * const y = useMotionValue(0)
  1111. * const opacity = useMotionValue(1)
  1112. *
  1113. * useEffect(() => {
  1114. * function updateOpacity() {
  1115. * const maxXY = Math.max(x.get(), y.get())
  1116. * const newOpacity = transform(maxXY, [0, 100], [1, 0])
  1117. * opacity.set(newOpacity)
  1118. * }
  1119. *
  1120. * const unsubscribeX = x.on("change", updateOpacity)
  1121. * const unsubscribeY = y.on("change", updateOpacity)
  1122. *
  1123. * return () => {
  1124. * unsubscribeX()
  1125. * unsubscribeY()
  1126. * }
  1127. * }, [])
  1128. *
  1129. * return <motion.div style={{ x }} />
  1130. * }
  1131. * ```
  1132. *
  1133. * @param subscriber - A function that receives the latest value.
  1134. * @returns A function that, when called, will cancel this subscription.
  1135. *
  1136. * @deprecated
  1137. */
  1138. onChange(subscription) {
  1139. {
  1140. motionUtils.warnOnce(false, `value.onChange(callback) is deprecated. Switch to value.on("change", callback).`);
  1141. }
  1142. return this.on("change", subscription);
  1143. }
  1144. on(eventName, callback) {
  1145. if (!this.events[eventName]) {
  1146. this.events[eventName] = new motionUtils.SubscriptionManager();
  1147. }
  1148. const unsubscribe = this.events[eventName].add(callback);
  1149. if (eventName === "change") {
  1150. return () => {
  1151. unsubscribe();
  1152. /**
  1153. * If we have no more change listeners by the start
  1154. * of the next frame, stop active animations.
  1155. */
  1156. frame.read(() => {
  1157. if (!this.events.change.getSize()) {
  1158. this.stop();
  1159. }
  1160. });
  1161. };
  1162. }
  1163. return unsubscribe;
  1164. }
  1165. clearListeners() {
  1166. for (const eventManagers in this.events) {
  1167. this.events[eventManagers].clear();
  1168. }
  1169. }
  1170. /**
  1171. * Attaches a passive effect to the `MotionValue`.
  1172. */
  1173. attach(passiveEffect, stopPassiveEffect) {
  1174. this.passiveEffect = passiveEffect;
  1175. this.stopPassiveEffect = stopPassiveEffect;
  1176. }
  1177. /**
  1178. * Sets the state of the `MotionValue`.
  1179. *
  1180. * @remarks
  1181. *
  1182. * ```jsx
  1183. * const x = useMotionValue(0)
  1184. * x.set(10)
  1185. * ```
  1186. *
  1187. * @param latest - Latest value to set.
  1188. * @param render - Whether to notify render subscribers. Defaults to `true`
  1189. *
  1190. * @public
  1191. */
  1192. set(v, render = true) {
  1193. if (!render || !this.passiveEffect) {
  1194. this.updateAndNotify(v, render);
  1195. }
  1196. else {
  1197. this.passiveEffect(v, this.updateAndNotify);
  1198. }
  1199. }
  1200. setWithVelocity(prev, current, delta) {
  1201. this.set(current);
  1202. this.prev = undefined;
  1203. this.prevFrameValue = prev;
  1204. this.prevUpdatedAt = this.updatedAt - delta;
  1205. }
  1206. /**
  1207. * Set the state of the `MotionValue`, stopping any active animations,
  1208. * effects, and resets velocity to `0`.
  1209. */
  1210. jump(v, endAnimation = true) {
  1211. this.updateAndNotify(v);
  1212. this.prev = v;
  1213. this.prevUpdatedAt = this.prevFrameValue = undefined;
  1214. endAnimation && this.stop();
  1215. if (this.stopPassiveEffect)
  1216. this.stopPassiveEffect();
  1217. }
  1218. /**
  1219. * Returns the latest state of `MotionValue`
  1220. *
  1221. * @returns - The latest state of `MotionValue`
  1222. *
  1223. * @public
  1224. */
  1225. get() {
  1226. if (collectMotionValues.current) {
  1227. collectMotionValues.current.push(this);
  1228. }
  1229. return this.current;
  1230. }
  1231. /**
  1232. * @public
  1233. */
  1234. getPrevious() {
  1235. return this.prev;
  1236. }
  1237. /**
  1238. * Returns the latest velocity of `MotionValue`
  1239. *
  1240. * @returns - The latest velocity of `MotionValue`. Returns `0` if the state is non-numerical.
  1241. *
  1242. * @public
  1243. */
  1244. getVelocity() {
  1245. const currentTime = time.now();
  1246. if (!this.canTrackVelocity ||
  1247. this.prevFrameValue === undefined ||
  1248. currentTime - this.updatedAt > MAX_VELOCITY_DELTA) {
  1249. return 0;
  1250. }
  1251. const delta = Math.min(this.updatedAt - this.prevUpdatedAt, MAX_VELOCITY_DELTA);
  1252. // Casts because of parseFloat's poor typing
  1253. return motionUtils.velocityPerSecond(parseFloat(this.current) -
  1254. parseFloat(this.prevFrameValue), delta);
  1255. }
  1256. /**
  1257. * Registers a new animation to control this `MotionValue`. Only one
  1258. * animation can drive a `MotionValue` at one time.
  1259. *
  1260. * ```jsx
  1261. * value.start()
  1262. * ```
  1263. *
  1264. * @param animation - A function that starts the provided animation
  1265. */
  1266. start(startAnimation) {
  1267. this.stop();
  1268. return new Promise((resolve) => {
  1269. this.hasAnimated = true;
  1270. this.animation = startAnimation(resolve);
  1271. if (this.events.animationStart) {
  1272. this.events.animationStart.notify();
  1273. }
  1274. }).then(() => {
  1275. if (this.events.animationComplete) {
  1276. this.events.animationComplete.notify();
  1277. }
  1278. this.clearAnimation();
  1279. });
  1280. }
  1281. /**
  1282. * Stop the currently active animation.
  1283. *
  1284. * @public
  1285. */
  1286. stop() {
  1287. if (this.animation) {
  1288. this.animation.stop();
  1289. if (this.events.animationCancel) {
  1290. this.events.animationCancel.notify();
  1291. }
  1292. }
  1293. this.clearAnimation();
  1294. }
  1295. /**
  1296. * Returns `true` if this value is currently animating.
  1297. *
  1298. * @public
  1299. */
  1300. isAnimating() {
  1301. return !!this.animation;
  1302. }
  1303. clearAnimation() {
  1304. delete this.animation;
  1305. }
  1306. /**
  1307. * Destroy and clean up subscribers to this `MotionValue`.
  1308. *
  1309. * The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically
  1310. * handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually
  1311. * created a `MotionValue` via the `motionValue` function.
  1312. *
  1313. * @public
  1314. */
  1315. destroy() {
  1316. this.clearListeners();
  1317. this.stop();
  1318. if (this.stopPassiveEffect) {
  1319. this.stopPassiveEffect();
  1320. }
  1321. }
  1322. }
  1323. function motionValue(init, options) {
  1324. return new MotionValue(init, options);
  1325. }
  1326. function chooseLayerType(valueName) {
  1327. if (valueName === "layout")
  1328. return "group";
  1329. if (valueName === "enter" || valueName === "new")
  1330. return "new";
  1331. if (valueName === "exit" || valueName === "old")
  1332. return "old";
  1333. return "group";
  1334. }
  1335. let pendingRules = {};
  1336. let style = null;
  1337. const css = {
  1338. set: (selector, values) => {
  1339. pendingRules[selector] = values;
  1340. },
  1341. commit: () => {
  1342. if (!style) {
  1343. style = document.createElement("style");
  1344. style.id = "motion-view";
  1345. }
  1346. let cssText = "";
  1347. for (const selector in pendingRules) {
  1348. const rule = pendingRules[selector];
  1349. cssText += `${selector} {\n`;
  1350. for (const [property, value] of Object.entries(rule)) {
  1351. cssText += ` ${property}: ${value};\n`;
  1352. }
  1353. cssText += "}\n";
  1354. }
  1355. style.textContent = cssText;
  1356. document.head.appendChild(style);
  1357. pendingRules = {};
  1358. },
  1359. remove: () => {
  1360. if (style && style.parentElement) {
  1361. style.parentElement.removeChild(style);
  1362. }
  1363. },
  1364. };
  1365. function getLayerName(pseudoElement) {
  1366. const match = pseudoElement.match(/::view-transition-(old|new|group|image-pair)\((.*?)\)/);
  1367. if (!match)
  1368. return null;
  1369. return { layer: match[2], type: match[1] };
  1370. }
  1371. function filterViewAnimations(animation) {
  1372. const { effect } = animation;
  1373. if (!effect)
  1374. return false;
  1375. return (effect.target === document.documentElement &&
  1376. effect.pseudoElement?.startsWith("::view-transition"));
  1377. }
  1378. function getViewAnimations() {
  1379. return document.getAnimations().filter(filterViewAnimations);
  1380. }
  1381. function hasTarget(target, targets) {
  1382. return targets.has(target) && Object.keys(targets.get(target)).length > 0;
  1383. }
  1384. const definitionNames = ["layout", "enter", "exit", "new", "old"];
  1385. function startViewAnimation(builder) {
  1386. const { update, targets, options: defaultOptions } = builder;
  1387. if (!document.startViewTransition) {
  1388. return new Promise(async (resolve) => {
  1389. await update();
  1390. resolve(new GroupAnimation([]));
  1391. });
  1392. }
  1393. // TODO: Go over existing targets and ensure they all have ids
  1394. /**
  1395. * If we don't have any animations defined for the root target,
  1396. * remove it from being captured.
  1397. */
  1398. if (!hasTarget("root", targets)) {
  1399. css.set(":root", {
  1400. "view-transition-name": "none",
  1401. });
  1402. }
  1403. /**
  1404. * Set the timing curve to linear for all view transition layers.
  1405. * This gets baked into the keyframes, which can't be changed
  1406. * without breaking the generated animation.
  1407. *
  1408. * This allows us to set easing via updateTiming - which can be changed.
  1409. */
  1410. css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
  1411. css.commit(); // Write
  1412. const transition = document.startViewTransition(async () => {
  1413. await update();
  1414. // TODO: Go over new targets and ensure they all have ids
  1415. });
  1416. transition.finished.finally(() => {
  1417. css.remove(); // Write
  1418. });
  1419. return new Promise((resolve) => {
  1420. transition.ready.then(() => {
  1421. const generatedViewAnimations = getViewAnimations();
  1422. const animations = [];
  1423. /**
  1424. * Create animations for each of our explicitly-defined subjects.
  1425. */
  1426. targets.forEach((definition, target) => {
  1427. // TODO: If target is not "root", resolve elements
  1428. // and iterate over each
  1429. for (const key of definitionNames) {
  1430. if (!definition[key])
  1431. continue;
  1432. const { keyframes, options } = definition[key];
  1433. for (let [valueName, valueKeyframes] of Object.entries(keyframes)) {
  1434. if (!valueKeyframes)
  1435. continue;
  1436. const valueOptions = {
  1437. ...getValueTransition(defaultOptions, valueName),
  1438. ...getValueTransition(options, valueName),
  1439. };
  1440. const type = chooseLayerType(key);
  1441. /**
  1442. * If this is an opacity animation, and keyframes are not an array,
  1443. * we need to convert them into an array and set an initial value.
  1444. */
  1445. if (valueName === "opacity" &&
  1446. !Array.isArray(valueKeyframes)) {
  1447. const initialValue = type === "new" ? 0 : 1;
  1448. valueKeyframes = [initialValue, valueKeyframes];
  1449. }
  1450. /**
  1451. * Resolve stagger function if provided.
  1452. */
  1453. if (typeof valueOptions.delay === "function") {
  1454. valueOptions.delay = valueOptions.delay(0, 1);
  1455. }
  1456. valueOptions.duration && (valueOptions.duration = motionUtils.secondsToMilliseconds(valueOptions.duration));
  1457. valueOptions.delay && (valueOptions.delay = motionUtils.secondsToMilliseconds(valueOptions.delay));
  1458. const animation = new NativeAnimation({
  1459. element: document.documentElement,
  1460. name: valueName,
  1461. pseudoElement: `::view-transition-${type}(${target})`,
  1462. keyframes: valueKeyframes,
  1463. transition: valueOptions,
  1464. });
  1465. animations.push(animation);
  1466. }
  1467. }
  1468. });
  1469. /**
  1470. * Handle browser generated animations
  1471. */
  1472. for (const animation of generatedViewAnimations) {
  1473. if (animation.playState === "finished")
  1474. continue;
  1475. const { effect } = animation;
  1476. if (!effect || !(effect instanceof KeyframeEffect))
  1477. continue;
  1478. const { pseudoElement } = effect;
  1479. if (!pseudoElement)
  1480. continue;
  1481. const name = getLayerName(pseudoElement);
  1482. if (!name)
  1483. continue;
  1484. const targetDefinition = targets.get(name.layer);
  1485. if (!targetDefinition) {
  1486. /**
  1487. * If transition name is group then update the timing of the animation
  1488. * whereas if it's old or new then we could possibly replace it using
  1489. * the above method.
  1490. */
  1491. const transitionName = name.type === "group" ? "layout" : "";
  1492. let animationTransition = {
  1493. ...getValueTransition(defaultOptions, transitionName),
  1494. };
  1495. animationTransition.duration && (animationTransition.duration = motionUtils.secondsToMilliseconds(animationTransition.duration));
  1496. animationTransition =
  1497. applyGeneratorOptions(animationTransition);
  1498. const easing = mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration);
  1499. effect.updateTiming({
  1500. delay: motionUtils.secondsToMilliseconds(animationTransition.delay ?? 0),
  1501. duration: animationTransition.duration,
  1502. easing,
  1503. });
  1504. animations.push(new NativeAnimation({ animation }));
  1505. }
  1506. else if (hasOpacity(targetDefinition, "enter") &&
  1507. hasOpacity(targetDefinition, "exit") &&
  1508. effect
  1509. .getKeyframes()
  1510. .some((keyframe) => keyframe.mixBlendMode)) {
  1511. animations.push(new NativeAnimation({ animation }));
  1512. }
  1513. else {
  1514. animation.cancel();
  1515. }
  1516. }
  1517. resolve(new GroupAnimation(animations));
  1518. });
  1519. });
  1520. }
  1521. function hasOpacity(target, key) {
  1522. return target?.[key]?.keyframes.opacity;
  1523. }
  1524. let builders = [];
  1525. let current = null;
  1526. function next() {
  1527. current = null;
  1528. const [nextBuilder] = builders;
  1529. if (nextBuilder)
  1530. start(nextBuilder);
  1531. }
  1532. function start(builder) {
  1533. motionUtils.removeItem(builders, builder);
  1534. current = builder;
  1535. startViewAnimation(builder).then((animation) => {
  1536. builder.notifyReady(animation);
  1537. animation.finished.finally(next);
  1538. });
  1539. }
  1540. function processQueue() {
  1541. /**
  1542. * Iterate backwards over the builders array. We can ignore the
  1543. * "wait" animations. If we have an interrupting animation in the
  1544. * queue then we need to batch all preceeding animations into it.
  1545. * Currently this only batches the update functions but will also
  1546. * need to batch the targets.
  1547. */
  1548. for (let i = builders.length - 1; i >= 0; i--) {
  1549. const builder = builders[i];
  1550. const { interrupt } = builder.options;
  1551. if (interrupt === "immediate") {
  1552. const batchedUpdates = builders.slice(0, i + 1).map((b) => b.update);
  1553. const remaining = builders.slice(i + 1);
  1554. builder.update = () => {
  1555. batchedUpdates.forEach((update) => update());
  1556. };
  1557. // Put the current builder at the front, followed by any "wait" builders
  1558. builders = [builder, ...remaining];
  1559. break;
  1560. }
  1561. }
  1562. if (!current || builders[0]?.options.interrupt === "immediate") {
  1563. next();
  1564. }
  1565. }
  1566. function addToQueue(builder) {
  1567. builders.push(builder);
  1568. microtask.render(processQueue);
  1569. }
  1570. class ViewTransitionBuilder {
  1571. constructor(update, options = {}) {
  1572. this.currentTarget = "root";
  1573. this.targets = new Map();
  1574. this.notifyReady = motionUtils.noop;
  1575. this.readyPromise = new Promise((resolve) => {
  1576. this.notifyReady = resolve;
  1577. });
  1578. this.update = update;
  1579. this.options = {
  1580. interrupt: "wait",
  1581. ...options,
  1582. };
  1583. addToQueue(this);
  1584. }
  1585. get(selector) {
  1586. this.currentTarget = selector;
  1587. return this;
  1588. }
  1589. layout(keyframes, options) {
  1590. this.updateTarget("layout", keyframes, options);
  1591. return this;
  1592. }
  1593. new(keyframes, options) {
  1594. this.updateTarget("new", keyframes, options);
  1595. return this;
  1596. }
  1597. old(keyframes, options) {
  1598. this.updateTarget("old", keyframes, options);
  1599. return this;
  1600. }
  1601. enter(keyframes, options) {
  1602. this.updateTarget("enter", keyframes, options);
  1603. return this;
  1604. }
  1605. exit(keyframes, options) {
  1606. this.updateTarget("exit", keyframes, options);
  1607. return this;
  1608. }
  1609. crossfade(options) {
  1610. this.updateTarget("enter", { opacity: 1 }, options);
  1611. this.updateTarget("exit", { opacity: 0 }, options);
  1612. return this;
  1613. }
  1614. updateTarget(target, keyframes, options = {}) {
  1615. const { currentTarget, targets } = this;
  1616. if (!targets.has(currentTarget)) {
  1617. targets.set(currentTarget, {});
  1618. }
  1619. const targetData = targets.get(currentTarget);
  1620. targetData[target] = { keyframes, options };
  1621. }
  1622. then(resolve, reject) {
  1623. return this.readyPromise.then(resolve, reject);
  1624. }
  1625. }
  1626. function animateView(update, defaultOptions = {}) {
  1627. return new ViewTransitionBuilder(update, defaultOptions);
  1628. }
  1629. /**
  1630. * @deprecated
  1631. *
  1632. * Import as `frame` instead.
  1633. */
  1634. const sync = frame;
  1635. /**
  1636. * @deprecated
  1637. *
  1638. * Use cancelFrame(callback) instead.
  1639. */
  1640. const cancelSync = stepsOrder.reduce((acc, key) => {
  1641. acc[key] = (process) => cancelFrame(process);
  1642. return acc;
  1643. }, {});
  1644. exports.GroupAnimation = GroupAnimation;
  1645. exports.GroupAnimationWithThen = GroupAnimationWithThen;
  1646. exports.MotionValue = MotionValue;
  1647. exports.NativeAnimation = NativeAnimation;
  1648. exports.ViewTransitionBuilder = ViewTransitionBuilder;
  1649. exports.activeAnimations = activeAnimations;
  1650. exports.animateView = animateView;
  1651. exports.attachTimeline = attachTimeline;
  1652. exports.calcGeneratorDuration = calcGeneratorDuration;
  1653. exports.cancelFrame = cancelFrame;
  1654. exports.cancelMicrotask = cancelMicrotask;
  1655. exports.cancelSync = cancelSync;
  1656. exports.collectMotionValues = collectMotionValues;
  1657. exports.createGeneratorEasing = createGeneratorEasing;
  1658. exports.createRenderBatcher = createRenderBatcher;
  1659. exports.cubicBezierAsString = cubicBezierAsString;
  1660. exports.frame = frame;
  1661. exports.frameData = frameData;
  1662. exports.frameSteps = frameSteps;
  1663. exports.generateLinearEasing = generateLinearEasing;
  1664. exports.getValueTransition = getValueTransition;
  1665. exports.hover = hover;
  1666. exports.isBezierDefinition = isBezierDefinition;
  1667. exports.isDragActive = isDragActive;
  1668. exports.isDragging = isDragging;
  1669. exports.isGenerator = isGenerator;
  1670. exports.isNodeOrChild = isNodeOrChild;
  1671. exports.isPrimaryPointer = isPrimaryPointer;
  1672. exports.isWaapiSupportedEasing = isWaapiSupportedEasing;
  1673. exports.mapEasingToNativeEasing = mapEasingToNativeEasing;
  1674. exports.maxGeneratorDuration = maxGeneratorDuration;
  1675. exports.microtask = microtask;
  1676. exports.motionValue = motionValue;
  1677. exports.press = press;
  1678. exports.recordStats = recordStats;
  1679. exports.resolveElements = resolveElements;
  1680. exports.setDragLock = setDragLock;
  1681. exports.startWaapiAnimation = startWaapiAnimation;
  1682. exports.statsBuffer = statsBuffer;
  1683. exports.supportedWaapiEasing = supportedWaapiEasing;
  1684. exports.supportsFlags = supportsFlags;
  1685. exports.supportsLinearEasing = supportsLinearEasing;
  1686. exports.supportsScrollTimeline = supportsScrollTimeline;
  1687. exports.sync = sync;
  1688. exports.time = time;
  1689. }));