| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- import { animateVisualElement } from '../../animation/interfaces/visual-element.mjs';
- 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 { getVariantContext } from './get-variant-context.mjs';
- import { isVariantLabel } from './is-variant-label.mjs';
- import { resolveVariant } from './resolve-dynamic-variants.mjs';
- import { variantPriorityOrder } from './variant-props.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);
- let 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 = (type) => (acc, definition) => {
- const resolved = resolveVariant(visualElement, definition, type === "exit"
- ? visualElement.presenceContext?.custom
- : undefined);
- 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(changedActiveType) {
- const { props } = visualElement;
- const context = getVariantContext(visualElement.parent) || {};
- /**
- * 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(type), {});
- 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;
- const motionValue = visualElement.getValue(key);
- if (motionValue)
- motionValue.liveStyle = false;
- };
- 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 && next !== null) {
- // 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 skip this animation
- * unless the inherited variants haven't changed on this render.
- */
- const willAnimateViaParent = isInherited && variantDidChange;
- const needsAnimating = !willAnimateViaParent || handledRemovedValues;
- if (shouldAnimateType && needsAnimating) {
- animations.push(...definitionList.map((animation) => ({
- animation: animation,
- options: { type },
- })));
- }
- }
- /**
- * 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 = {};
- /**
- * If the initial prop contains a transition we can use that, otherwise
- * allow the animation function to use the visual element's default.
- */
- if (typeof props.initial !== "boolean") {
- const initialTransition = resolveVariant(visualElement, Array.isArray(props.initial)
- ? props.initial[0]
- : props.initial);
- if (initialTransition && initialTransition.transition) {
- fallbackAnimation.transition = initialTransition.transition;
- }
- }
- removedKeys.forEach((key) => {
- const fallbackTarget = visualElement.getBaseTarget(key);
- const motionValue = visualElement.getValue(key);
- if (motionValue)
- motionValue.liveStyle = true;
- // @ts-expect-error - @mattgperry to figure if we should do something here
- fallbackAnimation[key] = fallbackTarget ?? null;
- });
- 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) {
- // 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
- visualElement.variantChildren?.forEach((child) => child.animationState?.setActive(type, isActive));
- state[type].isActive = isActive;
- const animations = animateChanges(type);
- for (const key in state) {
- state[key].protectedKeys = {};
- }
- return animations;
- }
- return {
- animateChanges,
- setActive,
- setAnimateFunction,
- getState: () => state,
- reset: () => {
- state = createState();
- isInitialRender = true;
- },
- };
- }
- 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 };
|