| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746 |
- (function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('motion-utils')) :
- typeof define === 'function' && define.amd ? define(['exports', 'motion-utils'], factory) :
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MotionDom = {}, global.MotionUtils));
- })(this, (function (exports, motionUtils) { 'use strict';
- const supportsScrollTimeline = /* @__PURE__ */ motionUtils.memo(() => window.ScrollTimeline !== undefined);
- class GroupAnimation {
- constructor(animations) {
- // Bound to accomodate common `return animation.stop` pattern
- this.stop = () => this.runAll("stop");
- this.animations = animations.filter(Boolean);
- }
- get finished() {
- return Promise.all(this.animations.map((animation) => animation.finished));
- }
- /**
- * TODO: Filter out cancelled or stopped animations before returning
- */
- getAll(propName) {
- return this.animations[0][propName];
- }
- setAll(propName, newValue) {
- for (let i = 0; i < this.animations.length; i++) {
- this.animations[i][propName] = newValue;
- }
- }
- attachTimeline(timeline, fallback) {
- const subscriptions = this.animations.map((animation) => {
- if (supportsScrollTimeline() && animation.attachTimeline) {
- return animation.attachTimeline(timeline);
- }
- else if (typeof fallback === "function") {
- return fallback(animation);
- }
- });
- return () => {
- subscriptions.forEach((cancel, i) => {
- cancel && cancel();
- this.animations[i].stop();
- });
- };
- }
- get time() {
- return this.getAll("time");
- }
- set time(time) {
- this.setAll("time", time);
- }
- get speed() {
- return this.getAll("speed");
- }
- set speed(speed) {
- this.setAll("speed", speed);
- }
- get startTime() {
- return this.getAll("startTime");
- }
- get duration() {
- let max = 0;
- for (let i = 0; i < this.animations.length; i++) {
- max = Math.max(max, this.animations[i].duration);
- }
- return max;
- }
- runAll(methodName) {
- this.animations.forEach((controls) => controls[methodName]());
- }
- flatten() {
- this.runAll("flatten");
- }
- play() {
- this.runAll("play");
- }
- pause() {
- this.runAll("pause");
- }
- cancel() {
- this.runAll("cancel");
- }
- complete() {
- this.runAll("complete");
- }
- }
- class GroupAnimationWithThen extends GroupAnimation {
- then(onResolve, _onReject) {
- return this.finished.finally(onResolve).then(() => { });
- }
- }
- const isCSSVar = (name) => name.startsWith("--");
- const style$1 = {
- set: (element, name, value) => {
- isCSSVar(name)
- ? element.style.setProperty(name, value)
- : (element.style[name] = value);
- },
- get: (element, name) => {
- return isCSSVar(name)
- ? element.style.getPropertyValue(name)
- : element.style[name];
- },
- };
- const isNotNull = (value) => value !== null;
- function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyframe) {
- const resolvedKeyframes = keyframes.filter(isNotNull);
- const index = repeat && repeatType !== "loop" && repeat % 2 === 1
- ? 0
- : resolvedKeyframes.length - 1;
- return !index || finalKeyframe === undefined
- ? resolvedKeyframes[index]
- : finalKeyframe;
- }
- const supportsPartialKeyframes = /*@__PURE__*/ motionUtils.memo(() => {
- try {
- document.createElement("div").animate({ opacity: [1] });
- }
- catch (e) {
- return false;
- }
- return true;
- });
- const pxValues = new Set([
- // Border props
- "borderWidth",
- "borderTopWidth",
- "borderRightWidth",
- "borderBottomWidth",
- "borderLeftWidth",
- "borderRadius",
- "radius",
- "borderTopLeftRadius",
- "borderTopRightRadius",
- "borderBottomRightRadius",
- "borderBottomLeftRadius",
- // Positioning props
- "width",
- "maxWidth",
- "height",
- "maxHeight",
- "top",
- "right",
- "bottom",
- "left",
- // Spacing props
- "padding",
- "paddingTop",
- "paddingRight",
- "paddingBottom",
- "paddingLeft",
- "margin",
- "marginTop",
- "marginRight",
- "marginBottom",
- "marginLeft",
- // Misc
- "backgroundPositionX",
- "backgroundPositionY",
- ]);
- function hydrateKeyframes(element, name, keyframes, pseudoElement) {
- if (!Array.isArray(keyframes)) {
- keyframes = [keyframes];
- }
- for (let i = 0; i < keyframes.length; i++) {
- if (keyframes[i] === null) {
- keyframes[i] =
- i === 0 && !pseudoElement
- ? style$1.get(element, name)
- : keyframes[i - 1];
- }
- if (typeof keyframes[i] === "number" && pxValues.has(name)) {
- keyframes[i] = keyframes[i] + "px";
- }
- }
- if (!pseudoElement && !supportsPartialKeyframes() && keyframes.length < 2) {
- keyframes.unshift(style$1.get(element, name));
- }
- return keyframes;
- }
- const activeAnimations = {
- layout: 0,
- mainThread: 0,
- waapi: 0,
- };
- const statsBuffer = {
- value: null,
- addProjectionMetrics: null,
- };
- const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
- /**
- * Add the ability for test suites to manually set support flags
- * to better test more environments.
- */
- const supportsFlags = {};
- function memoSupports(callback, supportsFlag) {
- const memoized = motionUtils.memo(callback);
- return () => supportsFlags[supportsFlag] ?? memoized();
- }
- const supportsLinearEasing = /*@__PURE__*/ memoSupports(() => {
- try {
- document
- .createElement("div")
- .animate({ opacity: 0 }, { easing: "linear(0, 1)" });
- }
- catch (e) {
- return false;
- }
- return true;
- }, "linearEasing");
- const generateLinearEasing = (easing, duration, // as milliseconds
- resolution = 10 // as milliseconds
- ) => {
- let points = "";
- const numPoints = Math.max(Math.round(duration / resolution), 2);
- for (let i = 0; i < numPoints; i++) {
- points += easing(i / (numPoints - 1)) + ", ";
- }
- return `linear(${points.substring(0, points.length - 2)})`;
- };
- const cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`;
- const supportedWaapiEasing = {
- linear: "linear",
- ease: "ease",
- easeIn: "ease-in",
- easeOut: "ease-out",
- easeInOut: "ease-in-out",
- circIn: /*@__PURE__*/ cubicBezierAsString([0, 0.65, 0.55, 1]),
- circOut: /*@__PURE__*/ cubicBezierAsString([0.55, 0, 1, 0.45]),
- backIn: /*@__PURE__*/ cubicBezierAsString([0.31, 0.01, 0.66, -0.59]),
- backOut: /*@__PURE__*/ cubicBezierAsString([0.33, 1.53, 0.69, 0.99]),
- };
- function mapEasingToNativeEasing(easing, duration) {
- if (!easing) {
- return undefined;
- }
- else if (typeof easing === "function" && supportsLinearEasing()) {
- return generateLinearEasing(easing, duration);
- }
- else if (isBezierDefinition(easing)) {
- return cubicBezierAsString(easing);
- }
- else if (Array.isArray(easing)) {
- return easing.map((segmentEasing) => mapEasingToNativeEasing(segmentEasing, duration) ||
- supportedWaapiEasing.easeOut);
- }
- else {
- return supportedWaapiEasing[easing];
- }
- }
- function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duration = 300, repeat = 0, repeatType = "loop", ease = "easeInOut", times, } = {}, pseudoElement = undefined) {
- const keyframeOptions = {
- [valueName]: keyframes,
- };
- if (times)
- keyframeOptions.offset = times;
- const easing = mapEasingToNativeEasing(ease, duration);
- /**
- * If this is an easing array, apply to keyframes, not animation as a whole
- */
- if (Array.isArray(easing))
- keyframeOptions.easing = easing;
- if (statsBuffer.value) {
- activeAnimations.waapi++;
- }
- const animation = element.animate(keyframeOptions, {
- delay,
- duration,
- easing: !Array.isArray(easing) ? easing : "linear",
- fill: "both",
- iterations: repeat + 1,
- direction: repeatType === "reverse" ? "alternate" : "normal",
- pseudoElement,
- });
- if (statsBuffer.value) {
- animation.finished.finally(() => {
- activeAnimations.waapi--;
- });
- }
- return animation;
- }
- function isGenerator(type) {
- return typeof type === "function" && "applyToOptions" in type;
- }
- function applyGeneratorOptions({ type, ...options }) {
- if (isGenerator(type)) {
- return type.applyToOptions(options);
- }
- else {
- options.duration ?? (options.duration = 300);
- options.ease ?? (options.ease = "easeOut");
- }
- return options;
- }
- const animationMaps = new WeakMap();
- const animationMapKey = (name, pseudoElement) => `${name}:${pseudoElement}`;
- function getAnimationMap(element) {
- const map = animationMaps.get(element) || new Map();
- animationMaps.set(element, map);
- return map;
- }
- /**
- * NativeAnimation implements AnimationPlaybackControls for the browser's Web Animations API.
- */
- class NativeAnimation {
- constructor(options) {
- /**
- * If we already have an animation, we don't need to instantiate one
- * and can just use this as a controls interface.
- */
- if ("animation" in options) {
- this.animation = options.animation;
- return;
- }
- const { element, name, keyframes: unresolvedKeyframes, pseudoElement, allowFlatten = false, } = options;
- let { transition } = options;
- this.isPseudoElement = Boolean(pseudoElement);
- this.allowFlatten = allowFlatten;
- /**
- * Stop any existing animations on the element before reading existing keyframes.
- *
- * TODO: Check for VisualElement before using animation state. This is a fallback
- * for mini animate(). Do this when implementing NativeAnimationExtended.
- */
- const animationMap = getAnimationMap(element);
- const key = animationMapKey(name, pseudoElement || "");
- const currentAnimation = animationMap.get(key);
- currentAnimation && currentAnimation.stop();
- /**
- * TODO: If these keyframes aren't correctly hydrated then we want to throw
- * run an instant animation.
- */
- const keyframes = hydrateKeyframes(element, name, unresolvedKeyframes, pseudoElement);
- motionUtils.invariant(typeof transition.type !== "string", `animateMini doesn't support "type" as a string. Did you mean to import { spring } from "motion"?`);
- transition = applyGeneratorOptions(transition);
- this.animation = startWaapiAnimation(element, name, keyframes, transition, pseudoElement);
- if (transition.autoplay === false) {
- this.animation.pause();
- }
- this.removeAnimation = () => animationMap.delete(key);
- this.animation.onfinish = () => {
- if (!pseudoElement) {
- style$1.set(element, name, getFinalKeyframe(keyframes, transition));
- this.cancel();
- }
- };
- /**
- * TODO: Check for VisualElement before using animation state.
- */
- animationMap.set(key, this);
- }
- play() {
- this.animation.play();
- }
- pause() {
- this.animation.pause();
- }
- complete() {
- this.animation.finish();
- }
- cancel() {
- try {
- this.animation.cancel();
- }
- catch (e) { }
- this.removeAnimation();
- }
- stop() {
- const { state } = this;
- if (state === "idle" || state === "finished") {
- return;
- }
- this.commitStyles();
- this.cancel();
- }
- /**
- * WAAPI doesn't natively have any interruption capabilities.
- *
- * In this method, we commit styles back to the DOM before cancelling
- * the animation.
- *
- * This is designed to be overridden by NativeAnimationExtended, which
- * will create a renderless JS animation and sample it twice to calculate
- * its current value, "previous" value, and therefore allow
- * Motion to also correctly calculate velocity for any subsequent animation
- * while deferring the commit until the next animation frame.
- */
- commitStyles() {
- if (!this.isPseudoElement) {
- this.animation.commitStyles?.();
- }
- }
- get duration() {
- const duration = this.animation.effect?.getComputedTiming().duration || 0;
- return motionUtils.millisecondsToSeconds(Number(duration));
- }
- get time() {
- return motionUtils.millisecondsToSeconds(Number(this.animation.currentTime) || 0);
- }
- set time(newTime) {
- this.animation.currentTime = motionUtils.secondsToMilliseconds(newTime);
- }
- /**
- * The playback speed of the animation.
- * 1 = normal speed, 2 = double speed, 0.5 = half speed.
- */
- get speed() {
- return this.animation.playbackRate;
- }
- set speed(newSpeed) {
- this.animation.playbackRate = newSpeed;
- }
- get state() {
- return this.animation.playState;
- }
- get startTime() {
- return Number(this.animation.startTime);
- }
- get finished() {
- return this.animation.finished;
- }
- flatten() {
- if (this.allowFlatten) {
- this.animation.effect?.updateTiming({ easing: "linear" });
- }
- }
- /**
- * Attaches a timeline to the animation, for instance the `ScrollTimeline`.
- */
- attachTimeline(timeline) {
- this.animation.timeline = timeline;
- this.animation.onfinish = null;
- return motionUtils.noop;
- }
- /**
- * Allows the animation to be awaited.
- *
- * @deprecated Use `finished` instead.
- */
- then(onResolve, onReject) {
- return this.finished.then(onResolve).catch(onReject);
- }
- }
- function getValueTransition(transition, key) {
- return (transition?.[key] ??
- transition?.["default"] ??
- transition);
- }
- /**
- * Implement a practical max duration for keyframe generation
- * to prevent infinite loops
- */
- const maxGeneratorDuration = 20000;
- function calcGeneratorDuration(generator) {
- let duration = 0;
- const timeStep = 50;
- let state = generator.next(duration);
- while (!state.done && duration < maxGeneratorDuration) {
- duration += timeStep;
- state = generator.next(duration);
- }
- return duration >= maxGeneratorDuration ? Infinity : duration;
- }
- /**
- * Create a progress => progress easing function from a generator.
- */
- function createGeneratorEasing(options, scale = 100, createGenerator) {
- const generator = createGenerator({ ...options, keyframes: [0, scale] });
- const duration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration);
- return {
- type: "keyframes",
- ease: (progress) => {
- return generator.next(duration * progress).value / scale;
- },
- duration: motionUtils.millisecondsToSeconds(duration),
- };
- }
- function isWaapiSupportedEasing(easing) {
- return Boolean((typeof easing === "function" && supportsLinearEasing()) ||
- !easing ||
- (typeof easing === "string" &&
- (easing in supportedWaapiEasing || supportsLinearEasing())) ||
- isBezierDefinition(easing) ||
- (Array.isArray(easing) && easing.every(isWaapiSupportedEasing)));
- }
- function attachTimeline(animation, timeline) {
- animation.timeline = timeline;
- animation.onfinish = null;
- }
- const stepsOrder = [
- "read", // Read
- "resolveKeyframes", // Write/Read/Write/Read
- "update", // Compute
- "preRender", // Compute
- "render", // Write
- "postRender", // Compute
- ];
- function createRenderStep(runNextFrame, stepName) {
- /**
- * We create and reuse two queues, one to queue jobs for the current frame
- * and one for the next. We reuse to avoid triggering GC after x frames.
- */
- let thisFrame = new Set();
- let nextFrame = new Set();
- /**
- * Track whether we're currently processing jobs in this step. This way
- * we can decide whether to schedule new jobs for this frame or next.
- */
- let isProcessing = false;
- let flushNextFrame = false;
- /**
- * A set of processes which were marked keepAlive when scheduled.
- */
- const toKeepAlive = new WeakSet();
- let latestFrameData = {
- delta: 0.0,
- timestamp: 0.0,
- isProcessing: false,
- };
- let numCalls = 0;
- function triggerCallback(callback) {
- if (toKeepAlive.has(callback)) {
- step.schedule(callback);
- runNextFrame();
- }
- numCalls++;
- callback(latestFrameData);
- }
- const step = {
- /**
- * Schedule a process to run on the next frame.
- */
- schedule: (callback, keepAlive = false, immediate = false) => {
- const addToCurrentFrame = immediate && isProcessing;
- const queue = addToCurrentFrame ? thisFrame : nextFrame;
- if (keepAlive)
- toKeepAlive.add(callback);
- if (!queue.has(callback))
- queue.add(callback);
- return callback;
- },
- /**
- * Cancel the provided callback from running on the next frame.
- */
- cancel: (callback) => {
- nextFrame.delete(callback);
- toKeepAlive.delete(callback);
- },
- /**
- * Execute all schedule callbacks.
- */
- process: (frameData) => {
- latestFrameData = frameData;
- /**
- * If we're already processing we've probably been triggered by a flushSync
- * inside an existing process. Instead of executing, mark flushNextFrame
- * as true and ensure we flush the following frame at the end of this one.
- */
- if (isProcessing) {
- flushNextFrame = true;
- return;
- }
- isProcessing = true;
- [thisFrame, nextFrame] = [nextFrame, thisFrame];
- // Execute this frame
- thisFrame.forEach(triggerCallback);
- /**
- * If we're recording stats then
- */
- if (stepName && statsBuffer.value) {
- statsBuffer.value.frameloop[stepName].push(numCalls);
- }
- numCalls = 0;
- // Clear the frame so no callbacks remain. This is to avoid
- // memory leaks should this render step not run for a while.
- thisFrame.clear();
- isProcessing = false;
- if (flushNextFrame) {
- flushNextFrame = false;
- step.process(frameData);
- }
- },
- };
- return step;
- }
- const maxElapsed = 40;
- function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
- let runNextFrame = false;
- let useDefaultElapsed = true;
- const state = {
- delta: 0.0,
- timestamp: 0.0,
- isProcessing: false,
- };
- const flagRunNextFrame = () => (runNextFrame = true);
- const steps = stepsOrder.reduce((acc, key) => {
- acc[key] = createRenderStep(flagRunNextFrame, allowKeepAlive ? key : undefined);
- return acc;
- }, {});
- const { read, resolveKeyframes, update, preRender, render, postRender } = steps;
- const processBatch = () => {
- const timestamp = motionUtils.MotionGlobalConfig.useManualTiming
- ? state.timestamp
- : performance.now();
- runNextFrame = false;
- if (!motionUtils.MotionGlobalConfig.useManualTiming) {
- state.delta = useDefaultElapsed
- ? 1000 / 60
- : Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1);
- }
- state.timestamp = timestamp;
- state.isProcessing = true;
- // Unrolled render loop for better per-frame performance
- read.process(state);
- resolveKeyframes.process(state);
- update.process(state);
- preRender.process(state);
- render.process(state);
- postRender.process(state);
- state.isProcessing = false;
- if (runNextFrame && allowKeepAlive) {
- useDefaultElapsed = false;
- scheduleNextBatch(processBatch);
- }
- };
- const wake = () => {
- runNextFrame = true;
- useDefaultElapsed = true;
- if (!state.isProcessing) {
- scheduleNextBatch(processBatch);
- }
- };
- const schedule = stepsOrder.reduce((acc, key) => {
- const step = steps[key];
- acc[key] = (process, keepAlive = false, immediate = false) => {
- if (!runNextFrame)
- wake();
- return step.schedule(process, keepAlive, immediate);
- };
- return acc;
- }, {});
- const cancel = (process) => {
- for (let i = 0; i < stepsOrder.length; i++) {
- steps[stepsOrder[i]].cancel(process);
- }
- };
- return { schedule, cancel, state, steps };
- }
- const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : motionUtils.noop, true);
- const { schedule: microtask, cancel: cancelMicrotask } =
- /* @__PURE__ */ createRenderBatcher(queueMicrotask, false);
- let now;
- function clearTime() {
- now = undefined;
- }
- /**
- * An eventloop-synchronous alternative to performance.now().
- *
- * Ensures that time measurements remain consistent within a synchronous context.
- * Usually calling performance.now() twice within the same synchronous context
- * will return different values which isn't useful for animations when we're usually
- * trying to sync animations to the same frame.
- */
- const time = {
- now: () => {
- if (now === undefined) {
- time.set(frameData.isProcessing || motionUtils.MotionGlobalConfig.useManualTiming
- ? frameData.timestamp
- : performance.now());
- }
- return now;
- },
- set: (newTime) => {
- now = newTime;
- queueMicrotask(clearTime);
- },
- };
- const isDragging = {
- x: false,
- y: false,
- };
- function isDragActive() {
- return isDragging.x || isDragging.y;
- }
- function setDragLock(axis) {
- if (axis === "x" || axis === "y") {
- if (isDragging[axis]) {
- return null;
- }
- else {
- isDragging[axis] = true;
- return () => {
- isDragging[axis] = false;
- };
- }
- }
- else {
- if (isDragging.x || isDragging.y) {
- return null;
- }
- else {
- isDragging.x = isDragging.y = true;
- return () => {
- isDragging.x = isDragging.y = false;
- };
- }
- }
- }
- function resolveElements(elementOrSelector, scope, selectorCache) {
- if (elementOrSelector instanceof EventTarget) {
- return [elementOrSelector];
- }
- else if (typeof elementOrSelector === "string") {
- let root = document;
- if (scope) {
- root = scope.current;
- }
- const elements = selectorCache?.[elementOrSelector] ??
- root.querySelectorAll(elementOrSelector);
- return elements ? Array.from(elements) : [];
- }
- return Array.from(elementOrSelector);
- }
- function setupGesture(elementOrSelector, options) {
- const elements = resolveElements(elementOrSelector);
- const gestureAbortController = new AbortController();
- const eventOptions = {
- passive: true,
- ...options,
- signal: gestureAbortController.signal,
- };
- const cancel = () => gestureAbortController.abort();
- return [elements, eventOptions, cancel];
- }
- function isValidHover(event) {
- return !(event.pointerType === "touch" || isDragActive());
- }
- /**
- * Create a hover gesture. hover() is different to .addEventListener("pointerenter")
- * in that it has an easier syntax, filters out polyfilled touch events, interoperates
- * with drag gestures, and automatically removes the "pointerennd" event listener when the hover ends.
- *
- * @public
- */
- function hover(elementOrSelector, onHoverStart, options = {}) {
- const [elements, eventOptions, cancel] = setupGesture(elementOrSelector, options);
- const onPointerEnter = (enterEvent) => {
- if (!isValidHover(enterEvent))
- return;
- const { target } = enterEvent;
- const onHoverEnd = onHoverStart(target, enterEvent);
- if (typeof onHoverEnd !== "function" || !target)
- return;
- const onPointerLeave = (leaveEvent) => {
- if (!isValidHover(leaveEvent))
- return;
- onHoverEnd(leaveEvent);
- target.removeEventListener("pointerleave", onPointerLeave);
- };
- target.addEventListener("pointerleave", onPointerLeave, eventOptions);
- };
- elements.forEach((element) => {
- element.addEventListener("pointerenter", onPointerEnter, eventOptions);
- });
- return cancel;
- }
- /**
- * Recursively traverse up the tree to check whether the provided child node
- * is the parent or a descendant of it.
- *
- * @param parent - Element to find
- * @param child - Element to test against parent
- */
- const isNodeOrChild = (parent, child) => {
- if (!child) {
- return false;
- }
- else if (parent === child) {
- return true;
- }
- else {
- return isNodeOrChild(parent, child.parentElement);
- }
- };
- const isPrimaryPointer = (event) => {
- if (event.pointerType === "mouse") {
- return typeof event.button !== "number" || event.button <= 0;
- }
- else {
- /**
- * isPrimary is true for all mice buttons, whereas every touch point
- * is regarded as its own input. So subsequent concurrent touch points
- * will be false.
- *
- * Specifically match against false here as incomplete versions of
- * PointerEvents in very old browser might have it set as undefined.
- */
- return event.isPrimary !== false;
- }
- };
- const focusableElements = new Set([
- "BUTTON",
- "INPUT",
- "SELECT",
- "TEXTAREA",
- "A",
- ]);
- function isElementKeyboardAccessible(element) {
- return (focusableElements.has(element.tagName) ||
- element.tabIndex !== -1);
- }
- const isPressing = new WeakSet();
- /**
- * Filter out events that are not "Enter" keys.
- */
- function filterEvents(callback) {
- return (event) => {
- if (event.key !== "Enter")
- return;
- callback(event);
- };
- }
- function firePointerEvent(target, type) {
- target.dispatchEvent(new PointerEvent("pointer" + type, { isPrimary: true, bubbles: true }));
- }
- const enableKeyboardPress = (focusEvent, eventOptions) => {
- const element = focusEvent.currentTarget;
- if (!element)
- return;
- const handleKeydown = filterEvents(() => {
- if (isPressing.has(element))
- return;
- firePointerEvent(element, "down");
- const handleKeyup = filterEvents(() => {
- firePointerEvent(element, "up");
- });
- const handleBlur = () => firePointerEvent(element, "cancel");
- element.addEventListener("keyup", handleKeyup, eventOptions);
- element.addEventListener("blur", handleBlur, eventOptions);
- });
- element.addEventListener("keydown", handleKeydown, eventOptions);
- /**
- * Add an event listener that fires on blur to remove the keydown events.
- */
- element.addEventListener("blur", () => element.removeEventListener("keydown", handleKeydown), eventOptions);
- };
- /**
- * Filter out events that are not primary pointer events, or are triggering
- * while a Motion gesture is active.
- */
- function isValidPressEvent(event) {
- return isPrimaryPointer(event) && !isDragActive();
- }
- /**
- * Create a press gesture.
- *
- * Press is different to `"pointerdown"`, `"pointerup"` in that it
- * automatically filters out secondary pointer events like right
- * click and multitouch.
- *
- * It also adds accessibility support for keyboards, where
- * an element with a press gesture will receive focus and
- * trigger on Enter `"keydown"` and `"keyup"` events.
- *
- * This is different to a browser's `"click"` event, which does
- * respond to keyboards but only for the `"click"` itself, rather
- * than the press start and end/cancel. The element also needs
- * to be focusable for this to work, whereas a press gesture will
- * make an element focusable by default.
- *
- * @public
- */
- function press(targetOrSelector, onPressStart, options = {}) {
- const [targets, eventOptions, cancelEvents] = setupGesture(targetOrSelector, options);
- const startPress = (startEvent) => {
- const target = startEvent.currentTarget;
- if (!isValidPressEvent(startEvent) || isPressing.has(target))
- return;
- isPressing.add(target);
- const onPressEnd = onPressStart(target, startEvent);
- const onPointerEnd = (endEvent, success) => {
- window.removeEventListener("pointerup", onPointerUp);
- window.removeEventListener("pointercancel", onPointerCancel);
- if (!isValidPressEvent(endEvent) || !isPressing.has(target)) {
- return;
- }
- isPressing.delete(target);
- if (typeof onPressEnd === "function") {
- onPressEnd(endEvent, { success });
- }
- };
- const onPointerUp = (upEvent) => {
- onPointerEnd(upEvent, target === window ||
- target === document ||
- options.useGlobalTarget ||
- isNodeOrChild(target, upEvent.target));
- };
- const onPointerCancel = (cancelEvent) => {
- onPointerEnd(cancelEvent, false);
- };
- window.addEventListener("pointerup", onPointerUp, eventOptions);
- window.addEventListener("pointercancel", onPointerCancel, eventOptions);
- };
- targets.forEach((target) => {
- const pointerDownTarget = options.useGlobalTarget ? window : target;
- pointerDownTarget.addEventListener("pointerdown", startPress, eventOptions);
- if (target instanceof HTMLElement) {
- target.addEventListener("focus", (event) => enableKeyboardPress(event, eventOptions));
- if (!isElementKeyboardAccessible(target) &&
- !target.hasAttribute("tabindex")) {
- target.tabIndex = 0;
- }
- }
- });
- return cancelEvents;
- }
- function record() {
- const { value } = statsBuffer;
- if (value === null) {
- cancelFrame(record);
- return;
- }
- value.frameloop.rate.push(frameData.delta);
- value.animations.mainThread.push(activeAnimations.mainThread);
- value.animations.waapi.push(activeAnimations.waapi);
- value.animations.layout.push(activeAnimations.layout);
- }
- function mean(values) {
- return values.reduce((acc, value) => acc + value, 0) / values.length;
- }
- function summarise(values, calcAverage = mean) {
- if (values.length === 0) {
- return {
- min: 0,
- max: 0,
- avg: 0,
- };
- }
- return {
- min: Math.min(...values),
- max: Math.max(...values),
- avg: calcAverage(values),
- };
- }
- const msToFps = (ms) => Math.round(1000 / ms);
- function clearStatsBuffer() {
- statsBuffer.value = null;
- statsBuffer.addProjectionMetrics = null;
- }
- function reportStats() {
- const { value } = statsBuffer;
- if (!value) {
- throw new Error("Stats are not being measured");
- }
- clearStatsBuffer();
- cancelFrame(record);
- const summary = {
- frameloop: {
- rate: summarise(value.frameloop.rate),
- read: summarise(value.frameloop.read),
- resolveKeyframes: summarise(value.frameloop.resolveKeyframes),
- update: summarise(value.frameloop.update),
- preRender: summarise(value.frameloop.preRender),
- render: summarise(value.frameloop.render),
- postRender: summarise(value.frameloop.postRender),
- },
- animations: {
- mainThread: summarise(value.animations.mainThread),
- waapi: summarise(value.animations.waapi),
- layout: summarise(value.animations.layout),
- },
- layoutProjection: {
- nodes: summarise(value.layoutProjection.nodes),
- calculatedTargetDeltas: summarise(value.layoutProjection.calculatedTargetDeltas),
- calculatedProjections: summarise(value.layoutProjection.calculatedProjections),
- },
- };
- /**
- * Convert the rate to FPS
- */
- const { rate } = summary.frameloop;
- rate.min = msToFps(rate.min);
- rate.max = msToFps(rate.max);
- rate.avg = msToFps(rate.avg);
- [rate.min, rate.max] = [rate.max, rate.min];
- return summary;
- }
- function recordStats() {
- if (statsBuffer.value) {
- clearStatsBuffer();
- throw new Error("Stats are already being measured");
- }
- const newStatsBuffer = statsBuffer;
- newStatsBuffer.value = {
- frameloop: {
- rate: [],
- read: [],
- resolveKeyframes: [],
- update: [],
- preRender: [],
- render: [],
- postRender: [],
- },
- animations: {
- mainThread: [],
- waapi: [],
- layout: [],
- },
- layoutProjection: {
- nodes: [],
- calculatedTargetDeltas: [],
- calculatedProjections: [],
- },
- };
- newStatsBuffer.addProjectionMetrics = (metrics) => {
- const { layoutProjection } = newStatsBuffer.value;
- layoutProjection.nodes.push(metrics.nodes);
- layoutProjection.calculatedTargetDeltas.push(metrics.calculatedTargetDeltas);
- layoutProjection.calculatedProjections.push(metrics.calculatedProjections);
- };
- frame.postRender(record, true);
- return reportStats;
- }
- /**
- * Maximum time between the value of two frames, beyond which we
- * assume the velocity has since been 0.
- */
- const MAX_VELOCITY_DELTA = 30;
- const isFloat = (value) => {
- return !isNaN(parseFloat(value));
- };
- const collectMotionValues = {
- current: undefined,
- };
- /**
- * `MotionValue` is used to track the state and velocity of motion values.
- *
- * @public
- */
- class MotionValue {
- /**
- * @param init - The initiating value
- * @param config - Optional configuration options
- *
- * - `transformer`: A function to transform incoming values with.
- */
- constructor(init, options = {}) {
- /**
- * This will be replaced by the build step with the latest version number.
- * When MotionValues are provided to motion components, warn if versions are mixed.
- */
- this.version = "12.7.3";
- /**
- * Tracks whether this value can output a velocity. Currently this is only true
- * if the value is numerical, but we might be able to widen the scope here and support
- * other value types.
- *
- * @internal
- */
- this.canTrackVelocity = null;
- /**
- * An object containing a SubscriptionManager for each active event.
- */
- this.events = {};
- this.updateAndNotify = (v, render = true) => {
- const currentTime = time.now();
- /**
- * If we're updating the value during another frame or eventloop
- * than the previous frame, then the we set the previous frame value
- * to current.
- */
- if (this.updatedAt !== currentTime) {
- this.setPrevFrameValue();
- }
- this.prev = this.current;
- this.setCurrent(v);
- // Update update subscribers
- if (this.current !== this.prev && this.events.change) {
- this.events.change.notify(this.current);
- }
- // Update render subscribers
- if (render && this.events.renderRequest) {
- this.events.renderRequest.notify(this.current);
- }
- };
- this.hasAnimated = false;
- this.setCurrent(init);
- this.owner = options.owner;
- }
- setCurrent(current) {
- this.current = current;
- this.updatedAt = time.now();
- if (this.canTrackVelocity === null && current !== undefined) {
- this.canTrackVelocity = isFloat(this.current);
- }
- }
- setPrevFrameValue(prevFrameValue = this.current) {
- this.prevFrameValue = prevFrameValue;
- this.prevUpdatedAt = this.updatedAt;
- }
- /**
- * Adds a function that will be notified when the `MotionValue` is updated.
- *
- * It returns a function that, when called, will cancel the subscription.
- *
- * When calling `onChange` inside a React component, it should be wrapped with the
- * `useEffect` hook. As it returns an unsubscribe function, this should be returned
- * from the `useEffect` function to ensure you don't add duplicate subscribers..
- *
- * ```jsx
- * export const MyComponent = () => {
- * const x = useMotionValue(0)
- * const y = useMotionValue(0)
- * const opacity = useMotionValue(1)
- *
- * useEffect(() => {
- * function updateOpacity() {
- * const maxXY = Math.max(x.get(), y.get())
- * const newOpacity = transform(maxXY, [0, 100], [1, 0])
- * opacity.set(newOpacity)
- * }
- *
- * const unsubscribeX = x.on("change", updateOpacity)
- * const unsubscribeY = y.on("change", updateOpacity)
- *
- * return () => {
- * unsubscribeX()
- * unsubscribeY()
- * }
- * }, [])
- *
- * return <motion.div style={{ x }} />
- * }
- * ```
- *
- * @param subscriber - A function that receives the latest value.
- * @returns A function that, when called, will cancel this subscription.
- *
- * @deprecated
- */
- onChange(subscription) {
- {
- motionUtils.warnOnce(false, `value.onChange(callback) is deprecated. Switch to value.on("change", callback).`);
- }
- return this.on("change", subscription);
- }
- on(eventName, callback) {
- if (!this.events[eventName]) {
- this.events[eventName] = new motionUtils.SubscriptionManager();
- }
- const unsubscribe = this.events[eventName].add(callback);
- if (eventName === "change") {
- return () => {
- unsubscribe();
- /**
- * If we have no more change listeners by the start
- * of the next frame, stop active animations.
- */
- frame.read(() => {
- if (!this.events.change.getSize()) {
- this.stop();
- }
- });
- };
- }
- return unsubscribe;
- }
- clearListeners() {
- for (const eventManagers in this.events) {
- this.events[eventManagers].clear();
- }
- }
- /**
- * Attaches a passive effect to the `MotionValue`.
- */
- attach(passiveEffect, stopPassiveEffect) {
- this.passiveEffect = passiveEffect;
- this.stopPassiveEffect = stopPassiveEffect;
- }
- /**
- * Sets the state of the `MotionValue`.
- *
- * @remarks
- *
- * ```jsx
- * const x = useMotionValue(0)
- * x.set(10)
- * ```
- *
- * @param latest - Latest value to set.
- * @param render - Whether to notify render subscribers. Defaults to `true`
- *
- * @public
- */
- set(v, render = true) {
- if (!render || !this.passiveEffect) {
- this.updateAndNotify(v, render);
- }
- else {
- this.passiveEffect(v, this.updateAndNotify);
- }
- }
- setWithVelocity(prev, current, delta) {
- this.set(current);
- this.prev = undefined;
- this.prevFrameValue = prev;
- this.prevUpdatedAt = this.updatedAt - delta;
- }
- /**
- * Set the state of the `MotionValue`, stopping any active animations,
- * effects, and resets velocity to `0`.
- */
- jump(v, endAnimation = true) {
- this.updateAndNotify(v);
- this.prev = v;
- this.prevUpdatedAt = this.prevFrameValue = undefined;
- endAnimation && this.stop();
- if (this.stopPassiveEffect)
- this.stopPassiveEffect();
- }
- /**
- * Returns the latest state of `MotionValue`
- *
- * @returns - The latest state of `MotionValue`
- *
- * @public
- */
- get() {
- if (collectMotionValues.current) {
- collectMotionValues.current.push(this);
- }
- return this.current;
- }
- /**
- * @public
- */
- getPrevious() {
- return this.prev;
- }
- /**
- * Returns the latest velocity of `MotionValue`
- *
- * @returns - The latest velocity of `MotionValue`. Returns `0` if the state is non-numerical.
- *
- * @public
- */
- getVelocity() {
- const currentTime = time.now();
- if (!this.canTrackVelocity ||
- this.prevFrameValue === undefined ||
- currentTime - this.updatedAt > MAX_VELOCITY_DELTA) {
- return 0;
- }
- const delta = Math.min(this.updatedAt - this.prevUpdatedAt, MAX_VELOCITY_DELTA);
- // Casts because of parseFloat's poor typing
- return motionUtils.velocityPerSecond(parseFloat(this.current) -
- parseFloat(this.prevFrameValue), delta);
- }
- /**
- * Registers a new animation to control this `MotionValue`. Only one
- * animation can drive a `MotionValue` at one time.
- *
- * ```jsx
- * value.start()
- * ```
- *
- * @param animation - A function that starts the provided animation
- */
- start(startAnimation) {
- this.stop();
- return new Promise((resolve) => {
- this.hasAnimated = true;
- this.animation = startAnimation(resolve);
- if (this.events.animationStart) {
- this.events.animationStart.notify();
- }
- }).then(() => {
- if (this.events.animationComplete) {
- this.events.animationComplete.notify();
- }
- this.clearAnimation();
- });
- }
- /**
- * Stop the currently active animation.
- *
- * @public
- */
- stop() {
- if (this.animation) {
- this.animation.stop();
- if (this.events.animationCancel) {
- this.events.animationCancel.notify();
- }
- }
- this.clearAnimation();
- }
- /**
- * Returns `true` if this value is currently animating.
- *
- * @public
- */
- isAnimating() {
- return !!this.animation;
- }
- clearAnimation() {
- delete this.animation;
- }
- /**
- * Destroy and clean up subscribers to this `MotionValue`.
- *
- * The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically
- * handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually
- * created a `MotionValue` via the `motionValue` function.
- *
- * @public
- */
- destroy() {
- this.clearListeners();
- this.stop();
- if (this.stopPassiveEffect) {
- this.stopPassiveEffect();
- }
- }
- }
- function motionValue(init, options) {
- return new MotionValue(init, options);
- }
- function chooseLayerType(valueName) {
- if (valueName === "layout")
- return "group";
- if (valueName === "enter" || valueName === "new")
- return "new";
- if (valueName === "exit" || valueName === "old")
- return "old";
- return "group";
- }
- let pendingRules = {};
- let style = null;
- const css = {
- set: (selector, values) => {
- pendingRules[selector] = values;
- },
- commit: () => {
- if (!style) {
- style = document.createElement("style");
- style.id = "motion-view";
- }
- let cssText = "";
- for (const selector in pendingRules) {
- const rule = pendingRules[selector];
- cssText += `${selector} {\n`;
- for (const [property, value] of Object.entries(rule)) {
- cssText += ` ${property}: ${value};\n`;
- }
- cssText += "}\n";
- }
- style.textContent = cssText;
- document.head.appendChild(style);
- pendingRules = {};
- },
- remove: () => {
- if (style && style.parentElement) {
- style.parentElement.removeChild(style);
- }
- },
- };
- function getLayerName(pseudoElement) {
- const match = pseudoElement.match(/::view-transition-(old|new|group|image-pair)\((.*?)\)/);
- if (!match)
- return null;
- return { layer: match[2], type: match[1] };
- }
- function filterViewAnimations(animation) {
- const { effect } = animation;
- if (!effect)
- return false;
- return (effect.target === document.documentElement &&
- effect.pseudoElement?.startsWith("::view-transition"));
- }
- function getViewAnimations() {
- return document.getAnimations().filter(filterViewAnimations);
- }
- function hasTarget(target, targets) {
- return targets.has(target) && Object.keys(targets.get(target)).length > 0;
- }
- const definitionNames = ["layout", "enter", "exit", "new", "old"];
- function startViewAnimation(builder) {
- const { update, targets, options: defaultOptions } = builder;
- if (!document.startViewTransition) {
- return new Promise(async (resolve) => {
- await update();
- resolve(new GroupAnimation([]));
- });
- }
- // TODO: Go over existing targets and ensure they all have ids
- /**
- * If we don't have any animations defined for the root target,
- * remove it from being captured.
- */
- if (!hasTarget("root", targets)) {
- css.set(":root", {
- "view-transition-name": "none",
- });
- }
- /**
- * Set the timing curve to linear for all view transition layers.
- * This gets baked into the keyframes, which can't be changed
- * without breaking the generated animation.
- *
- * This allows us to set easing via updateTiming - which can be changed.
- */
- css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
- css.commit(); // Write
- const transition = document.startViewTransition(async () => {
- await update();
- // TODO: Go over new targets and ensure they all have ids
- });
- transition.finished.finally(() => {
- css.remove(); // Write
- });
- return new Promise((resolve) => {
- transition.ready.then(() => {
- const generatedViewAnimations = getViewAnimations();
- const animations = [];
- /**
- * Create animations for each of our explicitly-defined subjects.
- */
- targets.forEach((definition, target) => {
- // TODO: If target is not "root", resolve elements
- // and iterate over each
- for (const key of definitionNames) {
- if (!definition[key])
- continue;
- const { keyframes, options } = definition[key];
- for (let [valueName, valueKeyframes] of Object.entries(keyframes)) {
- if (!valueKeyframes)
- continue;
- const valueOptions = {
- ...getValueTransition(defaultOptions, valueName),
- ...getValueTransition(options, valueName),
- };
- const type = chooseLayerType(key);
- /**
- * If this is an opacity animation, and keyframes are not an array,
- * we need to convert them into an array and set an initial value.
- */
- if (valueName === "opacity" &&
- !Array.isArray(valueKeyframes)) {
- const initialValue = type === "new" ? 0 : 1;
- valueKeyframes = [initialValue, valueKeyframes];
- }
- /**
- * Resolve stagger function if provided.
- */
- if (typeof valueOptions.delay === "function") {
- valueOptions.delay = valueOptions.delay(0, 1);
- }
- valueOptions.duration && (valueOptions.duration = motionUtils.secondsToMilliseconds(valueOptions.duration));
- valueOptions.delay && (valueOptions.delay = motionUtils.secondsToMilliseconds(valueOptions.delay));
- const animation = new NativeAnimation({
- element: document.documentElement,
- name: valueName,
- pseudoElement: `::view-transition-${type}(${target})`,
- keyframes: valueKeyframes,
- transition: valueOptions,
- });
- animations.push(animation);
- }
- }
- });
- /**
- * Handle browser generated animations
- */
- for (const animation of generatedViewAnimations) {
- if (animation.playState === "finished")
- continue;
- const { effect } = animation;
- if (!effect || !(effect instanceof KeyframeEffect))
- continue;
- const { pseudoElement } = effect;
- if (!pseudoElement)
- continue;
- const name = getLayerName(pseudoElement);
- if (!name)
- continue;
- const targetDefinition = targets.get(name.layer);
- if (!targetDefinition) {
- /**
- * If transition name is group then update the timing of the animation
- * whereas if it's old or new then we could possibly replace it using
- * the above method.
- */
- const transitionName = name.type === "group" ? "layout" : "";
- let animationTransition = {
- ...getValueTransition(defaultOptions, transitionName),
- };
- animationTransition.duration && (animationTransition.duration = motionUtils.secondsToMilliseconds(animationTransition.duration));
- animationTransition =
- applyGeneratorOptions(animationTransition);
- const easing = mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration);
- effect.updateTiming({
- delay: motionUtils.secondsToMilliseconds(animationTransition.delay ?? 0),
- duration: animationTransition.duration,
- easing,
- });
- animations.push(new NativeAnimation({ animation }));
- }
- else if (hasOpacity(targetDefinition, "enter") &&
- hasOpacity(targetDefinition, "exit") &&
- effect
- .getKeyframes()
- .some((keyframe) => keyframe.mixBlendMode)) {
- animations.push(new NativeAnimation({ animation }));
- }
- else {
- animation.cancel();
- }
- }
- resolve(new GroupAnimation(animations));
- });
- });
- }
- function hasOpacity(target, key) {
- return target?.[key]?.keyframes.opacity;
- }
- let builders = [];
- let current = null;
- function next() {
- current = null;
- const [nextBuilder] = builders;
- if (nextBuilder)
- start(nextBuilder);
- }
- function start(builder) {
- motionUtils.removeItem(builders, builder);
- current = builder;
- startViewAnimation(builder).then((animation) => {
- builder.notifyReady(animation);
- animation.finished.finally(next);
- });
- }
- function processQueue() {
- /**
- * Iterate backwards over the builders array. We can ignore the
- * "wait" animations. If we have an interrupting animation in the
- * queue then we need to batch all preceeding animations into it.
- * Currently this only batches the update functions but will also
- * need to batch the targets.
- */
- for (let i = builders.length - 1; i >= 0; i--) {
- const builder = builders[i];
- const { interrupt } = builder.options;
- if (interrupt === "immediate") {
- const batchedUpdates = builders.slice(0, i + 1).map((b) => b.update);
- const remaining = builders.slice(i + 1);
- builder.update = () => {
- batchedUpdates.forEach((update) => update());
- };
- // Put the current builder at the front, followed by any "wait" builders
- builders = [builder, ...remaining];
- break;
- }
- }
- if (!current || builders[0]?.options.interrupt === "immediate") {
- next();
- }
- }
- function addToQueue(builder) {
- builders.push(builder);
- microtask.render(processQueue);
- }
- class ViewTransitionBuilder {
- constructor(update, options = {}) {
- this.currentTarget = "root";
- this.targets = new Map();
- this.notifyReady = motionUtils.noop;
- this.readyPromise = new Promise((resolve) => {
- this.notifyReady = resolve;
- });
- this.update = update;
- this.options = {
- interrupt: "wait",
- ...options,
- };
- addToQueue(this);
- }
- get(selector) {
- this.currentTarget = selector;
- return this;
- }
- layout(keyframes, options) {
- this.updateTarget("layout", keyframes, options);
- return this;
- }
- new(keyframes, options) {
- this.updateTarget("new", keyframes, options);
- return this;
- }
- old(keyframes, options) {
- this.updateTarget("old", keyframes, options);
- return this;
- }
- enter(keyframes, options) {
- this.updateTarget("enter", keyframes, options);
- return this;
- }
- exit(keyframes, options) {
- this.updateTarget("exit", keyframes, options);
- return this;
- }
- crossfade(options) {
- this.updateTarget("enter", { opacity: 1 }, options);
- this.updateTarget("exit", { opacity: 0 }, options);
- return this;
- }
- updateTarget(target, keyframes, options = {}) {
- const { currentTarget, targets } = this;
- if (!targets.has(currentTarget)) {
- targets.set(currentTarget, {});
- }
- const targetData = targets.get(currentTarget);
- targetData[target] = { keyframes, options };
- }
- then(resolve, reject) {
- return this.readyPromise.then(resolve, reject);
- }
- }
- function animateView(update, defaultOptions = {}) {
- return new ViewTransitionBuilder(update, defaultOptions);
- }
- /**
- * @deprecated
- *
- * Import as `frame` instead.
- */
- const sync = frame;
- /**
- * @deprecated
- *
- * Use cancelFrame(callback) instead.
- */
- const cancelSync = stepsOrder.reduce((acc, key) => {
- acc[key] = (process) => cancelFrame(process);
- return acc;
- }, {});
- exports.GroupAnimation = GroupAnimation;
- exports.GroupAnimationWithThen = GroupAnimationWithThen;
- exports.MotionValue = MotionValue;
- exports.NativeAnimation = NativeAnimation;
- exports.ViewTransitionBuilder = ViewTransitionBuilder;
- exports.activeAnimations = activeAnimations;
- exports.animateView = animateView;
- exports.attachTimeline = attachTimeline;
- exports.calcGeneratorDuration = calcGeneratorDuration;
- exports.cancelFrame = cancelFrame;
- exports.cancelMicrotask = cancelMicrotask;
- exports.cancelSync = cancelSync;
- exports.collectMotionValues = collectMotionValues;
- exports.createGeneratorEasing = createGeneratorEasing;
- exports.createRenderBatcher = createRenderBatcher;
- exports.cubicBezierAsString = cubicBezierAsString;
- exports.frame = frame;
- exports.frameData = frameData;
- exports.frameSteps = frameSteps;
- exports.generateLinearEasing = generateLinearEasing;
- exports.getValueTransition = getValueTransition;
- exports.hover = hover;
- exports.isBezierDefinition = isBezierDefinition;
- exports.isDragActive = isDragActive;
- exports.isDragging = isDragging;
- exports.isGenerator = isGenerator;
- exports.isNodeOrChild = isNodeOrChild;
- exports.isPrimaryPointer = isPrimaryPointer;
- exports.isWaapiSupportedEasing = isWaapiSupportedEasing;
- exports.mapEasingToNativeEasing = mapEasingToNativeEasing;
- exports.maxGeneratorDuration = maxGeneratorDuration;
- exports.microtask = microtask;
- exports.motionValue = motionValue;
- exports.press = press;
- exports.recordStats = recordStats;
- exports.resolveElements = resolveElements;
- exports.setDragLock = setDragLock;
- exports.startWaapiAnimation = startWaapiAnimation;
- exports.statsBuffer = statsBuffer;
- exports.supportedWaapiEasing = supportedWaapiEasing;
- exports.supportsFlags = supportsFlags;
- exports.supportsLinearEasing = supportsLinearEasing;
- exports.supportsScrollTimeline = supportsScrollTimeline;
- exports.sync = sync;
- exports.time = time;
- }));
|