UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

1,434 lines (1,373 loc) • 207 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var motionDom = require('motion-dom'); var motionUtils = require('motion-utils'); const clamp = (min, max, v) => { if (v > max) return max; if (v < min) return min; return v; }; /* 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 velocitySampleDuration = 5; // ms function calcGeneratorVelocity(resolveValue, t, current) { const prevT = Math.max(t - velocitySampleDuration, 0); return velocityPerSecond(current - resolveValue(prevT), t - prevT); } const springDefaults = { // Default spring physics stiffness: 100, damping: 10, mass: 1.0, velocity: 0.0, // Default duration/bounce-based options duration: 800, // in ms bounce: 0.3, visualDuration: 0.3, // in seconds // Rest thresholds restSpeed: { granular: 0.01, default: 2, }, restDelta: { granular: 0.005, default: 0.5, }, // Limits minDuration: 0.01, // in seconds maxDuration: 10.0, // in seconds minDamping: 0.05, maxDamping: 1, }; const safeMin = 0.001; function findSpring({ duration = springDefaults.duration, bounce = springDefaults.bounce, velocity = springDefaults.velocity, mass = springDefaults.mass, }) { let envelope; let derivative; motionUtils.warning(duration <= motionUtils.secondsToMilliseconds(springDefaults.maxDuration), "Spring duration must be 10 seconds or less"); let dampingRatio = 1 - bounce; /** * Restrict dampingRatio and duration to within acceptable ranges. */ dampingRatio = clamp(springDefaults.minDamping, springDefaults.maxDamping, dampingRatio); duration = clamp(springDefaults.minDuration, springDefaults.maxDuration, motionUtils.millisecondsToSeconds(duration)); if (dampingRatio < 1) { /** * Underdamped spring */ envelope = (undampedFreq) => { const exponentialDecay = undampedFreq * dampingRatio; const delta = exponentialDecay * duration; const a = exponentialDecay - velocity; const b = calcAngularFreq(undampedFreq, dampingRatio); const c = Math.exp(-delta); return safeMin - (a / b) * c; }; derivative = (undampedFreq) => { const exponentialDecay = undampedFreq * dampingRatio; const delta = exponentialDecay * duration; const d = delta * velocity + velocity; const e = Math.pow(dampingRatio, 2) * Math.pow(undampedFreq, 2) * duration; const f = Math.exp(-delta); const g = calcAngularFreq(Math.pow(undampedFreq, 2), dampingRatio); const factor = -envelope(undampedFreq) + safeMin > 0 ? -1 : 1; return (factor * ((d - e) * f)) / g; }; } else { /** * Critically-damped spring */ envelope = (undampedFreq) => { const a = Math.exp(-undampedFreq * duration); const b = (undampedFreq - velocity) * duration + 1; return -safeMin + a * b; }; derivative = (undampedFreq) => { const a = Math.exp(-undampedFreq * duration); const b = (velocity - undampedFreq) * (duration * duration); return a * b; }; } const initialGuess = 5 / duration; const undampedFreq = approximateRoot(envelope, derivative, initialGuess); duration = motionUtils.secondsToMilliseconds(duration); if (isNaN(undampedFreq)) { return { stiffness: springDefaults.stiffness, damping: springDefaults.damping, duration, }; } else { const stiffness = Math.pow(undampedFreq, 2) * mass; return { stiffness, damping: dampingRatio * 2 * Math.sqrt(mass * stiffness), duration, }; } } const rootIterations = 12; function approximateRoot(envelope, derivative, initialGuess) { let result = initialGuess; for (let i = 1; i < rootIterations; i++) { result = result - envelope(result) / derivative(result); } return result; } function calcAngularFreq(undampedFreq, dampingRatio) { return undampedFreq * Math.sqrt(1 - dampingRatio * dampingRatio); } const durationKeys = ["duration", "bounce"]; const physicsKeys = ["stiffness", "damping", "mass"]; function isSpringType(options, keys) { return keys.some((key) => options[key] !== undefined); } function getSpringOptions(options) { let springOptions = { velocity: springDefaults.velocity, stiffness: springDefaults.stiffness, damping: springDefaults.damping, mass: springDefaults.mass, isResolvedFromDuration: false, ...options, }; // stiffness/damping/mass overrides duration/bounce if (!isSpringType(options, physicsKeys) && isSpringType(options, durationKeys)) { if (options.visualDuration) { const visualDuration = options.visualDuration; const root = (2 * Math.PI) / (visualDuration * 1.2); const stiffness = root * root; const damping = 2 * clamp(0.05, 1, 1 - (options.bounce || 0)) * Math.sqrt(stiffness); springOptions = { ...springOptions, mass: springDefaults.mass, stiffness, damping, }; } else { const derived = findSpring(options); springOptions = { ...springOptions, ...derived, mass: springDefaults.mass, }; springOptions.isResolvedFromDuration = true; } } return springOptions; } function spring(optionsOrVisualDuration = springDefaults.visualDuration, bounce = springDefaults.bounce) { const options = typeof optionsOrVisualDuration !== "object" ? { visualDuration: optionsOrVisualDuration, keyframes: [0, 1], bounce, } : optionsOrVisualDuration; let { restSpeed, restDelta } = options; const origin = options.keyframes[0]; const target = options.keyframes[options.keyframes.length - 1]; /** * This is the Iterator-spec return value. We ensure it's mutable rather than using a generator * to reduce GC during animation. */ const state = { done: false, value: origin }; const { stiffness, damping, mass, duration, velocity, isResolvedFromDuration, } = getSpringOptions({ ...options, velocity: -motionUtils.millisecondsToSeconds(options.velocity || 0), }); const initialVelocity = velocity || 0.0; const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass)); const initialDelta = target - origin; const undampedAngularFreq = motionUtils.millisecondsToSeconds(Math.sqrt(stiffness / mass)); /** * If we're working on a granular scale, use smaller defaults for determining * when the spring is finished. * * These defaults have been selected emprically based on what strikes a good * ratio between feeling good and finishing as soon as changes are imperceptible. */ const isGranularScale = Math.abs(initialDelta) < 5; restSpeed || (restSpeed = isGranularScale ? springDefaults.restSpeed.granular : springDefaults.restSpeed.default); restDelta || (restDelta = isGranularScale ? springDefaults.restDelta.granular : springDefaults.restDelta.default); let resolveSpring; if (dampingRatio < 1) { const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio); // Underdamped spring resolveSpring = (t) => { const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t); return (target - envelope * (((initialVelocity + dampingRatio * undampedAngularFreq * initialDelta) / angularFreq) * Math.sin(angularFreq * t) + initialDelta * Math.cos(angularFreq * t))); }; } else if (dampingRatio === 1) { // Critically damped spring resolveSpring = (t) => target - Math.exp(-undampedAngularFreq * t) * (initialDelta + (initialVelocity + undampedAngularFreq * initialDelta) * t); } else { // Overdamped spring const dampedAngularFreq = undampedAngularFreq * Math.sqrt(dampingRatio * dampingRatio - 1); resolveSpring = (t) => { const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t); // When performing sinh or cosh values can hit Infinity so we cap them here const freqForT = Math.min(dampedAngularFreq * t, 300); return (target - (envelope * ((initialVelocity + dampingRatio * undampedAngularFreq * initialDelta) * Math.sinh(freqForT) + dampedAngularFreq * initialDelta * Math.cosh(freqForT))) / dampedAngularFreq); }; } const generator = { calculatedDuration: isResolvedFromDuration ? duration || null : null, next: (t) => { const current = resolveSpring(t); if (!isResolvedFromDuration) { let currentVelocity = 0.0; /** * We only need to calculate velocity for under-damped springs * as over- and critically-damped springs can't overshoot, so * checking only for displacement is enough. */ if (dampingRatio < 1) { currentVelocity = t === 0 ? motionUtils.secondsToMilliseconds(initialVelocity) : calcGeneratorVelocity(resolveSpring, t, current); } const isBelowVelocityThreshold = Math.abs(currentVelocity) <= restSpeed; const isBelowDisplacementThreshold = Math.abs(target - current) <= restDelta; state.done = isBelowVelocityThreshold && isBelowDisplacementThreshold; } else { state.done = t >= duration; } state.value = state.done ? target : current; return state; }, toString: () => { const calculatedDuration = Math.min(motionDom.calcGeneratorDuration(generator), motionDom.maxGeneratorDuration); const easing = motionDom.generateLinearEasing((progress) => generator.next(calculatedDuration * progress).value, calculatedDuration, 30); return calculatedDuration + "ms " + easing; }, }; return generator; } const wrap = (min, max, v) => { const rangeSize = max - min; return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min; }; const isEasingArray = (ease) => { return Array.isArray(ease) && typeof ease[0] !== "number"; }; function getEasingForSegment(easing, i) { return isEasingArray(easing) ? easing[wrap(0, easing.length, i)] : easing; } /* Value in range from progress Given a lower limit and an upper limit, we return the value within that range as expressed by progress (usually a number from 0 to 1) So progress = 0.5 would change from -------- to to from ---- to E.g. from = 10, to = 20, progress = 0.5 => 15 @param [number]: Lower limit of range @param [number]: Upper limit of range @param [number]: The progress between lower and upper limits expressed 0-1 @return [number]: Value as calculated from progress within range (not limited within range) */ const mixNumber$1 = (from, to, progress) => { return from + (to - from) * progress; }; function fillOffset(offset, remaining) { const min = offset[offset.length - 1]; for (let i = 1; i <= remaining; i++) { const offsetProgress = motionUtils.progress(0, remaining, i); offset.push(mixNumber$1(min, 1, offsetProgress)); } } function defaultOffset$1(arr) { const offset = [0]; fillOffset(offset, arr.length - 1); return offset; } const isMotionValue = (value) => Boolean(value && value.getVelocity); function isDOMKeyframes(keyframes) { return typeof keyframes === "object" && !Array.isArray(keyframes); } function resolveSubjects(subject, keyframes, scope, selectorCache) { if (typeof subject === "string" && isDOMKeyframes(keyframes)) { return motionDom.resolveElements(subject, scope, selectorCache); } else if (subject instanceof NodeList) { return Array.from(subject); } else if (Array.isArray(subject)) { return subject; } else { return [subject]; } } function calculateRepeatDuration(duration, repeat, _repeatDelay) { return duration * (repeat + 1); } /** * Given a absolute or relative time definition and current/prev time state of the sequence, * calculate an absolute time for the next keyframes. */ function calcNextTime(current, next, prev, labels) { var _a; if (typeof next === "number") { return next; } else if (next.startsWith("-") || next.startsWith("+")) { return Math.max(0, current + parseFloat(next)); } else if (next === "<") { return prev; } else { return (_a = labels.get(next)) !== null && _a !== void 0 ? _a : current; } } 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); } function eraseKeyframes(sequence, startTime, endTime) { for (let i = 0; i < sequence.length; i++) { const keyframe = sequence[i]; if (keyframe.at > startTime && keyframe.at < endTime) { removeItem(sequence, keyframe); // If we remove this item we have to push the pointer back one i--; } } } function addKeyframes(sequence, keyframes, easing, offset, startTime, endTime) { /** * Erase every existing value between currentTime and targetTime, * this will essentially splice this timeline into any currently * defined ones. */ eraseKeyframes(sequence, startTime, endTime); for (let i = 0; i < keyframes.length; i++) { sequence.push({ value: keyframes[i], at: mixNumber$1(startTime, endTime, offset[i]), easing: getEasingForSegment(easing, i), }); } } /** * Take an array of times that represent repeated keyframes. For instance * if we have original times of [0, 0.5, 1] then our repeated times will * be [0, 0.5, 1, 1, 1.5, 2]. Loop over the times and scale them back * down to a 0-1 scale. */ function normalizeTimes(times, repeat) { for (let i = 0; i < times.length; i++) { times[i] = times[i] / (repeat + 1); } } function compareByTime(a, b) { if (a.at === b.at) { if (a.value === null) return 1; if (b.value === null) return -1; return 0; } else { return a.at - b.at; } } const defaultSegmentEasing = "easeInOut"; const MAX_REPEAT = 20; function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) { const defaultDuration = defaultTransition.duration || 0.3; const animationDefinitions = new Map(); const sequences = new Map(); const elementCache = {}; const timeLabels = new Map(); let prevTime = 0; let currentTime = 0; let totalDuration = 0; /** * Build the timeline by mapping over the sequence array and converting * the definitions into keyframes and offsets with absolute time values. * These will later get converted into relative offsets in a second pass. */ for (let i = 0; i < sequence.length; i++) { const segment = sequence[i]; /** * If this is a timeline label, mark it and skip the rest of this iteration. */ if (typeof segment === "string") { timeLabels.set(segment, currentTime); continue; } else if (!Array.isArray(segment)) { timeLabels.set(segment.name, calcNextTime(currentTime, segment.at, prevTime, timeLabels)); continue; } let [subject, keyframes, transition = {}] = segment; /** * If a relative or absolute time value has been specified we need to resolve * it in relation to the currentTime. */ if (transition.at !== undefined) { currentTime = calcNextTime(currentTime, transition.at, prevTime, timeLabels); } /** * Keep track of the maximum duration in this definition. This will be * applied to currentTime once the definition has been parsed. */ let maxDuration = 0; const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => { const valueKeyframesAsList = keyframesAsList(valueKeyframes); const { delay = 0, times = defaultOffset$1(valueKeyframesAsList), type = "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition; let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition; /** * Resolve stagger() if defined. */ const calculatedDelay = typeof delay === "function" ? delay(elementIndex, numSubjects) : delay; /** * If this animation should and can use a spring, generate a spring easing function. */ const numKeyframes = valueKeyframesAsList.length; const createGenerator = motionDom.isGenerator(type) ? type : generators === null || generators === void 0 ? void 0 : generators[type]; if (numKeyframes <= 2 && createGenerator) { /** * As we're creating an easing function from a spring, * ideally we want to generate it using the real distance * between the two keyframes. However this isn't always * possible - in these situations we use 0-100. */ let absoluteDelta = 100; if (numKeyframes === 2 && isNumberKeyframesArray(valueKeyframesAsList)) { const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0]; absoluteDelta = Math.abs(delta); } const springTransition = { ...remainingTransition }; if (duration !== undefined) { springTransition.duration = motionUtils.secondsToMilliseconds(duration); } const springEasing = motionDom.createGeneratorEasing(springTransition, absoluteDelta, createGenerator); ease = springEasing.ease; duration = springEasing.duration; } duration !== null && duration !== void 0 ? duration : (duration = defaultDuration); const startTime = currentTime + calculatedDelay; /** * If there's only one time offset of 0, fill in a second with length 1 */ if (times.length === 1 && times[0] === 0) { times[1] = 1; } /** * Fill out if offset if fewer offsets than keyframes */ const remainder = times.length - valueKeyframesAsList.length; remainder > 0 && fillOffset(times, remainder); /** * If only one value has been set, ie [1], push a null to the start of * the keyframe array. This will let us mark a keyframe at this point * that will later be hydrated with the previous value. */ valueKeyframesAsList.length === 1 && valueKeyframesAsList.unshift(null); /** * Handle repeat options */ if (repeat) { motionUtils.invariant(repeat < MAX_REPEAT, "Repeat count too high, must be less than 20"); duration = calculateRepeatDuration(duration, repeat); const originalKeyframes = [...valueKeyframesAsList]; const originalTimes = [...times]; ease = Array.isArray(ease) ? [...ease] : [ease]; const originalEase = [...ease]; for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) { valueKeyframesAsList.push(...originalKeyframes); for (let keyframeIndex = 0; keyframeIndex < originalKeyframes.length; keyframeIndex++) { times.push(originalTimes[keyframeIndex] + (repeatIndex + 1)); ease.push(keyframeIndex === 0 ? "linear" : getEasingForSegment(originalEase, keyframeIndex - 1)); } } normalizeTimes(times, repeat); } const targetTime = startTime + duration; /** * Add keyframes, mapping offsets to absolute time. */ addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime); maxDuration = Math.max(calculatedDelay + duration, maxDuration); totalDuration = Math.max(targetTime, totalDuration); }; if (isMotionValue(subject)) { const subjectSequence = getSubjectSequence(subject, sequences); resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence)); } else { const subjects = resolveSubjects(subject, keyframes, scope, elementCache); const numSubjects = subjects.length; /** * For every element in this segment, process the defined values. */ for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) { /** * Cast necessary, but we know these are of this type */ keyframes = keyframes; transition = transition; const thisSubject = subjects[subjectIndex]; const subjectSequence = getSubjectSequence(thisSubject, sequences); for (const key in keyframes) { resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects); } } } prevTime = currentTime; currentTime += maxDuration; } /** * For every element and value combination create a new animation. */ sequences.forEach((valueSequences, element) => { for (const key in valueSequences) { const valueSequence = valueSequences[key]; /** * Arrange all the keyframes in ascending time order. */ valueSequence.sort(compareByTime); const keyframes = []; const valueOffset = []; const valueEasing = []; /** * For each keyframe, translate absolute times into * relative offsets based on the total duration of the timeline. */ for (let i = 0; i < valueSequence.length; i++) { const { at, value, easing } = valueSequence[i]; keyframes.push(value); valueOffset.push(motionUtils.progress(0, totalDuration, at)); valueEasing.push(easing || "easeOut"); } /** * If the first keyframe doesn't land on offset: 0 * provide one by duplicating the initial keyframe. This ensures * it snaps to the first keyframe when the animation starts. */ if (valueOffset[0] !== 0) { valueOffset.unshift(0); keyframes.unshift(keyframes[0]); valueEasing.unshift(defaultSegmentEasing); } /** * If the last keyframe doesn't land on offset: 1 * provide one with a null wildcard value. This will ensure it * stays static until the end of the animation. */ if (valueOffset[valueOffset.length - 1] !== 1) { valueOffset.push(1); keyframes.push(null); } if (!animationDefinitions.has(element)) { animationDefinitions.set(element, { keyframes: {}, transition: {}, }); } const definition = animationDefinitions.get(element); definition.keyframes[key] = keyframes; definition.transition[key] = { ...defaultTransition, duration: totalDuration, ease: valueEasing, times: valueOffset, ...sequenceTransition, }; } }); return animationDefinitions; } function getSubjectSequence(subject, sequences) { !sequences.has(subject) && sequences.set(subject, {}); return sequences.get(subject); } function getValueSequence(name, sequences) { if (!sequences[name]) sequences[name] = []; return sequences[name]; } function keyframesAsList(keyframes) { return Array.isArray(keyframes) ? keyframes : [keyframes]; } function getValueTransition(transition, key) { return transition && transition[key] ? { ...transition, ...transition[key], } : { ...transition }; } const isNumber = (keyframe) => typeof keyframe === "number"; const isNumberKeyframesArray = (keyframes) => keyframes.every(isNumber); const visualElementStore = new WeakMap(); const MotionGlobalConfig = { skipAnimations: false, useManualTiming: false, }; 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]; // Execute this frame thisFrame.forEach(triggerCallback); // 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 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 = 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 : motionUtils.noop, true); /** * 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 resolveFinalValueInKeyframes = (v) => { // TODO maybe throw if v.length - 1 is placeholder token? return isKeyframesTarget(v) ? v[v.length - 1] || 0 : v; }; 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); }, }; 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; } } 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); } /** * 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. * * @internal */ 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.0.6"; /** * 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`. * * @internal */ 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 * * @internal */ 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 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); } /** * 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); } } 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 motionUtils.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%" */ cons