motion 12.3.0 → 12.4.0

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.
@@ -187,7 +187,21 @@ const camelToDash = (str) => str.replace(/([a-z])([A-Z])/gu, "$1-$2").toLowerCas
187
187
  const optimizedAppearDataId = "framerAppearId";
188
188
  const optimizedAppearDataAttribute = "data-" + camelToDash(optimizedAppearDataId);
189
189
 
190
- function createRenderStep(runNextFrame) {
190
+ const stepsOrder = [
191
+ "read", // Read
192
+ "resolveKeyframes", // Write/Read/Write/Read
193
+ "update", // Compute
194
+ "preRender", // Compute
195
+ "render", // Write
196
+ "postRender", // Compute
197
+ ];
198
+
199
+ const statsBuffer = {
200
+ value: null,
201
+ addProjectionMetrics: null,
202
+ };
203
+
204
+ function createRenderStep(runNextFrame, stepName) {
191
205
  /**
192
206
  * We create and reuse two queues, one to queue jobs for the current frame
193
207
  * and one for the next. We reuse to avoid triggering GC after x frames.
@@ -209,11 +223,13 @@ function createRenderStep(runNextFrame) {
209
223
  timestamp: 0.0,
210
224
  isProcessing: false,
211
225
  };
226
+ let numCalls = 0;
212
227
  function triggerCallback(callback) {
213
228
  if (toKeepAlive.has(callback)) {
214
229
  step.schedule(callback);
215
230
  runNextFrame();
216
231
  }
232
+ numCalls++;
217
233
  callback(latestFrameData);
218
234
  }
219
235
  const step = {
@@ -254,6 +270,13 @@ function createRenderStep(runNextFrame) {
254
270
  [thisFrame, nextFrame] = [nextFrame, thisFrame];
255
271
  // Execute this frame
256
272
  thisFrame.forEach(triggerCallback);
273
+ /**
274
+ * If we're recording stats then
275
+ */
276
+ if (stepName && statsBuffer.value) {
277
+ statsBuffer.value.frameloop[stepName].push(numCalls);
278
+ }
279
+ numCalls = 0;
257
280
  // Clear the frame so no callbacks remain. This is to avoid
258
281
  // memory leaks should this render step not run for a while.
259
282
  thisFrame.clear();
@@ -267,14 +290,6 @@ function createRenderStep(runNextFrame) {
267
290
  return step;
268
291
  }
269
292
 
270
- const stepsOrder = [
271
- "read", // Read
272
- "resolveKeyframes", // Write/Read/Write/Read
273
- "update", // Compute
274
- "preRender", // Compute
275
- "render", // Write
276
- "postRender", // Compute
277
- ];
278
293
  const maxElapsed = 40;
279
294
  function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
280
295
  let runNextFrame = false;
@@ -286,16 +301,18 @@ function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
286
301
  };
287
302
  const flagRunNextFrame = () => (runNextFrame = true);
288
303
  const steps = stepsOrder.reduce((acc, key) => {
289
- acc[key] = createRenderStep(flagRunNextFrame);
304
+ acc[key] = createRenderStep(flagRunNextFrame, allowKeepAlive ? key : undefined);
290
305
  return acc;
291
306
  }, {});
292
307
  const { read, resolveKeyframes, update, preRender, render, postRender } = steps;
293
308
  const processBatch = () => {
294
309
  const timestamp = performance.now();
295
310
  runNextFrame = false;
296
- state.delta = useDefaultElapsed
297
- ? 1000 / 60
298
- : Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1);
311
+ {
312
+ state.delta = useDefaultElapsed
313
+ ? 1000 / 60
314
+ : Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1);
315
+ }
299
316
  state.timestamp = timestamp;
300
317
  state.isProcessing = true;
301
318
  // Unrolled render loop for better per-frame performance
@@ -392,7 +392,7 @@ function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duratio
392
392
  */
393
393
  if (Array.isArray(easing))
394
394
  keyframeOptions.easing = easing;
395
- return element.animate(keyframeOptions, {
395
+ const animation = element.animate(keyframeOptions, {
396
396
  delay,
397
397
  duration,
398
398
  easing: !Array.isArray(easing) ? easing : "linear",
@@ -400,6 +400,7 @@ function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duratio
400
400
  iterations: repeat + 1,
401
401
  direction: repeatType === "reverse" ? "alternate" : "normal",
402
402
  });
403
+ return animation;
403
404
  }
404
405
 
405
406
  const createUnitType = (unit) => ({
@@ -459,7 +460,7 @@ function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyfr
459
460
  }
460
461
 
461
462
  function setCSSVar(element, name, value) {
462
- element.style.setProperty(`--${name}`, value);
463
+ element.style.setProperty(name, value);
463
464
  }
464
465
  function setStyle(element, name, value) {
465
466
  element.style[name] = value;
@@ -0,0 +1 @@
1
+ export * from 'framer-motion/debug';
@@ -3,6 +3,7 @@ import { millisecondsToSeconds, secondsToMilliseconds } from '../../../../../mot
3
3
  import { calcGeneratorDuration } from '../../../../../motion-dom/dist/es/animation/generators/utils/calc-duration.mjs';
4
4
  import { isGenerator } from '../../../../../motion-dom/dist/es/animation/generators/utils/is-generator.mjs';
5
5
  import { KeyframeResolver } from '../../render/utils/KeyframesResolver.mjs';
6
+ import { activeAnimations } from '../../stats/animation-count.mjs';
6
7
  import { clamp } from '../../utils/clamp.mjs';
7
8
  import { mix } from '../../utils/mix/index.mjs';
8
9
  import { pipe } from '../../utils/pipe.mjs';
@@ -140,6 +141,7 @@ class MainThreadAnimation extends BaseAnimation {
140
141
  }
141
142
  onPostResolved() {
142
143
  const { autoplay = true } = this.options;
144
+ activeAnimations.mainThread++;
143
145
  this.play();
144
146
  if (this.pendingPlayState === "paused" || !autoplay) {
145
147
  this.pause();
@@ -371,6 +373,7 @@ class MainThreadAnimation extends BaseAnimation {
371
373
  this.updateFinishedPromise();
372
374
  this.startTime = this.cancelTime = null;
373
375
  this.resolver.cancel();
376
+ activeAnimations.mainThread--;
374
377
  }
375
378
  stopDriver() {
376
379
  if (!this.driver)
@@ -1,5 +1,7 @@
1
1
  import '../../../../../../motion-utils/dist/es/errors.mjs';
2
2
  import { mapEasingToNativeEasing } from '../../../../../../motion-dom/dist/es/animation/waapi/utils/easing.mjs';
3
+ import { activeAnimations } from '../../../stats/animation-count.mjs';
4
+ import { statsBuffer } from '../../../stats/buffer.mjs';
3
5
 
4
6
  function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duration = 300, repeat = 0, repeatType = "loop", ease = "easeInOut", times, } = {}) {
5
7
  const keyframeOptions = { [valueName]: keyframes };
@@ -11,7 +13,10 @@ function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duratio
11
13
  */
12
14
  if (Array.isArray(easing))
13
15
  keyframeOptions.easing = easing;
14
- return element.animate(keyframeOptions, {
16
+ if (statsBuffer.value) {
17
+ activeAnimations.waapi++;
18
+ }
19
+ const animation = element.animate(keyframeOptions, {
15
20
  delay,
16
21
  duration,
17
22
  easing: !Array.isArray(easing) ? easing : "linear",
@@ -19,6 +24,12 @@ function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duratio
19
24
  iterations: repeat + 1,
20
25
  direction: repeatType === "reverse" ? "alternate" : "normal",
21
26
  });
27
+ if (statsBuffer.value) {
28
+ animation.finished.finally(() => {
29
+ activeAnimations.waapi--;
30
+ });
31
+ }
32
+ return animation;
22
33
  }
23
34
 
24
35
  export { startWaapiAnimation };
@@ -1,5 +1,5 @@
1
1
  function setCSSVar(element, name, value) {
2
- element.style.setProperty(`--${name}`, value);
2
+ element.style.setProperty(name, value);
3
3
  }
4
4
  function setStyle(element, name, value) {
5
5
  element.style[name] = value;
@@ -1,14 +1,7 @@
1
1
  import { MotionGlobalConfig } from '../utils/GlobalConfig.mjs';
2
+ import { stepsOrder } from './order.mjs';
2
3
  import { createRenderStep } from './render-step.mjs';
3
4
 
4
- const stepsOrder = [
5
- "read", // Read
6
- "resolveKeyframes", // Write/Read/Write/Read
7
- "update", // Compute
8
- "preRender", // Compute
9
- "render", // Write
10
- "postRender", // Compute
11
- ];
12
5
  const maxElapsed = 40;
13
6
  function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
14
7
  let runNextFrame = false;
@@ -20,7 +13,7 @@ function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
20
13
  };
21
14
  const flagRunNextFrame = () => (runNextFrame = true);
22
15
  const steps = stepsOrder.reduce((acc, key) => {
23
- acc[key] = createRenderStep(flagRunNextFrame);
16
+ acc[key] = createRenderStep(flagRunNextFrame, allowKeepAlive ? key : undefined);
24
17
  return acc;
25
18
  }, {});
26
19
  const { read, resolveKeyframes, update, preRender, render, postRender } = steps;
@@ -29,9 +22,11 @@ function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
29
22
  ? state.timestamp
30
23
  : performance.now();
31
24
  runNextFrame = false;
32
- state.delta = useDefaultElapsed
33
- ? 1000 / 60
34
- : Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1);
25
+ if (!MotionGlobalConfig.useManualTiming) {
26
+ state.delta = useDefaultElapsed
27
+ ? 1000 / 60
28
+ : Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1);
29
+ }
35
30
  state.timestamp = timestamp;
36
31
  state.isProcessing = true;
37
32
  // Unrolled render loop for better per-frame performance
@@ -71,4 +66,4 @@ function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
71
66
  return { schedule, cancel, state, steps };
72
67
  }
73
68
 
74
- export { createRenderBatcher, stepsOrder };
69
+ export { createRenderBatcher };
@@ -1,4 +1,4 @@
1
- import { stepsOrder } from './batcher.mjs';
1
+ import { stepsOrder } from './order.mjs';
2
2
  import { frame, cancelFrame } from './frame.mjs';
3
3
 
4
4
  /**
@@ -0,0 +1,10 @@
1
+ const stepsOrder = [
2
+ "read", // Read
3
+ "resolveKeyframes", // Write/Read/Write/Read
4
+ "update", // Compute
5
+ "preRender", // Compute
6
+ "render", // Write
7
+ "postRender", // Compute
8
+ ];
9
+
10
+ export { stepsOrder };
@@ -1,4 +1,6 @@
1
- function createRenderStep(runNextFrame) {
1
+ import { statsBuffer } from '../stats/buffer.mjs';
2
+
3
+ function createRenderStep(runNextFrame, stepName) {
2
4
  /**
3
5
  * We create and reuse two queues, one to queue jobs for the current frame
4
6
  * and one for the next. We reuse to avoid triggering GC after x frames.
@@ -20,11 +22,13 @@ function createRenderStep(runNextFrame) {
20
22
  timestamp: 0.0,
21
23
  isProcessing: false,
22
24
  };
25
+ let numCalls = 0;
23
26
  function triggerCallback(callback) {
24
27
  if (toKeepAlive.has(callback)) {
25
28
  step.schedule(callback);
26
29
  runNextFrame();
27
30
  }
31
+ numCalls++;
28
32
  callback(latestFrameData);
29
33
  }
30
34
  const step = {
@@ -65,6 +69,13 @@ function createRenderStep(runNextFrame) {
65
69
  [thisFrame, nextFrame] = [nextFrame, thisFrame];
66
70
  // Execute this frame
67
71
  thisFrame.forEach(triggerCallback);
72
+ /**
73
+ * If we're recording stats then
74
+ */
75
+ if (stepName && statsBuffer.value) {
76
+ statsBuffer.value.frameloop[stepName].push(numCalls);
77
+ }
78
+ numCalls = 0;
68
79
  // Clear the frame so no callbacks remain. This is to avoid
69
80
  // memory leaks should this render step not run for a while.
70
81
  thisFrame.clear();
@@ -8,6 +8,8 @@ import { microtask } from '../../frameloop/microtask.mjs';
8
8
  import { time } from '../../frameloop/sync-time.mjs';
9
9
  import { isSVGElement } from '../../render/dom/utils/is-svg-element.mjs';
10
10
  import { FlatTree } from '../../render/utils/flat-tree.mjs';
11
+ import { activeAnimations } from '../../stats/animation-count.mjs';
12
+ import { statsBuffer } from '../../stats/buffer.mjs';
11
13
  import { clamp } from '../../utils/clamp.mjs';
12
14
  import { delay } from '../../utils/delay.mjs';
13
15
  import { mixNumber } from '../../utils/mix/number.mjs';
@@ -28,12 +30,10 @@ import { hasTransform, hasScale, has2DTranslate } from '../utils/has-transform.m
28
30
  import { globalProjectionState } from './state.mjs';
29
31
 
30
32
  const metrics = {
31
- type: "projectionFrame",
32
- totalNodes: 0,
33
- resolvedTargetDeltas: 0,
34
- recalculatedProjection: 0,
33
+ nodes: 0,
34
+ calculatedTargetDeltas: 0,
35
+ calculatedProjections: 0,
35
36
  };
36
- const isDebug = typeof window !== "undefined" && window.MotionDebug !== undefined;
37
37
  const transformAxes = ["", "X", "Y", "Z"];
38
38
  const hiddenVisibility = { visibility: "hidden" };
39
39
  /**
@@ -187,18 +187,18 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
187
187
  * Reset debug counts. Manually resetting rather than creating a new
188
188
  * object each frame.
189
189
  */
190
- if (isDebug) {
191
- metrics.totalNodes =
192
- metrics.resolvedTargetDeltas =
193
- metrics.recalculatedProjection =
190
+ if (statsBuffer.value) {
191
+ metrics.nodes =
192
+ metrics.calculatedTargetDeltas =
193
+ metrics.calculatedProjections =
194
194
  0;
195
195
  }
196
196
  this.nodes.forEach(propagateDirtyNodes);
197
197
  this.nodes.forEach(resolveTargetDelta);
198
198
  this.nodes.forEach(calcProjection);
199
199
  this.nodes.forEach(cleanDirtyNodes);
200
- if (isDebug) {
201
- window.MotionDebug.record(metrics);
200
+ if (statsBuffer.addProjectionMetrics) {
201
+ statsBuffer.addProjectionMetrics(metrics);
202
202
  }
203
203
  };
204
204
  /**
@@ -845,8 +845,8 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
845
845
  /**
846
846
  * Increase debug counter for resolved target deltas
847
847
  */
848
- if (isDebug) {
849
- metrics.resolvedTargetDeltas++;
848
+ if (statsBuffer.value) {
849
+ metrics.calculatedTargetDeltas++;
850
850
  }
851
851
  }
852
852
  getClosestProjectingParent() {
@@ -976,8 +976,8 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
976
976
  /**
977
977
  * Increase debug counter for recalculated projections
978
978
  */
979
- if (isDebug) {
980
- metrics.recalculatedProjection++;
979
+ if (statsBuffer.value) {
980
+ metrics.calculatedProjections++;
981
981
  }
982
982
  }
983
983
  hide() {
@@ -1079,13 +1079,18 @@ function createProjectionNode({ attachResizeListener, defaultParent, measureScro
1079
1079
  */
1080
1080
  this.pendingAnimation = frame.update(() => {
1081
1081
  globalProjectionState.hasAnimatedSinceResize = true;
1082
+ activeAnimations.layout++;
1082
1083
  this.currentAnimation = animateSingleValue(0, animationTarget, {
1083
1084
  ...options,
1084
1085
  onUpdate: (latest) => {
1085
1086
  this.mixTargetDelta(latest);
1086
1087
  options.onUpdate && options.onUpdate(latest);
1087
1088
  },
1089
+ onStop: () => {
1090
+ activeAnimations.layout--;
1091
+ },
1088
1092
  onComplete: () => {
1093
+ activeAnimations.layout--;
1089
1094
  options.onComplete && options.onComplete();
1090
1095
  this.completeAnimation();
1091
1096
  },
@@ -1485,8 +1490,8 @@ function propagateDirtyNodes(node) {
1485
1490
  /**
1486
1491
  * Increase debug counter for nodes encountered this frame
1487
1492
  */
1488
- if (isDebug) {
1489
- metrics.totalNodes++;
1493
+ if (statsBuffer.value) {
1494
+ metrics.nodes++;
1490
1495
  }
1491
1496
  if (!node.parent)
1492
1497
  return;
@@ -17,7 +17,7 @@ function updateMotionValuesFromProps(element, next, prev) {
17
17
  * and warn against mismatches.
18
18
  */
19
19
  if (process.env.NODE_ENV === "development") {
20
- warnOnce(nextValue.version === "12.3.0", `Attempting to mix Motion versions ${nextValue.version} with 12.3.0 may not work as expected.`);
20
+ warnOnce(nextValue.version === "12.4.0", `Attempting to mix Motion versions ${nextValue.version} with 12.4.0 may not work as expected.`);
21
21
  }
22
22
  }
23
23
  else if (isMotionValue(prevValue)) {
@@ -0,0 +1,7 @@
1
+ const activeAnimations = {
2
+ layout: 0,
3
+ mainThread: 0,
4
+ waapi: 0,
5
+ };
6
+
7
+ export { activeAnimations };
@@ -0,0 +1,6 @@
1
+ const statsBuffer = {
2
+ value: null,
3
+ addProjectionMetrics: null,
4
+ };
5
+
6
+ export { statsBuffer };
@@ -0,0 +1,113 @@
1
+ import { activeAnimations } from './animation-count.mjs';
2
+ import { statsBuffer } from './buffer.mjs';
3
+ import { frame, cancelFrame, frameData } from '../frameloop/frame.mjs';
4
+
5
+ function record() {
6
+ const { value } = statsBuffer;
7
+ if (value === null) {
8
+ cancelFrame(record);
9
+ return;
10
+ }
11
+ value.frameloop.rate.push(frameData.delta);
12
+ value.animations.mainThread.push(activeAnimations.mainThread);
13
+ value.animations.waapi.push(activeAnimations.waapi);
14
+ value.animations.layout.push(activeAnimations.layout);
15
+ }
16
+ function mean(values) {
17
+ return values.reduce((acc, value) => acc + value, 0) / values.length;
18
+ }
19
+ function summarise(values, calcAverage = mean) {
20
+ if (values.length === 0) {
21
+ return {
22
+ min: 0,
23
+ max: 0,
24
+ avg: 0,
25
+ };
26
+ }
27
+ return {
28
+ min: Math.min(...values),
29
+ max: Math.max(...values),
30
+ avg: calcAverage(values),
31
+ };
32
+ }
33
+ const msToFps = (ms) => Math.round(1000 / ms);
34
+ function clearStatsBuffer() {
35
+ statsBuffer.value = null;
36
+ statsBuffer.addProjectionMetrics = null;
37
+ }
38
+ function reportStats() {
39
+ const { value } = statsBuffer;
40
+ if (!value) {
41
+ throw new Error("Stats are not being measured");
42
+ }
43
+ clearStatsBuffer();
44
+ cancelFrame(record);
45
+ const summary = {
46
+ frameloop: {
47
+ rate: summarise(value.frameloop.rate),
48
+ read: summarise(value.frameloop.read),
49
+ resolveKeyframes: summarise(value.frameloop.resolveKeyframes),
50
+ update: summarise(value.frameloop.update),
51
+ preRender: summarise(value.frameloop.preRender),
52
+ render: summarise(value.frameloop.render),
53
+ postRender: summarise(value.frameloop.postRender),
54
+ },
55
+ animations: {
56
+ mainThread: summarise(value.animations.mainThread),
57
+ waapi: summarise(value.animations.waapi),
58
+ layout: summarise(value.animations.layout),
59
+ },
60
+ layoutProjection: {
61
+ nodes: summarise(value.layoutProjection.nodes),
62
+ calculatedTargetDeltas: summarise(value.layoutProjection.calculatedTargetDeltas),
63
+ calculatedProjections: summarise(value.layoutProjection.calculatedProjections),
64
+ },
65
+ };
66
+ /**
67
+ * Convert the rate to FPS
68
+ */
69
+ const { rate } = summary.frameloop;
70
+ rate.min = msToFps(rate.min);
71
+ rate.max = msToFps(rate.max);
72
+ rate.avg = msToFps(rate.avg);
73
+ [rate.min, rate.max] = [rate.max, rate.min];
74
+ return summary;
75
+ }
76
+ function recordStats() {
77
+ if (statsBuffer.value) {
78
+ clearStatsBuffer();
79
+ throw new Error("Stats are already being measured");
80
+ }
81
+ const newStatsBuffer = statsBuffer;
82
+ newStatsBuffer.value = {
83
+ frameloop: {
84
+ rate: [],
85
+ read: [],
86
+ resolveKeyframes: [],
87
+ update: [],
88
+ preRender: [],
89
+ render: [],
90
+ postRender: [],
91
+ },
92
+ animations: {
93
+ mainThread: [],
94
+ waapi: [],
95
+ layout: [],
96
+ },
97
+ layoutProjection: {
98
+ nodes: [],
99
+ calculatedTargetDeltas: [],
100
+ calculatedProjections: [],
101
+ },
102
+ };
103
+ newStatsBuffer.addProjectionMetrics = (metrics) => {
104
+ const { layoutProjection } = newStatsBuffer.value;
105
+ layoutProjection.nodes.push(metrics.nodes);
106
+ layoutProjection.calculatedTargetDeltas.push(metrics.calculatedTargetDeltas);
107
+ layoutProjection.calculatedProjections.push(metrics.calculatedProjections);
108
+ };
109
+ frame.postRender(record, true);
110
+ return reportStats;
111
+ }
112
+
113
+ export { recordStats };
@@ -1,8 +1,8 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import { inView } from '../render/dom/viewport/index.mjs';
3
3
 
4
- function useInView(ref, { root, margin, amount, once = false } = {}) {
5
- const [isInView, setInView] = useState(false);
4
+ function useInView(ref, { root, margin, amount, once = false, initial = false, } = {}) {
5
+ const [isInView, setInView] = useState(initial);
6
6
  useEffect(() => {
7
7
  if (!ref.current || (once && isInView))
8
8
  return;
@@ -34,7 +34,7 @@ class MotionValue {
34
34
  * This will be replaced by the build step with the latest version number.
35
35
  * When MotionValues are provided to motion components, warn if versions are mixed.
36
36
  */
37
- this.version = "12.3.0";
37
+ this.version = "12.4.0";
38
38
  /**
39
39
  * Tracks whether this value can output a velocity. Currently this is only true
40
40
  * if the value is numerical, but we might be able to widen the scope here and support
@@ -0,0 +1 @@
1
+ export { recordStats } from '../../framer-motion/dist/es/stats/index.mjs';