UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

1,386 lines (1,319 loc) 525 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react')) : typeof define === 'function' && define.amd ? define(['exports', 'react'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Motion = {}, global.React)); })(this, (function (exports, React$1) { 'use strict'; function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React$1); 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); } function createDOMMotionComponentProxy(componentFactory) { if (typeof Proxy === "undefined") { return componentFactory; } /** * A cache of generated `motion` components, e.g `motion.div`, `motion.input` etc. * Rather than generating them anew every render. */ const componentCache = new Map(); const deprecatedFactoryFunction = (...args) => { { warnOnce(false, "motion() is deprecated. Use motion.create() instead."); } return componentFactory(...args); }; return new Proxy(deprecatedFactoryFunction, { /** * Called when `motion` is referenced with a prop: `motion.div`, `motion.input` etc. * The prop name is passed through as `key` and we can use that to generate a `motion` * DOM component with that name. */ get: (_target, key) => { if (key === "create") return componentFactory; /** * If this element doesn't exist in the component cache, create it and cache. */ if (!componentCache.has(key)) { componentCache.set(key, componentFactory(key)); } return componentCache.get(key); }, }); } function isAnimationControls(v) { return (v !== null && typeof v === "object" && typeof v.start === "function"); } const isKeyframesTarget = (v) => { return Array.isArray(v); }; function shallowCompare(next, prev) { if (!Array.isArray(prev)) return false; const prevLength = prev.length; if (prevLength !== next.length) return false; for (let i = 0; i < prevLength; i++) { if (prev[i] !== next[i]) return false; } return true; } /** * Decides if the supplied variable is variant label */ function isVariantLabel(v) { return typeof v === "string" || Array.isArray(v); } function getValueState(visualElement) { const state = [{}, {}]; visualElement === null || visualElement === void 0 ? void 0 : 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); } const variantPriorityOrder = [ "animate", "whileInView", "whileFocus", "whileHover", "whileTap", "whileDrag", "exit", ]; const variantProps = ["initial", ...variantPriorityOrder]; /** * 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); /** * Converts seconds to milliseconds * * @param seconds - Time in seconds. * @return milliseconds - Converted time in milliseconds. */ const secondsToMilliseconds = (seconds) => seconds * 1000; const millisecondsToSeconds = (milliseconds) => milliseconds / 1000; const underDampedSpring = { type: "spring", stiffness: 500, damping: 25, restSpeed: 10, }; const criticallyDampedSpring = (target) => ({ type: "spring", stiffness: 550, damping: target === 0 ? 2 * Math.sqrt(550) : 30, restSpeed: 10, }); const keyframesTransition = { type: "keyframes", duration: 0.8, }; /** * Default easing curve is a slightly shallower version of * the default browser easing curve. */ const ease = { type: "keyframes", ease: [0.25, 0.1, 0.35, 1], duration: 0.3, }; const getDefaultTransition = (valueKey, { keyframes }) => { if (keyframes.length > 2) { return keyframesTransition; } else if (transformProps.has(valueKey)) { return valueKey.startsWith("scale") ? criticallyDampedSpring(keyframes[1]) : underDampedSpring; } return ease; }; function getValueTransition$1(transition, key) { return transition ? transition[key] || transition["default"] || transition : undefined; } const MotionGlobalConfig = { skipAnimations: false, useManualTiming: false, }; const instantAnimationState = { current: false, }; 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 noop = (any) => any; function createRenderStep(runNextFrame) { /** * 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, }; function triggerCallback(callback) { if (toKeepAlive.has(callback)) { step.schedule(callback); runNextFrame(); } 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]; // Clear the next frame queue nextFrame.clear(); // Execute this frame thisFrame.forEach(triggerCallback); isProcessing = false; if (flushNextFrame) { flushNextFrame = false; step.process(frameData); } }, }; return step; } const stepsOrder = [ "read", // Read "resolveKeyframes", // Write/Read/Write/Read "update", // Compute "preRender", // Compute "render", // Write "postRender", // Compute ]; const maxElapsed$1 = 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); return acc; }, {}); const { read, resolveKeyframes, update, preRender, render, postRender } = steps; const processBatch = () => { const timestamp = MotionGlobalConfig.useManualTiming ? state.timestamp : performance.now(); runNextFrame = false; state.delta = useDefaultElapsed ? 1000 / 60 : Math.max(Math.min(timestamp - state.timestamp, maxElapsed$1), 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, } = createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true); /* 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; } } exports.warning = noop; exports.invariant = noop; { exports.warning = (check, message) => { if (!check && typeof console !== "undefined") { console.warn(message); } }; exports.invariant = (check, message) => { if (!check) { throw new Error(message); } }; } /** * Check if value is a numerical string, ie a string that is purely a number eg "100" or "-100.1" */ const isNumericalString = (v) => /^-?(?:\d+(?:\.\d+)?|\.\d+)$/u.test(v); const checkStringStartsWith = (token) => (key) => typeof key === "string" && key.startsWith(token); const isCSSVariableName = /*@__PURE__*/ checkStringStartsWith("--"); const startsAsVariableToken = /*@__PURE__*/ checkStringStartsWith("var(--"); const isCSSVariableToken = (value) => { const startsWithToken = startsAsVariableToken(value); if (!startsWithToken) return false; // Ensure any comments are stripped from the value as this can harm performance of the regex. return singleCssVariableRegex.test(value.split("/*")[0].trim()); }; const singleCssVariableRegex = /var\(--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)$/iu; /** * Parse Framer's special CSS variable format into a CSS token and a fallback. * * ``` * `var(--foo, #fff)` => [`--foo`, '#fff'] * ``` * * @param current */ const splitCSSVariableRegex = // eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive, as it can match a lot of words /^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u; function parseCSSVariable(current) { const match = splitCSSVariableRegex.exec(current); if (!match) return [,]; const [, token1, token2, fallback] = match; return [`--${token1 !== null && token1 !== void 0 ? token1 : token2}`, fallback]; } const maxDepth = 4; function getVariableValue(current, element, depth = 1) { exports.invariant(depth <= maxDepth, `Max CSS variable fallback depth detected in property "${current}". This may indicate a circular fallback dependency.`); const [token, fallback] = parseCSSVariable(current); // No CSS variable detected if (!token) return; // Attempt to read this CSS variable off the element const resolved = window.getComputedStyle(element).getPropertyValue(token); if (resolved) { const trimmed = resolved.trim(); return isNumericalString(trimmed) ? parseFloat(trimmed) : trimmed; } return isCSSVariableToken(fallback) ? getVariableValue(fallback, element, depth + 1) : fallback; } 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, }; 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 positionalKeys = new Set([ "width", "height", "top", "left", "right", "bottom", "x", "y", "translateX", "translateY", ]); const isNumOrPxType = (v) => v === number || v === px; const getPosFromMatrix = (matrix, pos) => parseFloat(matrix.split(", ")[pos]); const getTranslateFromMatrix = (pos2, pos3) => (_bbox, { transform }) => { if (transform === "none" || !transform) return 0; const matrix3d = transform.match(/^matrix3d\((.+)\)$/u); if (matrix3d) { return getPosFromMatrix(matrix3d[1], pos3); } else { const matrix = transform.match(/^matrix\((.+)\)$/u); if (matrix) { return getPosFromMatrix(matrix[1], pos2); } else { return 0; } } }; const transformKeys = new Set(["x", "y", "z"]); const nonTranslationalTransformKeys = transformPropOrder.filter((key) => !transformKeys.has(key)); function removeNonTranslationalTransform(visualElement) { const removedTransforms = []; nonTranslationalTransformKeys.forEach((key) => { const value = visualElement.getValue(key); if (value !== undefined) { removedTransforms.push([key, value.get()]); value.set(key.startsWith("scale") ? 1 : 0); } }); return removedTransforms; } const positionalValues = { // Dimensions width: ({ x }, { paddingLeft = "0", paddingRight = "0" }) => x.max - x.min - parseFloat(paddingLeft) - parseFloat(paddingRight), height: ({ y }, { paddingTop = "0", paddingBottom = "0" }) => y.max - y.min - parseFloat(paddingTop) - parseFloat(paddingBottom), top: (_bbox, { top }) => parseFloat(top), left: (_bbox, { left }) => parseFloat(left), bottom: ({ y }, { top }) => parseFloat(top) + (y.max - y.min), right: ({ x }, { left }) => parseFloat(left) + (x.max - x.min), // Transform x: getTranslateFromMatrix(4, 13), y: getTranslateFromMatrix(5, 14), }; // Alias translate longform names positionalValues.translateX = positionalValues.x; positionalValues.translateY = positionalValues.y; /** * Tests a provided value against a ValueType */ const testValueType = (v) => (type) => type.test(v); /** * ValueType for "auto" */ const auto = { test: (v) => v === "auto", parse: (v) => v, }; /** * A list of value types commonly used for dimensions */ const dimensionValueTypes = [number, px, percent, degrees, vw, vh, auto]; /** * Tests a dimensional value against the list of dimension ValueTypes */ const findDimensionValueType = (v) => dimensionValueTypes.find(testValueType(v)); const toResolve = new Set(); let isScheduled = false; let anyNeedsMeasurement = false; function measureAllKeyframes() { if (anyNeedsMeasurement) { const resolversToMeasure = Array.from(toResolve).filter((resolver) => resolver.needsMeasurement); const elementsToMeasure = new Set(resolversToMeasure.map((resolver) => resolver.element)); const transformsToRestore = new Map(); /** * Write pass * If we're measuring elements we want to remove bounding box-changing transforms. */ elementsToMeasure.forEach((element) => { const removedTransforms = removeNonTranslationalTransform(element); if (!removedTransforms.length) return; transformsToRestore.set(element, removedTransforms); element.render(); }); // Read resolversToMeasure.forEach((resolver) => resolver.measureInitialState()); // Write elementsToMeasure.forEach((element) => { element.render(); const restore = transformsToRestore.get(element); if (restore) { restore.forEach(([key, value]) => { var _a; (_a = element.getValue(key)) === null || _a === void 0 ? void 0 : _a.set(value); }); } }); // Read resolversToMeasure.forEach((resolver) => resolver.measureEndState()); // Write resolversToMeasure.forEach((resolver) => { if (resolver.suspendedScrollY !== undefined) { window.scrollTo(0, resolver.suspendedScrollY); } }); } anyNeedsMeasurement = false; isScheduled = false; toResolve.forEach((resolver) => resolver.complete()); toResolve.clear(); } function readAllKeyframes() { toResolve.forEach((resolver) => { resolver.readKeyframes(); if (resolver.needsMeasurement) { anyNeedsMeasurement = true; } }); } function flushKeyframeResolvers() { readAllKeyframes(); measureAllKeyframes(); } class KeyframeResolver { constructor(unresolvedKeyframes, onComplete, name, motionValue, element, isAsync = false) { /** * Track whether this resolver has completed. Once complete, it never * needs to attempt keyframe resolution again. */ this.isComplete = false; /** * Track whether this resolver is async. If it is, it'll be added to the * resolver queue and flushed in the next frame. Resolvers that aren't going * to trigger read/write thrashing don't need to be async. */ this.isAsync = false; /** * Track whether this resolver needs to perform a measurement * to resolve its keyframes. */ this.needsMeasurement = false; /** * Track whether this resolver is currently scheduled to resolve * to allow it to be cancelled and resumed externally. */ this.isScheduled = false; this.unresolvedKeyframes = [...unresolvedKeyframes]; this.onComplete = onComplete; this.name = name; this.motionValue = motionValue; this.element = element; this.isAsync = isAsync; } scheduleResolve() { this.isScheduled = true; if (this.isAsync) { toResolve.add(this); if (!isScheduled) { isScheduled = true; frame.read(readAllKeyframes); frame.resolveKeyframes(measureAllKeyframes); } } else { this.readKeyframes(); this.complete(); } } readKeyframes() { const { unresolvedKeyframes, name, element, motionValue } = this; /** * If a keyframe is null, we hydrate it either by reading it from * the instance, or propagating from previous keyframes. */ for (let i = 0; i < unresolvedKeyframes.length; i++) { if (unresolvedKeyframes[i] === null) { /** * If the first keyframe is null, we need to find its value by sampling the element */ if (i === 0) { const currentValue = motionValue === null || motionValue === void 0 ? void 0 : motionValue.get(); const finalKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1]; if (currentValue !== undefined) { unresolvedKeyframes[0] = currentValue; } else if (element && name) { const valueAsRead = element.readValue(name, finalKeyframe); if (valueAsRead !== undefined && valueAsRead !== null) { unresolvedKeyframes[0] = valueAsRead; } } if (unresolvedKeyframes[0] === undefined) { unresolvedKeyframes[0] = finalKeyframe; } if (motionValue && currentValue === undefined) { motionValue.set(unresolvedKeyframes[0]); } } else { unresolvedKeyframes[i] = unresolvedKeyframes[i - 1]; } } } } setFinalKeyframe() { } measureInitialState() { } renderEndStyles() { } measureEndState() { } complete() { this.isComplete = true; this.onComplete(this.unresolvedKeyframes, this.finalKeyframe); toResolve.delete(this); } cancel() { if (!this.isComplete) { this.isScheduled = false; toResolve.delete(this); } } resume() { if (!this.isComplete) this.scheduleResolve(); } } // 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 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) { var _a, _b; return (isNaN(v) && typeof v === "string" && (((_a = v.match(floatRegex)) === null || _a === void 0 ? void 0 : _a.length) || 0) + (((_b = v.match(colorRegex)) === null || _b === void 0 ? void 0 : _b.length) || 0) > 0); } const NUMBER_TOKEN = "number"; const COLOR_TOKEN = "color"; const VAR_TOKEN = "var"; const VAR_FUNCTION_TOKEN = "var("; const SPLIT_TOKEN = "${}"; // this regex consists of the `singleCssVariableRegex|rgbHSLValueRegex|digitRegex` const complexRegex = /var\s*\(\s*--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)|#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\)|-?(?:\d+(?:\.\d+)?|\.\d+)/giu; function analyseComplexValue(value) { const originalValue = value.toString(); const values = []; const indexes = { color: [], number: [], var: [], }; const types = []; let i = 0; const tokenised = originalValue.replace(complexRegex, (parsedValue) => { if (color.test(parsedValue)) { indexes.color.push(i); types.push(COLOR_TOKEN); values.push(color.parse(parsedValue)); } else if (parsedValue.startsWith(VAR_FUNCTION_TOKEN)) { indexes.var.push(i); types.push(VAR_TOKEN); values.push(parsedValue); } else { indexes.number.push(i); types.push(NUMBER_TOKEN); values.push(parseFloat(parsedValue)); } ++i; return SPLIT_TOKEN; }); const split = tokenised.split(SPLIT_TOKEN); return { values, split, indexes, types }; } function parseComplexValue(v) { return analyseComplexValue(v).values; } function createTransformer(source) { const { split, types } = analyseComplexValue(source); const numSections = split.length; return (v) => { let output = ""; for (let i = 0; i < numSections; i++) { output += split[i]; if (v[i] !== undefined) { const type = types[i]; if (type === NUMBER_TOKEN) { output += sanitize(v[i]); } else if (type === COLOR_TOKEN) { output += color.transform(v[i]); } else { output += v[i]; } } } return output; }; } const convertNumbersToZero = (v) => typeof v === "number" ? 0 : v; function getAnimatableNone$1(v) { const parsed = parseComplexValue(v); const transformer = createTransformer(v); return transformer(parsed.map(convertNumbersToZero)); } const complex = { test, parse: parseComplexValue, createTransformer, getAnimatableNone: getAnimatableNone$1, }; /** * Properties that should default to 1 or 100% */ const maxDefaults = new Set(["brightness", "contrast", "saturate", "opacity"]); function applyDefaultFilter(v) { const [name, value] = v.slice(0, -1).split("("); if (name === "drop-shadow") return v; const [number] = value.match(floatRegex) || []; if (!number) return v; const unit = value.replace(number, ""); let defaultValue = maxDefaults.has(name) ? 1 : 0; if (number !== value) defaultValue *= 100; return name + "(" + defaultValue + unit + ")"; } const functionRegex = /\b([a-z-]*)\(.*?\)/gu; const filter = { ...complex, getAnimatableNone: (v) => { const functions = v.match(functionRegex); return functions ? functions.map(applyDefaultFilter).join(" ") : v; }, }; const browserNumberValueTypes = { // Border props borderWidth: px, borderTopWidth: px, borderRightWidth: px, borderBottomWidth: px, borderLeftWidth: px, borderRadius: px, radius: px, borderTopLeftRadius: px, borderTopRightRadius: px, borderBottomRightRadius: px, borderBottomLeftRadius: px, // Positioning props width: px, maxWidth: px, height: px, maxHeight: px, top: px, right: px, bottom: px, left: px, // Spacing props padding: px, paddingTop: px, paddingRight: px, paddingBottom: px, paddingLeft: px, margin: px, marginTop: px, marginRight: px, marginBottom: px, marginLeft: px, // Misc backgroundPositionX: px, backgroundPositionY: px, }; const transformValueTypes = { rotate: degrees, rotateX: degrees, rotateY: degrees, rotateZ: degrees, scale, scaleX: scale, scaleY: scale, scaleZ: scale, skew: degrees, skewX: degrees, skewY: degrees, distance: px, translateX: px, translateY: px, translateZ: px, x: px, y: px, z: px, perspective: px, transformPerspective: px, opacity: alpha, originX: progressPercentage, originY: progressPercentage, originZ: px, }; const int = { ...number, transform: Math.round, }; const numberValueTypes = { ...browserNumberValueTypes, ...transformValueTypes, zIndex: int, size: px, // SVG fillOpacity: alpha, strokeOpacity: alpha, numOctaves: int, }; /** * A map of default value types for common values */ const defaultValueTypes = { ...numberValueTypes, // Color props color, backgroundColor: color, outlineColor: color, fill: color, stroke: color, // Border props borderColor: color, borderTopColor: color, borderRightColor: color, borderBottomColor: color, borderLeftColor: color, filter, WebkitFilter: filter, }; /** * Gets the default ValueType for the provided value key */ const getDefaultValueType = (key) => defaultValueTypes[key]; function getAnimatableNone(key, value) { let defaultValueType = getDefaultValueType(key); if (defaultValueType !== filter) defaultValueType = complex; // If value is not recognised as animatable, ie "none", create an animatable version origin based on the target return defaultValueType.getAnimatableNone ? defaultValueType.getAnimatableNone(value) : undefined; } /** * If we encounter keyframes like "none" or "0" and we also have keyframes like * "#fff" or "200px 200px" we want to find a keyframe to serve as a template for * the "none" keyframes. In this case "#fff" or "200px 200px" - then these get turned into * zero equivalents, i.e. "#fff0" or "0px 0px". */ const invalidTemplates = new Set(["auto", "none", "0"]); function makeNoneKeyframesAnimatable(unresolvedKeyframes, noneKeyframeIndexes, name) { let i = 0; let animatableTemplate = undefined; while (i < unresolvedKeyframes.length && !animatableTemplate) { const keyframe = unresolvedKeyframes[i]; if (typeof keyframe === "string" && !invalidTemplates.has(keyframe) && analyseComplexValue(keyframe).values.length) { animatableTemplate = unresolvedKeyframes[i]; } i++; } if (animatableTemplate && name) { for (const noneIndex of noneKeyframeIndexes) { unresolvedKeyframes[noneIndex] = getAnimatableNone(name, animatableTemplate); } } } class DOMKeyframesResolver extends KeyframeResolver { constructor(unresolvedKeyframes, onComplete, name, motionValue, element) { super(unresolvedKeyframes, onComplete, name, motionValue, element, true); } readKeyframes() { const { unresolvedKeyframes, element, name } = this; if (!element || !element.current) return; super.readKeyframes(); /** * If any keyframe is a CSS variable, we need to find its value by sampling the element */ for (let i = 0; i < unresolvedKeyframes.length; i++) { let keyframe = unresolvedKeyframes[i]; if (typeof keyframe === "string") { keyframe = keyframe.trim(); if (isCSSVariableToken(keyframe)) { const resolved = getVariableValue(keyframe, element.current); if (resolved !== undefined) { unresolvedKeyframes[i] = resolved; } if (i === unresolvedKeyframes.length - 1) { this.finalKeyframe = keyframe; } } } } /** * Resolve "none" values. We do this potentially twice - once before and once after measuring keyframes. * This could be seen as inefficient but it's a trade-off to avoid measurements in more situations, which * have a far bigger performance impact. */ this.resolveNoneKeyframes(); /** * Check to see if unit type has changed. If so schedule jobs that will * temporarily set styles to the destination keyframes. * Skip if we have more than two keyframes or this isn't a positional value. * TODO: We can throw if there are multiple keyframes and the value type changes. */ if (!positionalKeys.has(name) || unresolvedKeyframes.length !== 2) { return; } const [origin, target] = unresolvedKeyframes; const originType = findDimensionValueType(origin); const targetType = findDimensionValueType(target); /** * Either we don't recognise these value types or we can animate between them. */ if (originType === targetType) return; /** * If both values are numbers or pixels, we can animate between them by * converting them to numbers. */ if (isNumOrPxType(originType) && isNumOrPxType(targetType)) { for (let i = 0; i < unresolvedKeyframes.length; i++) { const value = unresolvedKeyframes[i]; if (typeof value === "string") { unresolvedKeyframes[i] = parseFloat(value); } } } else { /** * Else, the only way to resolve this is by measuring the element. */ this.needsMeasurement = true; } } resolveNoneKeyframes() { const { unresolvedKeyframes, name } = this; const noneKeyframeIndexes = []; for (let i = 0; i < unresolvedKeyframes.length; i++) { if (isNone(unresolvedKeyframes[i])) { noneKeyframeIndexes.push(i); } } if (noneKeyframeIndexes.length) { makeNoneKeyframesAnimatable(unresolvedKeyframes, noneKeyframeIndexes, name); } } measureInitialState() { const { element, unresolvedKeyframes, name } = this; if (!element || !element.current) return; if (name === "height") { this.suspendedScrollY = window.pageYOffset; } this.measuredOrigin = positionalValues[name](element.measureViewportBox(), window.getComputedStyle(element.current)); unresolvedKeyframes[0] = this.measuredOrigin; // Set final key frame to measure after next render const measureKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1]; if (measureKeyframe !== undefined) { element.getValue(name, measureKeyframe).jump(measureKeyframe, false); } } measureEndState() { var _a; const { element, name, unresolvedKeyframes } = this; if (!element || !element.current) return; const value = element.getValue(name); value && value.jump(this.measuredOrigin, false); const finalKeyframeIndex = unresolvedKeyframes.length - 1; const finalKeyframe = unresolvedKeyframes[finalKeyframeIndex]; unresolvedKeyframes[finalKeyframeIndex] = positionalValues[name](element.measureViewportBox(), window.getComputedStyle(element.current)); if (finalKeyframe !== null && this.finalKeyframe === undefined) { this.finalKeyframe = finalKeyframe; } // If we removed transform values, reapply them before the next render if ((_a = this.removedTransforms) === null || _a === void 0 ? void 0 : _a.length) { this.removedTransforms.forEach(([unsetTransformName, unsetTransformValue]) => { element .getValue(unsetTransformName) .set(unsetTransformValue); }); } this.resolveNoneKeyframes(); } } function isGenerator(type) { return typeof type === "function"; } 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); }, }; /** * Check if a value is animatable. Examples: * * ✅: 100, "100px", "#fff" * ❌: "block", "url(2.jpg)" * @param value * * @internal */