motion 14.2.0 → 14.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -556,8 +556,9 @@
556
556
  const VAR_TOKEN = "var";
557
557
  const VAR_FUNCTION_TOKEN = "var(";
558
558
  const SPLIT_TOKEN = "${}";
559
- // this regex consists of the `singleCssVariableRegex|rgbHSLValueRegex|digitRegex`
560
- const complexRegex = /var\s*\(\s*--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)|#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\)|-?(?:\d+(?:\.\d+)?|\.\d+)/giu;
559
+ // this regex consists of the `singleCssVariableRegex|rgbHSLValueRegex|digitRegex`.
560
+ // The lookahead skips digits in function names, like the 3 in matrix3d(.
561
+ const complexRegex = /var\s*\(\s*--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)|#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\)|-?(?:\d+(?:\.\d+)?|\.\d+)(?![a-z]*\()/giu;
561
562
  /**
562
563
  * Whether analyseComplexValue(value) would find any values, without
563
564
  * tokenising the string. CSS variables are resolved before this is
@@ -2499,6 +2500,7 @@
2499
2500
  translateY: 5,
2500
2501
  scaleX: 0,
2501
2502
  scaleY: 3,
2503
+ scaleZ: () => 1,
2502
2504
  scale: (v) => (Math.abs(v[0]) + Math.abs(v[3])) / 2,
2503
2505
  rotate,
2504
2506
  rotateZ: rotate,
@@ -2513,8 +2515,8 @@
2513
2515
  return angle;
2514
2516
  };
2515
2517
  const rotateZ = rotate;
2516
- const scaleX = (v) => Math.sqrt(v[0] * v[0] + v[1] * v[1]);
2517
- const scaleY = (v) => Math.sqrt(v[4] * v[4] + v[5] * v[5]);
2518
+ const scaleX = (v) => Math.hypot(v[0], v[1]);
2519
+ const scaleY = (v) => Math.hypot(v[4], v[5]);
2518
2520
  const matrix3dParsers = {
2519
2521
  x: 12,
2520
2522
  y: 13,
@@ -2524,6 +2526,7 @@
2524
2526
  translateZ: 14,
2525
2527
  scaleX,
2526
2528
  scaleY,
2529
+ scaleZ: (v) => Math.hypot(v[8], v[9], v[10]),
2527
2530
  scale: (v) => (scaleX(v) + scaleY(v)) / 2,
2528
2531
  rotateX: (v) => rebaseAngle(radToDeg(Math.atan2(v[6], v[5]))),
2529
2532
  rotateY: (v) => rebaseAngle(radToDeg(Math.atan2(-v[2], v[0]))),
@@ -2583,6 +2586,7 @@
2583
2586
  "scale",
2584
2587
  "scaleX",
2585
2588
  "scaleY",
2589
+ "scaleZ",
2586
2590
  "rotate",
2587
2591
  "rotateX",
2588
2592
  "rotateY",
@@ -3542,6 +3546,7 @@
3542
3546
  constructor(options) {
3543
3547
  const element = getOwner(options).current;
3544
3548
  const { driver = frameloopDriver } = options;
3549
+ let animation;
3545
3550
  /**
3546
3551
  * Before JSAnimation replaces the type with its generator.
3547
3552
  */
@@ -3571,11 +3576,12 @@
3571
3576
  stop: () => {
3572
3577
  js.stop();
3573
3578
  const group = getGroup(element);
3574
- group?.schedule(!group.isFinishing);
3579
+ group && !group.isFinishing && group.release(animation);
3575
3580
  },
3576
3581
  };
3577
3582
  };
3578
3583
  super(options);
3584
+ animation = this;
3579
3585
  this.canAccelerate = canAccelerate;
3580
3586
  /**
3581
3587
  * stop is bound in JSAnimation, so wrap it rather than override.
@@ -3731,8 +3737,14 @@
3731
3737
  }
3732
3738
  return super.time;
3733
3739
  }
3740
+ /**
3741
+ * A JS animation sets its motion value from a seek on its next tick.
3742
+ * This doesn't tick, so set it now, rather than leave it to be set
3743
+ * when this next stops.
3744
+ */
3734
3745
  set time(newTime) {
3735
3746
  super.time = newTime;
3747
+ this.isAccelerated() && this.sync();
3736
3748
  }
3737
3749
  /**
3738
3750
  * Scroll timelines set time every frame, so run these on the main
@@ -3760,6 +3772,7 @@
3760
3772
  let segments = [];
3761
3773
  let needsBuild = false;
3762
3774
  let isScheduled = false;
3775
+ let isEnded = false;
3763
3776
  let hasChurned = false;
3764
3777
  let lastChurn = -Infinity;
3765
3778
  const has = (key) => {
@@ -3876,8 +3889,19 @@
3876
3889
  for (const track of moving) {
3877
3890
  const values = at.map((t) => track.numberAt(t));
3878
3891
  const [first] = values;
3879
- const range = values[values.length - 1] - first;
3880
- const progress = values.map((value) => range ? (value - first) / range : value - first);
3892
+ const changes = values.map((value) => value - first);
3893
+ const end = changes[changes.length - 1];
3894
+ /**
3895
+ * A value that ends only a tiny way from where it started, as
3896
+ * a round trip does if it's built just after it starts, or
3897
+ * from a float residue, ends where it started. Dividing by
3898
+ * that tiny range would stretch the easing to values the
3899
+ * compositor can't render.
3900
+ */
3901
+ const range = changes.some((change) => Math.abs(change) * easingTolerance > Math.abs(end))
3902
+ ? 0
3903
+ : end;
3904
+ const progress = changes.map((change) => range ? change / range : change);
3881
3905
  /**
3882
3906
  * A value that doesn't change, like one still in its delay,
3883
3907
  * fits any easing. One that changes but ends where it started
@@ -4179,6 +4203,18 @@
4179
4203
  }
4180
4204
  return true;
4181
4205
  };
4206
+ /**
4207
+ * If no animations are left, write where they got to and cancel the
4208
+ * WAAPI animations. Returns whether the group ended.
4209
+ */
4210
+ const end = () => {
4211
+ if (tracks.size)
4212
+ return false;
4213
+ isEnded = true;
4214
+ transformGroups.delete(element);
4215
+ group.isJS || stop();
4216
+ return true;
4217
+ };
4182
4218
  const build = () => {
4183
4219
  const now = time.now();
4184
4220
  /**
@@ -4206,15 +4242,14 @@
4206
4242
  group.isFinishing = false;
4207
4243
  }
4208
4244
  isScheduled = false;
4245
+ if (isEnded)
4246
+ return;
4209
4247
  tracks.forEach(({ state }, track) => {
4210
4248
  if (state === "idle" || state === "finished")
4211
4249
  tracks.delete(track);
4212
4250
  });
4213
- if (!tracks.size) {
4214
- transformGroups.delete(element);
4215
- group.isJS || stop();
4251
+ if (end())
4216
4252
  return;
4217
- }
4218
4253
  for (const track of tracks) {
4219
4254
  if (!track.canAccelerate)
4220
4255
  return group.demote(true);
@@ -4246,6 +4281,8 @@
4246
4281
  isJS: false,
4247
4282
  isFinishing: false,
4248
4283
  schedule(build_, isChurn) {
4284
+ if (isEnded)
4285
+ return;
4249
4286
  needsBuild || (needsBuild = build_);
4250
4287
  hasChurned || (hasChurned = !!isChurn);
4251
4288
  /**
@@ -4260,6 +4297,21 @@
4260
4297
  : queueMicrotask(build);
4261
4298
  }
4262
4299
  },
4300
+ /**
4301
+ * End the group now, rather than when it next builds, so the
4302
+ * element's transform is the caller's again as soon as its last
4303
+ * animation is stopped.
4304
+ */
4305
+ release(track) {
4306
+ /**
4307
+ * Only drop the stopped animation. Others can be idle because
4308
+ * they're part way through play(), and are about to run.
4309
+ */
4310
+ const { state } = track;
4311
+ if (state === "idle" || state === "finished")
4312
+ tracks.delete(track);
4313
+ end() || group.schedule(true);
4314
+ },
4263
4315
  /**
4264
4316
  * An outside write. Rebuild once, but if it keeps happening, move
4265
4317
  * to the main thread.
@@ -4274,7 +4326,7 @@
4274
4326
  }
4275
4327
  },
4276
4328
  demote(canReturn) {
4277
- if (group.isJS)
4329
+ if (group.isJS || isEnded)
4278
4330
  return;
4279
4331
  /**
4280
4332
  * Unless only another animation is holding the group on the
@@ -5281,11 +5333,11 @@
5281
5333
  }
5282
5334
  return sample({ x: tx, y: ty }, { x: 0, y: 0 });
5283
5335
  },
5284
- animateVisualElement(visualElement, target, transition, delay, animations) {
5336
+ animateValues(getValue, target, transition, delay, animations) {
5285
5337
  if (!("x" in target || "y" in target))
5286
5338
  return;
5287
- const xValue = visualElement.getValue("x", visualElement.latestValues["x"] ?? 0);
5288
- const yValue = visualElement.getValue("y", visualElement.latestValues["y"] ?? 0);
5339
+ const xValue = getValue("x");
5340
+ const yValue = getValue("y");
5289
5341
  const xRaw = target.x;
5290
5342
  const yRaw = target.y;
5291
5343
  const xFrom = (Array.isArray(xRaw) && xRaw[0] != null
@@ -5308,7 +5360,7 @@
5308
5360
  // concurrent rotate animation composes and nothing accumulates
5309
5361
  // on interrupt.
5310
5362
  const pathRotationValue = interpolate(0).rotate !== undefined
5311
- ? visualElement.getValue("pathRotation", 0)
5363
+ ? getValue("pathRotation")
5312
5364
  : undefined;
5313
5365
  const pathTransition = {
5314
5366
  delay,
@@ -5518,7 +5570,9 @@
5518
5570
  const path = transition?.path;
5519
5571
  if (path) {
5520
5572
  // path mutates `target` to claim x/y; loop below skips them.
5521
- path.animateVisualElement(visualElement, target, transition, delay, animations);
5573
+ path.animateValues
5574
+ ? path.animateValues((key) => visualElement.getValue(key, visualElement.latestValues[key] ?? 0), target, transition, delay, animations)
5575
+ : path.animateVisualElement?.(visualElement, target, transition, delay, animations);
5522
5576
  }
5523
5577
  for (const key in target) {
5524
5578
  const value = visualElement.getValue(key, visualElement.latestValues[key] ?? null);
@@ -5977,8 +6031,16 @@
5977
6031
  */
5978
6032
  function animateValues(getValue, keyframes, transition = {}, element) {
5979
6033
  const animations = [];
5980
- const { velocity } = transition;
6034
+ const { velocity, path } = transition;
5981
6035
  const reduceMotion = transition.reduceMotion ?? element?.shouldReduceMotion;
6036
+ /**
6037
+ * Only elements have x/y to move along a path. path claims them by
6038
+ * deleting them, so hand it a copy.
6039
+ */
6040
+ if (path?.animateValues && element) {
6041
+ keyframes = { ...keyframes };
6042
+ path.animateValues(getValue, keyframes, transition, 0, animations);
6043
+ }
5982
6044
  for (const key in keyframes) {
5983
6045
  if (key === "transition" || key === "transitionEnd")
5984
6046
  continue;
@@ -6583,7 +6645,7 @@
6583
6645
  }
6584
6646
  }
6585
6647
  const addSVGValue = (element, state, key, value) => {
6586
- if (key.startsWith("path")) {
6648
+ if (key.startsWith("path") && !transformProps.has(key)) {
6587
6649
  return addSVGPathValue(element, state, key, value);
6588
6650
  }
6589
6651
  else if (key.startsWith("attr")) {
@@ -6684,6 +6746,7 @@
6684
6746
  values.rotate ||
6685
6747
  values.rotateX ||
6686
6748
  values.rotateY ||
6749
+ values.rotateZ ||
6687
6750
  values.skewX ||
6688
6751
  values.skewY);
6689
6752
  }
@@ -7367,13 +7430,46 @@
7367
7430
  return from === "last" ? lastIndex : lastIndex / 2;
7368
7431
  }
7369
7432
  }
7370
- function stagger(duration = 0.1, { startDelay = 0, from = 0, ease } = {}) {
7433
+ function stagger(duration = 0.1, { startDelay = 0, from = 0, ease, grid } = {}) {
7434
+ /**
7435
+ * Distances from the origin for each element count. The final entry
7436
+ * holds the distance used to normalise easing.
7437
+ */
7438
+ const cache = {};
7371
7439
  return (i, total) => {
7372
- const fromIndex = typeof from === "number" ? from : getOriginIndex(from, total);
7373
- const distance = Math.abs(fromIndex - i);
7374
- let delay = duration * distance;
7375
- if (ease) {
7376
- const maxDelay = total * duration;
7440
+ let distances = cache[total];
7441
+ if (!distances) {
7442
+ const [columns, rows] = grid || [Infinity, 1];
7443
+ const lastX = Math.min(columns, total) - 1;
7444
+ const lastY = Math.max(rows, Math.ceil(total / columns)) - 1;
7445
+ const isIndex = typeof from === "number";
7446
+ const scale = from === "first" ? 0 : from === "last" ? 1 : 0.5;
7447
+ const originX = isIndex ? from % columns : lastX * scale;
7448
+ const originY = isIndex ? Math.floor(from / columns) : lastY * scale;
7449
+ distances = cache[total] = [];
7450
+ for (let j = 0; j < total; j++) {
7451
+ const x = j % columns;
7452
+ const y = Math.floor(j / columns);
7453
+ const edgeX = Math.min(x, lastX - x);
7454
+ const edgeY = Math.min(y, lastY - y);
7455
+ if (from === "random") {
7456
+ // Inside-out shuffle, so each element gets a distinct step
7457
+ const k = (Math.random() * (j + 1)) | 0;
7458
+ distances[j] = distances[k];
7459
+ distances[k] = j;
7460
+ }
7461
+ else {
7462
+ distances[j] =
7463
+ from === "edges"
7464
+ ? Math.min(lastX ? edgeX : edgeY, lastY ? edgeY : edgeX)
7465
+ : Math.hypot(x - originX, y - originY);
7466
+ }
7467
+ }
7468
+ distances.push(grid ? Math.max(...distances) : total);
7469
+ }
7470
+ let delay = duration * distances[i];
7471
+ const maxDelay = duration * distances[total];
7472
+ if (ease && maxDelay) {
7377
7473
  const easingFunction = easingDefinitionToFunction(ease);
7378
7474
  delay = easingFunction(delay / maxDelay) * maxDelay;
7379
7475
  }
@@ -7607,7 +7703,7 @@
7607
7703
  }, stopAnimation);
7608
7704
  if (isMotionValue(source)) {
7609
7705
  let skipNextAnimation = options.skipInitialAnimation === true;
7610
- const removeSourceOnChange = source.on("change", (v) => {
7706
+ const onSourceChange = (v) => {
7611
7707
  if (skipNextAnimation) {
7612
7708
  skipNextAnimation = false;
7613
7709
  value.jump(parseValue(v, unit), false);
@@ -7615,11 +7711,28 @@
7615
7711
  else {
7616
7712
  value.set(parseValue(v, unit));
7617
7713
  }
7618
- });
7714
+ };
7715
+ const removeSourceOnChange = source.on("change", onSourceChange);
7716
+ /**
7717
+ * Catch up on a change made before we subscribed, for instance
7718
+ * between a React render reading the source and the effect
7719
+ * attaching to it. Subtracting is falsy for equal or non-numeric
7720
+ * (NaN) values.
7721
+ */
7722
+ const latest = source.get();
7723
+ if (asNumber(latest) - asNumber(value.get()))
7724
+ onSourceChange(latest);
7619
7725
  const removeValueOnDestroy = value.on("destroy", removeSourceOnChange);
7726
+ /**
7727
+ * Stop our animation on detach, otherwise a re-attach (new
7728
+ * options, Strict Mode) starts a second animation that fights
7729
+ * this orphaned one. The catch-up above resumes from the
7730
+ * current value and velocity.
7731
+ */
7620
7732
  return () => {
7621
7733
  removeSourceOnChange();
7622
7734
  removeValueOnDestroy();
7735
+ stopAnimation();
7623
7736
  };
7624
7737
  }
7625
7738
  return stopAnimation;
@@ -9782,8 +9895,13 @@
9782
9895
  function scrapeMotionValuesFromProps(props, prevProps, visualElement) {
9783
9896
  const newValues = scrapeMotionValuesFromProps$1(props, prevProps, visualElement);
9784
9897
  for (const key in props) {
9785
- if (isMotionValue(props[key]) ||
9786
- isMotionValue(prevProps[key])) {
9898
+ /**
9899
+ * A MotionValue child is rendered as text content by
9900
+ * DOMVisualElement, so it mustn't also be written as an attribute.
9901
+ */
9902
+ if (key !== "children" &&
9903
+ (isMotionValue(props[key]) ||
9904
+ isMotionValue(prevProps[key]))) {
9787
9905
  const targetKey = transformPropOrder.indexOf(key) !== -1
9788
9906
  ? "attr" + key.charAt(0).toUpperCase() + key.substring(1)
9789
9907
  : key;
@@ -10414,7 +10532,7 @@
10414
10532
  transform += `scale(${1 / treeScale.x}, ${1 / treeScale.y}) `;
10415
10533
  }
10416
10534
  if (latestTransform) {
10417
- const { transformPerspective, rotate, pathRotation, rotateX, rotateY, skewX, skewY, } = latestTransform;
10535
+ const { transformPerspective, rotate, pathRotation, rotateX, rotateY, rotateZ, skewX, skewY, } = latestTransform;
10418
10536
  if (transformPerspective)
10419
10537
  transform = `perspective(${transformPerspective}px) ${transform}`;
10420
10538
  if (rotate)
@@ -10426,6 +10544,8 @@
10426
10544
  transform += `rotateX(${rotateX}deg) `;
10427
10545
  if (rotateY)
10428
10546
  transform += `rotateY(${rotateY}deg) `;
10547
+ if (rotateZ)
10548
+ transform += `rotateZ(${rotateZ}deg) `;
10429
10549
  if (skewX)
10430
10550
  transform += `skewX(${skewX}deg) `;
10431
10551
  if (skewY)
@@ -10444,6 +10564,15 @@
10444
10564
  }
10445
10565
 
10446
10566
  const numBorders = cornerRadiusProps.length;
10567
+ const mixedTransforms = [
10568
+ "rotate",
10569
+ "rotateX",
10570
+ "rotateY",
10571
+ "rotateZ",
10572
+ "skewX",
10573
+ "skewY",
10574
+ ];
10575
+ const numMixedTransforms = mixedTransforms.length;
10447
10576
  const isPx = (value) => typeof value === "number" || px.test(value);
10448
10577
  function mixValues(target, follow, lead, progress, shouldCrossfadeOpacity, isOnlyMember) {
10449
10578
  if (shouldCrossfadeOpacity) {
@@ -10478,10 +10607,14 @@
10478
10607
  }
10479
10608
  }
10480
10609
  /**
10481
- * Mix rotation
10610
+ * Mix rotation and skew. These can't be projected, so mix them every
10611
+ * frame rather than leaving them frozen at the animation-start snapshot.
10482
10612
  */
10483
- if (follow.rotate || lead.rotate) {
10484
- target.rotate = mixNumber$1(follow.rotate || 0, lead.rotate || 0, progress);
10613
+ for (let i = 0; i < numMixedTransforms; i++) {
10614
+ const key = mixedTransforms[i];
10615
+ if (follow[key] || lead[key]) {
10616
+ target[key] = mixNumber$1(follow[key] || 0, lead[key] || 0, progress);
10617
+ }
10485
10618
  }
10486
10619
  }
10487
10620
  function getRadius(values, radiusName) {
@@ -10931,82 +11064,92 @@
10931
11064
  if (layoutId) {
10932
11065
  this.root.registerSharedNode(layoutId, this);
10933
11066
  }
10934
- // Only register the handler if it requires layout animation
10935
- if (this.options.animate !== false &&
10936
- visualElement &&
10937
- (layoutId || layout)) {
10938
- this.addEventListener("didUpdate", ({ delta, hasLayoutChanged, hasRelativeLayoutChanged, layout: newLayout, }) => {
10939
- if (this.isAnimationBlocked) {
10940
- this.target = undefined;
10941
- this.relativeTarget = undefined;
10942
- return;
11067
+ this.addLayoutListener();
11068
+ }
11069
+ /**
11070
+ * Only register the layout animation handler if this node requires
11071
+ * layout animation. This is called on mount, and again whenever options
11072
+ * change, as `layout` or `layoutId` may be enabled after mount.
11073
+ */
11074
+ addLayoutListener() {
11075
+ const { layoutId, layout, visualElement, animate } = this.options;
11076
+ if (!this.instance ||
11077
+ this.hasLayoutListener ||
11078
+ animate === false ||
11079
+ !visualElement ||
11080
+ !(layoutId || layout))
11081
+ return;
11082
+ this.hasLayoutListener = true;
11083
+ this.addEventListener("didUpdate", ({ delta, hasLayoutChanged, hasRelativeLayoutChanged, layout: newLayout, }) => {
11084
+ if (this.isAnimationBlocked) {
11085
+ this.target = undefined;
11086
+ this.relativeTarget = undefined;
11087
+ return;
11088
+ }
11089
+ // TODO: Check here if an animation exists
11090
+ const layoutTransition = this.options.transition ||
11091
+ visualElement.getDefaultTransition() ||
11092
+ defaultLayoutTransition;
11093
+ const { onLayoutAnimationStart, onLayoutAnimationComplete, } = visualElement.getProps();
11094
+ /**
11095
+ * The target layout of the element might stay the same,
11096
+ * but its position relative to its parent has changed.
11097
+ */
11098
+ const hasTargetChanged = !this.targetLayout ||
11099
+ !boxEqualsRounded(this.targetLayout, newLayout);
11100
+ /*
11101
+ * Note: Disabled to fix relative animations always triggering new
11102
+ * layout animations. If this causes further issues, we can try
11103
+ * a different approach to detecting relative target changes.
11104
+ */
11105
+ // || hasRelativeLayoutChanged
11106
+ /**
11107
+ * If the layout hasn't seemed to have changed, it might be that the
11108
+ * element is visually in the same place in the document but its position
11109
+ * relative to its parent has indeed changed. So here we check for that.
11110
+ */
11111
+ const hasOnlyRelativeTargetChanged = !hasLayoutChanged && hasRelativeLayoutChanged;
11112
+ if (this.options.layoutRoot ||
11113
+ this.resumeFrom ||
11114
+ hasOnlyRelativeTargetChanged ||
11115
+ (hasLayoutChanged &&
11116
+ (hasTargetChanged || !this.currentAnimation))) {
11117
+ if (this.resumeFrom) {
11118
+ this.resumingFrom = this.resumeFrom;
11119
+ this.resumingFrom.resumingFrom = undefined;
11120
+ }
11121
+ const animationOptions = {
11122
+ ...getValueTransition$1(layoutTransition, "layout"),
11123
+ onPlay: onLayoutAnimationStart,
11124
+ onComplete: onLayoutAnimationComplete,
11125
+ };
11126
+ if (visualElement.shouldReduceMotion ||
11127
+ this.options.layoutRoot) {
11128
+ animationOptions.delay = 0;
11129
+ animationOptions.type = false;
10943
11130
  }
10944
- // TODO: Check here if an animation exists
10945
- const layoutTransition = this.options.transition ||
10946
- visualElement.getDefaultTransition() ||
10947
- defaultLayoutTransition;
10948
- const { onLayoutAnimationStart, onLayoutAnimationComplete, } = visualElement.getProps();
11131
+ this.startAnimation(animationOptions);
10949
11132
  /**
10950
- * The target layout of the element might stay the same,
10951
- * but its position relative to its parent has changed.
10952
- */
10953
- const hasTargetChanged = !this.targetLayout ||
10954
- !boxEqualsRounded(this.targetLayout, newLayout);
10955
- /*
10956
- * Note: Disabled to fix relative animations always triggering new
10957
- * layout animations. If this causes further issues, we can try
10958
- * a different approach to detecting relative target changes.
11133
+ * Set animation origin after starting animation to avoid layout jump
11134
+ * caused by stopping previous layout animation
10959
11135
  */
10960
- // || hasRelativeLayoutChanged
11136
+ this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged, animationOptions.path);
11137
+ }
11138
+ else {
10961
11139
  /**
10962
- * If the layout hasn't seemed to have changed, it might be that the
10963
- * element is visually in the same place in the document but its position
10964
- * relative to its parent has indeed changed. So here we check for that.
11140
+ * If the layout hasn't changed and we have an animation that hasn't started yet,
11141
+ * finish it immediately. Otherwise it will be animating from a location
11142
+ * that was probably never committed to screen and look like a jumpy box.
10965
11143
  */
10966
- const hasOnlyRelativeTargetChanged = !hasLayoutChanged && hasRelativeLayoutChanged;
10967
- if (this.options.layoutRoot ||
10968
- this.resumeFrom ||
10969
- hasOnlyRelativeTargetChanged ||
10970
- (hasLayoutChanged &&
10971
- (hasTargetChanged || !this.currentAnimation))) {
10972
- if (this.resumeFrom) {
10973
- this.resumingFrom = this.resumeFrom;
10974
- this.resumingFrom.resumingFrom = undefined;
10975
- }
10976
- const animationOptions = {
10977
- ...getValueTransition$1(layoutTransition, "layout"),
10978
- onPlay: onLayoutAnimationStart,
10979
- onComplete: onLayoutAnimationComplete,
10980
- };
10981
- if (visualElement.shouldReduceMotion ||
10982
- this.options.layoutRoot) {
10983
- animationOptions.delay = 0;
10984
- animationOptions.type = false;
10985
- }
10986
- this.startAnimation(animationOptions);
10987
- /**
10988
- * Set animation origin after starting animation to avoid layout jump
10989
- * caused by stopping previous layout animation
10990
- */
10991
- this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged, animationOptions
10992
- .path);
11144
+ if (!hasLayoutChanged) {
11145
+ finishAnimation(this);
10993
11146
  }
10994
- else {
10995
- /**
10996
- * If the layout hasn't changed and we have an animation that hasn't started yet,
10997
- * finish it immediately. Otherwise it will be animating from a location
10998
- * that was probably never committed to screen and look like a jumpy box.
10999
- */
11000
- if (!hasLayoutChanged) {
11001
- finishAnimation(this);
11002
- }
11003
- if (this.isLead() && this.options.onExitComplete) {
11004
- this.options.onExitComplete();
11005
- }
11147
+ if (this.isLead() && this.options.onExitComplete) {
11148
+ this.options.onExitComplete();
11006
11149
  }
11007
- this.targetLayout = newLayout;
11008
- });
11009
- }
11150
+ }
11151
+ this.targetLayout = newLayout;
11152
+ });
11010
11153
  }
11011
11154
  unmount() {
11012
11155
  this.options.layoutId && this.willUpdate();
@@ -11016,6 +11159,7 @@
11016
11159
  this.parent && this.parent.children.delete(this);
11017
11160
  this.instance = undefined;
11018
11161
  this.eventHandlers.clear();
11162
+ this.hasLayoutListener = false;
11019
11163
  cancelFrame(this.updateProjection);
11020
11164
  }
11021
11165
  // only on the root
@@ -11186,7 +11330,16 @@
11186
11330
  }
11187
11331
  clearAllSnapshots() {
11188
11332
  this.nodes.forEach(clearSnapshot);
11189
- this.sharedNodes.forEach(removeLeadSnapshots);
11333
+ /**
11334
+ * A stack's last member can unmount while a replacement with the
11335
+ * same layoutId mounts in the same commit, so the stack (and its
11336
+ * lead) must outlive the unmount for the replacement to resume
11337
+ * from it. By now that handoff has happened, so stacks that are
11338
+ * still empty only retain unmounted nodes and can be released.
11339
+ */
11340
+ this.sharedNodes.forEach((stack, layoutId) => stack.members.length
11341
+ ? stack.removeLeadSnapshot()
11342
+ : this.sharedNodes.delete(layoutId));
11190
11343
  }
11191
11344
  scheduleUpdateProjection() {
11192
11345
  if (!this.projectionUpdateScheduled) {
@@ -11410,6 +11563,7 @@
11410
11563
  ...options,
11411
11564
  crossfade: options.crossfade !== undefined ? options.crossfade : true,
11412
11565
  };
11566
+ this.addLayoutListener();
11413
11567
  }
11414
11568
  clearMeasurements() {
11415
11569
  this.scroll = undefined;
@@ -12339,9 +12493,6 @@
12339
12493
  function resetSkewAndRotation(node) {
12340
12494
  node.resetSkewAndRotation();
12341
12495
  }
12342
- function removeLeadSnapshots(stack) {
12343
- stack.removeLeadSnapshot();
12344
- }
12345
12496
  function mixAxisDeltaLinear(output, delta, p) {
12346
12497
  output.translate = mixNumber$1(delta.translate, 0, p);
12347
12498
  output.scale = mixNumber$1(delta.scale, 1, p);