index.js 56 KB

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