%PDF- %PDF-
Direktori : /home/vacivi36/SiteVacivitta/vacivitta/node_modules/framer-motion/dist/cjs/ |
Current File : /home/vacivi36/SiteVacivitta/vacivitta/node_modules/framer-motion/dist/cjs/dom.js |
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var motionDom = require('motion-dom'); var motionUtils = require('motion-utils'); 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) { 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 labels.get(next) ?? current; } } function eraseKeyframes(sequence, startTime, endTime) { for (let i = 0; i < sequence.length; i++) { const keyframe = sequence[i]; if (keyframe.at > startTime && keyframe.at < endTime) { motionUtils.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: motionDom.mixNumber(startTime, endTime, offset[i]), easing: motionUtils.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 = motionDom.defaultOffset(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?.[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 ?? (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 && motionDom.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" : motionUtils.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 (motionDom.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 isKeyframesTarget = (v) => { return Array.isArray(v); }; 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); } /** * 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, motionDom.motionValue(value)); } } function resolveFinalValueInKeyframes(v) { // TODO maybe throw if v.length - 1 is placeholder token? return isKeyframesTarget(v) ? v[v.length - 1] || 0 : v; } 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(motionDom.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); } else if (!willChange && motionUtils.MotionGlobalConfig.WillChange) { const newWillChange = new motionUtils.MotionGlobalConfig.WillChange("auto"); visualElement.addValue("willChange", newWillChange); newWillChange.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]; } 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 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 (motionDom.transformProps.has(valueKey)) { return valueKey.startsWith("scale") ? criticallyDampedSpring(keyframes[1]) : underDampedSpring; } return ease; }; /** * Decide whether a transition is defined on a given Transition. * This filters out orchestration options and returns true * if any options are left. */ function isTransitionDefined({ when, delay: _delay, delayChildren, staggerChildren, staggerDirection, repeat, repeatType, repeatDelay, from, elapsed, ...transition }) { return !!Object.keys(transition).length; } const animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => { const valueTransition = motionDom.getValueTransition(transition, name) || {}; /** * Most transition values are currently completely overwritten by value-specific * transitions. In the future it'd be nicer to blend these transitions. But for now * delay actually does inherit from the root transition if not value-specific. */ const delay = valueTransition.delay || transition.delay || 0; /** * Elapsed isn't a public transition option but can be passed through from * optimized appear effects in milliseconds. */ let { elapsed = 0 } = transition; elapsed = elapsed - motionUtils.secondsToMilliseconds(delay); const options = { keyframes: Array.isArray(target) ? target : [null, target], ease: "easeOut", velocity: value.getVelocity(), ...valueTransition, delay: -elapsed, onUpdate: (v) => { value.set(v); valueTransition.onUpdate && valueTransition.onUpdate(v); }, onComplete: () => { onComplete(); valueTransition.onComplete && valueTransition.onComplete(); }, name, motionValue: value, element: isHandoff ? undefined : element, }; /** * If there's no transition defined for this value, we can generate * unique transition settings for this value. */ if (!isTransitionDefined(valueTransition)) { Object.assign(options, getDefaultTransition(name, options)); } /** * Both WAAPI and our internal animation functions use durations * as defined by milliseconds, while our external API defines them * as seconds. */ options.duration && (options.duration = motionUtils.secondsToMilliseconds(options.duration)); options.repeatDelay && (options.repeatDelay = motionUtils.secondsToMilliseconds(options.repeatDelay)); /** * Support deprecated way to set initial value. Prefer keyframe syntax. */ if (options.from !== undefined) { options.keyframes[0] = options.from; } let shouldSkip = false; if (options.type === false || (options.duration === 0 && !options.repeatDelay)) { options.duration = 0; if (options.delay === 0) { shouldSkip = true; } } if (motionUtils.MotionGlobalConfig.instantAnimations || motionUtils.MotionGlobalConfig.skipAnimations) { shouldSkip = true; options.duration = 0; options.delay = 0; } /** * If the transition type or easing has been explicitly set by the user * then we don't want to allow flattening the animation. */ options.allowFlatten = !valueTransition.type && !valueTransition.ease; /** * If we can or must skip creating the animation, and apply only * the final keyframe, do so. We also check once keyframes are resolved but * this early check prevents the need to create an animation at all. */ if (shouldSkip && !isHandoff && value.get() !== undefined) { const finalKeyframe = getFinalKeyframe(options.keyframes, valueTransition); if (finalKeyframe !== undefined) { motionDom.frame.update(() => { options.onUpdate(finalKeyframe); options.onComplete(); }); return; } } return valueTransition.isSync ? new motionDom.JSAnimation(options) : new motionDom.AsyncMotionValueAnimation(options); }; /** * Decide whether we should block this animation. Previously, we achieved this * just by checking whether the key was listed in protectedKeys, but this * posed problems if an animation was triggered by afterChildren and protectedKeys * had been set to true in the meantime. */ function shouldBlockAnimation({ protectedKeys, needsAnimating }, key) { const shouldBlock = protectedKeys.hasOwnProperty(key) && needsAnimating[key] !== true; needsAnimating[key] = false; return shouldBlock; } function animateTarget(visualElement, targetAndTransition, { delay = 0, transitionOverride, type } = {}) { let { transition = visualElement.getDefaultTransition(), transitionEnd, ...target } = targetAndTransition; if (transitionOverride) transition = transitionOverride; const animations = []; const animationTypeState = type && visualElement.animationState && visualElement.animationState.getState()[type]; for (const key in target) { const value = visualElement.getValue(key, visualElement.latestValues[key] ?? null); const valueTarget = target[key]; if (valueTarget === undefined || (animationTypeState && shouldBlockAnimation(animationTypeState, key))) { continue; } const valueTransition = { delay, ...motionDom.getValueTransition(transition || {}, key), }; /** * If the value is already at the defined target, skip the animation. */ const currentValue = value.get(); if (currentValue !== undefined && !value.isAnimating && !Array.isArray(valueTarget) && valueTarget === currentValue && !valueTransition.velocity) { continue; } /** * If this is the first time a value is being animated, check * to see if we're handling off from an existing animation. */ let isHandoff = false; if (window.MotionHandoffAnimation) { const appearId = getOptimisedAppearId(visualElement); if (appearId) { const startTime = window.MotionHandoffAnimation(appearId, key, motionDom.frame); if (startTime !== null) { valueTransition.startTime = startTime; isHandoff = true; } } } addValueToWillChange(visualElement, key); value.start(animateMotionValue(key, value, valueTarget, visualElement.shouldReduceMotion && motionDom.positionalKeys.has(key) ? { type: false } : valueTransition, visualElement, isHandoff)); const animation = value.animation; if (animation) { animations.push(animation); } } if (transitionEnd) { Promise.all(animations).then(() => { motionDom.frame.update(() => { transitionEnd && setTarget(visualElement, transitionEnd); }); }); } return animations; } /** * Bounding boxes tend to be defined as top, left, right, bottom. For various operations * it's easier to consider each axis individually. This function returns a bounding box * as a map of single-axis min/max values. */ function convertBoundingBoxToBox({ top, left, right, bottom, }) { return { x: { min: left, max: right }, y: { min: top, max: bottom }, }; } /** * Applies a TransformPoint function to a bounding box. TransformPoint is usually a function * provided by Framer to allow measured points to be corrected for device scaling. This is used * when measuring DOM elements and DOM event points. */ function transformBoxPoints(point, transformPoint) { if (!transformPoint) return point; const topLeft = transformPoint({ x: point.left, y: point.top }); const bottomRight = transformPoint({ x: point.right, y: point.bottom }); return { top: topLeft.y, left: topLeft.x, bottom: bottomRight.y, right: bottomRight.x, }; } function measureViewportBox(instance, transformPoint) { return convertBoundingBoxToBox(transformBoxPoints(instance.getBoundingClientRect(), transformPoint)); } const featureProps = { animation: [ "animate", "variants", "whileHover", "whileTap", "exit", "whileInView", "whileFocus", "whileDrag", ], exit: ["exit"], drag: ["drag", "dragControls"], focus: ["whileFocus"], hover: ["whileHover", "onHoverStart", "onHoverEnd"], tap: ["whileTap", "onTap", "onTapStart", "onTapCancel"], pan: ["onPan", "onPanStart", "onPanSessionStart", "onPanEnd"], inView: ["whileInView", "onViewportEnter", "onViewportLeave"], layout: ["layout", "layoutId"], }; const featureDefinitions = {}; for (const key in featureProps) { featureDefinitions[key] = { isEnabled: (props) => featureProps[key].some((name) => !!props[name]), }; } const createAxis = () => ({ min: 0, max: 0 }); const createBox = () => ({ x: createAxis(), y: createAxis(), }); const isBrowser = typeof window !== "undefined"; // Does this device prefer reduced motion? Returns `null` server-side. const prefersReducedMotion = { current: null }; const hasReducedMotionListener = { current: false }; function initPrefersReducedMotion() { hasReducedMotionListener.current = true; if (!isBrowser) return; if (window.matchMedia) { const motionMediaQuery = window.matchMedia("(prefers-reduced-motion)"); const setReducedMotionPreferences = () => (prefersReducedMotion.current = motionMediaQuery.matches); motionMediaQuery.addListener(setReducedMotionPreferences); setReducedMotionPreferences(); } else { prefersReducedMotion.current = false; } } function isAnimationControls(v) { return (v !== null && typeof v === "object" && typeof v.start === "function"); } /** * Decides if the supplied variable is variant label */ function isVariantLabel(v) { return typeof v === "string" || Array.isArray(v); } const variantPriorityOrder = [ "animate", "whileInView", "whileFocus", "whileHover", "whileTap", "whileDrag", "exit", ]; const variantProps = ["initial", ...variantPriorityOrder]; function isControllingVariants(props) { return (isAnimationControls(props.animate) || variantProps.some((name) => isVariantLabel(props[name]))); } function isVariantNode(props) { return Boolean(isControllingVariants(props) || props.variants); } function updateMotionValuesFromProps(element, next, prev) { for (const key in next) { const nextValue = next[key]; const prevValue = prev[key]; if (motionDom.isMotionValue(nextValue)) { /** * If this is a motion value found in props or style, we want to add it * to our visual element's motion value map. */ element.addValue(key, nextValue); } else if (motionDom.isMotionValue(prevValue)) { /** * If we're swapping from a motion value to a static value, * create a new motion value from that */ element.addValue(key, motionDom.motionValue(nextValue, { owner: element })); } else if (prevValue !== nextValue) { /** * If this is a flat value that has changed, update the motion value * or create one if it doesn't exist. We only want to do this if we're * not handling the value with our animation state. */ if (element.hasValue(key)) { const existingValue = element.getValue(key); if (existingValue.liveStyle === true) { existingValue.jump(nextValue); } else if (!existingValue.hasAnimated) { existingValue.set(nextValue); } } else { const latestValue = element.getStaticValue(key); element.addValue(key, motionDom.motionValue(latestValue !== undefined ? latestValue : nextValue, { owner: element })); } } } // Handle removed values for (const key in prev) { if (next[key] === undefined) element.removeValue(key); } return next; } const propEventHandlers = [ "AnimationStart", "AnimationComplete", "Update", "BeforeLayoutMeasure", "LayoutMeasure", "LayoutAnimationStart", "LayoutAnimationComplete", ]; /** * A VisualElement is an imperative abstraction around UI elements such as * HTMLElement, SVGElement, Three.Object3D etc. */ class VisualElement { /** * This method takes React props and returns found MotionValues. For example, HTML * MotionValues will be found within the style prop, whereas for Three.js within attribute arrays. * * This isn't an abstract method as it needs calling in the constructor, but it is * intended to be one. */ scrapeMotionValuesFromProps(_props, _prevProps, _visualElement) { return {}; } constructor({ parent, props, presenceContext, reducedMotionConfig, blockInitialAnimation, visualState, }, options = {}) { /** * A reference to the current underlying Instance, e.g. a HTMLElement * or Three.Mesh etc. */ this.current = null; /** * A set containing references to this VisualElement's children. */ this.children = new Set(); /** * Determine what role this visual element should take in the variant tree. */ this.isVariantNode = false; this.isControllingVariants = false; /** * Decides whether this VisualElement should animate in reduced motion * mode. * * TODO: This is currently set on every individual VisualElement but feels * like it could be set globally. */ this.shouldReduceMotion = null; /** * A map of all motion values attached to this visual element. Motion * values are source of truth for any given animated value. A motion * value might be provided externally by the component via props. */ this.values = new Map(); this.KeyframeResolver = motionDom.KeyframeResolver; /** * Cleanup functions for active features (hover/tap/exit etc) */ this.features = {}; /** * A map of every subscription that binds the provided or generated * motion values onChange listeners to this visual element. */ this.valueSubscriptions = new Map(); /** * A reference to the previously-provided motion values as returned * from scrapeMotionValuesFromProps. We use the keys in here to determine * if any motion values need to be removed after props are updated. */ this.prevMotionValues = {}; /** * An object containing a SubscriptionManager for each active event. */ this.events = {}; /** * An object containing an unsubscribe function for each prop event subscription. * For example, every "Update" event can have multiple subscribers via * VisualElement.on(), but only one of those can be defined via the onUpdate prop. */ this.propEventSubscriptions = {}; this.notifyUpdate = () => this.notify("Update", this.latestValues); this.render = () => { if (!this.current) return; this.triggerBuild(); this.renderInstance(this.current, this.renderState, this.props.style, this.projection); }; this.renderScheduledAt = 0.0; this.scheduleRender = () => { const now = motionDom.time.now(); if (this.renderScheduledAt < now) { this.renderScheduledAt = now; motionDom.frame.render(this.render, false, true); } }; const { latestValues, renderState } = visualState; this.latestValues = latestValues; this.baseTarget = { ...latestValues }; this.initialValues = props.initial ? { ...latestValues } : {}; this.renderState = renderState; this.parent = parent; this.props = props; this.presenceContext = presenceContext; this.depth = parent ? parent.depth + 1 : 0; this.reducedMotionConfig = reducedMotionConfig; this.options = options; this.blockInitialAnimation = Boolean(blockInitialAnimation); this.isControllingVariants = isControllingVariants(props); this.isVariantNode = isVariantNode(props); if (this.isVariantNode) { this.variantChildren = new Set(); } this.manuallyAnimateOnMount = Boolean(parent && parent.current); /** * Any motion values that are provided to the element when created * aren't yet bound to the element, as this would technically be impure. * However, we iterate through the motion values and set them to the * initial values for this component. * * TODO: This is impure and we should look at changing this to run on mount. * Doing so will break some tests but this isn't necessarily a breaking change, * more a reflection of the test. */ const { willChange, ...initialMotionValues } = this.scrapeMotionValuesFromProps(props, {}, this); for (const key in initialMotionValues) { const value = initialMotionValues[key]; if (latestValues[key] !== undefined && motionDom.isMotionValue(value)) { value.set(latestValues[key], false); } } } mount(instance) { this.current = instance; visualElementStore.set(instance, this); if (this.projection && !this.projection.instance) { this.projection.mount(instance); } if (this.parent && this.isVariantNode && !this.isControllingVariants) { this.removeFromVariantTree = this.parent.addVariantChild(this); } this.values.forEach((value, key) => this.bindToMotionValue(key, value)); if (!hasReducedMotionListener.current) { initPrefersReducedMotion(); } this.shouldReduceMotion = this.reducedMotionConfig === "never" ? false : this.reducedMotionConfig === "always" ? true : prefersReducedMotion.current; if (process.env.NODE_ENV !== "production") { motionUtils.warnOnce(this.shouldReduceMotion !== true, "You have Reduced Motion enabled on your device. Animations may not appear as expected."); } if (this.parent) this.parent.children.add(this); this.update(this.props, this.presenceContext); } unmount() { this.projection && this.projection.unmount(); motionDom.cancelFrame(this.notifyUpdate); motionDom.cancelFrame(this.render); this.valueSubscriptions.forEach((remove) => remove()); this.valueSubscriptions.clear(); this.removeFromVariantTree && this.removeFromVariantTree(); this.parent && this.parent.children.delete(this); for (const key in this.events) { this.events[key].clear(); } for (const key in this.features) { const feature = this.features[key]; if (feature) { feature.unmount(); feature.isMounted = false; } } this.current = null; } bindToMotionValue(key, value) { if (this.valueSubscriptions.has(key)) { this.valueSubscriptions.get(key)(); } const valueIsTransform = motionDom.transformProps.has(key); if (valueIsTransform && this.onBindTransform) { this.onBindTransform(); } const removeOnChange = value.on("change", (latestValue) => { this.latestValues[key] = latestValue; this.props.onUpdate && motionDom.frame.preRender(this.notifyUpdate); if (valueIsTransform && this.projection) { this.projection.isTransformDirty = true; } }); const removeOnRenderRequest = value.on("renderRequest", this.scheduleRender); let removeSyncCheck; if (window.MotionCheckAppearSync) { removeSyncCheck = window.MotionCheckAppearSync(this, key, value); } this.valueSubscriptions.set(key, () => { removeOnChange(); removeOnRenderRequest(); if (removeSyncCheck) removeSyncCheck(); if (value.owner) value.stop(); }); } sortNodePosition(other) { /** * If these nodes aren't even of the same type we can't compare their depth. */ if (!this.current || !this.sortInstanceNodePosition || this.type !== other.type) { return 0; } return this.sortInstanceNodePosition(this.current, other.current); } updateFeatures() { let key = "animation"; for (key in featureDefinitions) { const featureDefinition = featureDefinitions[key]; if (!featureDefinition) continue; const { isEnabled, Feature: FeatureConstructor } = featureDefinition; /** * If this feature is enabled but not active, make a new instance. */ if (!this.features[key] && FeatureConstructor && isEnabled(this.props)) { this.features[key] = new FeatureConstructor(this); } /** * If we have a feature, mount or update it. */ if (this.features[key]) { const feature = this.features[key]; if (feature.isMounted) { feature.update(); } else { feature.mount(); feature.isMounted = true; } } } } triggerBuild() { this.build(this.renderState, this.latestValues, this.props); } /** * Measure the current viewport box with or without transforms. * Only measures axis-aligned boxes, rotate and skew must be manually * removed with a re-render to work. */ measureViewportBox() { return this.current ? this.measureInstanceViewportBox(this.current, this.props) : createBox(); } getStaticValue(key) { return this.latestValues[key]; } setStaticValue(key, value) { this.latestValues[key] = value; } /** * Update the provided props. Ensure any newly-added motion values are * added to our map, old ones removed, and listeners updated. */ update(props, presenceContext) { if (props.transformTemplate || this.props.transformTemplate) { this.scheduleRender(); } this.prevProps = this.props; this.props = props; this.prevPresenceContext = this.presenceContext; this.presenceContext = presenceContext; /** * Update prop event handlers ie onAnimationStart, onAnimationComplete */ for (let i = 0; i < propEventHandlers.length; i++) { const key = propEventHandlers[i]; if (this.propEventSubscriptions[key]) { this.propEventSubscriptions[key](); delete this.propEventSubscriptions[key]; } const listenerName = ("on" + key); const listener = props[listenerName]; if (listener) { this.propEventSubscriptions[key] = this.on(key, listener); } } this.prevMotionValues = updateMotionValuesFromProps(this, this.scrapeMotionValuesFromProps(props, this.prevProps, this), this.prevMotionValues); if (this.handleChildMotionValue) { this.handleChildMotionValue(); } } getProps() { return this.props; } /** * Returns the variant definition with a given name. */ getVariant(name) { return this.props.variants ? this.props.variants[name] : undefined; } /** * Returns the defined default transition on this component. */ getDefaultTransition() { return this.props.transition; } getTransformPagePoint() { return this.props.transformPagePoint; } getClosestVariantNode() { return this.isVariantNode ? this : this.parent ? this.parent.getClosestVariantNode() : undefined; } /** * Add a child visual element to our set of children. */ addVariantChild(child) { const closestVariantNode = this.getClosestVariantNode(); if (closestVariantNode) { closestVariantNode.variantChildren && closestVariantNode.variantChildren.add(child); return () => closestVariantNode.variantChildren.delete(child); } } /** * Add a motion value and bind it to this visual element. */ addValue(key, value) { // Remove existing value if it exists const existingValue = this.values.get(key); if (value !== existingValue) { if (existingValue) this.removeValue(key); this.bindToMotionValue(key, value); this.values.set(key, value); this.latestValues[key] = value.get(); } } /** * Remove a motion value and unbind any active subscriptions. */ removeValue(key) { this.values.delete(key); const unsubscribe = this.valueSubscriptions.get(key); if (unsubscribe) { unsubscribe(); this.valueSubscriptions.delete(key); } delete this.latestValues[key]; this.removeValueFromRenderState(key, this.renderState); } /** * Check whether we have a motion value for this key */ hasValue(key) { return this.values.has(key); } getValue(key, defaultValue) { if (this.props.values && this.props.values[key]) { return this.props.values[key]; } let value = this.values.get(key); if (value === undefined && defaultValue !== undefined) { value = motionDom.motionValue(defaultValue === null ? undefined : defaultValue, { owner: this }); this.addValue(key, value); } return value; } /** * If we're trying to animate to a previously unencountered value, * we need to check for it in our state and as a last resort read it * directly from the instance (which might have performance implications). */ readValue(key, target) { let value = this.latestValues[key] !== undefined || !this.current ? this.latestValues[key] : this.getBaseTargetFromProps(this.props, key) ?? this.readValueFromInstance(this.current, key, this.options); if (value !== undefined && value !== null) { if (typeof value === "string" && (motionUtils.isNumericalString(value) || motionUtils.isZeroValueString(value))) { // If this is a number read as a string, ie "0" or "200", convert it to a number value = parseFloat(value); } else if (!motionDom.findValueType(value) && motionDom.complex.test(target)) { value = motionDom.getAnimatableNone(key, target); } this.setBaseTarget(key, motionDom.isMotionValue(value) ? value.get() : value); } return motionDom.isMotionValue(value) ? value.get() : value; } /** * Set the base target to later animate back to. This is currently * only hydrated on creation and when we first read a value. */ setBaseTarget(key, value) { this.baseTarget[key] = value; } /** * Find the base target for a value thats been removed from all animation * props. */ getBaseTarget(key) { const { initial } = this.props; let valueFromInitial; if (typeof initial === "string" || typeof initial === "object") { const variant = resolveVariantFromProps(this.props, initial, this.presenceContext?.custom); if (variant) { valueFromInitial = variant[key]; } } /** * If this value still exists in the current initial variant, read that. */ if (initial && valueFromInitial !== undefined) { return valueFromInitial; } /** * Alternatively, if this VisualElement config has defined a getBaseTarget * so we can read the value from an alternative source, try that. */ const target = this.getBaseTargetFromProps(this.props, key); if (target !== undefined && !motionDom.isMotionValue(target)) return target; /** * If the value was initially defined on initial, but it doesn't any more, * return undefined. Otherwise return the value as initially read from the DOM. */ return this.initialValues[key] !== undefined && valueFromInitial === undefined ? undefined : this.baseTarget[key]; } on(eventName, callback) { if (!this.events[eventName]) { this.events[eventName] = new motionUtils.SubscriptionManager(); } return this.events[eventName].add(callback); } notify(eventName, ...args) { if (this.events[eventName]) { this.events[eventName].notify(...args); } } } class DOMVisualElement extends VisualElement { constructor() { super(...arguments); this.KeyframeResolver = motionDom.DOMKeyframesResolver; } sortInstanceNodePosition(a, b) { /** * compareDocumentPosition returns a bitmask, by using the bitwise & * we're returning true if 2 in that bitmask is set to true. 2 is set * to true if b preceeds a. */ return a.compareDocumentPosition(b) & 2 ? 1 : -1; } getBaseTargetFromProps(props, key) { return props.style ? props.style[key] : undefined; } removeValueFromRenderState(key, { vars, style }) { delete vars[key]; delete style[key]; } handleChildMotionValue() { if (this.childSubscription) { this.childSubscription(); delete this.childSubscription; } const { children } = this.props; if (motionDom.isMotionValue(children)) { this.childSubscription = children.on("change", (latest) => { if (this.current) { this.current.textContent = `${latest}`; } }); } } } const translateAlias = { x: "translateX", y: "translateY", z: "translateZ", transformPerspective: "perspective", }; const numTransforms = motionDom.transformPropOrder.length; /** * Build a CSS transform style from individual x/y/scale etc properties. * * This outputs with a default order of transforms/scales/rotations, this can be customised by * providing a transformTemplate function. */ function buildTransform(latestValues, transform, transformTemplate) { // The transform string we're going to build into. let transformString = ""; let transformIsDefault = true; /** * Loop over all possible transforms in order, adding the ones that * are present to the transform string. */ for (let i = 0; i < numTransforms; i++) { const key = motionDom.transformPropOrder[i]; const value = latestValues[key]; if (value === undefined) continue; let valueIsDefault = true; if (typeof value === "number") { valueIsDefault = value === (key.startsWith("scale") ? 1 : 0); } else { valueIsDefault = parseFloat(value) === 0; } if (!valueIsDefault || transformTemplate) { const valueAsType = motionDom.getValueAsType(value, motionDom.numberValueTypes[key]); if (!valueIsDefault) { transformIsDefault = false; const transformName = translateAlias[key] || key; transformString += `${transformName}(${valueAsType}) `; } if (transformTemplate) { transform[key] = valueAsType; } } } transformString = transformString.trim(); // If we have a custom `transform` template, pass our transform values and // generated transformString to that before returning if (transformTemplate) { transformString = transformTemplate(transform, transformIsDefault ? "" : transformString); } else if (transformIsDefault) { transformString = "none"; } return transformString; } function buildHTMLStyles(state, latestValues, transformTemplate) { const { style, vars, transformOrigin } = state; // Track whether we encounter any transform or transformOrigin values. let hasTransform = false; let hasTransformOrigin = false; /** * Loop over all our latest animated values and decide whether to handle them * as a style or CSS variable. * * Transforms and transform origins are kept separately for further processing. */ for (const key in latestValues) { const value = latestValues[key]; if (motionDom.transformProps.has(key)) { // If this is a transform, flag to enable further transform processing hasTransform = true; continue; } else if (motionDom.isCSSVariableName(key)) { vars[key] = value; continue; } else { // Convert the value to its default value type, ie 0 -> "0px" const valueAsType = motionDom.getValueAsType(value, motionDom.numberValueTypes[key]); if (key.startsWith("origin")) { // If this is a transform origin, flag and enable further transform-origin processing hasTransformOrigin = true; transformOrigin[key] = valueAsType; } else { style[key] = valueAsType; } } } if (!latestValues.transform) { if (hasTransform || transformTemplate) { style.transform = buildTransform(latestValues, state.transform, transformTemplate); } else if (style.transform) { /** * If we have previously created a transform but currently don't have any, * reset transform style to none. */ style.transform = "none"; } } /** * Build a transformOrigin style. Uses the same defaults as the browser for * undefined origins. */ if (hasTransformOrigin) { const { originX = "50%", originY = "50%", originZ = 0, } = transformOrigin; style.transformOrigin = `${originX} ${originY} ${originZ}`; } } function renderHTML(element, { style, vars }, styleProp, projection) { Object.assign(element.style, style, projection && projection.getProjectionStyles(styleProp)); // Loop over any CSS variables and assign those. for (const key in vars) { element.style.setProperty(key, vars[key]); } } const scaleCorrectors = {}; function isForcedMotionValue(key, { layout, layoutId }) { return (motionDom.transformProps.has(key) || key.startsWith("origin") || ((layout || layoutId !== undefined) && (!!scaleCorrectors[key] || key === "opacity"))); } function scrapeMotionValuesFromProps$1(props, prevProps, visualElement) { const { style } = props; const newValues = {}; for (const key in style) { if (motionDom.isMotionValue(style[key]) || (prevProps.style && motionDom.isMotionValue(prevProps.style[key])) || isForcedMotionValue(key, props) || visualElement?.getValue(key)?.liveStyle !== undefined) { newValues[key] = style[key]; } } return newValues; } function getComputedStyle$1(element) { return window.getComputedStyle(element); } class HTMLVisualElement extends DOMVisualElement { constructor() { super(...arguments); this.type = "html"; this.renderInstance = renderHTML; } readValueFromInstance(instance, key) { if (motionDom.transformProps.has(key)) { return this.projection?.isProjecting ? motionDom.defaultTransformValue(key) : motionDom.readTransformValue(instance, key); } else { const computedStyle = getComputedStyle$1(instance); const value = (motionDom.isCSSVariableName(key) ? computedStyle.getPropertyValue(key) : computedStyle[key]) || 0; return typeof value === "string" ? value.trim() : value; } } measureInstanceViewportBox(instance, { transformPagePoint }) { return measureViewportBox(instance, transformPagePoint); } build(renderState, latestValues, props) { buildHTMLStyles(renderState, latestValues, props.transformTemplate); } scrapeMotionValuesFromProps(props, prevProps, visualElement) { return scrapeMotionValuesFromProps$1(props, prevProps, visualElement); } } function isObjectKey(key, object) { return key in object; } class ObjectVisualElement extends VisualElement { constructor() { super(...arguments); this.type = "object"; } readValueFromInstance(instance, key) { if (isObjectKey(key, instance)) { const value = instance[key]; if (typeof value === "string" || typeof value === "number") { return value; } } return undefined; } getBaseTargetFromProps() { return undefined; } removeValueFromRenderState(key, renderState) { delete renderState.output[key]; } measureInstanceViewportBox() { return createBox(); } build(renderState, latestValues) { Object.assign(renderState.output, latestValues); } renderInstance(instance, { output }) { Object.assign(instance, output); } sortInstanceNodePosition() { return 0; } } const dashKeys = { offset: "stroke-dashoffset", array: "stroke-dasharray", }; const camelKeys = { offset: "strokeDashoffset", array: "strokeDasharray", }; /** * Build SVG path properties. Uses the path's measured length to convert * our custom pathLength, pathSpacing and pathOffset into stroke-dashoffset * and stroke-dasharray attributes. * * This function is mutative to reduce per-frame GC. */ function buildSVGPath(attrs, length, spacing = 1, offset = 0, useDashCase = true) { // Normalise path length by setting SVG attribute pathLength to 1 attrs.pathLength = 1; // We use dash case when setting attributes directly to the DOM node and camel case // when defining props on a React component. const keys = useDashCase ? dashKeys : camelKeys; // Build the dash offset attrs[keys.offset] = motionDom.px.transform(-offset); // Build the dash array const pathLength = motionDom.px.transform(length); const pathSpacing = motionDom.px.transform(spacing); attrs[keys.array] = `${pathLength} ${pathSpacing}`; } /** * Build SVG visual attrbutes, like cx and style.transform */ function buildSVGAttrs(state, { attrX, attrY, attrScale, pathLength, pathSpacing = 1, pathOffset = 0, // This is object creation, which we try to avoid per-frame. ...latest }, isSVGTag, transformTemplate, styleProp) { buildHTMLStyles(state, latest, transformTemplate); /** * For svg tags we just want to make sure viewBox is animatable and treat all the styles * as normal HTML tags. */ if (isSVGTag) { if (state.style.viewBox) { state.attrs.viewBox = state.style.viewBox; } return; } state.attrs = state.style; state.style = {}; const { attrs, style } = state; /** * However, we apply transforms as CSS transforms. * So if we detect a transform, transformOrigin we take it from attrs and copy it into style. */ if (attrs.transform) { style.transform = attrs.transform; delete attrs.transform; } if (style.transform || attrs.transformOrigin) { style.transformOrigin = attrs.transformOrigin ?? "50% 50%"; delete attrs.transformOrigin; } if (style.transform) { /** * SVG's element transform-origin uses its own median as a reference. * Therefore, transformBox becomes a fill-box */ style.transformBox = styleProp?.transformBox ?? "fill-box"; delete attrs.transformBox; } // Render attrX/attrY/attrScale as attributes if (attrX !== undefined) attrs.x = attrX; if (attrY !== undefined) attrs.y = attrY; if (attrScale !== undefined) attrs.scale = attrScale; // Build SVG path if one has been defined if (pathLength !== undefined) { buildSVGPath(attrs, pathLength, pathSpacing, pathOffset, false); } } /** * A set of attribute names that are always read/written as camel case. */ const camelCaseAttributes = new Set([ "baseFrequency", "diffuseConstant", "kernelMatrix", "kernelUnitLength", "keySplines", "keyTimes", "limitingConeAngle", "markerHeight", "markerWidth", "numOctaves", "targetX", "targetY", "surfaceScale", "specularConstant", "specularExponent", "stdDeviation", "tableValues", "viewBox", "gradientTransform", "pathLength", "startOffset", "textLength", "lengthAdjust", ]); const isSVGTag = (tag) => typeof tag === "string" && tag.toLowerCase() === "svg"; function renderSVG(element, renderState, _styleProp, projection) { renderHTML(element, renderState, undefined, projection); for (const key in renderState.attrs) { element.setAttribute(!camelCaseAttributes.has(key) ? camelToDash(key) : key, renderState.attrs[key]); } } function scrapeMotionValuesFromProps(props, prevProps, visualElement) { const newValues = scrapeMotionValuesFromProps$1(props, prevProps, visualElement); for (const key in props) { if (motionDom.isMotionValue(props[key]) || motionDom.isMotionValue(prevProps[key])) { const targetKey = motionDom.transformPropOrder.indexOf(key) !== -1 ? "attr" + key.charAt(0).toUpperCase() + key.substring(1) : key; newValues[targetKey] = props[key]; } } return newValues; } class SVGVisualElement extends DOMVisualElement { constructor() { super(...arguments); this.type = "svg"; this.isSVGTag = false; this.measureInstanceViewportBox = createBox; } getBaseTargetFromProps(props, key) { return props[key]; } readValueFromInstance(instance, key) { if (motionDom.transformProps.has(key)) { const defaultType = motionDom.getDefaultValueType(key); return defaultType ? defaultType.default || 0 : 0; } key = !camelCaseAttributes.has(key) ? camelToDash(key) : key; return instance.getAttribute(key); } scrapeMotionValuesFromProps(props, prevProps, visualElement) { return scrapeMotionValuesFromProps(props, prevProps, visualElement); } build(renderState, latestValues, props) { buildSVGAttrs(renderState, latestValues, this.isSVGTag, props.transformTemplate, props.style); } renderInstance(instance, renderState, styleProp, projection) { renderSVG(instance, renderState, styleProp, projection); } mount(instance) { this.isSVGTag = isSVGTag(instance.tagName); super.mount(instance); } } function createDOMVisualElement(element) { const options = { presenceContext: null, props: {}, visualState: { renderState: { transform: {}, transformOrigin: {}, style: {}, vars: {}, attrs: {}, }, latestValues: {}, }, }; const node = motionDom.isSVGElement(element) && !motionDom.isSVGSVGElement(element) ? new SVGVisualElement(options) : new HTMLVisualElement(options); node.mount(element); visualElementStore.set(element, node); } function createObjectVisualElement(subject) { const options = { presenceContext: null, props: {}, visualState: { renderState: { output: {}, }, latestValues: {}, }, }; const node = new ObjectVisualElement(options); node.mount(subject); visualElementStore.set(subject, node); } function animateSingleValue(value, keyframes, options) { const motionValue = motionDom.isMotionValue(value) ? value : motionDom.motionValue(value); motionValue.start(animateMotionValue("", motionValue, keyframes, options)); return motionValue.animation; } function isSingleValue(subject, keyframes) { return (motionDom.isMotionValue(subject) || typeof subject === "number" || (typeof subject === "string" && !isDOMKeyframes(keyframes))); } /** * Implementation */ function animateSubject(subject, keyframes, options, scope) { const animations = []; if (isSingleValue(subject, keyframes)) { animations.push(animateSingleValue(subject, isDOMKeyframes(keyframes) ? keyframes.default || keyframes : keyframes, options ? options.default || options : options)); } else { const subjects = resolveSubjects(subject, keyframes, scope); const numSubjects = subjects.length; motionUtils.invariant(Boolean(numSubjects), "No valid elements provided."); for (let i = 0; i < numSubjects; i++) { const thisSubject = subjects[i]; const createVisualElement = thisSubject instanceof Element ? createDOMVisualElement : createObjectVisualElement; if (!visualElementStore.has(thisSubject)) { createVisualElement(thisSubject); } const visualElement = visualElementStore.get(thisSubject); const transition = { ...options }; /** * Resolve stagger function if provided. */ if ("delay" in transition && typeof transition.delay === "function") { transition.delay = transition.delay(i, numSubjects); } animations.push(...animateTarget(visualElement, { ...keyframes, transition }, {})); } } return animations; } function animateSequence(sequence, options, scope) { const animations = []; const animationDefinitions = createAnimationsFromSequence(sequence, options, scope, { spring: motionDom.spring }); animationDefinitions.forEach(({ keyframes, transition }, subject) => { animations.push(...animateSubject(subject, keyframes, transition)); }); return animations; } function isSequence(value) { return Array.isArray(value) && value.some(Array.isArray); } /** * Creates an animation function that is optionally scoped * to a specific element. */ function createScopedAnimate(scope) { /** * Implementation */ function scopedAnimate(subjectOrSequence, optionsOrKeyframes, options) { let animations = []; if (isSequence(subjectOrSequence)) { animations = animateSequence(subjectOrSequence, optionsOrKeyframes, scope); } else { animations = animateSubject(subjectOrSequence, optionsOrKeyframes, options, scope); } const animation = new motionDom.GroupAnimationWithThen(animations); if (scope) { scope.animations.push(animation); } return animation; } return scopedAnimate; } const animate = createScopedAnimate(); function animateElements(elementOrSelector, keyframes, options, scope) { const elements = motionDom.resolveElements(elementOrSelector, scope); const numElements = elements.length; motionUtils.invariant(Boolean(numElements), "No valid element provided."); /** * WAAPI doesn't support interrupting animations. * * Therefore, starting animations requires a three-step process: * 1. Stop existing animations (write styles to DOM) * 2. Resolve keyframes (read styles from DOM) * 3. Create new animations (write styles to DOM) * * The hybrid `animate()` function uses AsyncAnimation to resolve * keyframes before creating new animations, which removes style * thrashing. Here, we have much stricter filesize constraints. * Therefore we do this in a synchronous way that ensures that * at least within `animate()` calls there is no style thrashing. * * In the motion-native-animate-mini-interrupt benchmark this * was 80% faster than a single loop. */ const animationDefinitions = []; /** * Step 1: Build options and stop existing animations (write) */ for (let i = 0; i < numElements; i++) { const element = elements[i]; const elementTransition = { ...options }; /** * Resolve stagger function if provided. */ if (typeof elementTransition.delay === "function") { elementTransition.delay = elementTransition.delay(i, numElements); } for (const valueName in keyframes) { let valueKeyframes = keyframes[valueName]; if (!Array.isArray(valueKeyframes)) { valueKeyframes = [valueKeyframes]; } const valueOptions = { ...motionDom.getValueTransition(elementTransition, valueName), }; valueOptions.duration && (valueOptions.duration = motionUtils.secondsToMilliseconds(valueOptions.duration)); valueOptions.delay && (valueOptions.delay = motionUtils.secondsToMilliseconds(valueOptions.delay)); /** * If there's an existing animation playing on this element then stop it * before creating a new one. */ const map = motionDom.getAnimationMap(element); const key = motionDom.animationMapKey(valueName, valueOptions.pseudoElement || ""); const currentAnimation = map.get(key); currentAnimation && currentAnimation.stop(); animationDefinitions.push({ map, key, unresolvedKeyframes: valueKeyframes, options: { ...valueOptions, element, name: valueName, allowFlatten: !elementTransition.type && !elementTransition.ease, }, }); } } /** * Step 2: Resolve keyframes (read) */ for (let i = 0; i < animationDefinitions.length; i++) { const { unresolvedKeyframes, options: animationOptions } = animationDefinitions[i]; const { element, name, pseudoElement } = animationOptions; if (!pseudoElement && unresolvedKeyframes[0] === null) { unresolvedKeyframes[0] = motionDom.getComputedStyle(element, name); } motionDom.fillWildcards(unresolvedKeyframes); motionDom.applyPxDefaults(unresolvedKeyframes, name); /** * If we only have one keyframe, explicitly read the initial keyframe * from the computed style. This is to ensure consistency with WAAPI behaviour * for restarting animations, for instance .play() after finish, when it * has one vs two keyframes. */ if (!pseudoElement && unresolvedKeyframes.length < 2) { unresolvedKeyframes.unshift(motionDom.getComputedStyle(element, name)); } animationOptions.keyframes = unresolvedKeyframes; } /** * Step 3: Create new animations (write) */ const animations = []; for (let i = 0; i < animationDefinitions.length; i++) { const { map, key, options: animationOptions } = animationDefinitions[i]; const animation = new motionDom.NativeAnimation(animationOptions); map.set(key, animation); animation.finished.finally(() => map.delete(key)); animations.push(animation); } return animations; } const createScopedWaapiAnimate = (scope) => { function scopedAnimate(elementOrSelector, keyframes, options) { return new motionDom.GroupAnimationWithThen(animateElements(elementOrSelector, keyframes, options, scope)); } return scopedAnimate; }; const animateMini = /*@__PURE__*/ createScopedWaapiAnimate(); const resizeHandlers = new WeakMap(); let observer; function getElementSize(target, borderBoxSize) { if (borderBoxSize) { const { inlineSize, blockSize } = borderBoxSize[0]; return { width: inlineSize, height: blockSize }; } else if (motionDom.isSVGElement(target) && "getBBox" in target) { return target.getBBox(); } else { return { width: target.offsetWidth, height: target.offsetHeight, }; } } function notifyTarget({ target, contentRect, borderBoxSize, }) { resizeHandlers.get(target)?.forEach((handler) => { handler({ target, contentSize: contentRect, get size() { return getElementSize(target, borderBoxSize); }, }); }); } function notifyAll(entries) { entries.forEach(notifyTarget); } function createResizeObserver() { if (typeof ResizeObserver === "undefined") return; observer = new ResizeObserver(notifyAll); } function resizeElement(target, handler) { if (!observer) createResizeObserver(); const elements = motionDom.resolveElements(target); elements.forEach((element) => { let elementHandlers = resizeHandlers.get(element); if (!elementHandlers) { elementHandlers = new Set(); resizeHandlers.set(element, elementHandlers); } elementHandlers.add(handler); observer?.observe(element); }); return () => { elements.forEach((element) => { const elementHandlers = resizeHandlers.get(element); elementHandlers?.delete(handler); if (!elementHandlers?.size) { observer?.unobserve(element); } }); }; } const windowCallbacks = new Set(); let windowResizeHandler; function createWindowResizeHandler() { windowResizeHandler = () => { const size = { width: window.innerWidth, height: window.innerHeight, }; const info = { target: window, size, contentSize: size, }; windowCallbacks.forEach((callback) => callback(info)); }; window.addEventListener("resize", windowResizeHandler); } function resizeWindow(callback) { windowCallbacks.add(callback); if (!windowResizeHandler) createWindowResizeHandler(); return () => { windowCallbacks.delete(callback); if (!windowCallbacks.size && windowResizeHandler) { windowResizeHandler = undefined; } }; } function resize(a, b) { return typeof a === "function" ? resizeWindow(a) : resizeElement(a, b); } /** * A time in milliseconds, beyond which we consider the scroll velocity to be 0. */ const maxElapsed = 50; const createAxisInfo = () => ({ current: 0, offset: [], progress: 0, scrollLength: 0, targetOffset: 0, targetLength: 0, containerLength: 0, velocity: 0, }); const createScrollInfo = () => ({ time: 0, x: createAxisInfo(), y: createAxisInfo(), }); const keys = { x: { length: "Width", position: "Left", }, y: { length: "Height", position: "Top", }, }; function updateAxisInfo(element, axisName, info, time) { const axis = info[axisName]; const { length, position } = keys[axisName]; const prev = axis.current; const prevTime = info.time; axis.current = element[`scroll${position}`]; axis.scrollLength = element[`scroll${length}`] - element[`client${length}`]; axis.offset.length = 0; axis.offset[0] = 0; axis.offset[1] = axis.scrollLength; axis.progress = motionUtils.progress(0, axis.scrollLength, axis.current); const elapsed = time - prevTime; axis.velocity = elapsed > maxElapsed ? 0 : motionUtils.velocityPerSecond(axis.current - prev, elapsed); } function updateScrollInfo(element, info, time) { updateAxisInfo(element, "x", info, time); updateAxisInfo(element, "y", info, time); info.time = time; } function calcInset(element, container) { const inset = { x: 0, y: 0 }; let current = element; while (current && current !== container) { if (motionDom.isHTMLElement(current)) { inset.x += current.offsetLeft; inset.y += current.offsetTop; current = current.offsetParent; } else if (current.tagName === "svg") { /** * This isn't an ideal approach to measuring the offset of <svg /> tags. * It would be preferable, given they behave like HTMLElements in most ways * to use offsetLeft/Top. But these don't exist on <svg />. Likewise we * can't use .getBBox() like most SVG elements as these provide the offset * relative to the SVG itself, which for <svg /> is usually 0x0. */ const svgBoundingBox = current.getBoundingClientRect(); current = current.parentElement; const parentBoundingBox = current.getBoundingClientRect(); inset.x += svgBoundingBox.left - parentBoundingBox.left; inset.y += svgBoundingBox.top - parentBoundingBox.top; } else if (current instanceof SVGGraphicsElement) { const { x, y } = current.getBBox(); inset.x += x; inset.y += y; let svg = null; let parent = current.parentNode; while (!svg) { if (parent.tagName === "svg") { svg = parent; } parent = current.parentNode; } current = svg; } else { break; } } return inset; } const namedEdges = { start: 0, center: 0.5, end: 1, }; function resolveEdge(edge, length, inset = 0) { let delta = 0; /** * If we have this edge defined as a preset, replace the definition * with the numerical value. */ if (edge in namedEdges) { edge = namedEdges[edge]; } /** * Handle unit values */ if (typeof edge === "string") { const asNumber = parseFloat(edge); if (edge.endsWith("px")) { delta = asNumber; } else if (edge.endsWith("%")) { edge = asNumber / 100; } else if (edge.endsWith("vw")) { delta = (asNumber / 100) * document.documentElement.clientWidth; } else if (edge.endsWith("vh")) { delta = (asNumber / 100) * document.documentElement.clientHeight; } else { edge = asNumber; } } /** * If the edge is defined as a number, handle as a progress value. */ if (typeof edge === "number") { delta = length * edge; } return inset + delta; } const defaultOffset = [0, 0]; function resolveOffset(offset, containerLength, targetLength, targetInset) { let offsetDefinition = Array.isArray(offset) ? offset : defaultOffset; let targetPoint = 0; let containerPoint = 0; if (typeof offset === "number") { /** * If we're provided offset: [0, 0.5, 1] then each number x should become * [x, x], so we default to the behaviour of mapping 0 => 0 of both target * and container etc. */ offsetDefinition = [offset, offset]; } else if (typeof offset === "string") { offset = offset.trim(); if (offset.includes(" ")) { offsetDefinition = offset.split(" "); } else { /** * If we're provided a definition like "100px" then we want to apply * that only to the top of the target point, leaving the container at 0. * Whereas a named offset like "end" should be applied to both. */ offsetDefinition = [offset, namedEdges[offset] ? offset : `0`]; } } targetPoint = resolveEdge(offsetDefinition[0], targetLength, targetInset); containerPoint = resolveEdge(offsetDefinition[1], containerLength); return targetPoint - containerPoint; } const ScrollOffset = { Enter: [ [0, 1], [1, 1], ], Exit: [ [0, 0], [1, 0], ], Any: [ [1, 0], [0, 1], ], All: [ [0, 0], [1, 1], ], }; const point = { x: 0, y: 0 }; function getTargetSize(target) { return "getBBox" in target && target.tagName !== "svg" ? target.getBBox() : { width: target.clientWidth, height: target.clientHeight }; } function resolveOffsets(container, info, options) { const { offset: offsetDefinition = ScrollOffset.All } = options; const { target = container, axis = "y" } = options; const lengthLabel = axis === "y" ? "height" : "width"; const inset = target !== container ? calcInset(target, container) : point; /** * Measure the target and container. If they're the same thing then we * use the container's scrollWidth/Height as the target, from there * all other calculations can remain the same. */ const targetSize = target === container ? { width: container.scrollWidth, height: container.scrollHeight } : getTargetSize(target); const containerSize = { width: container.clientWidth, height: container.clientHeight, }; /** * Reset the length of the resolved offset array rather than creating a new one. * TODO: More reusable data structures for targetSize/containerSize would also be good. */ info[axis].offset.length = 0; /** * Populate the offset array by resolving the user's offset definition into * a list of pixel scroll offets. */ let hasChanged = !info[axis].interpolate; const numOffsets = offsetDefinition.length; for (let i = 0; i < numOffsets; i++) { const offset = resolveOffset(offsetDefinition[i], containerSize[lengthLabel], targetSize[lengthLabel], inset[axis]); if (!hasChanged && offset !== info[axis].interpolatorOffsets[i]) { hasChanged = true; } info[axis].offset[i] = offset; } /** * If the pixel scroll offsets have changed, create a new interpolator function * to map scroll value into a progress. */ if (hasChanged) { info[axis].interpolate = motionDom.interpolate(info[axis].offset, motionDom.defaultOffset(offsetDefinition), { clamp: false }); info[axis].interpolatorOffsets = [...info[axis].offset]; } info[axis].progress = motionUtils.clamp(0, 1, info[axis].interpolate(info[axis].current)); } function measure(container, target = container, info) { /** * Find inset of target within scrollable container */ info.x.targetOffset = 0; info.y.targetOffset = 0; if (target !== container) { let node = target; while (node && node !== container) { info.x.targetOffset += node.offsetLeft; info.y.targetOffset += node.offsetTop; node = node.offsetParent; } } info.x.targetLength = target === container ? target.scrollWidth : target.clientWidth; info.y.targetLength = target === container ? target.scrollHeight : target.clientHeight; info.x.containerLength = container.clientWidth; info.y.containerLength = container.clientHeight; /** * In development mode ensure scroll containers aren't position: static as this makes * it difficult to measure their relative positions. */ if (process.env.NODE_ENV !== "production") { if (container && target && target !== container) { motionUtils.warnOnce(getComputedStyle(container).position !== "static", "Please ensure that the container has a non-static position, like 'relative', 'fixed', or 'absolute' to ensure scroll offset is calculated correctly."); } } } function createOnScrollHandler(element, onScroll, info, options = {}) { return { measure: () => measure(element, options.target, info), update: (time) => { updateScrollInfo(element, info, time); if (options.offset || options.target) { resolveOffsets(element, info, options); } }, notify: () => onScroll(info), }; } const scrollListeners = new WeakMap(); const resizeListeners = new WeakMap(); const onScrollHandlers = new WeakMap(); const getEventTarget = (element) => element === document.scrollingElement ? window : element; function scrollInfo(onScroll, { container = document.scrollingElement, ...options } = {}) { if (!container) return motionUtils.noop; let containerHandlers = onScrollHandlers.get(container); /** * Get the onScroll handlers for this container. * If one isn't found, create a new one. */ if (!containerHandlers) { containerHandlers = new Set(); onScrollHandlers.set(container, containerHandlers); } /** * Create a new onScroll handler for the provided callback. */ const info = createScrollInfo(); const containerHandler = createOnScrollHandler(container, onScroll, info, options); containerHandlers.add(containerHandler); /** * Check if there's a scroll event listener for this container. * If not, create one. */ if (!scrollListeners.has(container)) { const measureAll = () => { for (const handler of containerHandlers) handler.measure(); }; const updateAll = () => { for (const handler of containerHandlers) { handler.update(motionDom.frameData.timestamp); } }; const notifyAll = () => { for (const handler of containerHandlers) handler.notify(); }; const listener = () => { motionDom.frame.read(measureAll); motionDom.frame.read(updateAll); motionDom.frame.preUpdate(notifyAll); }; scrollListeners.set(container, listener); const target = getEventTarget(container); window.addEventListener("resize", listener, { passive: true }); if (container !== document.documentElement) { resizeListeners.set(container, resize(container, listener)); } target.addEventListener("scroll", listener, { passive: true }); listener(); } const listener = scrollListeners.get(container); motionDom.frame.read(listener, false, true); return () => { motionDom.cancelFrame(listener); /** * Check if we even have any handlers for this container. */ const currentHandlers = onScrollHandlers.get(container); if (!currentHandlers) return; currentHandlers.delete(containerHandler); if (currentHandlers.size) return; /** * If no more handlers, remove the scroll listener too. */ const scrollListener = scrollListeners.get(container); scrollListeners.delete(container); if (scrollListener) { getEventTarget(container).removeEventListener("scroll", scrollListener); resizeListeners.get(container)?.(); window.removeEventListener("resize", scrollListener); } }; } const timelineCache = new Map(); function scrollTimelineFallback(options) { const currentTime = { value: 0 }; const cancel = scrollInfo((info) => { currentTime.value = info[options.axis].progress * 100; }, options); return { currentTime, cancel }; } function getTimeline({ source, container, ...options }) { const { axis } = options; if (source) container = source; const containerCache = timelineCache.get(container) ?? new Map(); timelineCache.set(container, containerCache); const targetKey = options.target ?? "self"; const targetCache = containerCache.get(targetKey) ?? {}; const axisKey = axis + (options.offset ?? []).join(","); if (!targetCache[axisKey]) { targetCache[axisKey] = !options.target && motionDom.supportsScrollTimeline() ? new ScrollTimeline({ source: container, axis }) : scrollTimelineFallback({ container, ...options }); } return targetCache[axisKey]; } function attachToAnimation(animation, options) { const timeline = getTimeline(options); return animation.attachTimeline({ timeline: options.target ? undefined : timeline, observe: (valueAnimation) => { valueAnimation.pause(); return motionDom.observeTimeline((progress) => { valueAnimation.time = valueAnimation.duration * progress; }, timeline); }, }); } /** * If the onScroll function has two arguments, it's expecting * more specific information about the scroll from scrollInfo. */ function isOnScrollWithInfo(onScroll) { return onScroll.length === 2; } function attachToFunction(onScroll, options) { if (isOnScrollWithInfo(onScroll)) { return scrollInfo((info) => { onScroll(info[options.axis].progress, info); }, options); } else { return motionDom.observeTimeline(onScroll, getTimeline(options)); } } function scroll(onScroll, { axis = "y", container = document.scrollingElement, ...options } = {}) { if (!container) return motionUtils.noop; const optionsWithDefaults = { axis, container, ...options }; return typeof onScroll === "function" ? attachToFunction(onScroll, optionsWithDefaults) : attachToAnimation(onScroll, optionsWithDefaults); } const thresholds = { some: 0, all: 1, }; function inView(elementOrSelector, onStart, { root, margin: rootMargin, amount = "some" } = {}) { const elements = motionDom.resolveElements(elementOrSelector); const activeIntersections = new WeakMap(); const onIntersectionChange = (entries) => { entries.forEach((entry) => { const onEnd = activeIntersections.get(entry.target); /** * If there's no change to the intersection, we don't need to * do anything here. */ if (entry.isIntersecting === Boolean(onEnd)) return; if (entry.isIntersecting) { const newOnEnd = onStart(entry.target, entry); if (typeof newOnEnd === "function") { activeIntersections.set(entry.target, newOnEnd); } else { observer.unobserve(entry.target); } } else if (typeof onEnd === "function") { onEnd(entry); activeIntersections.delete(entry.target); } }); }; const observer = new IntersectionObserver(onIntersectionChange, { root, rootMargin, threshold: typeof amount === "number" ? amount : thresholds[amount], }); elements.forEach((element) => observer.observe(element)); return () => observer.disconnect(); } function getOriginIndex(from, total) { if (from === "first") { return 0; } else { const lastIndex = total - 1; return from === "last" ? lastIndex : lastIndex / 2; } } function stagger(duration = 0.1, { startDelay = 0, from = 0, ease } = {}) { return (i, total) => { const fromIndex = typeof from === "number" ? from : getOriginIndex(from, total); const distance = Math.abs(fromIndex - i); let delay = duration * distance; if (ease) { const maxDelay = total * duration; const easingFunction = motionUtils.easingDefinitionToFunction(ease); delay = easingFunction(delay / maxDelay) * maxDelay; } return startDelay + delay; }; } /** * Timeout defined in ms */ function delay(callback, timeout) { const start = motionDom.time.now(); const checkElapsed = ({ timestamp }) => { const elapsed = timestamp - start; if (elapsed >= timeout) { motionDom.cancelFrame(checkElapsed); callback(elapsed - timeout); } }; motionDom.frame.setup(checkElapsed, true); return () => motionDom.cancelFrame(checkElapsed); } function delayInSeconds(callback, timeout) { return delay(callback, motionUtils.secondsToMilliseconds(timeout)); } const distance = (a, b) => Math.abs(a - b); function distance2D(a, b) { // Multi-dimensional const xDelta = distance(a.x, b.x); const yDelta = distance(a.y, b.y); return Math.sqrt(xDelta ** 2 + yDelta ** 2); } exports.animate = animate; exports.animateMini = animateMini; exports.createScopedAnimate = createScopedAnimate; exports.delay = delayInSeconds; exports.distance = distance; exports.distance2D = distance2D; exports.inView = inView; exports.scroll = scroll; exports.scrollInfo = scrollInfo; exports.stagger = stagger; Object.keys(motionDom).forEach(function (k) { if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, { enumerable: true, get: function () { return motionDom[k]; } }); }); Object.keys(motionUtils).forEach(function (k) { if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, { enumerable: true, get: function () { return motionUtils[k]; } }); });