framer-motion 8.5.0 → 8.5.2-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/index.js CHANGED
@@ -90,7 +90,7 @@ function useVisualElement(Component, visualState, props, createVisualElement) {
90
90
  * So if we detect a situtation where optimised appear animations
91
91
  * are running, we use useLayoutEffect to trigger animations.
92
92
  */
93
- const useAnimateChangesEffect = window.MotionAppearAnimations
93
+ const useAnimateChangesEffect = window.HandoffAppearAnimations
94
94
  ? useIsomorphicLayoutEffect
95
95
  : React.useEffect;
96
96
  useAnimateChangesEffect(() => {
@@ -2105,7 +2105,7 @@ class MotionValue {
2105
2105
  * This will be replaced by the build step with the latest version number.
2106
2106
  * When MotionValues are provided to motion components, warn if versions are mixed.
2107
2107
  */
2108
- this.version = "8.5.0";
2108
+ this.version = "8.5.2-alpha.1";
2109
2109
  /**
2110
2110
  * Duration, in milliseconds, since last updating frame.
2111
2111
  *
@@ -2799,54 +2799,6 @@ function isWillChangeMotionValue(value) {
2799
2799
  return Boolean(isMotionValue(value) && value.add);
2800
2800
  }
2801
2801
 
2802
- const appearStoreId = (id, value) => `${id}: ${value}`;
2803
-
2804
- function handoffOptimizedAppearAnimation(id, name, value) {
2805
- const { MotionAppearAnimations } = window;
2806
- const animationId = appearStoreId(id, transformProps.has(name) ? "transform" : name);
2807
- const animation = MotionAppearAnimations && MotionAppearAnimations.get(animationId);
2808
- if (animation) {
2809
- const sampledTime = performance.now();
2810
- /**
2811
- * Resync handoff animation with optimised animation.
2812
- *
2813
- * This step would be unnecessary if we triggered animateChanges() in useEffect,
2814
- * but due to potential hydration errors we currently fire them in useLayoutEffect.
2815
- *
2816
- * By the time we're safely ready to cancel the optimised WAAPI animation,
2817
- * the main thread might have been blocked and desynced the two animations.
2818
- *
2819
- * Here, we resync the two animations before the optimised WAAPI animation is cancelled.
2820
- */
2821
- sync.update(() => {
2822
- if (value.animation) {
2823
- value.animation.currentTime = performance.now() - sampledTime;
2824
- }
2825
- });
2826
- /**
2827
- * We allow the animation to persist until the next frame:
2828
- * 1. So it continues to play until Framer Motion is ready to render
2829
- * (avoiding a potential flash of the element's original state)
2830
- * 2. As all independent transforms share a single transform animation, stopping
2831
- * it synchronously would prevent subsequent transforms from handing off.
2832
- */
2833
- sync.render(() => {
2834
- MotionAppearAnimations.delete(animationId);
2835
- /**
2836
- * Animation.cancel() throws so it needs to be wrapped in a try/catch
2837
- */
2838
- try {
2839
- animation.cancel();
2840
- }
2841
- catch (e) { }
2842
- });
2843
- return animation.currentTime || 0;
2844
- }
2845
- else {
2846
- return 0;
2847
- }
2848
- }
2849
-
2850
2802
  const optimizedAppearDataId = "framerAppearId";
2851
2803
  const optimizedAppearDataAttribute = "data-" + camelToDash(optimizedAppearDataId);
2852
2804
 
@@ -3384,7 +3336,7 @@ const velocitySampleDuration = 5;
3384
3336
  /**
3385
3337
  * This is based on the spring implementation of Wobble https://github.com/skevy/wobble
3386
3338
  */
3387
- function spring({ keyframes, restSpeed = 2, restDelta = 0.01, ...options }) {
3339
+ function spring({ keyframes, restDelta, restSpeed, ...options }) {
3388
3340
  let origin = keyframes[0];
3389
3341
  let target = keyframes[keyframes.length - 1];
3390
3342
  /**
@@ -3400,12 +3352,15 @@ function spring({ keyframes, restSpeed = 2, restDelta = 0.01, ...options }) {
3400
3352
  const initialDelta = target - origin;
3401
3353
  const undampedAngularFreq = Math.sqrt(stiffness / mass) / 1000;
3402
3354
  /**
3403
- * If we're working within what looks like a 0-1 range, change the default restDelta
3404
- * to 0.01
3355
+ * If we're working on a granular scale, use smaller defaults for determining
3356
+ * when the spring is finished.
3357
+ *
3358
+ * These defaults have been selected emprically based on what strikes a good
3359
+ * ratio between feeling good and finishing as soon as changes are imperceptible.
3405
3360
  */
3406
- if (restDelta === undefined) {
3407
- restDelta = Math.min(Math.abs(target - origin) / 100, 0.4);
3408
- }
3361
+ const isGranularScale = Math.abs(initialDelta) < 5;
3362
+ restSpeed || (restSpeed = isGranularScale ? 0.01 : 2);
3363
+ restDelta || (restDelta = isGranularScale ? 0.005 : 0.5);
3409
3364
  if (dampingRatio < 1) {
3410
3365
  const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio);
3411
3366
  // Underdamped spring
@@ -4245,10 +4200,10 @@ function animateTarget(visualElement, definition, { delay = 0, transitionOverrid
4245
4200
  * If this is the first time a value is being animated, check
4246
4201
  * to see if we're handling off from an existing animation.
4247
4202
  */
4248
- if (!value.hasAnimated) {
4203
+ if (!value.hasAnimated && window.HandoffAppearAnimations) {
4249
4204
  const appearId = visualElement.getProps()[optimizedAppearDataAttribute];
4250
4205
  if (appearId) {
4251
- valueTransition.elapsed = handoffOptimizedAppearAnimation(appearId, key, value);
4206
+ valueTransition.elapsed = window.HandoffAppearAnimations(appearId, key, value, sync);
4252
4207
  }
4253
4208
  }
4254
4209
  let animation = value.start(createMotionValueAnimation(key, value, valueTarget, visualElement.shouldReduceMotion && transformProps.has(key)
@@ -5998,7 +5953,7 @@ function updateMotionValuesFromProps(element, next, prev) {
5998
5953
  * and warn against mismatches.
5999
5954
  */
6000
5955
  if (process.env.NODE_ENV === "development") {
6001
- warnOnce(nextValue.version === "8.5.0", `Attempting to mix Framer Motion versions ${nextValue.version} with 8.5.0 may not work as expected.`);
5956
+ warnOnce(nextValue.version === "8.5.2-alpha.1", `Attempting to mix Framer Motion versions ${nextValue.version} with 8.5.2-alpha.1 may not work as expected.`);
6002
5957
  }
6003
5958
  }
6004
5959
  else if (isMotionValue(prevValue)) {
@@ -9860,14 +9815,99 @@ function useResetProjection() {
9860
9815
  return reset;
9861
9816
  }
9862
9817
 
9863
- function startOptimizedAppearAnimation(element, name, keyframes, options) {
9864
- window.MotionAppearAnimations || (window.MotionAppearAnimations = new Map());
9818
+ const appearStoreId = (id, value) => `${id}: ${value}`;
9819
+
9820
+ const appearAnimationStore = new Map();
9821
+
9822
+ function handoffOptimizedAppearAnimation(id, name, value,
9823
+ /**
9824
+ * This function is loaded via window by startOptimisedAnimation.
9825
+ * By accepting `sync` as an argument, rather than using it via
9826
+ * import, it can be kept out of the first-load Framer bundle,
9827
+ * while also allowing this function to not be included in
9828
+ * Framer Motion bundles where it's not needed.
9829
+ */
9830
+ sync) {
9831
+ const storeId = appearStoreId(id, transformProps.has(name) ? "transform" : name);
9832
+ const { animation, ready } = appearAnimationStore.get(storeId) || {};
9833
+ if (!animation)
9834
+ return 0;
9835
+ const cancelOptimisedAnimation = () => {
9836
+ appearAnimationStore.delete(storeId);
9837
+ /**
9838
+ * Animation.cancel() throws so it needs to be wrapped in a try/catch
9839
+ */
9840
+ try {
9841
+ animation.cancel();
9842
+ }
9843
+ catch (e) { }
9844
+ };
9845
+ if (ready) {
9846
+ const sampledTime = performance.now();
9847
+ /**
9848
+ * Resync handoff animation with optimised animation.
9849
+ *
9850
+ * This step would be unnecessary if we triggered animateChanges() in useEffect,
9851
+ * but due to potential hydration errors we currently fire them in useLayoutEffect.
9852
+ *
9853
+ * By the time we're safely ready to cancel the optimised WAAPI animation,
9854
+ * the main thread might have been blocked and desynced the two animations.
9855
+ *
9856
+ * Here, we resync the two animations before the optimised WAAPI animation is cancelled.
9857
+ */
9858
+ sync.update(() => {
9859
+ if (value.animation) {
9860
+ value.animation.currentTime = performance.now() - sampledTime;
9861
+ }
9862
+ });
9863
+ /**
9864
+ * We allow the animation to persist until the next frame:
9865
+ * 1. So it continues to play until Framer Motion is ready to render
9866
+ * (avoiding a potential flash of the element's original state)
9867
+ * 2. As all independent transforms share a single transform animation, stopping
9868
+ * it synchronously would prevent subsequent transforms from handing off.
9869
+ */
9870
+ sync.render(cancelOptimisedAnimation);
9871
+ console.log("handing off from", animation.currentTime);
9872
+ return animation.currentTime || 0;
9873
+ }
9874
+ else {
9875
+ cancelOptimisedAnimation();
9876
+ return 0;
9877
+ }
9878
+ }
9879
+
9880
+ function startOptimizedAppearAnimation(element, name, keyframes, options, onReady) {
9865
9881
  const id = element.dataset[optimizedAppearDataId];
9866
- const animation = animateStyle(element, name, keyframes, options);
9867
- if (id && animation) {
9868
- window.MotionAppearAnimations.set(appearStoreId(id, name), animation);
9882
+ if (!id)
9883
+ return;
9884
+ window.HandoffAppearAnimations = handoffOptimizedAppearAnimation;
9885
+ const storeId = appearStoreId(id, name);
9886
+ /**
9887
+ * Use a dummy animation to detect when Chrome is ready to start
9888
+ * painting the page and hold off from triggering the real animation
9889
+ * until then.
9890
+ */
9891
+ const readyAnimation = animateStyle(element, name, [keyframes[0], keyframes[0]], { duration: 1 });
9892
+ appearAnimationStore.set(storeId, {
9893
+ animation: readyAnimation,
9894
+ ready: false,
9895
+ });
9896
+ const startAnimation = () => {
9897
+ const animation = animateStyle(element, name, keyframes, options);
9898
+ appearAnimationStore.set(storeId, { animation, ready: true });
9899
+ if (onReady)
9900
+ onReady(animation);
9901
+ };
9902
+ if (readyAnimation.ready) {
9903
+ readyAnimation.ready.then(() => {
9904
+ readyAnimation.cancel();
9905
+ startAnimation();
9906
+ });
9907
+ }
9908
+ else {
9909
+ startAnimation();
9869
9910
  }
9870
- return animation;
9871
9911
  }
9872
9912
 
9873
9913
  const createObject = () => ({});
@@ -33,7 +33,7 @@ const velocitySampleDuration = 5;
33
33
  /**
34
34
  * This is based on the spring implementation of Wobble https://github.com/skevy/wobble
35
35
  */
36
- function spring({ keyframes, restSpeed = 2, restDelta = 0.01, ...options }) {
36
+ function spring({ keyframes, restDelta, restSpeed, ...options }) {
37
37
  let origin = keyframes[0];
38
38
  let target = keyframes[keyframes.length - 1];
39
39
  /**
@@ -49,12 +49,15 @@ function spring({ keyframes, restSpeed = 2, restDelta = 0.01, ...options }) {
49
49
  const initialDelta = target - origin;
50
50
  const undampedAngularFreq = Math.sqrt(stiffness / mass) / 1000;
51
51
  /**
52
- * If we're working within what looks like a 0-1 range, change the default restDelta
53
- * to 0.01
52
+ * If we're working on a granular scale, use smaller defaults for determining
53
+ * when the spring is finished.
54
+ *
55
+ * These defaults have been selected emprically based on what strikes a good
56
+ * ratio between feeling good and finishing as soon as changes are imperceptible.
54
57
  */
55
- if (restDelta === undefined) {
56
- restDelta = Math.min(Math.abs(target - origin) / 100, 0.4);
57
- }
58
+ const isGranularScale = Math.abs(initialDelta) < 5;
59
+ restSpeed || (restSpeed = isGranularScale ? 0.01 : 2);
60
+ restDelta || (restDelta = isGranularScale ? 0.005 : 0.5);
58
61
  if (dampingRatio < 1) {
59
62
  const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio);
60
63
  // Underdamped spring
@@ -1,12 +1,31 @@
1
- import { sync } from '../../frameloop/index.mjs';
2
1
  import { transformProps } from '../../render/html/utils/transform.mjs';
2
+ import { appearAnimationStore } from './store.mjs';
3
3
  import { appearStoreId } from './store-id.mjs';
4
4
 
5
- function handoffOptimizedAppearAnimation(id, name, value) {
6
- const { MotionAppearAnimations } = window;
7
- const animationId = appearStoreId(id, transformProps.has(name) ? "transform" : name);
8
- const animation = MotionAppearAnimations && MotionAppearAnimations.get(animationId);
9
- if (animation) {
5
+ function handoffOptimizedAppearAnimation(id, name, value,
6
+ /**
7
+ * This function is loaded via window by startOptimisedAnimation.
8
+ * By accepting `sync` as an argument, rather than using it via
9
+ * import, it can be kept out of the first-load Framer bundle,
10
+ * while also allowing this function to not be included in
11
+ * Framer Motion bundles where it's not needed.
12
+ */
13
+ sync) {
14
+ const storeId = appearStoreId(id, transformProps.has(name) ? "transform" : name);
15
+ const { animation, ready } = appearAnimationStore.get(storeId) || {};
16
+ if (!animation)
17
+ return 0;
18
+ const cancelOptimisedAnimation = () => {
19
+ appearAnimationStore.delete(storeId);
20
+ /**
21
+ * Animation.cancel() throws so it needs to be wrapped in a try/catch
22
+ */
23
+ try {
24
+ animation.cancel();
25
+ }
26
+ catch (e) { }
27
+ };
28
+ if (ready) {
10
29
  const sampledTime = performance.now();
11
30
  /**
12
31
  * Resync handoff animation with optimised animation.
@@ -31,19 +50,12 @@ function handoffOptimizedAppearAnimation(id, name, value) {
31
50
  * 2. As all independent transforms share a single transform animation, stopping
32
51
  * it synchronously would prevent subsequent transforms from handing off.
33
52
  */
34
- sync.render(() => {
35
- MotionAppearAnimations.delete(animationId);
36
- /**
37
- * Animation.cancel() throws so it needs to be wrapped in a try/catch
38
- */
39
- try {
40
- animation.cancel();
41
- }
42
- catch (e) { }
43
- });
53
+ sync.render(cancelOptimisedAnimation);
54
+ console.log("handing off from", animation.currentTime);
44
55
  return animation.currentTime || 0;
45
56
  }
46
57
  else {
58
+ cancelOptimisedAnimation();
47
59
  return 0;
48
60
  }
49
61
  }
@@ -1,15 +1,40 @@
1
1
  import { appearStoreId } from './store-id.mjs';
2
2
  import { animateStyle } from '../waapi/index.mjs';
3
3
  import { optimizedAppearDataId } from './data-id.mjs';
4
+ import { handoffOptimizedAppearAnimation } from './handoff.mjs';
5
+ import { appearAnimationStore } from './store.mjs';
4
6
 
5
- function startOptimizedAppearAnimation(element, name, keyframes, options) {
6
- window.MotionAppearAnimations || (window.MotionAppearAnimations = new Map());
7
+ function startOptimizedAppearAnimation(element, name, keyframes, options, onReady) {
7
8
  const id = element.dataset[optimizedAppearDataId];
8
- const animation = animateStyle(element, name, keyframes, options);
9
- if (id && animation) {
10
- window.MotionAppearAnimations.set(appearStoreId(id, name), animation);
9
+ if (!id)
10
+ return;
11
+ window.HandoffAppearAnimations = handoffOptimizedAppearAnimation;
12
+ const storeId = appearStoreId(id, name);
13
+ /**
14
+ * Use a dummy animation to detect when Chrome is ready to start
15
+ * painting the page and hold off from triggering the real animation
16
+ * until then.
17
+ */
18
+ const readyAnimation = animateStyle(element, name, [keyframes[0], keyframes[0]], { duration: 1 });
19
+ appearAnimationStore.set(storeId, {
20
+ animation: readyAnimation,
21
+ ready: false,
22
+ });
23
+ const startAnimation = () => {
24
+ const animation = animateStyle(element, name, keyframes, options);
25
+ appearAnimationStore.set(storeId, { animation, ready: true });
26
+ if (onReady)
27
+ onReady(animation);
28
+ };
29
+ if (readyAnimation.ready) {
30
+ readyAnimation.ready.then(() => {
31
+ readyAnimation.cancel();
32
+ startAnimation();
33
+ });
34
+ }
35
+ else {
36
+ startAnimation();
11
37
  }
12
- return animation;
13
38
  }
14
39
 
15
40
  export { startOptimizedAppearAnimation };
@@ -0,0 +1,3 @@
1
+ const appearAnimationStore = new Map();
2
+
3
+ export { appearAnimationStore };
@@ -41,7 +41,7 @@ function useVisualElement(Component, visualState, props, createVisualElement) {
41
41
  * So if we detect a situtation where optimised appear animations
42
42
  * are running, we use useLayoutEffect to trigger animations.
43
43
  */
44
- const useAnimateChangesEffect = window.MotionAppearAnimations
44
+ const useAnimateChangesEffect = window.HandoffAppearAnimations
45
45
  ? useIsomorphicLayoutEffect
46
46
  : useEffect;
47
47
  useAnimateChangesEffect(() => {
@@ -2,9 +2,9 @@ import { setTarget } from './setters.mjs';
2
2
  import { resolveVariant } from './resolve-dynamic-variants.mjs';
3
3
  import { transformProps } from '../html/utils/transform.mjs';
4
4
  import { isWillChangeMotionValue } from '../../value/use-will-change/is.mjs';
5
- import { handoffOptimizedAppearAnimation } from '../../animation/optimized-appear/handoff.mjs';
6
5
  import { optimizedAppearDataAttribute } from '../../animation/optimized-appear/data-id.mjs';
7
6
  import { createMotionValueAnimation } from '../../animation/index.mjs';
7
+ import { sync } from '../../frameloop/index.mjs';
8
8
 
9
9
  function animateVisualElement(visualElement, definition, options = {}) {
10
10
  visualElement.notify("AnimationStart", definition);
@@ -88,10 +88,10 @@ function animateTarget(visualElement, definition, { delay = 0, transitionOverrid
88
88
  * If this is the first time a value is being animated, check
89
89
  * to see if we're handling off from an existing animation.
90
90
  */
91
- if (!value.hasAnimated) {
91
+ if (!value.hasAnimated && window.HandoffAppearAnimations) {
92
92
  const appearId = visualElement.getProps()[optimizedAppearDataAttribute];
93
93
  if (appearId) {
94
- valueTransition.elapsed = handoffOptimizedAppearAnimation(appearId, key, value);
94
+ valueTransition.elapsed = window.HandoffAppearAnimations(appearId, key, value, sync);
95
95
  }
96
96
  }
97
97
  let animation = value.start(createMotionValueAnimation(key, value, valueTarget, visualElement.shouldReduceMotion && transformProps.has(key)
@@ -22,7 +22,7 @@ function updateMotionValuesFromProps(element, next, prev) {
22
22
  * and warn against mismatches.
23
23
  */
24
24
  if (process.env.NODE_ENV === "development") {
25
- warnOnce(nextValue.version === "8.5.0", `Attempting to mix Framer Motion versions ${nextValue.version} with 8.5.0 may not work as expected.`);
25
+ warnOnce(nextValue.version === "8.5.2-alpha.1", `Attempting to mix Framer Motion versions ${nextValue.version} with 8.5.2-alpha.1 may not work as expected.`);
26
26
  }
27
27
  }
28
28
  else if (isMotionValue(prevValue)) {
@@ -25,7 +25,7 @@ class MotionValue {
25
25
  * This will be replaced by the build step with the latest version number.
26
26
  * When MotionValues are provided to motion components, warn if versions are mixed.
27
27
  */
28
- this.version = "8.5.0";
28
+ this.version = "8.5.2-alpha.1";
29
29
  /**
30
30
  * Duration, in milliseconds, since last updating frame.
31
31
  *
@@ -88,7 +88,7 @@
88
88
  * So if we detect a situtation where optimised appear animations
89
89
  * are running, we use useLayoutEffect to trigger animations.
90
90
  */
91
- const useAnimateChangesEffect = window.MotionAppearAnimations
91
+ const useAnimateChangesEffect = window.HandoffAppearAnimations
92
92
  ? useIsomorphicLayoutEffect
93
93
  : React.useEffect;
94
94
  useAnimateChangesEffect(() => {
@@ -2103,7 +2103,7 @@
2103
2103
  * This will be replaced by the build step with the latest version number.
2104
2104
  * When MotionValues are provided to motion components, warn if versions are mixed.
2105
2105
  */
2106
- this.version = "8.5.0";
2106
+ this.version = "8.5.2-alpha.1";
2107
2107
  /**
2108
2108
  * Duration, in milliseconds, since last updating frame.
2109
2109
  *
@@ -2797,54 +2797,6 @@
2797
2797
  return Boolean(isMotionValue(value) && value.add);
2798
2798
  }
2799
2799
 
2800
- const appearStoreId = (id, value) => `${id}: ${value}`;
2801
-
2802
- function handoffOptimizedAppearAnimation(id, name, value) {
2803
- const { MotionAppearAnimations } = window;
2804
- const animationId = appearStoreId(id, transformProps.has(name) ? "transform" : name);
2805
- const animation = MotionAppearAnimations && MotionAppearAnimations.get(animationId);
2806
- if (animation) {
2807
- const sampledTime = performance.now();
2808
- /**
2809
- * Resync handoff animation with optimised animation.
2810
- *
2811
- * This step would be unnecessary if we triggered animateChanges() in useEffect,
2812
- * but due to potential hydration errors we currently fire them in useLayoutEffect.
2813
- *
2814
- * By the time we're safely ready to cancel the optimised WAAPI animation,
2815
- * the main thread might have been blocked and desynced the two animations.
2816
- *
2817
- * Here, we resync the two animations before the optimised WAAPI animation is cancelled.
2818
- */
2819
- sync.update(() => {
2820
- if (value.animation) {
2821
- value.animation.currentTime = performance.now() - sampledTime;
2822
- }
2823
- });
2824
- /**
2825
- * We allow the animation to persist until the next frame:
2826
- * 1. So it continues to play until Framer Motion is ready to render
2827
- * (avoiding a potential flash of the element's original state)
2828
- * 2. As all independent transforms share a single transform animation, stopping
2829
- * it synchronously would prevent subsequent transforms from handing off.
2830
- */
2831
- sync.render(() => {
2832
- MotionAppearAnimations.delete(animationId);
2833
- /**
2834
- * Animation.cancel() throws so it needs to be wrapped in a try/catch
2835
- */
2836
- try {
2837
- animation.cancel();
2838
- }
2839
- catch (e) { }
2840
- });
2841
- return animation.currentTime || 0;
2842
- }
2843
- else {
2844
- return 0;
2845
- }
2846
- }
2847
-
2848
2800
  const optimizedAppearDataId = "framerAppearId";
2849
2801
  const optimizedAppearDataAttribute = "data-" + camelToDash(optimizedAppearDataId);
2850
2802
 
@@ -3397,7 +3349,7 @@
3397
3349
  /**
3398
3350
  * This is based on the spring implementation of Wobble https://github.com/skevy/wobble
3399
3351
  */
3400
- function spring({ keyframes, restSpeed = 2, restDelta = 0.01, ...options }) {
3352
+ function spring({ keyframes, restDelta, restSpeed, ...options }) {
3401
3353
  let origin = keyframes[0];
3402
3354
  let target = keyframes[keyframes.length - 1];
3403
3355
  /**
@@ -3413,12 +3365,15 @@
3413
3365
  const initialDelta = target - origin;
3414
3366
  const undampedAngularFreq = Math.sqrt(stiffness / mass) / 1000;
3415
3367
  /**
3416
- * If we're working within what looks like a 0-1 range, change the default restDelta
3417
- * to 0.01
3368
+ * If we're working on a granular scale, use smaller defaults for determining
3369
+ * when the spring is finished.
3370
+ *
3371
+ * These defaults have been selected emprically based on what strikes a good
3372
+ * ratio between feeling good and finishing as soon as changes are imperceptible.
3418
3373
  */
3419
- if (restDelta === undefined) {
3420
- restDelta = Math.min(Math.abs(target - origin) / 100, 0.4);
3421
- }
3374
+ const isGranularScale = Math.abs(initialDelta) < 5;
3375
+ restSpeed || (restSpeed = isGranularScale ? 0.01 : 2);
3376
+ restDelta || (restDelta = isGranularScale ? 0.005 : 0.5);
3422
3377
  if (dampingRatio < 1) {
3423
3378
  const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio);
3424
3379
  // Underdamped spring
@@ -4258,10 +4213,10 @@
4258
4213
  * If this is the first time a value is being animated, check
4259
4214
  * to see if we're handling off from an existing animation.
4260
4215
  */
4261
- if (!value.hasAnimated) {
4216
+ if (!value.hasAnimated && window.HandoffAppearAnimations) {
4262
4217
  const appearId = visualElement.getProps()[optimizedAppearDataAttribute];
4263
4218
  if (appearId) {
4264
- valueTransition.elapsed = handoffOptimizedAppearAnimation(appearId, key, value);
4219
+ valueTransition.elapsed = window.HandoffAppearAnimations(appearId, key, value, sync);
4265
4220
  }
4266
4221
  }
4267
4222
  let animation = value.start(createMotionValueAnimation(key, value, valueTarget, visualElement.shouldReduceMotion && transformProps.has(key)
@@ -6011,7 +5966,7 @@
6011
5966
  * and warn against mismatches.
6012
5967
  */
6013
5968
  {
6014
- warnOnce(nextValue.version === "8.5.0", `Attempting to mix Framer Motion versions ${nextValue.version} with 8.5.0 may not work as expected.`);
5969
+ warnOnce(nextValue.version === "8.5.2-alpha.1", `Attempting to mix Framer Motion versions ${nextValue.version} with 8.5.2-alpha.1 may not work as expected.`);
6015
5970
  }
6016
5971
  }
6017
5972
  else if (isMotionValue(prevValue)) {
@@ -10479,14 +10434,99 @@
10479
10434
  return reset;
10480
10435
  }
10481
10436
 
10482
- function startOptimizedAppearAnimation(element, name, keyframes, options) {
10483
- window.MotionAppearAnimations || (window.MotionAppearAnimations = new Map());
10437
+ const appearStoreId = (id, value) => `${id}: ${value}`;
10438
+
10439
+ const appearAnimationStore = new Map();
10440
+
10441
+ function handoffOptimizedAppearAnimation(id, name, value,
10442
+ /**
10443
+ * This function is loaded via window by startOptimisedAnimation.
10444
+ * By accepting `sync` as an argument, rather than using it via
10445
+ * import, it can be kept out of the first-load Framer bundle,
10446
+ * while also allowing this function to not be included in
10447
+ * Framer Motion bundles where it's not needed.
10448
+ */
10449
+ sync) {
10450
+ const storeId = appearStoreId(id, transformProps.has(name) ? "transform" : name);
10451
+ const { animation, ready } = appearAnimationStore.get(storeId) || {};
10452
+ if (!animation)
10453
+ return 0;
10454
+ const cancelOptimisedAnimation = () => {
10455
+ appearAnimationStore.delete(storeId);
10456
+ /**
10457
+ * Animation.cancel() throws so it needs to be wrapped in a try/catch
10458
+ */
10459
+ try {
10460
+ animation.cancel();
10461
+ }
10462
+ catch (e) { }
10463
+ };
10464
+ if (ready) {
10465
+ const sampledTime = performance.now();
10466
+ /**
10467
+ * Resync handoff animation with optimised animation.
10468
+ *
10469
+ * This step would be unnecessary if we triggered animateChanges() in useEffect,
10470
+ * but due to potential hydration errors we currently fire them in useLayoutEffect.
10471
+ *
10472
+ * By the time we're safely ready to cancel the optimised WAAPI animation,
10473
+ * the main thread might have been blocked and desynced the two animations.
10474
+ *
10475
+ * Here, we resync the two animations before the optimised WAAPI animation is cancelled.
10476
+ */
10477
+ sync.update(() => {
10478
+ if (value.animation) {
10479
+ value.animation.currentTime = performance.now() - sampledTime;
10480
+ }
10481
+ });
10482
+ /**
10483
+ * We allow the animation to persist until the next frame:
10484
+ * 1. So it continues to play until Framer Motion is ready to render
10485
+ * (avoiding a potential flash of the element's original state)
10486
+ * 2. As all independent transforms share a single transform animation, stopping
10487
+ * it synchronously would prevent subsequent transforms from handing off.
10488
+ */
10489
+ sync.render(cancelOptimisedAnimation);
10490
+ console.log("handing off from", animation.currentTime);
10491
+ return animation.currentTime || 0;
10492
+ }
10493
+ else {
10494
+ cancelOptimisedAnimation();
10495
+ return 0;
10496
+ }
10497
+ }
10498
+
10499
+ function startOptimizedAppearAnimation(element, name, keyframes, options, onReady) {
10484
10500
  const id = element.dataset[optimizedAppearDataId];
10485
- const animation = animateStyle(element, name, keyframes, options);
10486
- if (id && animation) {
10487
- window.MotionAppearAnimations.set(appearStoreId(id, name), animation);
10501
+ if (!id)
10502
+ return;
10503
+ window.HandoffAppearAnimations = handoffOptimizedAppearAnimation;
10504
+ const storeId = appearStoreId(id, name);
10505
+ /**
10506
+ * Use a dummy animation to detect when Chrome is ready to start
10507
+ * painting the page and hold off from triggering the real animation
10508
+ * until then.
10509
+ */
10510
+ const readyAnimation = animateStyle(element, name, [keyframes[0], keyframes[0]], { duration: 1 });
10511
+ appearAnimationStore.set(storeId, {
10512
+ animation: readyAnimation,
10513
+ ready: false,
10514
+ });
10515
+ const startAnimation = () => {
10516
+ const animation = animateStyle(element, name, keyframes, options);
10517
+ appearAnimationStore.set(storeId, { animation, ready: true });
10518
+ if (onReady)
10519
+ onReady(animation);
10520
+ };
10521
+ if (readyAnimation.ready) {
10522
+ readyAnimation.ready.then(() => {
10523
+ readyAnimation.cancel();
10524
+ startAnimation();
10525
+ });
10526
+ }
10527
+ else {
10528
+ startAnimation();
10488
10529
  }
10489
- return animation;
10490
10530
  }
10491
10531
 
10492
10532
  const createObject = () => ({});