securityos/node_modules/framer-motion/dist/es/render/utils/animation-state.mjs

313 lines
14 KiB
JavaScript

import { isAnimationControls } from '../../animation/utils/is-animation-controls.mjs';
import { isKeyframesTarget } from '../../animation/utils/is-keyframes-target.mjs';
import { shallowCompare } from '../../utils/shallow-compare.mjs';
import { isVariantLabel } from './is-variant-label.mjs';
import { resolveVariant } from './resolve-dynamic-variants.mjs';
import { variantPriorityOrder } from './variant-props.mjs';
import { animateVisualElement } from '../../animation/interfaces/visual-element.mjs';
const reversePriorityOrder = [...variantPriorityOrder].reverse();
const numAnimationTypes = variantPriorityOrder.length;
function animateList(visualElement) {
return (animations) => Promise.all(animations.map(({ animation, options }) => animateVisualElement(visualElement, animation, options)));
}
function createAnimationState(visualElement) {
let animate = animateList(visualElement);
const state = createState();
let isInitialRender = true;
/**
* This function will be used to reduce the animation definitions for
* each active animation type into an object of resolved values for it.
*/
const buildResolvedTypeValues = (acc, definition) => {
const resolved = resolveVariant(visualElement, definition);
if (resolved) {
const { transition, transitionEnd, ...target } = resolved;
acc = { ...acc, ...target, ...transitionEnd };
}
return acc;
};
/**
* This just allows us to inject mocked animation functions
* @internal
*/
function setAnimateFunction(makeAnimator) {
animate = makeAnimator(visualElement);
}
/**
* When we receive new props, we need to:
* 1. Create a list of protected keys for each type. This is a directory of
* value keys that are currently being "handled" by types of a higher priority
* so that whenever an animation is played of a given type, these values are
* protected from being animated.
* 2. Determine if an animation type needs animating.
* 3. Determine if any values have been removed from a type and figure out
* what to animate those to.
*/
function animateChanges(options, changedActiveType) {
const props = visualElement.getProps();
const context = visualElement.getVariantContext(true) || {};
/**
* A list of animations that we'll build into as we iterate through the animation
* types. This will get executed at the end of the function.
*/
const animations = [];
/**
* Keep track of which values have been removed. Then, as we hit lower priority
* animation types, we can check if they contain removed values and animate to that.
*/
const removedKeys = new Set();
/**
* A dictionary of all encountered keys. This is an object to let us build into and
* copy it without iteration. Each time we hit an animation type we set its protected
* keys - the keys its not allowed to animate - to the latest version of this object.
*/
let encounteredKeys = {};
/**
* If a variant has been removed at a given index, and this component is controlling
* variant animations, we want to ensure lower-priority variants are forced to animate.
*/
let removedVariantIndex = Infinity;
/**
* Iterate through all animation types in reverse priority order. For each, we want to
* detect which values it's handling and whether or not they've changed (and therefore
* need to be animated). If any values have been removed, we want to detect those in
* lower priority props and flag for animation.
*/
for (let i = 0; i < numAnimationTypes; i++) {
const type = reversePriorityOrder[i];
const typeState = state[type];
const prop = props[type] !== undefined ? props[type] : context[type];
const propIsVariant = isVariantLabel(prop);
/**
* If this type has *just* changed isActive status, set activeDelta
* to that status. Otherwise set to null.
*/
const activeDelta = type === changedActiveType ? typeState.isActive : null;
if (activeDelta === false)
removedVariantIndex = i;
/**
* If this prop is an inherited variant, rather than been set directly on the
* component itself, we want to make sure we allow the parent to trigger animations.
*
* TODO: Can probably change this to a !isControllingVariants check
*/
let isInherited = prop === context[type] && prop !== props[type] && propIsVariant;
/**
*
*/
if (isInherited &&
isInitialRender &&
visualElement.manuallyAnimateOnMount) {
isInherited = false;
}
/**
* Set all encountered keys so far as the protected keys for this type. This will
* be any key that has been animated or otherwise handled by active, higher-priortiy types.
*/
typeState.protectedKeys = { ...encounteredKeys };
// Check if we can skip analysing this prop early
if (
// If it isn't active and hasn't *just* been set as inactive
(!typeState.isActive && activeDelta === null) ||
// If we didn't and don't have any defined prop for this animation type
(!prop && !typeState.prevProp) ||
// Or if the prop doesn't define an animation
isAnimationControls(prop) ||
typeof prop === "boolean") {
continue;
}
/**
* As we go look through the values defined on this type, if we detect
* a changed value or a value that was removed in a higher priority, we set
* this to true and add this prop to the animation list.
*/
const variantDidChange = checkVariantsDidChange(typeState.prevProp, prop);
let shouldAnimateType = variantDidChange ||
// If we're making this variant active, we want to always make it active
(type === changedActiveType &&
typeState.isActive &&
!isInherited &&
propIsVariant) ||
// If we removed a higher-priority variant (i is in reverse order)
(i > removedVariantIndex && propIsVariant);
let handledRemovedValues = false;
/**
* As animations can be set as variant lists, variants or target objects, we
* coerce everything to an array if it isn't one already
*/
const definitionList = Array.isArray(prop) ? prop : [prop];
/**
* Build an object of all the resolved values. We'll use this in the subsequent
* animateChanges calls to determine whether a value has changed.
*/
let resolvedValues = definitionList.reduce(buildResolvedTypeValues, {});
if (activeDelta === false)
resolvedValues = {};
/**
* Now we need to loop through all the keys in the prev prop and this prop,
* and decide:
* 1. If the value has changed, and needs animating
* 2. If it has been removed, and needs adding to the removedKeys set
* 3. If it has been removed in a higher priority type and needs animating
* 4. If it hasn't been removed in a higher priority but hasn't changed, and
* needs adding to the type's protectedKeys list.
*/
const { prevResolvedValues = {} } = typeState;
const allKeys = {
...prevResolvedValues,
...resolvedValues,
};
const markToAnimate = (key) => {
shouldAnimateType = true;
if (removedKeys.has(key)) {
handledRemovedValues = true;
removedKeys.delete(key);
}
typeState.needsAnimating[key] = true;
};
for (const key in allKeys) {
const next = resolvedValues[key];
const prev = prevResolvedValues[key];
// If we've already handled this we can just skip ahead
if (encounteredKeys.hasOwnProperty(key))
continue;
/**
* If the value has changed, we probably want to animate it.
*/
let valueHasChanged = false;
if (isKeyframesTarget(next) && isKeyframesTarget(prev)) {
valueHasChanged = !shallowCompare(next, prev);
}
else {
valueHasChanged = next !== prev;
}
if (valueHasChanged) {
if (next !== undefined) {
// If next is defined and doesn't equal prev, it needs animating
markToAnimate(key);
}
else {
// If it's undefined, it's been removed.
removedKeys.add(key);
}
}
else if (next !== undefined && removedKeys.has(key)) {
/**
* If next hasn't changed and it isn't undefined, we want to check if it's
* been removed by a higher priority
*/
markToAnimate(key);
}
else {
/**
* If it hasn't changed, we add it to the list of protected values
* to ensure it doesn't get animated.
*/
typeState.protectedKeys[key] = true;
}
}
/**
* Update the typeState so next time animateChanges is called we can compare the
* latest prop and resolvedValues to these.
*/
typeState.prevProp = prop;
typeState.prevResolvedValues = resolvedValues;
/**
*
*/
if (typeState.isActive) {
encounteredKeys = { ...encounteredKeys, ...resolvedValues };
}
if (isInitialRender && visualElement.blockInitialAnimation) {
shouldAnimateType = false;
}
/**
* If this is an inherited prop we want to hard-block animations
*/
if (shouldAnimateType && (!isInherited || handledRemovedValues)) {
animations.push(...definitionList.map((animation) => ({
animation: animation,
options: { type, ...options },
})));
}
}
/**
* If there are some removed value that haven't been dealt with,
* we need to create a new animation that falls back either to the value
* defined in the style prop, or the last read value.
*/
if (removedKeys.size) {
const fallbackAnimation = {};
removedKeys.forEach((key) => {
const fallbackTarget = visualElement.getBaseTarget(key);
if (fallbackTarget !== undefined) {
fallbackAnimation[key] = fallbackTarget;
}
});
animations.push({ animation: fallbackAnimation });
}
let shouldAnimate = Boolean(animations.length);
if (isInitialRender &&
(props.initial === false || props.initial === props.animate) &&
!visualElement.manuallyAnimateOnMount) {
shouldAnimate = false;
}
isInitialRender = false;
return shouldAnimate ? animate(animations) : Promise.resolve();
}
/**
* Change whether a certain animation type is active.
*/
function setActive(type, isActive, options) {
var _a;
// If the active state hasn't changed, we can safely do nothing here
if (state[type].isActive === isActive)
return Promise.resolve();
// Propagate active change to children
(_a = visualElement.variantChildren) === null || _a === void 0 ? void 0 : _a.forEach((child) => { var _a; return (_a = child.animationState) === null || _a === void 0 ? void 0 : _a.setActive(type, isActive); });
state[type].isActive = isActive;
const animations = animateChanges(options, type);
for (const key in state) {
state[key].protectedKeys = {};
}
return animations;
}
return {
animateChanges,
setActive,
setAnimateFunction,
getState: () => state,
};
}
function checkVariantsDidChange(prev, next) {
if (typeof next === "string") {
return next !== prev;
}
else if (Array.isArray(next)) {
return !shallowCompare(next, prev);
}
return false;
}
function createTypeState(isActive = false) {
return {
isActive,
protectedKeys: {},
needsAnimating: {},
prevResolvedValues: {},
};
}
function createState() {
return {
animate: createTypeState(true),
whileInView: createTypeState(),
whileHover: createTypeState(),
whileTap: createTypeState(),
whileDrag: createTypeState(),
whileFocus: createTypeState(),
exit: createTypeState(),
};
}
export { checkVariantsDidChange, createAnimationState };