animation-state.mjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import { animateVisualElement } from '../../animation/interfaces/visual-element.mjs';
  2. import { isAnimationControls } from '../../animation/utils/is-animation-controls.mjs';
  3. import { isKeyframesTarget } from '../../animation/utils/is-keyframes-target.mjs';
  4. import { shallowCompare } from '../../utils/shallow-compare.mjs';
  5. import { getVariantContext } from './get-variant-context.mjs';
  6. import { isVariantLabel } from './is-variant-label.mjs';
  7. import { resolveVariant } from './resolve-dynamic-variants.mjs';
  8. import { variantPriorityOrder } from './variant-props.mjs';
  9. const reversePriorityOrder = [...variantPriorityOrder].reverse();
  10. const numAnimationTypes = variantPriorityOrder.length;
  11. function animateList(visualElement) {
  12. return (animations) => Promise.all(animations.map(({ animation, options }) => animateVisualElement(visualElement, animation, options)));
  13. }
  14. function createAnimationState(visualElement) {
  15. let animate = animateList(visualElement);
  16. let state = createState();
  17. let isInitialRender = true;
  18. /**
  19. * This function will be used to reduce the animation definitions for
  20. * each active animation type into an object of resolved values for it.
  21. */
  22. const buildResolvedTypeValues = (type) => (acc, definition) => {
  23. const resolved = resolveVariant(visualElement, definition, type === "exit"
  24. ? visualElement.presenceContext?.custom
  25. : undefined);
  26. if (resolved) {
  27. const { transition, transitionEnd, ...target } = resolved;
  28. acc = { ...acc, ...target, ...transitionEnd };
  29. }
  30. return acc;
  31. };
  32. /**
  33. * This just allows us to inject mocked animation functions
  34. * @internal
  35. */
  36. function setAnimateFunction(makeAnimator) {
  37. animate = makeAnimator(visualElement);
  38. }
  39. /**
  40. * When we receive new props, we need to:
  41. * 1. Create a list of protected keys for each type. This is a directory of
  42. * value keys that are currently being "handled" by types of a higher priority
  43. * so that whenever an animation is played of a given type, these values are
  44. * protected from being animated.
  45. * 2. Determine if an animation type needs animating.
  46. * 3. Determine if any values have been removed from a type and figure out
  47. * what to animate those to.
  48. */
  49. function animateChanges(changedActiveType) {
  50. const { props } = visualElement;
  51. const context = getVariantContext(visualElement.parent) || {};
  52. /**
  53. * A list of animations that we'll build into as we iterate through the animation
  54. * types. This will get executed at the end of the function.
  55. */
  56. const animations = [];
  57. /**
  58. * Keep track of which values have been removed. Then, as we hit lower priority
  59. * animation types, we can check if they contain removed values and animate to that.
  60. */
  61. const removedKeys = new Set();
  62. /**
  63. * A dictionary of all encountered keys. This is an object to let us build into and
  64. * copy it without iteration. Each time we hit an animation type we set its protected
  65. * keys - the keys its not allowed to animate - to the latest version of this object.
  66. */
  67. let encounteredKeys = {};
  68. /**
  69. * If a variant has been removed at a given index, and this component is controlling
  70. * variant animations, we want to ensure lower-priority variants are forced to animate.
  71. */
  72. let removedVariantIndex = Infinity;
  73. /**
  74. * Iterate through all animation types in reverse priority order. For each, we want to
  75. * detect which values it's handling and whether or not they've changed (and therefore
  76. * need to be animated). If any values have been removed, we want to detect those in
  77. * lower priority props and flag for animation.
  78. */
  79. for (let i = 0; i < numAnimationTypes; i++) {
  80. const type = reversePriorityOrder[i];
  81. const typeState = state[type];
  82. const prop = props[type] !== undefined
  83. ? props[type]
  84. : context[type];
  85. const propIsVariant = isVariantLabel(prop);
  86. /**
  87. * If this type has *just* changed isActive status, set activeDelta
  88. * to that status. Otherwise set to null.
  89. */
  90. const activeDelta = type === changedActiveType ? typeState.isActive : null;
  91. if (activeDelta === false)
  92. removedVariantIndex = i;
  93. /**
  94. * If this prop is an inherited variant, rather than been set directly on the
  95. * component itself, we want to make sure we allow the parent to trigger animations.
  96. *
  97. * TODO: Can probably change this to a !isControllingVariants check
  98. */
  99. let isInherited = prop === context[type] &&
  100. prop !== props[type] &&
  101. propIsVariant;
  102. /**
  103. *
  104. */
  105. if (isInherited &&
  106. isInitialRender &&
  107. visualElement.manuallyAnimateOnMount) {
  108. isInherited = false;
  109. }
  110. /**
  111. * Set all encountered keys so far as the protected keys for this type. This will
  112. * be any key that has been animated or otherwise handled by active, higher-priortiy types.
  113. */
  114. typeState.protectedKeys = { ...encounteredKeys };
  115. // Check if we can skip analysing this prop early
  116. if (
  117. // If it isn't active and hasn't *just* been set as inactive
  118. (!typeState.isActive && activeDelta === null) ||
  119. // If we didn't and don't have any defined prop for this animation type
  120. (!prop && !typeState.prevProp) ||
  121. // Or if the prop doesn't define an animation
  122. isAnimationControls(prop) ||
  123. typeof prop === "boolean") {
  124. continue;
  125. }
  126. /**
  127. * As we go look through the values defined on this type, if we detect
  128. * a changed value or a value that was removed in a higher priority, we set
  129. * this to true and add this prop to the animation list.
  130. */
  131. const variantDidChange = checkVariantsDidChange(typeState.prevProp, prop);
  132. let shouldAnimateType = variantDidChange ||
  133. // If we're making this variant active, we want to always make it active
  134. (type === changedActiveType &&
  135. typeState.isActive &&
  136. !isInherited &&
  137. propIsVariant) ||
  138. // If we removed a higher-priority variant (i is in reverse order)
  139. (i > removedVariantIndex && propIsVariant);
  140. let handledRemovedValues = false;
  141. /**
  142. * As animations can be set as variant lists, variants or target objects, we
  143. * coerce everything to an array if it isn't one already
  144. */
  145. const definitionList = Array.isArray(prop) ? prop : [prop];
  146. /**
  147. * Build an object of all the resolved values. We'll use this in the subsequent
  148. * animateChanges calls to determine whether a value has changed.
  149. */
  150. let resolvedValues = definitionList.reduce(buildResolvedTypeValues(type), {});
  151. if (activeDelta === false)
  152. resolvedValues = {};
  153. /**
  154. * Now we need to loop through all the keys in the prev prop and this prop,
  155. * and decide:
  156. * 1. If the value has changed, and needs animating
  157. * 2. If it has been removed, and needs adding to the removedKeys set
  158. * 3. If it has been removed in a higher priority type and needs animating
  159. * 4. If it hasn't been removed in a higher priority but hasn't changed, and
  160. * needs adding to the type's protectedKeys list.
  161. */
  162. const { prevResolvedValues = {} } = typeState;
  163. const allKeys = {
  164. ...prevResolvedValues,
  165. ...resolvedValues,
  166. };
  167. const markToAnimate = (key) => {
  168. shouldAnimateType = true;
  169. if (removedKeys.has(key)) {
  170. handledRemovedValues = true;
  171. removedKeys.delete(key);
  172. }
  173. typeState.needsAnimating[key] = true;
  174. const motionValue = visualElement.getValue(key);
  175. if (motionValue)
  176. motionValue.liveStyle = false;
  177. };
  178. for (const key in allKeys) {
  179. const next = resolvedValues[key];
  180. const prev = prevResolvedValues[key];
  181. // If we've already handled this we can just skip ahead
  182. if (encounteredKeys.hasOwnProperty(key))
  183. continue;
  184. /**
  185. * If the value has changed, we probably want to animate it.
  186. */
  187. let valueHasChanged = false;
  188. if (isKeyframesTarget(next) && isKeyframesTarget(prev)) {
  189. valueHasChanged = !shallowCompare(next, prev);
  190. }
  191. else {
  192. valueHasChanged = next !== prev;
  193. }
  194. if (valueHasChanged) {
  195. if (next !== undefined && next !== null) {
  196. // If next is defined and doesn't equal prev, it needs animating
  197. markToAnimate(key);
  198. }
  199. else {
  200. // If it's undefined, it's been removed.
  201. removedKeys.add(key);
  202. }
  203. }
  204. else if (next !== undefined && removedKeys.has(key)) {
  205. /**
  206. * If next hasn't changed and it isn't undefined, we want to check if it's
  207. * been removed by a higher priority
  208. */
  209. markToAnimate(key);
  210. }
  211. else {
  212. /**
  213. * If it hasn't changed, we add it to the list of protected values
  214. * to ensure it doesn't get animated.
  215. */
  216. typeState.protectedKeys[key] = true;
  217. }
  218. }
  219. /**
  220. * Update the typeState so next time animateChanges is called we can compare the
  221. * latest prop and resolvedValues to these.
  222. */
  223. typeState.prevProp = prop;
  224. typeState.prevResolvedValues = resolvedValues;
  225. /**
  226. *
  227. */
  228. if (typeState.isActive) {
  229. encounteredKeys = { ...encounteredKeys, ...resolvedValues };
  230. }
  231. if (isInitialRender && visualElement.blockInitialAnimation) {
  232. shouldAnimateType = false;
  233. }
  234. /**
  235. * If this is an inherited prop we want to skip this animation
  236. * unless the inherited variants haven't changed on this render.
  237. */
  238. const willAnimateViaParent = isInherited && variantDidChange;
  239. const needsAnimating = !willAnimateViaParent || handledRemovedValues;
  240. if (shouldAnimateType && needsAnimating) {
  241. animations.push(...definitionList.map((animation) => ({
  242. animation: animation,
  243. options: { type },
  244. })));
  245. }
  246. }
  247. /**
  248. * If there are some removed value that haven't been dealt with,
  249. * we need to create a new animation that falls back either to the value
  250. * defined in the style prop, or the last read value.
  251. */
  252. if (removedKeys.size) {
  253. const fallbackAnimation = {};
  254. /**
  255. * If the initial prop contains a transition we can use that, otherwise
  256. * allow the animation function to use the visual element's default.
  257. */
  258. if (typeof props.initial !== "boolean") {
  259. const initialTransition = resolveVariant(visualElement, Array.isArray(props.initial)
  260. ? props.initial[0]
  261. : props.initial);
  262. if (initialTransition && initialTransition.transition) {
  263. fallbackAnimation.transition = initialTransition.transition;
  264. }
  265. }
  266. removedKeys.forEach((key) => {
  267. const fallbackTarget = visualElement.getBaseTarget(key);
  268. const motionValue = visualElement.getValue(key);
  269. if (motionValue)
  270. motionValue.liveStyle = true;
  271. // @ts-expect-error - @mattgperry to figure if we should do something here
  272. fallbackAnimation[key] = fallbackTarget ?? null;
  273. });
  274. animations.push({ animation: fallbackAnimation });
  275. }
  276. let shouldAnimate = Boolean(animations.length);
  277. if (isInitialRender &&
  278. (props.initial === false || props.initial === props.animate) &&
  279. !visualElement.manuallyAnimateOnMount) {
  280. shouldAnimate = false;
  281. }
  282. isInitialRender = false;
  283. return shouldAnimate ? animate(animations) : Promise.resolve();
  284. }
  285. /**
  286. * Change whether a certain animation type is active.
  287. */
  288. function setActive(type, isActive) {
  289. // If the active state hasn't changed, we can safely do nothing here
  290. if (state[type].isActive === isActive)
  291. return Promise.resolve();
  292. // Propagate active change to children
  293. visualElement.variantChildren?.forEach((child) => child.animationState?.setActive(type, isActive));
  294. state[type].isActive = isActive;
  295. const animations = animateChanges(type);
  296. for (const key in state) {
  297. state[key].protectedKeys = {};
  298. }
  299. return animations;
  300. }
  301. return {
  302. animateChanges,
  303. setActive,
  304. setAnimateFunction,
  305. getState: () => state,
  306. reset: () => {
  307. state = createState();
  308. isInitialRender = true;
  309. },
  310. };
  311. }
  312. function checkVariantsDidChange(prev, next) {
  313. if (typeof next === "string") {
  314. return next !== prev;
  315. }
  316. else if (Array.isArray(next)) {
  317. return !shallowCompare(next, prev);
  318. }
  319. return false;
  320. }
  321. function createTypeState(isActive = false) {
  322. return {
  323. isActive,
  324. protectedKeys: {},
  325. needsAnimating: {},
  326. prevResolvedValues: {},
  327. };
  328. }
  329. function createState() {
  330. return {
  331. animate: createTypeState(true),
  332. whileInView: createTypeState(),
  333. whileHover: createTypeState(),
  334. whileTap: createTypeState(),
  335. whileDrag: createTypeState(),
  336. whileFocus: createTypeState(),
  337. exit: createTypeState(),
  338. };
  339. }
  340. export { checkVariantsDidChange, createAnimationState };