Files
Basilosaurusrex f027651f9b main repo
2025-11-24 18:09:40 +01:00

2493 lines
89 KiB
JavaScript

'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 if (next.startsWith("<")) {
return Math.max(0, prev + parseFloat(next.slice(1)));
}
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 || "keyframes"];
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", "repeat-count-high");
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, 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)) {
motionDom.makeAnimationInstant(options);
if (options.delay === 0) {
shouldSkip = true;
}
}
if (motionUtils.MotionGlobalConfig.instantAnimations ||
motionUtils.MotionGlobalConfig.skipAnimations) {
shouldSkip = true;
motionDom.makeAnimationInstant(options);
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.addEventListener("change", 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]);
}
}
}
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.", "reduced-motion-disabled");
}
this.parent?.addChild(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?.removeChild(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;
}
addChild(child) {
this.children.add(child);
this.enteringChildren ?? (this.enteringChildren = new Set());
this.enteringChildren.add(child);
}
removeChild(child) {
this.children.delete(child);
this.enteringChildren && this.enteringChildren.delete(child);
}
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;
}
this.scheduleRender();
});
let removeSyncCheck;
if (window.MotionCheckAppearSync) {
removeSyncCheck = window.MotionCheckAppearSync(this, key, value);
}
this.valueSubscriptions.set(key, () => {
removeOnChange();
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);
}
}
scheduleRenderMicrotask() {
motionDom.microtask.render(this.render);
}
}
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) {
const elementStyle = element.style;
let key;
for (key in style) {
// CSSStyleDeclaration has [index: number]: string; in the types, so we use that as key type.
elementStyle[key] = style[key];
}
// Write projection styles directly to element style
projection?.applyProjectionStyles(elementStyle, styleProp);
for (key in vars) {
// Loop over any CSS variables and assign those.
// They can only be assigned using `setProperty`.
elementStyle.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 attributes, 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.", "no-valid-elements");
for (let i = 0; i < numSubjects; i++) {
const thisSubject = subjects[i];
motionUtils.invariant(thisSubject !== null, "You're trying to perform an animation on null. Ensure that selectors are correctly finding elements and refs are correctly hydrated.", "animate-null");
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 = [];
let animationOnComplete;
if (isSequence(subjectOrSequence)) {
animations = animateSequence(subjectOrSequence, optionsOrKeyframes, scope);
}
else {
// Extract top-level onComplete so it doesn't get applied per-value
const { onComplete, ...rest } = options || {};
if (typeof onComplete === "function") {
animationOnComplete = onComplete;
}
animations = animateSubject(subjectOrSequence, optionsOrKeyframes, rest, scope);
}
const animation = new motionDom.GroupAnimationWithThen(animations);
if (animationOnComplete) {
animation.finished.then(animationOnComplete);
}
if (scope) {
scope.animations.push(animation);
animation.finished.then(() => {
motionUtils.removeItem(scope.animations, 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 elements provided.", "no-valid-elements");
/**
* 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();
/**
* 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: (time) => {
measure(element, options.target, info);
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(motionDom.frameData.timestamp);
}
motionDom.frame.preUpdate(notifyAll);
};
const notifyAll = () => {
for (const handler of containerHandlers) {
handler.notify();
}
};
const listener = () => motionDom.frame.read(measureAll);
scrollListeners.set(container, listener);
const target = getEventTarget(container);
window.addEventListener("resize", listener, { passive: true });
if (container !== document.documentElement) {
resizeListeners.set(container, motionDom.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.iterationDuration * 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();
}
/**
* 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;
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]; }
});
});