motion 13.4.3 → 13.4.5

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.
@@ -1118,7 +1118,6 @@
1118
1118
  stiffness: 100,
1119
1119
  damping: 10,
1120
1120
  mass: 1.0,
1121
- velocity: 0.0,
1122
1121
  // Default duration/bounce-based options
1123
1122
  duration: 800, // in ms
1124
1123
  bounce: 0.3,
@@ -1153,7 +1152,12 @@
1153
1152
  * This is ported from the Framer implementation of duration-based spring resolution.
1154
1153
  */
1155
1154
  const safeMin = 0.001;
1156
- function findSpring({ duration = springDefaults.duration, bounce = springDefaults.bounce, velocity = springDefaults.velocity, mass = springDefaults.mass, }) {
1155
+ /**
1156
+ * Assumes zero initial velocity and the default mass: time-defined springs
1157
+ * ignore inherited velocity and only resolve without valid physics. Returns
1158
+ * NaN physics when the root search doesn't converge.
1159
+ */
1160
+ function findSpring({ duration = springDefaults.duration, bounce = springDefaults.bounce, }) {
1157
1161
  let envelope;
1158
1162
  let derivative;
1159
1163
  exports.warning(duration <= secondsToMilliseconds(springDefaults.maxDuration), "Spring duration must be 10 seconds or less", "spring-duration-limit");
@@ -1170,15 +1174,13 @@
1170
1174
  envelope = (undampedFreq) => {
1171
1175
  const exponentialDecay = undampedFreq * dampingRatio;
1172
1176
  const delta = exponentialDecay * duration;
1173
- const a = exponentialDecay - velocity;
1174
1177
  const b = calcAngularFreq(undampedFreq, dampingRatio);
1175
1178
  const c = Math.exp(-delta);
1176
- return safeMin - (a / b) * c;
1179
+ return safeMin - (exponentialDecay / b) * c;
1177
1180
  };
1178
1181
  derivative = (undampedFreq) => {
1179
1182
  const exponentialDecay = undampedFreq * dampingRatio;
1180
1183
  const delta = exponentialDecay * duration;
1181
- const d = delta * velocity + velocity;
1182
1184
  const e = dampingRatio *
1183
1185
  dampingRatio *
1184
1186
  undampedFreq *
@@ -1187,7 +1189,7 @@
1187
1189
  const f = Math.exp(-delta);
1188
1190
  const g = calcAngularFreq(undampedFreq * undampedFreq, dampingRatio);
1189
1191
  const factor = -envelope(undampedFreq) + safeMin > 0 ? -1 : 1;
1190
- return (factor * ((d - e) * f)) / g;
1192
+ return (factor * -e * f) / g;
1191
1193
  };
1192
1194
  }
1193
1195
  else {
@@ -1196,79 +1198,89 @@
1196
1198
  */
1197
1199
  envelope = (undampedFreq) => {
1198
1200
  const a = Math.exp(-undampedFreq * duration);
1199
- const b = (undampedFreq - velocity) * duration + 1;
1201
+ const b = undampedFreq * duration + 1;
1200
1202
  return -safeMin + a * b;
1201
1203
  };
1202
1204
  derivative = (undampedFreq) => {
1203
1205
  const a = Math.exp(-undampedFreq * duration);
1204
- const b = (velocity - undampedFreq) * (duration * duration);
1206
+ const b = -undampedFreq * (duration * duration);
1205
1207
  return a * b;
1206
1208
  };
1207
1209
  }
1208
1210
  const initialGuess = 5 / duration;
1209
1211
  const undampedFreq = approximateRoot(envelope, derivative, initialGuess);
1210
- duration = secondsToMilliseconds(duration);
1211
- if (isNaN(undampedFreq)) {
1212
- return {
1213
- stiffness: springDefaults.stiffness,
1214
- damping: springDefaults.damping,
1215
- duration,
1216
- };
1217
- }
1218
- else {
1219
- const stiffness = undampedFreq * undampedFreq * mass;
1220
- return {
1221
- stiffness,
1222
- damping: dampingRatio * 2 * Math.sqrt(mass * stiffness),
1223
- duration,
1224
- };
1225
- }
1212
+ const stiffness = undampedFreq * undampedFreq;
1213
+ return {
1214
+ stiffness,
1215
+ damping: dampingRatio * 2 * Math.sqrt(stiffness),
1216
+ duration: secondsToMilliseconds(duration),
1217
+ };
1226
1218
  }
1227
- const durationKeys = ["duration", "bounce"];
1228
- const physicsKeys = ["stiffness", "damping", "mass"];
1229
- function isSpringType(options, keys) {
1230
- return keys.some((key) => options[key] !== undefined);
1219
+ /**
1220
+ * Spring physics must be finite. stiffness and mass are also divisors so must
1221
+ * be positive, whereas a damping of 0 is a valid, perpetually oscillating
1222
+ * spring. Relational rather than Number.isFinite so numeric strings still
1223
+ * coerce.
1224
+ */
1225
+ const isValidPhysics = (value, canBeZero) => (canBeZero ? value >= 0 : value > 0) && value < Infinity;
1226
+ /**
1227
+ * Returns value if it's usable spring physics, otherwise undefined so callers
1228
+ * fall back to the default. An explicit `undefined`, e.g. from a forwarded
1229
+ * optional prop, must fall back too. Invalid physics would otherwise resolve
1230
+ * to NaN spring values that never report done.
1231
+ */
1232
+ function resolvePhysics(value, canBeZero) {
1233
+ if (isValidPhysics(value, canBeZero))
1234
+ return value;
1235
+ {
1236
+ exports.warning(value === undefined, "Spring stiffness and mass must be positive, damping 0 or greater", "spring-invalid-physics");
1237
+ }
1238
+ return undefined;
1231
1239
  }
1232
1240
  function getSpringOptions(options) {
1233
- let springOptions = {
1234
- velocity: springDefaults.velocity,
1235
- stiffness: springDefaults.stiffness,
1236
- damping: springDefaults.damping,
1237
- mass: springDefaults.mass,
1238
- isResolvedFromDuration: false,
1241
+ /**
1242
+ * Resolve physics before choosing between physics- and duration-based
1243
+ * resolution, so an invalid stiffness doesn't also silently discard a
1244
+ * valid duration/bounce.
1245
+ */
1246
+ const validStiffness = resolvePhysics(options.stiffness);
1247
+ const validDamping = resolvePhysics(options.damping, true);
1248
+ const validMass = resolvePhysics(options.mass);
1249
+ const springOptions = {
1239
1250
  ...options,
1251
+ stiffness: validStiffness ?? springDefaults.stiffness,
1252
+ damping: validDamping ?? springDefaults.damping,
1253
+ mass: validMass ?? springDefaults.mass,
1254
+ isResolvedFromDuration: false,
1255
+ // stiffness/damping/mass overrides duration/bounce
1256
+ isTimeDefined: (validStiffness ?? validDamping ?? validMass) === undefined &&
1257
+ (options.duration !== undefined || options.bounce !== undefined),
1240
1258
  };
1241
- // stiffness/damping/mass overrides duration/bounce
1242
- if (!isSpringType(options, physicsKeys) &&
1243
- isSpringType(options, durationKeys)) {
1244
- // Time-defined springs should ignore inherited velocity.
1245
- // Velocity from interrupted animations can cause findSpring()
1246
- // to compute wildly different spring parameters, leading to
1247
- // massive oscillation on small-range animations.
1248
- springOptions.velocity = 0;
1259
+ if (springOptions.isTimeDefined) {
1249
1260
  if (options.visualDuration) {
1250
- const visualDuration = options.visualDuration;
1251
- const root = (2 * Math.PI) / (visualDuration * 1.2);
1252
- const stiffness = root * root;
1253
- const damping = 2 *
1254
- clamp(0.05, 1, 1 - (options.bounce || 0)) *
1255
- Math.sqrt(stiffness);
1256
- springOptions = {
1257
- ...springOptions,
1258
- mass: springDefaults.mass,
1259
- stiffness,
1260
- damping,
1261
- };
1261
+ const root = (2 * Math.PI) / (options.visualDuration * 1.2);
1262
+ springOptions.stiffness = root * root;
1263
+ springOptions.damping =
1264
+ 2 *
1265
+ clamp(0.05, 1, 1 - (options.bounce || 0)) *
1266
+ Math.sqrt(springOptions.stiffness);
1262
1267
  }
1263
1268
  else {
1264
- const derived = findSpring({ ...options, velocity: 0 });
1265
- springOptions = {
1266
- ...springOptions,
1267
- ...derived,
1268
- mass: springDefaults.mass,
1269
- };
1269
+ Object.assign(springOptions, findSpring(springOptions));
1270
1270
  springOptions.isResolvedFromDuration = true;
1271
1271
  }
1272
+ /**
1273
+ * Time options can degenerate: a NaN bounce gives a NaN damping, an
1274
+ * infinite visualDuration a 0 stiffness, and findSpring NaN for both
1275
+ * when it doesn't converge. Replace the two together, so the
1276
+ * relationship duration resolution establishes between them is never
1277
+ * left half-overwritten.
1278
+ */
1279
+ if (!isValidPhysics(springOptions.stiffness) ||
1280
+ !isValidPhysics(springOptions.damping, true)) {
1281
+ springOptions.stiffness = springDefaults.stiffness;
1282
+ springOptions.damping = springDefaults.damping;
1283
+ }
1272
1284
  }
1273
1285
  return springOptions;
1274
1286
  }
@@ -1287,10 +1299,13 @@
1287
1299
  * to reduce GC during animation.
1288
1300
  */
1289
1301
  const state = { done: false, value: origin };
1290
- const { stiffness, damping, mass, duration, velocity, isResolvedFromDuration, } = getSpringOptions({
1291
- ...options,
1292
- velocity: -millisecondsToSeconds(options.velocity || 0),
1293
- });
1302
+ const { stiffness, damping, mass, duration, isResolvedFromDuration, isTimeDefined, } = getSpringOptions({ ...options });
1303
+ /**
1304
+ * Time-defined springs ignore inherited velocity. Velocity from
1305
+ * interrupted animations causes massive oscillation on small-range
1306
+ * animations.
1307
+ */
1308
+ const inheritVelocity = (velocity) => isTimeDefined ? 0 : -millisecondsToSeconds(velocity);
1294
1309
  const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass));
1295
1310
  const undampedAngularFreq = millisecondsToSeconds(Math.sqrt(stiffness / mass));
1296
1311
  const decay = dampingRatio * undampedAngularFreq;
@@ -1304,7 +1319,7 @@
1304
1319
  const s = {
1305
1320
  target,
1306
1321
  delta: target - origin,
1307
- velocity: velocity || 0.0,
1322
+ velocity: inheritVelocity(options.velocity || 0) || 0,
1308
1323
  restSpeed: 0,
1309
1324
  restDelta: 0,
1310
1325
  };
@@ -1312,7 +1327,7 @@
1312
1327
  * If we're working on a granular scale, use smaller defaults for determining
1313
1328
  * when the spring is finished.
1314
1329
  *
1315
- * These defaults have been selected emprically based on what strikes a good
1330
+ * These defaults have been selected empirically based on what strikes a good
1316
1331
  * ratio between feeling good and finishing as soon as changes are imperceptible.
1317
1332
  */
1318
1333
  const setRestThresholds = () => {
@@ -1410,11 +1425,6 @@
1410
1425
  };
1411
1426
  }
1412
1427
  update();
1413
- /**
1414
- * Time-defined springs ignore inherited velocity, see getSpringOptions.
1415
- */
1416
- const ignoreVelocity = !isSpringType(options, physicsKeys) &&
1417
- isSpringType(options, durationKeys);
1418
1428
  const calculatedDuration = isResolvedFromDuration ? duration || null : null;
1419
1429
  const generator = {
1420
1430
  calculatedDuration,
@@ -1425,9 +1435,7 @@
1425
1435
  retarget: (keyframes, newVelocity) => {
1426
1436
  s.target = keyframes[keyframes.length - 1];
1427
1437
  s.delta = s.target - keyframes[0];
1428
- s.velocity = ignoreVelocity
1429
- ? 0
1430
- : -millisecondsToSeconds(newVelocity);
1438
+ s.velocity = inheritVelocity(newVelocity);
1431
1439
  // Default thresholds depend on the scale of the new delta
1432
1440
  if (!(options.restSpeed && options.restDelta))
1433
1441
  setRestThresholds();
@@ -3445,7 +3453,7 @@
3445
3453
 
3446
3454
  class GroupAnimation {
3447
3455
  constructor(animations) {
3448
- // Bound to accomadate common `return animation.stop` pattern
3456
+ // Bound to accommodate common `return animation.stop` pattern
3449
3457
  this.stop = () => this.runAll("stop");
3450
3458
  this.animations = animations.filter(Boolean);
3451
3459
  }
@@ -5777,7 +5785,7 @@
5777
5785
  translateAxis(box.y, -node.scroll.offset.y);
5778
5786
  }
5779
5787
  if (delta) {
5780
- // Incoporate each ancestor's scale into a cumulative treeScale for this component
5788
+ // Incorporate each ancestor's scale into a cumulative treeScale for this component
5781
5789
  treeScale.x *= delta.x.scale;
5782
5790
  treeScale.y *= delta.y.scale;
5783
5791
  // Apply each ancestor's calculated delta into this component's recorded layout box
@@ -5985,7 +5993,7 @@
5985
5993
  /**
5986
5994
  * Create a hover gesture. hover() is different to .addEventListener("pointerenter")
5987
5995
  * in that it has an easier syntax, filters out polyfilled touch events, interoperates
5988
- * with drag gestures, and automatically removes the "pointerennd" event listener when the hover ends.
5996
+ * with drag gestures, and automatically removes the "pointerleave" event listener when the hover ends.
5989
5997
  *
5990
5998
  * @public
5991
5999
  */
@@ -7721,7 +7729,7 @@
7721
7729
  /**
7722
7730
  * Iterate backwards over the builders array. We can ignore the
7723
7731
  * "wait" animations. If we have an interrupting animation in the
7724
- * queue then we need to batch all preceeding animations into it.
7732
+ * queue then we need to batch all preceding animations into it.
7725
7733
  * Currently this only batches the update functions but will also
7726
7734
  * need to batch the targets.
7727
7735
  */
@@ -8601,7 +8609,7 @@
8601
8609
  /**
8602
8610
  * compareDocumentPosition returns a bitmask, by using the bitwise &
8603
8611
  * we're returning true if 2 in that bitmask is set to true. 2 is set
8604
- * to true if b preceeds a.
8612
+ * to true if b precedes a.
8605
8613
  */
8606
8614
  return a.compareDocumentPosition(b) & 2 ? 1 : -1;
8607
8615
  }
@@ -9093,7 +9101,7 @@
9093
9101
  }
9094
9102
  /**
9095
9103
  * Set all encountered keys so far as the protected keys for this type. This will
9096
- * be any key that has been animated or otherwise handled by active, higher-priortiy types.
9104
+ * be any key that has been animated or otherwise handled by active, higher-priority types.
9097
9105
  */
9098
9106
  typeState.protectedKeys = { ...encounteredKeys };
9099
9107
  // Check if we can skip analysing this prop early
@@ -9918,8 +9926,8 @@
9918
9926
  this.hasCheckedOptimisedAppear = false;
9919
9927
  /**
9920
9928
  * An object representing the calculated contextual/accumulated/tree scale.
9921
- * This will be used to scale calculcated projection transforms, as these are
9922
- * calculated in screen-space but need to be scaled for elements to layoutly
9929
+ * This will be used to scale calculated projection transforms, as these are
9930
+ * calculated in screen-space but need to be scaled for elements to visually
9923
9931
  * make it to their calculated destinations.
9924
9932
  *
9925
9933
  * TODO: Lazy-init
@@ -9930,7 +9938,6 @@
9930
9938
  */
9931
9939
  this.eventHandlers = new Map();
9932
9940
  this.hasTreeAnimated = false;
9933
- this.layoutVersion = 0;
9934
9941
  // Note: Currently only running on root node
9935
9942
  this.updateScheduled = false;
9936
9943
  this.scheduleUpdate = () => this.update();
@@ -9970,7 +9977,6 @@
9970
9977
  * Frame calculations
9971
9978
  */
9972
9979
  this.resolvedRelativeTargetAt = 0.0;
9973
- this.linkedParentVersion = 0;
9974
9980
  this.hasProjected = false;
9975
9981
  this.isVisible = true;
9976
9982
  this.animationProgress = 0;
@@ -10251,6 +10257,7 @@
10251
10257
  * get measured even when memoized (willUpdate skipped).
10252
10258
  */
10253
10259
  this.nodes.forEach(ensureDraggedNodesSnapshotted);
10260
+ this.nodes.forEach(syncRelativeLayout);
10254
10261
  /**
10255
10262
  * Write
10256
10263
  */
@@ -10348,7 +10355,6 @@
10348
10355
  }
10349
10356
  const prevLayout = this.layout;
10350
10357
  this.layout = this.measure(false);
10351
- this.layoutVersion++;
10352
10358
  if (!this.layoutCorrected)
10353
10359
  this.layoutCorrected = createBox();
10354
10360
  this.isLayoutDirty = false;
@@ -10568,11 +10574,6 @@
10568
10574
  return;
10569
10575
  this.resolvedRelativeTargetAt = frameData.timestamp;
10570
10576
  const relativeParent = this.getClosestProjectingParent();
10571
- if (relativeParent &&
10572
- this.linkedParentVersion !== relativeParent.layoutVersion &&
10573
- !relativeParent.options.layoutRoot) {
10574
- this.removeRelativeTarget();
10575
- }
10576
10577
  /**
10577
10578
  * If we don't have a targetDelta but do have a layout, we can attempt to resolve
10578
10579
  * a relativeParent. This will allow a component to perform scale correction
@@ -10675,7 +10676,6 @@
10675
10676
  }
10676
10677
  createRelativeTarget(relativeParent, layout, parentLayout) {
10677
10678
  this.relativeParent = relativeParent;
10678
- this.linkedParentVersion = relativeParent.layoutVersion;
10679
10679
  this.forceRelativeParentToResolveTarget();
10680
10680
  this.relativeTarget = createBox();
10681
10681
  this.relativeTargetOrigin = createBox();
@@ -11385,6 +11385,26 @@
11385
11385
  node.isLayoutDirty = true;
11386
11386
  }
11387
11387
  }
11388
+ /**
11389
+ * If a node's relative parent is about to be re-measured but the node isn't
11390
+ * (e.g. it's in another LayoutGroup), its relative target is out of date.
11391
+ * If it's layout animating, re-measure it too, so its relative target keeps
11392
+ * resolving against current layouts. Otherwise its layout is still valid
11393
+ * relative to its parent, so drop the relative target, and any targetDelta
11394
+ * left over from a finished animation while an ancestor still animates, to be
11395
+ * rebuilt from its layout.
11396
+ */
11397
+ function syncRelativeLayout(node) {
11398
+ if (node.relativeTarget && node.relativeParent?.isLayoutDirty) {
11399
+ if (node.currentAnimation) {
11400
+ node.isLayoutDirty = true;
11401
+ }
11402
+ else {
11403
+ node.targetDelta = undefined;
11404
+ node.removeRelativeTarget();
11405
+ }
11406
+ }
11407
+ }
11388
11408
  function resetTransformStyle(node) {
11389
11409
  const { visualElement } = node.options;
11390
11410
  if (visualElement && visualElement.getProps().onBeforeLayoutMeasure) {
@@ -12561,7 +12581,10 @@
12561
12581
  x: createAxisInfo(),
12562
12582
  y: createAxisInfo(),
12563
12583
  });
12564
- const keys = {
12584
+ /**
12585
+ * Also iterated with for...in as the list of axes.
12586
+ */
12587
+ const axisKeys = {
12565
12588
  x: {
12566
12589
  length: "Width",
12567
12590
  position: "Left",
@@ -12573,11 +12596,13 @@
12573
12596
  };
12574
12597
  function updateAxisInfo(element, axisName, info, time) {
12575
12598
  const axis = info[axisName];
12576
- const { length, position } = keys[axisName];
12599
+ const { length, position } = axisKeys[axisName];
12577
12600
  const prev = axis.current;
12578
12601
  const prevTime = info.time;
12579
12602
  axis.current = Math.abs(element[`scroll${position}`]);
12580
- axis.scrollLength = element[`scroll${length}`] - element[`client${length}`];
12603
+ axis.containerLength = element[`client${length}`];
12604
+ axis.targetLength = element[`scroll${length}`];
12605
+ axis.scrollLength = axis.targetLength - axis.containerLength;
12581
12606
  axis.offset.length = 0;
12582
12607
  axis.offset[0] = 0;
12583
12608
  axis.offset[1] = axis.scrollLength;
@@ -12588,54 +12613,15 @@
12588
12613
  ? 0
12589
12614
  : velocityPerSecond(axis.current - prev, elapsed);
12590
12615
  }
12616
+ /**
12617
+ * Measures a scroll container. Runs once per container per frame; every
12618
+ * handler on that container derives its info from the result.
12619
+ */
12591
12620
  function updateScrollInfo(element, info, time) {
12592
- updateAxisInfo(element, "x", info, time);
12593
- updateAxisInfo(element, "y", info, time);
12594
- info.time = time;
12595
- }
12596
-
12597
- function calcInset(element, container) {
12598
- const inset = { x: 0, y: 0 };
12599
- let current = element;
12600
- while (current && current !== container) {
12601
- if (isHTMLElement(current)) {
12602
- inset.x += current.offsetLeft;
12603
- inset.y += current.offsetTop;
12604
- current = current.offsetParent;
12605
- }
12606
- else if (current.tagName === "svg") {
12607
- /**
12608
- * This isn't an ideal approach to measuring the offset of <svg /> tags.
12609
- * It would be preferable, given they behave like HTMLElements in most ways
12610
- * to use offsetLeft/Top. But these don't exist on <svg />. Likewise we
12611
- * can't use .getBBox() like most SVG elements as these provide the offset
12612
- * relative to the SVG itself, which for <svg /> is usually 0x0.
12613
- */
12614
- const svgBoundingBox = current.getBoundingClientRect();
12615
- current = current.parentElement;
12616
- const parentBoundingBox = current.getBoundingClientRect();
12617
- inset.x += svgBoundingBox.left - parentBoundingBox.left;
12618
- inset.y += svgBoundingBox.top - parentBoundingBox.top;
12619
- }
12620
- else if (current instanceof SVGGraphicsElement) {
12621
- const { x, y } = current.getBBox();
12622
- inset.x += x;
12623
- inset.y += y;
12624
- let svg = null;
12625
- let parent = current.parentNode;
12626
- while (!svg) {
12627
- if (parent.tagName === "svg") {
12628
- svg = parent;
12629
- }
12630
- parent = current.parentNode;
12631
- }
12632
- current = svg;
12633
- }
12634
- else {
12635
- break;
12636
- }
12621
+ for (const axis in axisKeys) {
12622
+ updateAxisInfo(element, axis, info, time);
12637
12623
  }
12638
- return inset;
12624
+ info.time = time;
12639
12625
  }
12640
12626
 
12641
12627
  const namedEdges = {
@@ -12733,86 +12719,104 @@
12733
12719
  ],
12734
12720
  };
12735
12721
 
12736
- const point = { x: 0, y: 0 };
12737
- function getTargetSize(target) {
12738
- return "getBBox" in target && target.tagName !== "svg"
12739
- ? target.getBBox()
12740
- : { width: target.clientWidth, height: target.clientHeight };
12722
+ /**
12723
+ * Resolved offsets map to evenly spaced progress values, so progress is
12724
+ * derived from the segment index rather than building an interpolator.
12725
+ */
12726
+ function offsetsToProgress(offsets, v) {
12727
+ const n = offsets.length - 1;
12728
+ if (n < 1)
12729
+ return 0;
12730
+ const reverse = offsets[0] > offsets[n];
12731
+ const at = (i) => offsets[reverse ? n - i : i];
12732
+ /**
12733
+ * Matches interpolate(), which checks for a zero-length first range
12734
+ * before reversing descending offsets.
12735
+ */
12736
+ if (offsets[0] === offsets[1] && v < at(0))
12737
+ return reverse ? 1 : 0;
12738
+ let i = 0;
12739
+ while (i < n - 1 && v >= at(i + 1))
12740
+ i++;
12741
+ const p = (i + progress(at(i), at(i + 1), v)) / n;
12742
+ return reverse ? 1 - p : p;
12741
12743
  }
12742
- function resolveOffsets(container, info, options) {
12743
- const { offset: offsetDefinition = ScrollOffset.All } = options;
12744
- const { target = container, axis = "y" } = options;
12745
- const lengthLabel = axis === "y" ? "height" : "width";
12746
- const inset = target !== container ? calcInset(target, container) : point;
12747
- /**
12748
- * Measure the target and container. If they're the same thing then we
12749
- * use the container's scrollWidth/Height as the target, from there
12750
- * all other calculations can remain the same.
12751
- */
12752
- const targetSize = target === container
12753
- ? { width: container.scrollWidth, height: container.scrollHeight }
12754
- : getTargetSize(target);
12755
- const containerSize = {
12756
- width: container.clientWidth,
12757
- height: container.clientHeight,
12758
- };
12744
+ function resolveOffsets(info, options) {
12745
+ const { offset: offsetDefinition = ScrollOffset.All, axis = "y" } = options;
12746
+ const axisInfo = info[axis];
12759
12747
  /**
12760
12748
  * Reset the length of the resolved offset array rather than creating a new one.
12761
- * TODO: More reusable data structures for targetSize/containerSize would also be good.
12762
12749
  */
12763
- info[axis].offset.length = 0;
12750
+ axisInfo.offset.length = 0;
12764
12751
  /**
12765
12752
  * Populate the offset array by resolving the user's offset definition into
12766
- * a list of pixel scroll offets.
12753
+ * a list of pixel scroll offsets.
12767
12754
  */
12768
- let hasChanged = !info[axis].interpolate;
12769
12755
  const numOffsets = offsetDefinition.length;
12770
12756
  for (let i = 0; i < numOffsets; i++) {
12771
- const offset = resolveOffset(offsetDefinition[i], containerSize[lengthLabel], targetSize[lengthLabel], inset[axis]);
12772
- if (!hasChanged && offset !== info[axis].interpolatorOffsets[i]) {
12773
- hasChanged = true;
12774
- }
12775
- info[axis].offset[i] = offset;
12757
+ axisInfo.offset[i] = resolveOffset(offsetDefinition[i], axisInfo.containerLength, axisInfo.targetLength, axisInfo.targetOffset);
12776
12758
  }
12777
- /**
12778
- * If the pixel scroll offsets have changed, create a new interpolator function
12779
- * to map scroll value into a progress.
12780
- */
12781
- if (hasChanged) {
12782
- info[axis].interpolate = interpolate(info[axis].offset, defaultOffset$1(offsetDefinition), { clamp: false });
12783
- info[axis].interpolatorOffsets = [...info[axis].offset];
12784
- }
12785
- info[axis].progress = clamp(0, 1, info[axis].interpolate(info[axis].current));
12759
+ axisInfo.progress = clamp(0, 1, offsetsToProgress(axisInfo.offset, axisInfo.current));
12786
12760
  }
12787
12761
 
12788
- function measure(container, target = container, info) {
12789
- /**
12790
- * Find inset of target within scrollable container
12791
- */
12792
- info.x.targetOffset = 0;
12793
- info.y.targetOffset = 0;
12794
- if (target !== container) {
12795
- let node = target;
12796
- while (node && node !== container) {
12797
- info.x.targetOffset += node.offsetLeft;
12798
- info.y.targetOffset += node.offsetTop;
12799
- node = node.offsetParent;
12762
+ function calcInset(element, container) {
12763
+ const inset = { x: 0, y: 0 };
12764
+ let current = element;
12765
+ while (current && current !== container) {
12766
+ if (isHTMLElement(current)) {
12767
+ inset.x += current.offsetLeft;
12768
+ inset.y += current.offsetTop;
12769
+ current = current.offsetParent;
12770
+ }
12771
+ else if (current.tagName === "svg") {
12772
+ /**
12773
+ * This isn't an ideal approach to measuring the offset of <svg /> tags.
12774
+ * It would be preferable, given they behave like HTMLElements in most ways
12775
+ * to use offsetLeft/Top. But these don't exist on <svg />. Likewise we
12776
+ * can't use .getBBox() like most SVG elements as these provide the offset
12777
+ * relative to the SVG itself, which for <svg /> is usually 0x0.
12778
+ */
12779
+ const svgBoundingBox = current.getBoundingClientRect();
12780
+ current = current.parentElement;
12781
+ const parentBoundingBox = current.getBoundingClientRect();
12782
+ inset.x += svgBoundingBox.left - parentBoundingBox.left;
12783
+ inset.y += svgBoundingBox.top - parentBoundingBox.top;
12784
+ }
12785
+ else if (current instanceof SVGGraphicsElement) {
12786
+ const { x, y } = current.getBBox();
12787
+ inset.x += x;
12788
+ inset.y += y;
12789
+ let svg = null;
12790
+ let parent = current.parentNode;
12791
+ while (!svg) {
12792
+ if (parent.tagName === "svg") {
12793
+ svg = parent;
12794
+ }
12795
+ parent = current.parentNode;
12796
+ }
12797
+ current = svg;
12798
+ }
12799
+ else {
12800
+ break;
12800
12801
  }
12801
12802
  }
12802
- info.x.targetLength =
12803
- target === container ? target.scrollWidth : target.clientWidth;
12804
- info.y.targetLength =
12805
- target === container ? target.scrollHeight : target.clientHeight;
12806
- info.x.containerLength = container.clientWidth;
12807
- info.y.containerLength = container.clientHeight;
12803
+ return inset;
12804
+ }
12805
+
12806
+ function getTargetSize(target) {
12807
+ return "getBBox" in target && target.tagName !== "svg"
12808
+ ? target.getBBox()
12809
+ : { width: target.clientWidth, height: target.clientHeight };
12810
+ }
12811
+ function createOnScrollHandler(container, onScroll, info, options = {}) {
12812
+ const { target } = options;
12808
12813
  /**
12809
12814
  * In development mode ensure scroll containers aren't position: static as this makes
12810
12815
  * it difficult to measure their relative positions. The document scrolling element
12811
12816
  * is exempt: offsetParent measurements naturally resolve relative to the document.
12812
12817
  */
12813
12818
  {
12814
- if (container &&
12815
- target &&
12819
+ if (target &&
12816
12820
  target !== container &&
12817
12821
  container !== document.documentElement &&
12818
12822
  container !== document.scrollingElement &&
@@ -12820,24 +12824,39 @@
12820
12824
  warnOnce(getComputedStyle(container).position !== "static", "Please ensure that the container has a non-static position, like 'relative', 'fixed', or 'absolute' to ensure scroll offset is calculated correctly.");
12821
12825
  }
12822
12826
  }
12823
- }
12824
- function createOnScrollHandler(element, onScroll, info, options = {}) {
12827
+ /**
12828
+ * Handlers without a target or offset are notified with the container's
12829
+ * shared info object, so they measure nothing themselves.
12830
+ */
12831
+ const needsOwnInfo = target || options.offset;
12825
12832
  return {
12826
- measure: (time) => {
12827
- measure(element, options.target, info);
12828
- updateScrollInfo(element, info, time);
12829
- if (options.offset || options.target) {
12830
- resolveOffsets(element, info, options);
12831
- }
12833
+ measure: (containerInfo) => {
12834
+ if (!needsOwnInfo)
12835
+ return;
12836
+ info.time = containerInfo.time;
12837
+ for (const key in axisKeys) {
12838
+ const axis = key;
12839
+ const { offset } = info[axis];
12840
+ Object.assign(info[axis], containerInfo[axis]);
12841
+ info[axis].offset = offset;
12842
+ }
12843
+ if (target && target !== container) {
12844
+ const inset = calcInset(target, container);
12845
+ const size = getTargetSize(target);
12846
+ info.x.targetOffset = inset.x;
12847
+ info.y.targetOffset = inset.y;
12848
+ info.x.targetLength = size.width;
12849
+ info.y.targetLength = size.height;
12850
+ }
12851
+ resolveOffsets(info, options);
12832
12852
  },
12833
- notify: () => onScroll(info),
12853
+ notify: (containerInfo) => onScroll(needsOwnInfo ? info : containerInfo),
12834
12854
  };
12835
12855
  }
12836
12856
 
12837
12857
  const scrollListeners = new WeakMap();
12838
12858
  const resizeListeners = new WeakMap();
12839
12859
  const onScrollHandlers = new WeakMap();
12840
- const scrollSize = new WeakMap();
12841
12860
  const dimensionCheckProcesses = new WeakMap();
12842
12861
  const getEventTarget = (element) => element === document.scrollingElement ? window : element;
12843
12862
  function scrollInfo(onScroll, { container = document.scrollingElement, trackContentSize = false, ...options } = {}) {
@@ -12863,15 +12882,17 @@
12863
12882
  * If not, create one.
12864
12883
  */
12865
12884
  if (!scrollListeners.has(container)) {
12885
+ const containerInfo = createScrollInfo();
12866
12886
  const measureAll = () => {
12887
+ updateScrollInfo(container, containerInfo, frameData.timestamp);
12867
12888
  for (const handler of containerHandlers) {
12868
- handler.measure(frameData.timestamp);
12889
+ handler.measure(containerInfo);
12869
12890
  }
12870
12891
  frame.preUpdate(notifyAll);
12871
12892
  };
12872
12893
  const notifyAll = () => {
12873
12894
  for (const handler of containerHandlers) {
12874
- handler.notify();
12895
+ handler.notify(containerInfo);
12875
12896
  }
12876
12897
  };
12877
12898
  const listener = () => frame.read(measureAll);
@@ -12894,7 +12915,6 @@
12894
12915
  width: container.scrollWidth,
12895
12916
  height: container.scrollHeight,
12896
12917
  };
12897
- scrollSize.set(container, size);
12898
12918
  // Add frame-based scroll dimension checking to detect content changes
12899
12919
  const checkScrollDimensions = () => {
12900
12920
  const newWidth = container.scrollWidth;
@@ -12923,8 +12943,11 @@
12923
12943
  if (currentHandlers.size)
12924
12944
  return;
12925
12945
  /**
12926
- * If no more handlers, remove the scroll listener too.
12946
+ * If no more handlers, remove the scroll listener too. The handler
12947
+ * set goes with it, as a measure still queued from this listener
12948
+ * would otherwise notify handlers added by a later scrollInfo call.
12927
12949
  */
12950
+ onScrollHandlers.delete(container);
12928
12951
  const scrollListener = scrollListeners.get(container);
12929
12952
  scrollListeners.delete(container);
12930
12953
  if (scrollListener) {
@@ -12938,7 +12961,6 @@
12938
12961
  cancelFrame(dimensionCheckProcess);
12939
12962
  dimensionCheckProcesses.delete(container);
12940
12963
  }
12941
- scrollSize.delete(container);
12942
12964
  };
12943
12965
  }
12944
12966
 
@@ -13019,10 +13041,8 @@
13019
13041
  }, options);
13020
13042
  return { currentTime, cancel };
13021
13043
  }
13022
- function getTimeline({ source, container, ...options }) {
13044
+ function getTimeline({ container, ...options }) {
13023
13045
  const { axis } = options;
13024
- if (source)
13025
- container = source;
13026
13046
  let containerCache = timelineCache.get(container);
13027
13047
  if (!containerCache) {
13028
13048
  containerCache = new Map();
@@ -13098,38 +13118,16 @@
13098
13118
  });
13099
13119
  }
13100
13120
 
13101
- /**
13102
- * Currently, we only support element tracking with `scrollInfo`, though in
13103
- * the future we can also offer ViewTimeline support.
13104
- */
13105
- function isElementTracking(options) {
13106
- return options && (options.target || options.offset);
13107
- }
13108
-
13109
- /**
13110
- * If the onScroll function has two arguments, it's expecting
13111
- * more specific information about the scroll from scrollInfo.
13112
- */
13113
- function isOnScrollWithInfo(onScroll) {
13114
- return onScroll.length === 2;
13115
- }
13116
- function attachToFunction(onScroll, options) {
13117
- if (isOnScrollWithInfo(onScroll) || isElementTracking(options)) {
13118
- return scrollInfo((info) => {
13119
- onScroll(info[options.axis].progress, info);
13120
- }, options);
13121
- }
13122
- else {
13123
- return observeTimeline(onScroll, getTimeline(options));
13124
- }
13125
- }
13126
-
13127
- function scroll(onScroll, { axis = "y", container = document.scrollingElement, ...options } = {}) {
13121
+ function scroll(onScroll, { axis = "y", source, container = document.scrollingElement, ...options } = {}) {
13128
13122
  if (!container)
13129
13123
  return noop;
13130
- const optionsWithDefaults = { axis, container, ...options };
13124
+ const optionsWithDefaults = {
13125
+ axis,
13126
+ container: source || container,
13127
+ ...options,
13128
+ };
13131
13129
  return typeof onScroll === "function"
13132
- ? attachToFunction(onScroll, optionsWithDefaults)
13130
+ ? scrollInfo((info) => onScroll(info[axis].progress, info), optionsWithDefaults)
13133
13131
  : attachToAnimation(onScroll, optionsWithDefaults);
13134
13132
  }
13135
13133