UNPKG

motion

Version:

An animation library for JavaScript and React.

1,618 lines (1,528 loc) 381 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var jsxRuntime = require('react/jsx-runtime'); var react = require('react'); function isAnimationControls(v) { return (v !== null && typeof v === "object" && typeof v.start === "function"); } function getValueState(visualElement) { const state = [{}, {}]; visualElement?.values.forEach((value, key) => { state[0][key] = value.get(); state[1][key] = value.getVelocity(); }); return state; } function resolveVariantFromProps(props, definition, custom, visualElement) { /** * If the variant definition is a function, resolve. */ if (typeof definition === "function") { const [current, velocity] = getValueState(visualElement); definition = definition(custom !== undefined ? custom : props.custom, current, velocity); } /** * If the variant definition is a variant label, or * the function returned a variant label, resolve. */ if (typeof definition === "string") { definition = props.variants && props.variants[definition]; } /** * At this point we've resolved both functions and variant labels, * but the resolved variant label might itself have been a function. * If so, resolve. This can only have returned a valid target object. */ if (typeof definition === "function") { const [current, velocity] = getValueState(visualElement); definition = definition(custom !== undefined ? custom : props.custom, current, velocity); } return definition; } function resolveVariant(visualElement, definition, custom) { const props = visualElement.getProps(); return resolveVariantFromProps(props, definition, custom !== undefined ? custom : props.custom, visualElement); } function addUniqueItem(arr, item) { if (arr.indexOf(item) === -1) arr.push(item); } function removeItem(arr, item) { const index = arr.indexOf(item); if (index > -1) arr.splice(index, 1); } let warning = () => { }; let invariant = () => { }; if (process.env.NODE_ENV !== "production") { warning = (check, message) => { if (!check && typeof console !== "undefined") { console.warn(message); } }; invariant = (check, message) => { if (!check) { throw new Error(message); } }; } const MotionGlobalConfig = { skipAnimations: false, useManualTiming: false, }; /*#__NO_SIDE_EFFECTS__*/ function memo(callback) { let result; return () => { if (result === undefined) result = callback(); return result; }; } /*#__NO_SIDE_EFFECTS__*/ const noop = (any) => any; /* Progress within given range Given a lower limit and an upper limit, we return the progress (expressed as a number 0-1) represented by the given value, and limit that progress to within 0-1. @param [number]: Lower limit @param [number]: Upper limit @param [number]: Value to find progress within given range @return [number]: Progress of value within range as expressed 0-1 */ /*#__NO_SIDE_EFFECTS__*/ const progress = (from, to, value) => { const toFromDifference = to - from; return toFromDifference === 0 ? 1 : (value - from) / toFromDifference; }; class SubscriptionManager { constructor() { this.subscriptions = []; } add(handler) { addUniqueItem(this.subscriptions, handler); return () => removeItem(this.subscriptions, handler); } notify(a, b, c) { const numSubscriptions = this.subscriptions.length; if (!numSubscriptions) return; if (numSubscriptions === 1) { /** * If there's only a single handler we can just call it without invoking a loop. */ this.subscriptions[0](a, b, c); } else { for (let i = 0; i < numSubscriptions; i++) { /** * Check whether the handler exists before firing as it's possible * the subscriptions were modified during this loop running. */ const handler = this.subscriptions[i]; handler && handler(a, b, c); } } } getSize() { return this.subscriptions.length; } clear() { this.subscriptions.length = 0; } } /** * Converts seconds to milliseconds * * @param seconds - Time in seconds. * @return milliseconds - Converted time in milliseconds. */ /*#__NO_SIDE_EFFECTS__*/ const secondsToMilliseconds = (seconds) => seconds * 1000; /*#__NO_SIDE_EFFECTS__*/ const millisecondsToSeconds = (milliseconds) => milliseconds / 1000; /* Convert velocity into velocity per second @param [number]: Unit per frame @param [number]: Frame duration in ms */ function velocityPerSecond(velocity, frameDuration) { return frameDuration ? velocity * (1000 / frameDuration) : 0; } const warned = new Set(); function warnOnce(condition, message, element) { if (condition || warned.has(message)) return; console.warn(message); if (element) console.warn(element); warned.add(message); } const supportsScrollTimeline = /* @__PURE__ */ 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 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 = 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; const animation = element.animate(keyframeOptions, { delay, duration, easing: !Array.isArray(easing) ? easing : "linear", fill: "both", iterations: repeat + 1, direction: repeatType === "reverse" ? "alternate" : "normal", pseudoElement, }); return animation; } function isGenerator(type) { return typeof type === "function" && "applyToOptions" in type; } 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: 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 = performance.now(); runNextFrame = false; { 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 : 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 || 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; } /** * 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)); }; /** * `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.6.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) { if (process.env.NODE_ENV !== "production") { 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 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() { 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 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); } /** * Generate a list of every possible transform key. */ const transformPropOrder = [ "transformPerspective", "x", "y", "z", "translateX", "translateY", "translateZ", "scale", "scaleX", "scaleY", "rotate", "rotateX", "rotateY", "rotateZ", "skew", "skewX", "skewY", ]; /** * A quick lookup for transform props. */ const transformProps = new Set(transformPropOrder); const positionalKeys = new Set([ "width", "height", "top", "left", "right", "bottom", ...transformPropOrder, ]); const isKeyframesTarget = (v) => { return Array.isArray(v); }; const isCustomValue = (v) => { return Boolean(v && typeof v === "object" && v.mix && v.toValue); }; const resolveFinalValueInKeyframes = (v) => { // TODO maybe throw if v.length - 1 is placeholder token? return isKeyframesTarget(v) ? v[v.length - 1] || 0 : v; }; /** * Set VisualElement's MotionValue, creating a new MotionValue for it if * it doesn't exist. */ function setMotionValue(visualElement, key, value) { if (visualElement.hasValue(key)) { visualElement.getValue(key).set(value); } else { visualElement.addValue(key, motionValue(value)); } } function setTarget(visualElement, definition) { const resolved = resolveVariant(visualElement, definition); let { transitionEnd = {}, transition = {}, ...target } = resolved || {}; target = { ...target, ...transitionEnd }; for (const key in target) { const value = resolveFinalValueInKeyframes(target[key]); setMotionValue(visualElement, key, value); } } const isMotionValue = (value) => Boolean(value && value.getVelocity); function isWillChangeMotionValue(value) { return Boolean(isMotionValue(value) && value.add); } function addValueToWillChange(visualElement, key) { const willChange = visualElement.getValue("willChange"); /** * It could be that a user has set willChange to a regular MotionValue, * in which case we can't add the value to it. */ if (isWillChangeMotionValue(willChange)) { return willChange.add(key); } } /** * Convert camelCase to dash-case properties. */ const camelToDash = (str) => str.replace(/([a-z])([A-Z])/gu, "$1-$2").toLowerCase(); const optimizedAppearDataId = "framerAppearId"; const optimizedAppearDataAttribute = "data-" + camelToDash(optimizedAppearDataId); function getOptimisedAppearId(visualElement) { return visualElement.props[optimizedAppearDataAttribute]; } /* Bezier function generator This has been modified from Gaëtan Renaudeau's BezierEasing https://github.com/gre/bezier-easing/blob/master/src/index.js https://github.com/gre/bezier-easing/blob/master/LICENSE I've removed the newtonRaphsonIterate algo because in benchmarking it wasn't noticiably faster than binarySubdivision, indeed removing it usually improved times, depending on the curve. I also removed the lookup table, as for the added bundle size and loop we're only cutting ~4 or so subdivision iterations. I bumped the max iterations up to 12 to compensate and this still tended to be faster for no perceivable loss in accuracy. Usage const easeOut = cubicBezier(.17,.67,.83,.67); const x = easeOut(0.5); // returns 0.627... */ // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. const calcBezier = (t, a1, a2) => (((1.0 - 3.0 * a2 + 3.0 * a1) * t + (3.0 * a2 - 6.0 * a1)) * t + 3.0 * a1) * t; const subdivisionPrecision = 0.0000001; const subdivisionMaxIterations = 12; function binarySubdivide(x, lowerBound, upperBound, mX1, mX2) { let currentX; let currentT; let i = 0; do { currentT = lowerBound + (upperBound - lowerBound) / 2.0; currentX = calcBezier(currentT, mX1, mX2) - x; if (currentX > 0.0) { upperBound = currentT; } else { lowerBound = currentT; } } while (Math.abs(currentX) > subdivisionPrecision && ++i < subdivisionMaxIterations); return currentT; } function cubicBezier(mX1, mY1, mX2, mY2) { // If this is a linear gradient, return linear easing if (mX1 === mY1 && mX2 === mY2) return noop; const getTForX = (aX) => binarySubdivide(aX, 0, 1, mX1, mX2); // If animation is at start/end, return t without easing return (t) => t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2); } // Accepts an easing function and returns a new one that outputs mirrored values for // the second half of the animation. Turns easeIn into easeInOut. const mirrorEasing = (easing) => (p) => p <= 0.5 ? easing(2 * p) / 2 : (2 - easing(2 * (1 - p))) / 2; // Accepts an easing function and returns a new one that outputs reversed values. // Turns easeIn into easeOut. const reverseEasing = (easing) => (p) => 1 - easing(1 - p); const backOut = /*@__PURE__*/ cubicBezier(0.33, 1.53, 0.69, 0.99); const backIn = /*@__PURE__*/ reverseEasing(backOut); const backInOut = /*@__PURE__*/ mirrorEasing(backIn); const anticipate = (p) => (p *= 2) < 1 ? 0.5 * backIn(p) : 0.5 * (2 - Math.pow(2, -10 * (p - 1))); const circIn = (p) => 1 - Math.sin(Math.acos(p)); const circOut = reverseEasing(circIn); const circInOut = mirrorEasing(circIn); /** * Check if the value is a zero value string like "0px" or "0%" */ const isZeroValueString = (v) => /^0[^.\s]+$/u.test(v); function isNone(value) { if (typeof value === "number") { return value === 0; } else if (value !== null) { return value === "none" || value === "0" || isZeroValueString(value); } else { return true; } } const clamp = (min, max, v) => { if (v > max) return max; if (v < min) return min; return v; }; const number = { test: (v) => typeof v === "number", parse: parseFloat, transform: (v) => v, }; const alpha = { ...number, transform: (v) => clamp(0, 1, v), }; const scale = { ...number, default: 1, }; // If this number is a decimal, make it just five decimal places // to avoid exponents const sanitize = (v) => Math.round(v * 100000) / 100000; const floatRegex = /-?(?:\d+(?:\.\d+)?|\.\d+)/gu; function isNullish(v) { return v == null; } const singleColorRegex = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))$/iu; /** * Returns true if the provided string is a color, ie rgba(0,0,0,0) or #000, * but false if a number or multiple colors */ const isColorString = (type, testProp) => (v) => { return Boolean((typeof v === "string" && singleColorRegex.test(v) && v.startsWith(type)) || (testProp && !isNullish(v) && Object.prototype.hasOwnProperty.call(v, testProp))); }; const splitColor = (aName, bName, cName) => (v) => { if (typeof v !== "string") return v; const [a, b, c, alpha] = v.match(floatRegex); return { [aName]: parseFloat(a), [bName]: parseFloat(b), [cName]: parseFloat(c), alpha: alpha !== undefined ? parseFloat(alpha) : 1, }; }; const clampRgbUnit = (v) => clamp(0, 255, v); const rgbUnit = { ...number, transform: (v) => Math.round(clampRgbUnit(v)), }; const rgba = { test: /*@__PURE__*/ isColorString("rgb", "red"), parse: /*@__PURE__*/ splitColor("red", "green", "blue"), transform: ({ red, green, blue, alpha: alpha$1 = 1 }) => "rgba(" + rgbUnit.transform(red) + ", " + rgbUnit.transform(green) + ", " + rgbUnit.transform(blue) + ", " + sanitize(alpha.transform(alpha$1)) + ")", }; function parseHex(v) { let r = ""; let g = ""; let b = ""; let a = ""; // If we have 6 characters, ie #FF0000 if (v.length > 5) { r = v.substring(1, 3); g = v.substring(3, 5); b = v.substring(5, 7); a = v.substring(7, 9); // Or we have 3 characters, ie #F00 } else { r = v.substring(1, 2); g = v.substring(2, 3); b = v.substring(3, 4); a = v.substring(4, 5); r += r; g += g; b += b; a += a; } return { red: parseInt(r, 16), green: parseInt(g, 16), blue: parseInt(b, 16), alpha: a ? parseInt(a, 16) / 255 : 1, }; } const hex = { test: /*@__PURE__*/ isColorString("#"), parse: parseHex, transform: rgba.transform, }; const createUnitType = (unit) => ({ test: (v) => typeof v === "string" && v.endsWith(unit) && v.split(" ").length === 1, parse: parseFloat, transform: (v) => `${v}${unit}`, }); const degrees = /*@__PURE__*/ createUnitType("deg"); const percent = /*@__PURE__*/ createUnitType("%"); const px = /*@__PURE__*/ createUnitType("px"); const vh = /*@__PURE__*/ createUnitType("vh"); const vw = /*@__PURE__*/ createUnitType("vw"); const progressPercentage = { ...percent, parse: (v) => percent.parse(v) / 100, transform: (v) => percent.transform(v * 100), }; const hsla = { test: /*@__PURE__*/ isColorString("hsl", "hue"), parse: /*@__PURE__*/ splitColor("hue", "saturation", "lightness"), transform: ({ hue, saturation, lightness, alpha: alpha$1 = 1 }) => { return ("hsla(" + Math.round(hue) + ", " + percent.transform(sanitize(saturation)) + ", " + percent.transform(sanitize(lightness)) + ", " + sanitize(alpha.transform(alpha$1)) + ")"); }, }; const color = { test: (v) => rgba.test(v) || hex.test(v) || hsla.test(v), parse: (v) => { if (rgba.test(v)) { return rgba.parse(v); } else if (hsla.test(v)) { return hsla.parse(v); } else { return hex.parse(v); } }, transform: (v) => { return typeof v === "string" ? v : v.hasOwnProperty("red") ? rgba.transform(v) : hsla.transform(v); }, }; const colorRegex = /(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))/giu; function test(v) { return (isNaN(v) && typeof v === "string" && (v.match(floatRegex)?.length || 0) + (v.match(colorRegex)?.length || 0) > 0); } const NUMBER_TOKEN = "number"; const COLOR_TOKEN = "color"; const VAR_TOKEN = "var"; const VAR_FUNCTION_TOKEN = "var("; const SPLIT_TOKEN = "${}"; // this regex consists of the `singleCssVariableRegex|rgbHSLValueRegex|digitRegex` const complexRegex = /var\s*\(\s*--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)|#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\)|-?(?:\d+(?:\.\d+)?|\.\d+)/giu; function analyseComplexValue(value) { const originalValue = value.toString(); const values = []; const indexes = { color: [], number: [], var: [], }; const types = []; let i = 0; const tokenised = originalValue.replace(complexRegex, (parsedValue) => { if (color.test(parsedValue)) { indexes.color.push(i); types.push(COLOR_TOKEN); values.push(color.parse(parsedValue)); } else if (parsedValue.startsWith(VAR_FUNCTION_TOKEN)) { indexes.var.push(i); types.push(VAR_TOKEN); values.push(parsedValue); } else { indexes.number.push(i); types.push(NUMBER_TOKEN); values.push(parseFloat(parsedValue)); } ++i; return SPLIT_TOKEN; }); const split = tokenised.split(SPLIT_TOKEN); return { values, split, indexes, types }; } function parseComplexValue(v) { return analyseComplexValue(v).values; } function createTransformer(source) { const { split, types } = analyseComplexValue(source); const numSections = split.length; return (v) => { let output = ""; for (let i = 0; i < numSections; i++) { output += split[i]; if (v[i] !== undefined) { const type = types[i]; if (type === NUMBER_TOKEN) { output += sanitize(v[i]); } else if (type === COLOR_TOKEN) { output += color.transform(v[i]); } else { output += v[i]; } } } return output; }; } const convertNumbersToZero = (v) => typeof v === "number" ? 0 : v; function getAnimatableNone$1(v) { const parsed = parseComplexValue(v); const transformer = createTransformer(v); return transformer(parsed.map(convertNumbersToZero)); } const complex = { test, parse: parseComplexValue, createTransformer, getAnimatableNone: getAnimatableNone$1, }; /** * Properties that should default to 1 or 100% */ const maxDefaults = new Set(["brightness", "contrast", "saturate", "opacity"]); function applyDefaultFilter(v) { const [name, value] = v.slice(0, -1).split("("); if (name === "drop-shadow") return v; const [number] = value.match(floatRegex) || []; if (!number) return v; const unit = value.replace(number, ""); let defaultValue = maxDefaults.has(name) ? 1 : 0; if (number !== value) defaultValue *= 100; return name + "(" + defaultValue + unit + ")"; } const functionRegex =