motion 13.4.6 → 13.5.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.
@@ -40,8 +40,7 @@
40
40
 
41
41
  exports.warning = () => { };
42
42
  exports.invariant = () => { };
43
- if (typeof process !== "undefined" &&
44
- process.env?.NODE_ENV !== "production") {
43
+ if (typeof process !== "undefined" && "development" !== "production") {
45
44
  exports.warning = (check, message, errorCode) => {
46
45
  if (!check && typeof console !== "undefined") {
47
46
  console.warn(formatErrorMessage(message, errorCode));
@@ -310,170 +309,15 @@
310
309
  return definition;
311
310
  };
312
311
 
313
- const stepsOrder = [
314
- "setup", // Compute
315
- "read", // Read
316
- "resolveKeyframes", // Write/Read/Write/Read
317
- "preUpdate", // Compute
318
- "update", // Compute
319
- "preRender", // Compute
320
- "render", // Write
321
- "postRender", // Compute
322
- ];
323
-
324
- function createRenderStep(runNextFrame) {
325
- /**
326
- * We create and reuse two queues, one to queue jobs for the current frame
327
- * and one for the next. We reuse to avoid triggering GC after x frames.
328
- */
329
- let thisFrame = new Set();
330
- let nextFrame = new Set();
331
- /**
332
- * Track whether we're currently processing jobs in this step. This way
333
- * we can decide whether to schedule new jobs for this frame or next.
334
- */
335
- let isProcessing = false;
336
- let flushNextFrame = false;
337
- /**
338
- * A set of processes which were marked keepAlive when scheduled.
339
- * A keepAlive process is always also held by a frame queue until
340
- * it's cancelled, so a Set has the same lifetime semantics as a
341
- * WeakSet here while being considerably faster to query every frame.
342
- */
343
- const toKeepAlive = new Set();
344
- let latestFrameData = {
345
- delta: 0.0,
346
- timestamp: 0.0,
347
- isProcessing: false,
348
- };
349
- function triggerCallback(callback) {
350
- if (toKeepAlive.has(callback)) {
351
- nextFrame.add(callback);
352
- runNextFrame();
353
- }
354
- callback(latestFrameData);
355
- }
356
- const step = {
357
- /**
358
- * Schedule a process to run on the next frame.
359
- */
360
- schedule: (callback, keepAlive = false, immediate = false) => {
361
- const addToCurrentFrame = immediate && isProcessing;
362
- const queue = addToCurrentFrame ? thisFrame : nextFrame;
363
- if (keepAlive)
364
- toKeepAlive.add(callback);
365
- queue.add(callback);
366
- return callback;
367
- },
368
- /**
369
- * Cancel the provided callback from running on the next frame.
370
- */
371
- cancel: (callback) => {
372
- nextFrame.delete(callback);
373
- toKeepAlive.delete(callback);
374
- },
375
- /**
376
- * Execute all schedule callbacks.
377
- */
378
- process: (frameData) => {
379
- latestFrameData = frameData;
380
- /**
381
- * If we're already processing we've probably been triggered by a flushSync
382
- * inside an existing process. Instead of executing, mark flushNextFrame
383
- * as true and ensure we flush the following frame at the end of this one.
384
- */
385
- if (isProcessing) {
386
- flushNextFrame = true;
387
- return;
388
- }
389
- isProcessing = true;
390
- // Swap this frame and the next to avoid GC
391
- const prevFrame = thisFrame;
392
- thisFrame = nextFrame;
393
- nextFrame = prevFrame;
394
- // Execute this frame
395
- thisFrame.forEach(triggerCallback);
396
- // Clear the frame so no callbacks remain. This is to avoid
397
- // memory leaks should this render step not run for a while.
398
- thisFrame.clear();
399
- isProcessing = false;
400
- if (flushNextFrame) {
401
- flushNextFrame = false;
402
- step.process(frameData);
403
- }
404
- },
405
- };
406
- return step;
407
- }
408
-
409
- const maxElapsed$1 = 40;
410
- function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
411
- let runNextFrame = false;
412
- let useDefaultElapsed = true;
413
- const state = {
414
- delta: 0.0,
415
- timestamp: 0.0,
416
- isProcessing: false,
417
- };
418
- const flagRunNextFrame = () => (runNextFrame = true);
419
- const steps = stepsOrder.reduce((acc, key) => {
420
- acc[key] = createRenderStep(flagRunNextFrame);
421
- return acc;
422
- }, {});
423
- const { setup, read, resolveKeyframes, preUpdate, update, preRender, render, postRender, } = steps;
424
- const processBatch = () => {
425
- const useManualTiming = MotionGlobalConfig.useManualTiming;
426
- const timestamp = useManualTiming
427
- ? state.timestamp
428
- : performance.now();
429
- runNextFrame = false;
430
- if (!useManualTiming) {
431
- state.delta = useDefaultElapsed
432
- ? 1000 / 60
433
- : Math.max(Math.min(timestamp - state.timestamp, maxElapsed$1), 1);
434
- }
435
- state.timestamp = timestamp;
436
- state.isProcessing = true;
437
- // Unrolled render loop for better per-frame performance
438
- setup.process(state);
439
- read.process(state);
440
- resolveKeyframes.process(state);
441
- preUpdate.process(state);
442
- update.process(state);
443
- preRender.process(state);
444
- render.process(state);
445
- postRender.process(state);
446
- state.isProcessing = false;
447
- if (runNextFrame && allowKeepAlive) {
448
- useDefaultElapsed = false;
449
- scheduleNextBatch(processBatch);
450
- }
451
- };
452
- const wake = () => {
453
- runNextFrame = true;
454
- useDefaultElapsed = true;
455
- if (!state.isProcessing) {
456
- scheduleNextBatch(processBatch);
457
- }
458
- };
459
- const schedule = stepsOrder.reduce((acc, key) => {
460
- const step = steps[key];
461
- acc[key] = (process, keepAlive = false, immediate = false) => {
462
- if (!runNextFrame)
463
- wake();
464
- return step.schedule(process, keepAlive, immediate);
465
- };
466
- return acc;
467
- }, {});
468
- const cancel = (process) => {
469
- for (let i = 0; i < stepsOrder.length; i++) {
470
- steps[stepsOrder[i]].cancel(process);
471
- }
472
- };
473
- return { schedule, cancel, state, steps };
474
- }
475
-
476
- const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true);
312
+ /**
313
+ * The frameloop's state, kept apart from the frameloop itself so that
314
+ * reading it doesn't bundle the scheduler.
315
+ */
316
+ const frameData = {
317
+ delta: 0.0,
318
+ timestamp: 0.0,
319
+ isProcessing: false,
320
+ };
477
321
 
478
322
  let now;
479
323
  function clearTime() {
@@ -1057,6 +901,170 @@
1057
901
  return mixer(from, to);
1058
902
  }
1059
903
 
904
+ const stepsOrder = [
905
+ "setup", // Compute
906
+ "read", // Read
907
+ "resolveKeyframes", // Write/Read/Write/Read
908
+ "preUpdate", // Compute
909
+ "update", // Compute
910
+ "preRender", // Compute
911
+ "render", // Write
912
+ "postRender", // Compute
913
+ ];
914
+
915
+ function createRenderStep(runNextFrame) {
916
+ /**
917
+ * We create and reuse two queues, one to queue jobs for the current frame
918
+ * and one for the next. We reuse to avoid triggering GC after x frames.
919
+ */
920
+ let thisFrame = new Set();
921
+ let nextFrame = new Set();
922
+ /**
923
+ * Track whether we're currently processing jobs in this step. This way
924
+ * we can decide whether to schedule new jobs for this frame or next.
925
+ */
926
+ let isProcessing = false;
927
+ let flushNextFrame = false;
928
+ /**
929
+ * A set of processes which were marked keepAlive when scheduled.
930
+ * A keepAlive process is always also held by a frame queue until
931
+ * it's cancelled, so a Set has the same lifetime semantics as a
932
+ * WeakSet here while being considerably faster to query every frame.
933
+ */
934
+ const toKeepAlive = new Set();
935
+ let latestFrameData = {
936
+ delta: 0.0,
937
+ timestamp: 0.0,
938
+ isProcessing: false,
939
+ };
940
+ function triggerCallback(callback) {
941
+ if (toKeepAlive.has(callback)) {
942
+ nextFrame.add(callback);
943
+ runNextFrame();
944
+ }
945
+ callback(latestFrameData);
946
+ }
947
+ const step = {
948
+ /**
949
+ * Schedule a process to run on the next frame.
950
+ */
951
+ schedule: (callback, keepAlive = false, immediate = false) => {
952
+ const addToCurrentFrame = immediate && isProcessing;
953
+ const queue = addToCurrentFrame ? thisFrame : nextFrame;
954
+ if (keepAlive)
955
+ toKeepAlive.add(callback);
956
+ queue.add(callback);
957
+ return callback;
958
+ },
959
+ /**
960
+ * Cancel the provided callback from running on the next frame.
961
+ */
962
+ cancel: (callback) => {
963
+ nextFrame.delete(callback);
964
+ toKeepAlive.delete(callback);
965
+ },
966
+ /**
967
+ * Execute all schedule callbacks.
968
+ */
969
+ process: (frameData) => {
970
+ latestFrameData = frameData;
971
+ /**
972
+ * If we're already processing we've probably been triggered by a flushSync
973
+ * inside an existing process. Instead of executing, mark flushNextFrame
974
+ * as true and ensure we flush the following frame at the end of this one.
975
+ */
976
+ if (isProcessing) {
977
+ flushNextFrame = true;
978
+ return;
979
+ }
980
+ isProcessing = true;
981
+ // Swap this frame and the next to avoid GC
982
+ const prevFrame = thisFrame;
983
+ thisFrame = nextFrame;
984
+ nextFrame = prevFrame;
985
+ // Execute this frame
986
+ thisFrame.forEach(triggerCallback);
987
+ // Clear the frame so no callbacks remain. This is to avoid
988
+ // memory leaks should this render step not run for a while.
989
+ thisFrame.clear();
990
+ isProcessing = false;
991
+ if (flushNextFrame) {
992
+ flushNextFrame = false;
993
+ step.process(frameData);
994
+ }
995
+ },
996
+ };
997
+ return step;
998
+ }
999
+
1000
+ const maxElapsed$1 = 40;
1001
+ function createRenderBatcher(scheduleNextBatch, allowKeepAlive, state = {
1002
+ delta: 0.0,
1003
+ timestamp: 0.0,
1004
+ isProcessing: false,
1005
+ }) {
1006
+ let runNextFrame = false;
1007
+ let useDefaultElapsed = true;
1008
+ const flagRunNextFrame = () => (runNextFrame = true);
1009
+ const steps = stepsOrder.reduce((acc, key) => {
1010
+ acc[key] = createRenderStep(flagRunNextFrame);
1011
+ return acc;
1012
+ }, {});
1013
+ const { setup, read, resolveKeyframes, preUpdate, update, preRender, render, postRender, } = steps;
1014
+ const processBatch = () => {
1015
+ const useManualTiming = MotionGlobalConfig.useManualTiming;
1016
+ const timestamp = useManualTiming
1017
+ ? state.timestamp
1018
+ : performance.now();
1019
+ runNextFrame = false;
1020
+ if (!useManualTiming) {
1021
+ state.delta = useDefaultElapsed
1022
+ ? 1000 / 60
1023
+ : Math.max(Math.min(timestamp - state.timestamp, maxElapsed$1), 1);
1024
+ }
1025
+ state.timestamp = timestamp;
1026
+ state.isProcessing = true;
1027
+ // Unrolled render loop for better per-frame performance
1028
+ setup.process(state);
1029
+ read.process(state);
1030
+ resolveKeyframes.process(state);
1031
+ preUpdate.process(state);
1032
+ update.process(state);
1033
+ preRender.process(state);
1034
+ render.process(state);
1035
+ postRender.process(state);
1036
+ state.isProcessing = false;
1037
+ if (runNextFrame && allowKeepAlive) {
1038
+ useDefaultElapsed = false;
1039
+ scheduleNextBatch(processBatch);
1040
+ }
1041
+ };
1042
+ const wake = () => {
1043
+ runNextFrame = true;
1044
+ useDefaultElapsed = true;
1045
+ if (!state.isProcessing) {
1046
+ scheduleNextBatch(processBatch);
1047
+ }
1048
+ };
1049
+ const schedule = stepsOrder.reduce((acc, key) => {
1050
+ const step = steps[key];
1051
+ acc[key] = (process, keepAlive = false, immediate = false) => {
1052
+ if (!runNextFrame)
1053
+ wake();
1054
+ return step.schedule(process, keepAlive, immediate);
1055
+ };
1056
+ return acc;
1057
+ }, {});
1058
+ const cancel = (process) => {
1059
+ for (let i = 0; i < stepsOrder.length; i++) {
1060
+ steps[stepsOrder[i]].cancel(process);
1061
+ }
1062
+ };
1063
+ return { schedule, cancel, state, steps };
1064
+ }
1065
+
1066
+ const { schedule: frame, cancel: cancelFrame, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true, frameData);
1067
+
1060
1068
  const frameloopDriver = (update) => {
1061
1069
  const passTimestamp = ({ timestamp }) => update(timestamp);
1062
1070
  return {
@@ -1135,8 +1143,37 @@
1135
1143
  minDuration: 0.01, // in seconds
1136
1144
  maxDuration: 10.0, // in seconds
1137
1145
  minDamping: 0.05,
1138
- maxDamping: 1,
1139
1146
  };
1147
+ /**
1148
+ * Maps bounce to a damping ratio as SwiftUI does: 0 is critically damped, and
1149
+ * it approaches undamped towards 1 and infinitely overdamped towards -1. Both
1150
+ * ends are limited so the spring still moves and settles.
1151
+ */
1152
+ const bounceToDampingRatio = (bounce) => bounce < 0
1153
+ ? 1 / Math.max(1 + bounce, springDefaults.minDamping)
1154
+ : Math.max(1 - bounce, springDefaults.minDamping);
1155
+ /**
1156
+ * Scales an overdamped spring's undamped frequency so that, at its defining
1157
+ * time, it has as far left to go as the critically damped spring that's
1158
+ * criticalFreqTime radians in by then. A negative bounce then changes the
1159
+ * shape of the curve, not when it arrives. 1 at a damping ratio of 1, so
1160
+ * there's no jump at a bounce of 0.
1161
+ *
1162
+ * Solved by Newton on the remaining distance (times fast - slow), which is
1163
+ * convex from criticalFreqTime onwards. criticalFreqTime is always below the
1164
+ * answer, so it converges from below without overshooting.
1165
+ */
1166
+ function overdampedFreqScale(dampingRatio, criticalFreqTime) {
1167
+ if (!(dampingRatio > 1))
1168
+ return 1;
1169
+ const root = Math.sqrt(dampingRatio * dampingRatio - 1);
1170
+ const slow = dampingRatio - root;
1171
+ const fast = dampingRatio + root;
1172
+ const target = 2 * root * Math.exp(-criticalFreqTime) * (1 + criticalFreqTime);
1173
+ return (approximateRoot((freqTime) => fast * Math.exp(-slow * freqTime) -
1174
+ slow * Math.exp(-fast * freqTime) -
1175
+ target, (freqTime) => Math.exp(-fast * freqTime) - Math.exp(-slow * freqTime), criticalFreqTime) / criticalFreqTime);
1176
+ }
1140
1177
  function calcAngularFreq(undampedFreq, dampingRatio) {
1141
1178
  return undampedFreq * Math.sqrt(1 - dampingRatio * dampingRatio);
1142
1179
  }
@@ -1161,11 +1198,7 @@
1161
1198
  let envelope;
1162
1199
  let derivative;
1163
1200
  exports.warning(duration <= secondsToMilliseconds(springDefaults.maxDuration), "Spring duration must be 10 seconds or less", "spring-duration-limit");
1164
- let dampingRatio = 1 - bounce;
1165
- /**
1166
- * Restrict dampingRatio and duration to within acceptable ranges.
1167
- */
1168
- dampingRatio = clamp(springDefaults.minDamping, springDefaults.maxDamping, dampingRatio);
1201
+ const dampingRatio = bounceToDampingRatio(bounce);
1169
1202
  duration = clamp(springDefaults.minDuration, springDefaults.maxDuration, millisecondsToSeconds(duration));
1170
1203
  if (dampingRatio < 1) {
1171
1204
  /**
@@ -1194,7 +1227,8 @@
1194
1227
  }
1195
1228
  else {
1196
1229
  /**
1197
- * Critically-damped spring
1230
+ * Critically-damped spring. Overdamped springs reuse this root,
1231
+ * scaled below.
1198
1232
  */
1199
1233
  envelope = (undampedFreq) => {
1200
1234
  const a = Math.exp(-undampedFreq * duration);
@@ -1208,7 +1242,9 @@
1208
1242
  };
1209
1243
  }
1210
1244
  const initialGuess = 5 / duration;
1211
- const undampedFreq = approximateRoot(envelope, derivative, initialGuess);
1245
+ const criticalFreq = approximateRoot(envelope, derivative, initialGuess);
1246
+ const undampedFreq = criticalFreq *
1247
+ overdampedFreqScale(dampingRatio, criticalFreq * duration);
1212
1248
  const stiffness = undampedFreq * undampedFreq;
1213
1249
  return {
1214
1250
  stiffness,
@@ -1258,12 +1294,12 @@
1258
1294
  };
1259
1295
  if (springOptions.isTimeDefined) {
1260
1296
  if (options.visualDuration) {
1261
- const root = (2 * Math.PI) / (options.visualDuration * 1.2);
1297
+ const dampingRatio = bounceToDampingRatio(options.bounce || 0);
1298
+ const root = ((2 * Math.PI) / (options.visualDuration * 1.2)) *
1299
+ overdampedFreqScale(dampingRatio, (2 * Math.PI) / 1.2);
1262
1300
  springOptions.stiffness = root * root;
1263
1301
  springOptions.damping =
1264
- 2 *
1265
- clamp(0.05, 1, 1 - (options.bounce || 0)) *
1266
- Math.sqrt(springOptions.stiffness);
1302
+ 2 * dampingRatio * Math.sqrt(springOptions.stiffness);
1267
1303
  }
1268
1304
  else {
1269
1305
  Object.assign(springOptions, findSpring(springOptions));
@@ -1398,31 +1434,32 @@
1398
1434
  (undampedAngularFreq * c.C * t - s.velocity);
1399
1435
  }
1400
1436
  else {
1401
- // Overdamped spring
1437
+ /**
1438
+ * Overdamped spring: the sum of a slow and a fast decaying
1439
+ * exponential, so no term can overflow however heavily it's damped.
1440
+ */
1402
1441
  const dampedAngularFreq = undampedAngularFreq * Math.sqrt(dampingRatio * dampingRatio - 1);
1403
- resolveSpring = (t) => {
1404
- const envelope = Math.exp(-decay * t);
1405
- // When performing sinh or cosh values can hit Infinity so we cap them here
1406
- const freqForT = Math.min(dampedAngularFreq * t, 300);
1407
- return (s.target -
1408
- (envelope *
1409
- ((s.velocity + decay * s.delta) * Math.sinh(freqForT) +
1410
- dampedAngularFreq * s.delta * Math.cosh(freqForT))) /
1411
- dampedAngularFreq);
1412
- };
1413
- // Analytical derivative of overdamped spring (px/ms)
1414
- const c = { P: 0, sinh: 0, cosh: 0 };
1442
+ const slow = decay - dampedAngularFreq;
1443
+ const fast = decay + dampedAngularFreq;
1444
+ /**
1445
+ * Physics-defined springs keep a non-physical limit: once
1446
+ * dampedAngularFreq * t passes 300, both terms decay at the damping
1447
+ * rate. Heavily overdamped drag springs (dragElastic: 0,
1448
+ * dragTransition) rely on this to settle almost at once rather than
1449
+ * creep at stiffness / damping. Negative bounce needs the exact
1450
+ * curve, so time-defined springs are exempt.
1451
+ */
1452
+ const limit = isTimeDefined ? Infinity : 300 / dampedAngularFreq;
1453
+ const decayAt = (rate, t) => Math.exp(t > limit ? -rate * limit - decay * (t - limit) : -rate * t);
1454
+ const c = { S: 0, F: 0 };
1415
1455
  update = () => {
1416
- c.P = (s.velocity + decay * s.delta) / dampedAngularFreq;
1417
- c.sinh = decay * c.P - s.delta * dampedAngularFreq;
1418
- c.cosh = decay * s.delta - c.P * dampedAngularFreq;
1419
- };
1420
- resolveVelocity = (t) => {
1421
- const envelope = Math.exp(-decay * t);
1422
- const freqForT = Math.min(dampedAngularFreq * t, 300);
1423
- return (envelope *
1424
- (c.sinh * Math.sinh(freqForT) + c.cosh * Math.cosh(freqForT)));
1456
+ const P = (s.velocity + decay * s.delta) / dampedAngularFreq;
1457
+ c.S = (s.delta + P) / 2;
1458
+ c.F = (s.delta - P) / 2;
1425
1459
  };
1460
+ resolveSpring = (t) => s.target - c.S * decayAt(slow, t) - c.F * decayAt(fast, t);
1461
+ // Analytical derivative of overdamped spring (px/ms)
1462
+ resolveVelocity = (t) => slow * c.S * decayAt(slow, t) + fast * c.F * decayAt(fast, t);
1426
1463
  }
1427
1464
  update();
1428
1465
  const calculatedDuration = isResolvedFromDuration ? duration || null : null;
@@ -3000,17 +3037,14 @@
3000
3037
  /**
3001
3038
  * Attaches a timeline to the animation, for instance the `ScrollTimeline`.
3002
3039
  */
3003
- attachTimeline({ timeline, rangeStart, rangeEnd, observe, }) {
3040
+ attachTimeline({ timeline, onAttach, observe, }) {
3004
3041
  if (this.allowFlatten) {
3005
3042
  this.animation.effect?.updateTiming({ easing: "linear" });
3006
3043
  }
3007
3044
  this.animation.onfinish = null;
3008
3045
  if (timeline && supportsScrollTimeline()) {
3009
3046
  this.animation.timeline = timeline;
3010
- if (rangeStart)
3011
- this.animation.rangeStart = rangeStart;
3012
- if (rangeEnd)
3013
- this.animation.rangeEnd = rangeEnd;
3047
+ onAttach?.(this.animation);
3014
3048
  return noop;
3015
3049
  }
3016
3050
  else {
@@ -6343,21 +6377,6 @@
6343
6377
  return typeof a === "function" ? resizeWindow(a) : resizeElement(a, b);
6344
6378
  }
6345
6379
 
6346
- function observeTimeline(update, timeline) {
6347
- let prevProgress;
6348
- const onFrame = () => {
6349
- const { currentTime } = timeline;
6350
- const percentage = currentTime === null ? 0 : currentTime.value;
6351
- const progress = percentage / 100;
6352
- if (prevProgress !== progress) {
6353
- update(progress);
6354
- }
6355
- prevProgress = progress;
6356
- };
6357
- frame.preUpdate(onFrame, true);
6358
- return () => cancelFrame(onFrame);
6359
- }
6360
-
6361
6380
  const statsBuffer = {
6362
6381
  value: null,
6363
6382
  addProjectionMetrics: null,
@@ -6471,8 +6490,7 @@
6471
6490
  this.options.onStop?.();
6472
6491
  };
6473
6492
  this.options = options;
6474
- replaceTransitionType(options);
6475
- this.factory = options.type || keyframes;
6493
+ this.factory = options.type;
6476
6494
  this.generator = this.factory(options);
6477
6495
  const { driver } = options;
6478
6496
  if (driver)
@@ -6573,41 +6591,15 @@
6573
6591
  }
6574
6592
  }
6575
6593
 
6594
+ const asNumber = (v) => typeof v === "number" ? v : parseFloat(v);
6595
+
6576
6596
  /**
6577
- * Create a `MotionValue` that animates to its latest value using any transition type.
6578
- * Can either be a value or track another `MotionValue`.
6579
- *
6580
- * ```jsx
6581
- * const x = motionValue(0)
6582
- * const y = followValue(x, { type: "spring", stiffness: 300 })
6583
- * // or with tween
6584
- * const z = followValue(x, { type: "tween", duration: 0.5, ease: "easeOut" })
6585
- * ```
6586
- *
6587
- * @param source - Initial value or MotionValue to track
6588
- * @param options - Animation transition options
6589
- * @returns `MotionValue`
6590
- *
6591
- * @public
6592
- */
6593
- function followValue(source, options) {
6594
- const initialValue = isMotionValue(source) ? source.get() : source;
6595
- const value = motionValue(initialValue);
6596
- attachFollow(value, source, options);
6597
- return value;
6598
- }
6599
- /**
6600
- * Attach an animation to a MotionValue that will animate whenever the value changes.
6601
- * Similar to attachSpring but supports any transition type (spring, tween, inertia, etc.)
6602
- *
6603
- * @param value - The MotionValue to animate
6604
- * @param source - Initial value or MotionValue to track
6605
- * @param options - Animation transition options
6606
- * @returns Cleanup function
6607
- *
6608
- * @public
6597
+ * Animate `value` to its latest value with the generator in
6598
+ * `options.type`, tracking `source` if it's a `MotionValue`. Takes a
6599
+ * generator rather than a type name, so spring-only callers don't
6600
+ * bundle every generator.
6609
6601
  */
6610
- function attachFollow(value, source, options = {}) {
6602
+ function follow(value, source, options) {
6611
6603
  const initialValue = value.get();
6612
6604
  let activeAnimation = null;
6613
6605
  let set;
@@ -6625,7 +6617,7 @@
6625
6617
  };
6626
6618
  value.attach((v, safeSet) => {
6627
6619
  set = safeSet;
6628
- const target = asNumber$1(v);
6620
+ const target = asNumber(v);
6629
6621
  if (activeAnimation?.state === "running") {
6630
6622
  /**
6631
6623
  * Steer the running animation rather than replacing it. This
@@ -6636,7 +6628,7 @@
6636
6628
  activeAnimation.setTarget(target, options.velocity);
6637
6629
  return;
6638
6630
  }
6639
- const current = asNumber$1(value.get());
6631
+ const current = asNumber(value.get());
6640
6632
  const velocity = activeAnimation
6641
6633
  ? activeAnimation.getGeneratorVelocity()
6642
6634
  : value.getVelocity();
@@ -6647,8 +6639,6 @@
6647
6639
  const animationOptions = {
6648
6640
  keyframes: [current, target],
6649
6641
  velocity,
6650
- // Default to spring if no type specified (matches useSpring behavior)
6651
- type: "spring",
6652
6642
  restDelta: 0.001,
6653
6643
  restSpeed: 0.01,
6654
6644
  ...options,
@@ -6690,8 +6680,47 @@
6690
6680
  function parseValue(v, unit) {
6691
6681
  return unit ? v + unit : v;
6692
6682
  }
6693
- function asNumber$1(v) {
6694
- return typeof v === "number" ? v : parseFloat(v);
6683
+
6684
+ /**
6685
+ * Create a `MotionValue` that animates to its latest value using any transition type.
6686
+ * Can either be a value or track another `MotionValue`.
6687
+ *
6688
+ * ```jsx
6689
+ * const x = motionValue(0)
6690
+ * const y = followValue(x, { type: "spring", stiffness: 300 })
6691
+ * // or with tween
6692
+ * const z = followValue(x, { type: "tween", duration: 0.5, ease: "easeOut" })
6693
+ * ```
6694
+ *
6695
+ * @param source - Initial value or MotionValue to track
6696
+ * @param options - Animation transition options
6697
+ * @returns `MotionValue`
6698
+ *
6699
+ * @public
6700
+ */
6701
+ function followValue(source, options) {
6702
+ const initialValue = isMotionValue(source) ? source.get() : source;
6703
+ const value = motionValue(initialValue);
6704
+ attachFollow(value, source, options);
6705
+ return value;
6706
+ }
6707
+ /**
6708
+ * Attach an animation to a MotionValue that will animate whenever the value changes.
6709
+ * Similar to attachSpring but supports any transition type (spring, tween, inertia, etc.)
6710
+ *
6711
+ * @param value - The MotionValue to animate
6712
+ * @param source - Initial value or MotionValue to track
6713
+ * @param options - Animation transition options
6714
+ * @returns Cleanup function
6715
+ *
6716
+ * @public
6717
+ */
6718
+ function attachFollow(value, source, options = {}) {
6719
+ // Default to spring if no type specified (matches useSpring behavior)
6720
+ const transition = { type: "spring", ...options };
6721
+ replaceTransitionType(transition);
6722
+ transition.type = transition.type || keyframes;
6723
+ return follow(value, source, transition);
6695
6724
  }
6696
6725
 
6697
6726
  function subscribeValue(inputValues, outputValue, getLatest) {
@@ -6793,7 +6822,9 @@
6793
6822
  * @public
6794
6823
  */
6795
6824
  function springValue(source, options) {
6796
- return followValue(source, { type: "spring", ...options });
6825
+ const value = motionValue(isMotionValue(source) ? source.get() : source);
6826
+ attachSpring(value, source, options);
6827
+ return value;
6797
6828
  }
6798
6829
  /**
6799
6830
  * Attach a spring animation to a MotionValue that will animate whenever the value changes.
@@ -6806,7 +6837,7 @@
6806
6837
  * @public
6807
6838
  */
6808
6839
  function attachSpring(value, source, options) {
6809
- return attachFollow(value, source, { type: "spring", ...options });
6840
+ return follow(value, source, { ...options, type: spring });
6810
6841
  }
6811
6842
 
6812
6843
  /**
@@ -8476,7 +8507,15 @@
8476
8507
  this.removeValue(key);
8477
8508
  this.bindToMotionValue(key, value);
8478
8509
  this.values.set(key, value);
8479
- this.latestValues[key] = value.get();
8510
+ /**
8511
+ * An animated value with no base value is undefined until the
8512
+ * keyframe resolver reads its origin. Rendering it before then
8513
+ * writes an invalid placeholder (e.g. points="undefined") that
8514
+ * the resolver can read back from the DOM as the origin.
8515
+ */
8516
+ const latest = value.get();
8517
+ if (latest !== undefined)
8518
+ this.latestValues[key] = latest;
8480
8519
  }
8481
8520
  }
8482
8521
  /**
@@ -8504,7 +8543,9 @@
8504
8543
  }
8505
8544
  let value = this.values.get(key);
8506
8545
  if (value === undefined && defaultValue !== undefined) {
8507
- value = motionValue(defaultValue === null ? undefined : defaultValue, { owner: this });
8546
+ value = motionValue(defaultValue ?? this.getDefaultValue?.(key), {
8547
+ owner: this,
8548
+ });
8508
8549
  this.addValue(key, value);
8509
8550
  }
8510
8551
  return value;
@@ -8666,86 +8707,7 @@
8666
8707
  }
8667
8708
  }
8668
8709
 
8669
- function pixelsToPercent(pixels, axis) {
8670
- if (axis.max === axis.min)
8671
- return 0;
8672
- return (pixels / (axis.max - axis.min)) * 100;
8673
- }
8674
- /**
8675
- * We always correct borderRadius as a percentage rather than pixels to reduce paints.
8676
- * For example, if you are projecting a box that is 100px wide with a 10px borderRadius
8677
- * into a box that is 200px wide with a 20px borderRadius, that is actually a 10%
8678
- * borderRadius in both states. If we animate between the two in pixels that will trigger
8679
- * a paint each time. If we animate between the two in percentage we'll avoid a paint.
8680
- */
8681
- const correctBorderRadius = {
8682
- correct: (latest, node) => {
8683
- if (!node.target)
8684
- return latest;
8685
- /**
8686
- * If latest is a string, if it's a percentage we can return immediately as it's
8687
- * going to be stretched appropriately. Otherwise, if it's a pixel, convert it to a number.
8688
- */
8689
- if (typeof latest === "string") {
8690
- if (px.test(latest)) {
8691
- latest = parseFloat(latest);
8692
- }
8693
- else {
8694
- return latest;
8695
- }
8696
- }
8697
- /**
8698
- * If latest is a number, it's a pixel value. We use the current viewportBox to calculate that
8699
- * pixel value as a percentage of each axis
8700
- */
8701
- const x = pixelsToPercent(latest, node.target.x);
8702
- const y = pixelsToPercent(latest, node.target.y);
8703
- return `${x}% ${y}%`;
8704
- },
8705
- };
8706
-
8707
- const correctBoxShadow = {
8708
- correct: (latest, { treeScale, projectionDelta }) => {
8709
- const original = latest;
8710
- const shadow = complex.parse(latest);
8711
- // TODO: Doesn't support multiple shadows
8712
- if (shadow.length > 5)
8713
- return original;
8714
- const template = complex.createTransformer(latest);
8715
- const offset = typeof shadow[0] !== "number" ? 1 : 0;
8716
- // Calculate the overall context scale
8717
- const xScale = projectionDelta.x.scale * treeScale.x;
8718
- const yScale = projectionDelta.y.scale * treeScale.y;
8719
- shadow[0 + offset] /= xScale;
8720
- shadow[1 + offset] /= yScale;
8721
- /**
8722
- * Ideally we'd correct x and y scales individually, but because blur and
8723
- * spread apply to both we have to take a scale average and apply that instead.
8724
- * We could potentially improve the outcome of this by incorporating the ratio between
8725
- * the two scales.
8726
- */
8727
- const averageScale = mixNumber$1(xScale, yScale, 0.5);
8728
- // Blur
8729
- if (typeof shadow[2 + offset] === "number")
8730
- shadow[2 + offset] /= averageScale;
8731
- // Spread
8732
- if (typeof shadow[3 + offset] === "number")
8733
- shadow[3 + offset] /= averageScale;
8734
- return template(shadow);
8735
- },
8736
- };
8737
-
8738
- const scaleCorrectors = {
8739
- borderRadius: {
8740
- ...correctBorderRadius,
8741
- applyTo: [...cornerRadiusProps],
8742
- },
8743
- borderTopLeftRadius: correctBorderRadius,
8744
- borderTopRightRadius: correctBorderRadius,
8745
- borderBottomLeftRadius: correctBorderRadius,
8746
- borderBottomRightRadius: correctBorderRadius,
8747
- boxShadow: correctBoxShadow,
8748
- };
8710
+ const scaleCorrectors = {};
8749
8711
  function addScaleCorrector(correctors) {
8750
8712
  for (const key in correctors) {
8751
8713
  scaleCorrectors[key] = correctors[key];
@@ -8919,7 +8881,24 @@
8919
8881
  this.measureInstanceViewportBox = createBox;
8920
8882
  }
8921
8883
  getBaseTargetFromProps(props, key) {
8922
- return props[key];
8884
+ /**
8885
+ * An independent transform's base is never an attribute of the same
8886
+ * name, e.g. <rect x> is a position, not a translate. Like HTML it
8887
+ * comes from style, falling back to the transform's default.
8888
+ */
8889
+ return transformProps.has(key)
8890
+ ? super.getBaseTargetFromProps(props, key) ??
8891
+ defaultTransformValue(key)
8892
+ : props[key];
8893
+ }
8894
+ /**
8895
+ * An independent transform's origin is never read from the DOM, so it
8896
+ * starts from its latest or default value and renders on mount.
8897
+ */
8898
+ getDefaultValue(key) {
8899
+ return transformProps.has(key)
8900
+ ? this.latestValues[key] ?? defaultTransformValue(key)
8901
+ : undefined;
8923
8902
  }
8924
8903
  readValueFromInstance(instance, key) {
8925
8904
  if (transformProps.has(key)) {
@@ -9532,6 +9511,75 @@
9532
9511
  return [callback("x"), callback("y")];
9533
9512
  }
9534
9513
 
9514
+ function pixelsToPercent(pixels, axis) {
9515
+ if (axis.max === axis.min)
9516
+ return 0;
9517
+ return (pixels / (axis.max - axis.min)) * 100;
9518
+ }
9519
+ /**
9520
+ * We always correct borderRadius as a percentage rather than pixels to reduce paints.
9521
+ * For example, if you are projecting a box that is 100px wide with a 10px borderRadius
9522
+ * into a box that is 200px wide with a 20px borderRadius, that is actually a 10%
9523
+ * borderRadius in both states. If we animate between the two in pixels that will trigger
9524
+ * a paint each time. If we animate between the two in percentage we'll avoid a paint.
9525
+ */
9526
+ const correctBorderRadius = {
9527
+ correct: (latest, node) => {
9528
+ if (!node.target)
9529
+ return latest;
9530
+ /**
9531
+ * If latest is a string, if it's a percentage we can return immediately as it's
9532
+ * going to be stretched appropriately. Otherwise, if it's a pixel, convert it to a number.
9533
+ */
9534
+ if (typeof latest === "string") {
9535
+ if (px.test(latest)) {
9536
+ latest = parseFloat(latest);
9537
+ }
9538
+ else {
9539
+ return latest;
9540
+ }
9541
+ }
9542
+ /**
9543
+ * If latest is a number, it's a pixel value. We use the current viewportBox to calculate that
9544
+ * pixel value as a percentage of each axis
9545
+ */
9546
+ const x = pixelsToPercent(latest, node.target.x);
9547
+ const y = pixelsToPercent(latest, node.target.y);
9548
+ return `${x}% ${y}%`;
9549
+ },
9550
+ };
9551
+
9552
+ const correctBoxShadow = {
9553
+ correct: (latest, { treeScale, projectionDelta }) => {
9554
+ const original = latest;
9555
+ const shadow = complex.parse(latest);
9556
+ // TODO: Doesn't support multiple shadows
9557
+ if (shadow.length > 5)
9558
+ return original;
9559
+ const template = complex.createTransformer(latest);
9560
+ const offset = typeof shadow[0] !== "number" ? 1 : 0;
9561
+ // Calculate the overall context scale
9562
+ const xScale = projectionDelta.x.scale * treeScale.x;
9563
+ const yScale = projectionDelta.y.scale * treeScale.y;
9564
+ shadow[0 + offset] /= xScale;
9565
+ shadow[1 + offset] /= yScale;
9566
+ /**
9567
+ * Ideally we'd correct x and y scales individually, but because blur and
9568
+ * spread apply to both we have to take a scale average and apply that instead.
9569
+ * We could potentially improve the outcome of this by incorporating the ratio between
9570
+ * the two scales.
9571
+ */
9572
+ const averageScale = mixNumber$1(xScale, yScale, 0.5);
9573
+ // Blur
9574
+ if (typeof shadow[2 + offset] === "number")
9575
+ shadow[2 + offset] /= averageScale;
9576
+ // Spread
9577
+ if (typeof shadow[3 + offset] === "number")
9578
+ shadow[3 + offset] /= averageScale;
9579
+ return template(shadow);
9580
+ },
9581
+ };
9582
+
9535
9583
  function buildProjectionTransform(delta, treeScale, latestTransform) {
9536
9584
  let transform = "";
9537
9585
  /**
@@ -9584,7 +9632,6 @@
9584
9632
  }
9585
9633
 
9586
9634
  const numBorders = cornerRadiusProps.length;
9587
- const asNumber = (value) => typeof value === "string" ? parseFloat(value) : value;
9588
9635
  const isPx = (value) => typeof value === "number" || px.test(value);
9589
9636
  function mixValues(target, follow, lead, progress, shouldCrossfadeOpacity, isOnlyMember) {
9590
9637
  if (shouldCrossfadeOpacity) {
@@ -9783,6 +9830,18 @@
9783
9830
  }
9784
9831
  }
9785
9832
 
9833
+ const defaultScaleCorrectors = {
9834
+ borderRadius: {
9835
+ ...correctBorderRadius,
9836
+ applyTo: [...cornerRadiusProps],
9837
+ },
9838
+ borderTopLeftRadius: correctBorderRadius,
9839
+ borderTopRightRadius: correctBorderRadius,
9840
+ borderBottomLeftRadius: correctBorderRadius,
9841
+ borderBottomRightRadius: correctBorderRadius,
9842
+ boxShadow: correctBoxShadow,
9843
+ };
9844
+
9786
9845
  /**
9787
9846
  * This should only ever be modified on the client otherwise it'll
9788
9847
  * persist through server requests. If we need instanced states we
@@ -9801,6 +9860,14 @@
9801
9860
  hasEverUpdated: false,
9802
9861
  };
9803
9862
 
9863
+ /**
9864
+ * Correctors registered via addScaleCorrector before projection loaded
9865
+ * (e.g. with an async LazyMotion bundle) take precedence over the defaults.
9866
+ */
9867
+ Object.assign(scaleCorrectors, {
9868
+ ...defaultScaleCorrectors,
9869
+ ...scaleCorrectors,
9870
+ });
9804
9871
  const metrics = {
9805
9872
  nodes: 0,
9806
9873
  calculatedTargetDeltas: 0,
@@ -12556,12 +12623,6 @@
12556
12623
  };
12557
12624
  const animateMini = /*@__PURE__*/ createScopedWaapiAnimate();
12558
12625
 
12559
- function canUseNativeTimeline(target) {
12560
- if (typeof window === "undefined")
12561
- return false;
12562
- return target ? supportsViewTimeline() : supportsScrollTimeline();
12563
- }
12564
-
12565
12626
  /**
12566
12627
  * A time in milliseconds, beyond which we consider the scroll velocity to be 0.
12567
12628
  */
@@ -12701,18 +12762,6 @@
12701
12762
  }
12702
12763
 
12703
12764
  const ScrollOffset = {
12704
- Enter: [
12705
- [0, 1],
12706
- [1, 1],
12707
- ],
12708
- Exit: [
12709
- [0, 0],
12710
- [1, 0],
12711
- ],
12712
- Any: [
12713
- [1, 0],
12714
- [0, 1],
12715
- ],
12716
12765
  All: [
12717
12766
  [0, 0],
12718
12767
  [1, 1],
@@ -12964,158 +13013,163 @@
12964
13013
  };
12965
13014
  }
12966
13015
 
12967
- /**
12968
- * Maps from ProgressIntersection pairs used by Motion's preset offsets to
12969
- * ViewTimeline named ranges. Returns undefined for unrecognised patterns,
12970
- * which signals the caller to fall back to JS-based scroll tracking.
12971
- */
12972
- const presets = [
12973
- [ScrollOffset.Enter, "entry"],
12974
- [ScrollOffset.Exit, "exit"],
12975
- [ScrollOffset.Any, "cover"],
12976
- [ScrollOffset.All, "contain"],
12977
- ];
12978
- const stringToProgress = {
12979
- start: 0,
12980
- end: 1,
12981
- };
12982
- function parseStringOffset(s) {
12983
- const parts = s.trim().split(/\s+/);
12984
- if (parts.length !== 2)
12985
- return undefined;
12986
- const a = stringToProgress[parts[0]];
12987
- const b = stringToProgress[parts[1]];
12988
- if (a === undefined || b === undefined)
12989
- return undefined;
12990
- return [a, b];
12991
- }
12992
- function normaliseOffset(offset) {
12993
- if (offset.length !== 2)
12994
- return undefined;
12995
- const result = [];
12996
- for (const item of offset) {
12997
- if (Array.isArray(item)) {
12998
- result.push(item);
12999
- }
13000
- else if (typeof item === "string") {
13001
- const parsed = parseStringOffset(item);
13002
- if (!parsed)
13003
- return undefined;
13004
- result.push(parsed);
13005
- }
13006
- else {
13007
- return undefined;
13008
- }
13009
- }
13010
- return result;
13011
- }
13012
- function matchesPreset(offset, preset) {
13013
- const normalised = normaliseOffset(offset);
13014
- if (!normalised)
13016
+ function canUseNativeTimeline(target) {
13017
+ if (typeof window === "undefined")
13015
13018
  return false;
13016
- for (let i = 0; i < 2; i++) {
13017
- const o = normalised[i];
13018
- const p = preset[i];
13019
- if (o[0] !== p[0] || o[1] !== p[1])
13020
- return false;
13021
- }
13022
- return true;
13023
- }
13024
- function offsetToViewTimelineRange(offset) {
13025
- if (!offset) {
13026
- return { rangeStart: "contain 0%", rangeEnd: "contain 100%" };
13027
- }
13028
- for (const [preset, name] of presets) {
13029
- if (matchesPreset(offset, preset)) {
13030
- return { rangeStart: `${name} 0%`, rangeEnd: `${name} 100%` };
13031
- }
13032
- }
13033
- return undefined;
13019
+ return target ? supportsViewTimeline() : supportsScrollTimeline();
13034
13020
  }
13035
13021
 
13036
13022
  const timelineCache = new Map();
13037
- function scrollTimelineFallback(options) {
13038
- const currentTime = { value: 0 };
13039
- const cancel = scrollInfo((info) => {
13040
- currentTime.value = info[options.axis].progress * 100;
13041
- }, options);
13042
- return { currentTime, cancel };
13043
- }
13044
- function getTimeline({ container, ...options }) {
13045
- const { axis } = options;
13023
+ /**
13024
+ * Native timelines are only attached to WAAPI animations. Offsets are
13025
+ * applied to each animation as a range, so a timeline is shared by every
13026
+ * offset.
13027
+ */
13028
+ function getTimeline({ container, target, axis, }) {
13046
13029
  let containerCache = timelineCache.get(container);
13047
13030
  if (!containerCache) {
13048
13031
  containerCache = new Map();
13049
13032
  timelineCache.set(container, containerCache);
13050
13033
  }
13051
- const targetKey = options.target ?? "self";
13034
+ const targetKey = target ?? "self";
13052
13035
  let targetCache = containerCache.get(targetKey);
13053
13036
  if (!targetCache) {
13054
13037
  targetCache = {};
13055
13038
  containerCache.set(targetKey, targetCache);
13056
13039
  }
13057
- const axisKey = axis + (options.offset ?? []).join(",");
13058
- if (!targetCache[axisKey]) {
13059
- if (options.target && canUseNativeTimeline(options.target)) {
13060
- const range = offsetToViewTimelineRange(options.offset);
13061
- if (range) {
13062
- targetCache[axisKey] = new ViewTimeline({
13063
- subject: options.target,
13064
- axis,
13065
- });
13066
- }
13067
- else {
13068
- targetCache[axisKey] = scrollTimelineFallback({
13069
- container,
13070
- ...options,
13071
- });
13072
- }
13073
- }
13074
- else if (canUseNativeTimeline()) {
13075
- targetCache[axisKey] = new ScrollTimeline({
13076
- source: container,
13077
- axis,
13078
- });
13079
- }
13080
- else {
13081
- targetCache[axisKey] = scrollTimelineFallback({
13082
- container,
13083
- ...options,
13084
- });
13085
- }
13040
+ return (targetCache[axis] || (targetCache[axis] = target
13041
+ ? new ViewTimeline({ subject: target, axis })
13042
+ : new ScrollTimeline({ source: container, axis })));
13043
+ }
13044
+
13045
+ /**
13046
+ * Resolved offsets are linear in the target and container lengths, so
13047
+ * probing them gives [target progress, container progress, pixels].
13048
+ * vw/vh can resolve to 0px, so they're rejected up front.
13049
+ */
13050
+ const toIntersection = (o) => {
13051
+ if (/v/u.test(o))
13052
+ return [];
13053
+ const px = resolveOffset(o, 0, 0, 0);
13054
+ return [resolveOffset(o, 0, 1, 0) - px, px - resolveOffset(o, 1, 0, 0), px];
13055
+ };
13056
+ const toRange = ([t, c, px]) => !px &&
13057
+ (c === 0 || c === 1) &&
13058
+ `${c ? "entry" : "exit"}-crossing ${t * 100}%`;
13059
+ /**
13060
+ * Maps an offset to an equivalent ViewTimeline range. Returns undefined when
13061
+ * there isn't one, which signals the caller to fall back to JS-based scroll
13062
+ * tracking.
13063
+ */
13064
+ function offsetToViewTimelineRange(offset = ScrollOffset.All) {
13065
+ if (offset.length !== 2)
13066
+ return;
13067
+ const [start, end] = offset.map(toIntersection);
13068
+ const points = [toRange(start), toRange(end)];
13069
+ const a = end[0] - start[0];
13070
+ const b = start[1] - end[1];
13071
+ if (points[0] && points[1] && (a || b)) {
13072
+ return {
13073
+ points: points,
13074
+ a,
13075
+ b,
13076
+ cover: !start[0] && a === 1 && b === 1,
13077
+ };
13086
13078
  }
13087
- return targetCache[axisKey];
13088
13079
  }
13089
13080
 
13090
13081
  function attachToAnimation(animation, options) {
13091
- const timeline = getTimeline(options);
13092
- const range = options.target
13093
- ? offsetToViewTimelineRange(options.offset)
13094
- : undefined;
13082
+ const { target, container, axis } = options;
13083
+ const range = target && offsetToViewTimelineRange(options.offset);
13095
13084
  /**
13096
13085
  * Use native timeline when:
13097
- * - No target: ScrollTimeline (existing behaviour)
13086
+ * - No target and no offset: ScrollTimeline
13098
13087
  * - Target with mappable offset: ViewTimeline with named range
13099
- * - Target with unmappable offset: fall back to JS observe
13100
- */
13101
- const useNative = options.target
13102
- ? canUseNativeTimeline(options.target) && !!range
13103
- : canUseNativeTimeline();
13104
- return animation.attachTimeline({
13105
- timeline: useNative ? timeline : undefined,
13106
- ...(range &&
13107
- useNative && {
13108
- rangeStart: range.rangeStart,
13109
- rangeEnd: range.rangeEnd,
13088
+ * - Otherwise: fall back to JS observe. A ScrollTimeline has no offset,
13089
+ * so page offsets are applied by the JS timeline.
13090
+ */
13091
+ const native = canUseNativeTimeline(target) && (target ? !!range : !options.offset);
13092
+ const animations = new Map();
13093
+ const observed = new Set();
13094
+ let stopObserving;
13095
+ let reverse = false;
13096
+ /**
13097
+ * When an offset's second point comes first, its range is swapped and
13098
+ * the animation played backwards. The range is set after the direction,
13099
+ * as that's what realigns a running animation with its timeline. Chrome
13100
+ * skips that realignment when the range changes in the frame the
13101
+ * animation starts, leaving it offset for good, so it's replayed too.
13102
+ */
13103
+ const apply = (direction, waapi) => {
13104
+ const [start, end] = range.points;
13105
+ waapi.effect.updateTiming({
13106
+ direction: reverse
13107
+ ? direction === "normal"
13108
+ ? "reverse"
13109
+ : "alternate-reverse"
13110
+ : direction,
13111
+ });
13112
+ Object.assign(waapi, {
13113
+ rangeStart: reverse ? end : start,
13114
+ rangeEnd: reverse ? start : end,
13115
+ });
13116
+ waapi.play();
13117
+ };
13118
+ const stops = [
13119
+ animation.attachTimeline({
13120
+ /**
13121
+ * Read only by WAAPI animations, so values driven from JS don't
13122
+ * create a native timeline they won't use.
13123
+ */
13124
+ get timeline() {
13125
+ return native ? getTimeline(options) : undefined;
13126
+ },
13127
+ onAttach: range &&
13128
+ ((waapi) => {
13129
+ const direction = waapi.effect.getTiming().direction;
13130
+ animations.set(waapi, direction);
13131
+ apply(direction, waapi);
13132
+ }),
13133
+ /**
13134
+ * Values driven from JS all track the offset with scrollInfo,
13135
+ * which measures once per frame for all of them.
13136
+ */
13137
+ observe: (valueAnimation) => {
13138
+ valueAnimation.pause();
13139
+ observed.add(valueAnimation);
13140
+ stopObserving || (stopObserving = scrollInfo((info) => {
13141
+ observed.forEach((observedAnimation) => {
13142
+ observedAnimation.time =
13143
+ observedAnimation.iterationDuration *
13144
+ info[axis].progress;
13145
+ });
13146
+ }, options));
13147
+ return () => observed.delete(valueAnimation);
13148
+ },
13110
13149
  }),
13111
- observe: (valueAnimation) => {
13112
- valueAnimation.pause();
13113
- return observeTimeline((progress) => {
13114
- valueAnimation.time =
13115
- valueAnimation.iterationDuration * progress;
13116
- }, timeline);
13117
- },
13118
- });
13150
+ ];
13151
+ if (native && range) {
13152
+ const { a, b } = range;
13153
+ const length = axis === "y" ? "clientHeight" : "clientWidth";
13154
+ /**
13155
+ * Offsets like All only run forwards when the target is longer than
13156
+ * the container, so their direction is remeasured on resize.
13157
+ */
13158
+ const update = () => {
13159
+ if (reverse !==
13160
+ (reverse = a * target[length] + b * container[length] < 0)) {
13161
+ animations.forEach(apply);
13162
+ }
13163
+ };
13164
+ update();
13165
+ if (a * b < 0) {
13166
+ stops.push(resize(update), resize([target, container], update));
13167
+ }
13168
+ }
13169
+ return () => {
13170
+ stops.forEach((stop) => stop());
13171
+ stopObserving?.();
13172
+ };
13119
13173
  }
13120
13174
 
13121
13175
  function scroll(onScroll, { axis = "y", source, container = document.scrollingElement, ...options } = {}) {
@@ -13415,7 +13469,6 @@
13415
13469
  exports.noop = noop;
13416
13470
  exports.number = number;
13417
13471
  exports.numberValueTypes = numberValueTypes;
13418
- exports.observeTimeline = observeTimeline;
13419
13472
  exports.optimizedAppearDataAttribute = optimizedAppearDataAttribute;
13420
13473
  exports.optimizedAppearDataId = optimizedAppearDataId;
13421
13474
  exports.parseAnimateLayoutArgs = parseAnimateLayoutArgs;