motion 12.3.1 → 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) => ({
@@ -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,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.1", `Attempting to mix Motion versions ${nextValue.version} with 12.3.1 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.1";
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';
@@ -1320,7 +1320,21 @@
1320
1320
  useManualTiming: false,
1321
1321
  };
1322
1322
 
1323
- function createRenderStep(runNextFrame) {
1323
+ const stepsOrder = [
1324
+ "read", // Read
1325
+ "resolveKeyframes", // Write/Read/Write/Read
1326
+ "update", // Compute
1327
+ "preRender", // Compute
1328
+ "render", // Write
1329
+ "postRender", // Compute
1330
+ ];
1331
+
1332
+ const statsBuffer = {
1333
+ value: null,
1334
+ addProjectionMetrics: null,
1335
+ };
1336
+
1337
+ function createRenderStep(runNextFrame, stepName) {
1324
1338
  /**
1325
1339
  * We create and reuse two queues, one to queue jobs for the current frame
1326
1340
  * and one for the next. We reuse to avoid triggering GC after x frames.
@@ -1342,11 +1356,13 @@
1342
1356
  timestamp: 0.0,
1343
1357
  isProcessing: false,
1344
1358
  };
1359
+ let numCalls = 0;
1345
1360
  function triggerCallback(callback) {
1346
1361
  if (toKeepAlive.has(callback)) {
1347
1362
  step.schedule(callback);
1348
1363
  runNextFrame();
1349
1364
  }
1365
+ numCalls++;
1350
1366
  callback(latestFrameData);
1351
1367
  }
1352
1368
  const step = {
@@ -1387,6 +1403,13 @@
1387
1403
  [thisFrame, nextFrame] = [nextFrame, thisFrame];
1388
1404
  // Execute this frame
1389
1405
  thisFrame.forEach(triggerCallback);
1406
+ /**
1407
+ * If we're recording stats then
1408
+ */
1409
+ if (stepName && statsBuffer.value) {
1410
+ statsBuffer.value.frameloop[stepName].push(numCalls);
1411
+ }
1412
+ numCalls = 0;
1390
1413
  // Clear the frame so no callbacks remain. This is to avoid
1391
1414
  // memory leaks should this render step not run for a while.
1392
1415
  thisFrame.clear();
@@ -1400,14 +1423,6 @@
1400
1423
  return step;
1401
1424
  }
1402
1425
 
1403
- const stepsOrder = [
1404
- "read", // Read
1405
- "resolveKeyframes", // Write/Read/Write/Read
1406
- "update", // Compute
1407
- "preRender", // Compute
1408
- "render", // Write
1409
- "postRender", // Compute
1410
- ];
1411
1426
  const maxElapsed$1 = 40;
1412
1427
  function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
1413
1428
  let runNextFrame = false;
@@ -1419,16 +1434,18 @@
1419
1434
  };
1420
1435
  const flagRunNextFrame = () => (runNextFrame = true);
1421
1436
  const steps = stepsOrder.reduce((acc, key) => {
1422
- acc[key] = createRenderStep(flagRunNextFrame);
1437
+ acc[key] = createRenderStep(flagRunNextFrame, allowKeepAlive ? key : undefined);
1423
1438
  return acc;
1424
1439
  }, {});
1425
1440
  const { read, resolveKeyframes, update, preRender, render, postRender } = steps;
1426
1441
  const processBatch = () => {
1427
1442
  const timestamp = performance.now();
1428
1443
  runNextFrame = false;
1429
- state.delta = useDefaultElapsed
1430
- ? 1000 / 60
1431
- : Math.max(Math.min(timestamp - state.timestamp, maxElapsed$1), 1);
1444
+ {
1445
+ state.delta = useDefaultElapsed
1446
+ ? 1000 / 60
1447
+ : Math.max(Math.min(timestamp - state.timestamp, maxElapsed$1), 1);
1448
+ }
1432
1449
  state.timestamp = timestamp;
1433
1450
  state.isProcessing = true;
1434
1451
  // Unrolled render loop for better per-frame performance
@@ -1571,7 +1588,7 @@
1571
1588
  * This will be replaced by the build step with the latest version number.
1572
1589
  * When MotionValues are provided to motion components, warn if versions are mixed.
1573
1590
  */
1574
- this.version = "12.3.1";
1591
+ this.version = "12.4.0";
1575
1592
  /**
1576
1593
  * Tracks whether this value can output a velocity. Currently this is only true
1577
1594
  * if the value is numerical, but we might be able to widen the scope here and support
@@ -3870,7 +3887,7 @@
3870
3887
  */
3871
3888
  if (Array.isArray(easing))
3872
3889
  keyframeOptions.easing = easing;
3873
- return element.animate(keyframeOptions, {
3890
+ const animation = element.animate(keyframeOptions, {
3874
3891
  delay,
3875
3892
  duration,
3876
3893
  easing: !Array.isArray(easing) ? easing : "linear",
@@ -3878,6 +3895,7 @@
3878
3895
  iterations: repeat + 1,
3879
3896
  direction: repeatType === "reverse" ? "alternate" : "normal",
3880
3897
  });
3898
+ return animation;
3881
3899
  }
3882
3900
 
3883
3901
  const supportsWaapi = /*@__PURE__*/ memo(() => Object.hasOwnProperty.call(Element.prototype, "animate"));
@@ -4505,7 +4523,7 @@
4505
4523
  * and warn against mismatches.
4506
4524
  */
4507
4525
  {
4508
- warnOnce(nextValue.version === "12.3.1", `Attempting to mix Motion versions ${nextValue.version} with 12.3.1 may not work as expected.`);
4526
+ warnOnce(nextValue.version === "12.4.0", `Attempting to mix Motion versions ${nextValue.version} with 12.4.0 may not work as expected.`);
4509
4527
  }
4510
4528
  }
4511
4529
  else if (isMotionValue(prevValue)) {