motion 14.3.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.
@@ -3546,6 +3546,7 @@
3546
3546
  constructor(options) {
3547
3547
  const element = getOwner(options).current;
3548
3548
  const { driver = frameloopDriver } = options;
3549
+ let animation;
3549
3550
  /**
3550
3551
  * Before JSAnimation replaces the type with its generator.
3551
3552
  */
@@ -3575,11 +3576,12 @@
3575
3576
  stop: () => {
3576
3577
  js.stop();
3577
3578
  const group = getGroup(element);
3578
- group?.schedule(!group.isFinishing);
3579
+ group && !group.isFinishing && group.release(animation);
3579
3580
  },
3580
3581
  };
3581
3582
  };
3582
3583
  super(options);
3584
+ animation = this;
3583
3585
  this.canAccelerate = canAccelerate;
3584
3586
  /**
3585
3587
  * stop is bound in JSAnimation, so wrap it rather than override.
@@ -3735,8 +3737,14 @@
3735
3737
  }
3736
3738
  return super.time;
3737
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
+ */
3738
3745
  set time(newTime) {
3739
3746
  super.time = newTime;
3747
+ this.isAccelerated() && this.sync();
3740
3748
  }
3741
3749
  /**
3742
3750
  * Scroll timelines set time every frame, so run these on the main
@@ -3764,6 +3772,7 @@
3764
3772
  let segments = [];
3765
3773
  let needsBuild = false;
3766
3774
  let isScheduled = false;
3775
+ let isEnded = false;
3767
3776
  let hasChurned = false;
3768
3777
  let lastChurn = -Infinity;
3769
3778
  const has = (key) => {
@@ -3880,8 +3889,19 @@
3880
3889
  for (const track of moving) {
3881
3890
  const values = at.map((t) => track.numberAt(t));
3882
3891
  const [first] = values;
3883
- const range = values[values.length - 1] - first;
3884
- 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);
3885
3905
  /**
3886
3906
  * A value that doesn't change, like one still in its delay,
3887
3907
  * fits any easing. One that changes but ends where it started
@@ -4183,6 +4203,18 @@
4183
4203
  }
4184
4204
  return true;
4185
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
+ };
4186
4218
  const build = () => {
4187
4219
  const now = time.now();
4188
4220
  /**
@@ -4210,15 +4242,14 @@
4210
4242
  group.isFinishing = false;
4211
4243
  }
4212
4244
  isScheduled = false;
4245
+ if (isEnded)
4246
+ return;
4213
4247
  tracks.forEach(({ state }, track) => {
4214
4248
  if (state === "idle" || state === "finished")
4215
4249
  tracks.delete(track);
4216
4250
  });
4217
- if (!tracks.size) {
4218
- transformGroups.delete(element);
4219
- group.isJS || stop();
4251
+ if (end())
4220
4252
  return;
4221
- }
4222
4253
  for (const track of tracks) {
4223
4254
  if (!track.canAccelerate)
4224
4255
  return group.demote(true);
@@ -4250,6 +4281,8 @@
4250
4281
  isJS: false,
4251
4282
  isFinishing: false,
4252
4283
  schedule(build_, isChurn) {
4284
+ if (isEnded)
4285
+ return;
4253
4286
  needsBuild || (needsBuild = build_);
4254
4287
  hasChurned || (hasChurned = !!isChurn);
4255
4288
  /**
@@ -4264,6 +4297,21 @@
4264
4297
  : queueMicrotask(build);
4265
4298
  }
4266
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
+ },
4267
4315
  /**
4268
4316
  * An outside write. Rebuild once, but if it keeps happening, move
4269
4317
  * to the main thread.
@@ -4278,7 +4326,7 @@
4278
4326
  }
4279
4327
  },
4280
4328
  demote(canReturn) {
4281
- if (group.isJS)
4329
+ if (group.isJS || isEnded)
4282
4330
  return;
4283
4331
  /**
4284
4332
  * Unless only another animation is holding the group on the
@@ -5285,11 +5333,11 @@
5285
5333
  }
5286
5334
  return sample({ x: tx, y: ty }, { x: 0, y: 0 });
5287
5335
  },
5288
- animateVisualElement(visualElement, target, transition, delay, animations) {
5336
+ animateValues(getValue, target, transition, delay, animations) {
5289
5337
  if (!("x" in target || "y" in target))
5290
5338
  return;
5291
- const xValue = visualElement.getValue("x", visualElement.latestValues["x"] ?? 0);
5292
- const yValue = visualElement.getValue("y", visualElement.latestValues["y"] ?? 0);
5339
+ const xValue = getValue("x");
5340
+ const yValue = getValue("y");
5293
5341
  const xRaw = target.x;
5294
5342
  const yRaw = target.y;
5295
5343
  const xFrom = (Array.isArray(xRaw) && xRaw[0] != null
@@ -5312,7 +5360,7 @@
5312
5360
  // concurrent rotate animation composes and nothing accumulates
5313
5361
  // on interrupt.
5314
5362
  const pathRotationValue = interpolate(0).rotate !== undefined
5315
- ? visualElement.getValue("pathRotation", 0)
5363
+ ? getValue("pathRotation")
5316
5364
  : undefined;
5317
5365
  const pathTransition = {
5318
5366
  delay,
@@ -5522,7 +5570,9 @@
5522
5570
  const path = transition?.path;
5523
5571
  if (path) {
5524
5572
  // path mutates `target` to claim x/y; loop below skips them.
5525
- 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);
5526
5576
  }
5527
5577
  for (const key in target) {
5528
5578
  const value = visualElement.getValue(key, visualElement.latestValues[key] ?? null);
@@ -5981,8 +6031,16 @@
5981
6031
  */
5982
6032
  function animateValues(getValue, keyframes, transition = {}, element) {
5983
6033
  const animations = [];
5984
- const { velocity } = transition;
6034
+ const { velocity, path } = transition;
5985
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
+ }
5986
6044
  for (const key in keyframes) {
5987
6045
  if (key === "transition" || key === "transitionEnd")
5988
6046
  continue;
@@ -6587,7 +6645,7 @@
6587
6645
  }
6588
6646
  }
6589
6647
  const addSVGValue = (element, state, key, value) => {
6590
- if (key.startsWith("path")) {
6648
+ if (key.startsWith("path") && !transformProps.has(key)) {
6591
6649
  return addSVGPathValue(element, state, key, value);
6592
6650
  }
6593
6651
  else if (key.startsWith("attr")) {
@@ -6688,6 +6746,7 @@
6688
6746
  values.rotate ||
6689
6747
  values.rotateX ||
6690
6748
  values.rotateY ||
6749
+ values.rotateZ ||
6691
6750
  values.skewX ||
6692
6751
  values.skewY);
6693
6752
  }
@@ -10473,7 +10532,7 @@
10473
10532
  transform += `scale(${1 / treeScale.x}, ${1 / treeScale.y}) `;
10474
10533
  }
10475
10534
  if (latestTransform) {
10476
- const { transformPerspective, rotate, pathRotation, rotateX, rotateY, skewX, skewY, } = latestTransform;
10535
+ const { transformPerspective, rotate, pathRotation, rotateX, rotateY, rotateZ, skewX, skewY, } = latestTransform;
10477
10536
  if (transformPerspective)
10478
10537
  transform = `perspective(${transformPerspective}px) ${transform}`;
10479
10538
  if (rotate)
@@ -10485,6 +10544,8 @@
10485
10544
  transform += `rotateX(${rotateX}deg) `;
10486
10545
  if (rotateY)
10487
10546
  transform += `rotateY(${rotateY}deg) `;
10547
+ if (rotateZ)
10548
+ transform += `rotateZ(${rotateZ}deg) `;
10488
10549
  if (skewX)
10489
10550
  transform += `skewX(${skewX}deg) `;
10490
10551
  if (skewY)
@@ -10503,6 +10564,15 @@
10503
10564
  }
10504
10565
 
10505
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;
10506
10576
  const isPx = (value) => typeof value === "number" || px.test(value);
10507
10577
  function mixValues(target, follow, lead, progress, shouldCrossfadeOpacity, isOnlyMember) {
10508
10578
  if (shouldCrossfadeOpacity) {
@@ -10537,10 +10607,14 @@
10537
10607
  }
10538
10608
  }
10539
10609
  /**
10540
- * 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.
10541
10612
  */
10542
- if (follow.rotate || lead.rotate) {
10543
- 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
+ }
10544
10618
  }
10545
10619
  }
10546
10620
  function getRadius(values, radiusName) {
@@ -10990,82 +11064,92 @@
10990
11064
  if (layoutId) {
10991
11065
  this.root.registerSharedNode(layoutId, this);
10992
11066
  }
10993
- // Only register the handler if it requires layout animation
10994
- if (this.options.animate !== false &&
10995
- visualElement &&
10996
- (layoutId || layout)) {
10997
- this.addEventListener("didUpdate", ({ delta, hasLayoutChanged, hasRelativeLayoutChanged, layout: newLayout, }) => {
10998
- if (this.isAnimationBlocked) {
10999
- this.target = undefined;
11000
- this.relativeTarget = undefined;
11001
- 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;
11002
11130
  }
11003
- // TODO: Check here if an animation exists
11004
- const layoutTransition = this.options.transition ||
11005
- visualElement.getDefaultTransition() ||
11006
- defaultLayoutTransition;
11007
- const { onLayoutAnimationStart, onLayoutAnimationComplete, } = visualElement.getProps();
11131
+ this.startAnimation(animationOptions);
11008
11132
  /**
11009
- * The target layout of the element might stay the same,
11010
- * but its position relative to its parent has changed.
11011
- */
11012
- const hasTargetChanged = !this.targetLayout ||
11013
- !boxEqualsRounded(this.targetLayout, newLayout);
11014
- /*
11015
- * Note: Disabled to fix relative animations always triggering new
11016
- * layout animations. If this causes further issues, we can try
11017
- * 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
11018
11135
  */
11019
- // || hasRelativeLayoutChanged
11136
+ this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged, animationOptions.path);
11137
+ }
11138
+ else {
11020
11139
  /**
11021
- * If the layout hasn't seemed to have changed, it might be that the
11022
- * element is visually in the same place in the document but its position
11023
- * 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.
11024
11143
  */
11025
- const hasOnlyRelativeTargetChanged = !hasLayoutChanged && hasRelativeLayoutChanged;
11026
- if (this.options.layoutRoot ||
11027
- this.resumeFrom ||
11028
- hasOnlyRelativeTargetChanged ||
11029
- (hasLayoutChanged &&
11030
- (hasTargetChanged || !this.currentAnimation))) {
11031
- if (this.resumeFrom) {
11032
- this.resumingFrom = this.resumeFrom;
11033
- this.resumingFrom.resumingFrom = undefined;
11034
- }
11035
- const animationOptions = {
11036
- ...getValueTransition$1(layoutTransition, "layout"),
11037
- onPlay: onLayoutAnimationStart,
11038
- onComplete: onLayoutAnimationComplete,
11039
- };
11040
- if (visualElement.shouldReduceMotion ||
11041
- this.options.layoutRoot) {
11042
- animationOptions.delay = 0;
11043
- animationOptions.type = false;
11044
- }
11045
- this.startAnimation(animationOptions);
11046
- /**
11047
- * Set animation origin after starting animation to avoid layout jump
11048
- * caused by stopping previous layout animation
11049
- */
11050
- this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged, animationOptions
11051
- .path);
11144
+ if (!hasLayoutChanged) {
11145
+ finishAnimation(this);
11052
11146
  }
11053
- else {
11054
- /**
11055
- * If the layout hasn't changed and we have an animation that hasn't started yet,
11056
- * finish it immediately. Otherwise it will be animating from a location
11057
- * that was probably never committed to screen and look like a jumpy box.
11058
- */
11059
- if (!hasLayoutChanged) {
11060
- finishAnimation(this);
11061
- }
11062
- if (this.isLead() && this.options.onExitComplete) {
11063
- this.options.onExitComplete();
11064
- }
11147
+ if (this.isLead() && this.options.onExitComplete) {
11148
+ this.options.onExitComplete();
11065
11149
  }
11066
- this.targetLayout = newLayout;
11067
- });
11068
- }
11150
+ }
11151
+ this.targetLayout = newLayout;
11152
+ });
11069
11153
  }
11070
11154
  unmount() {
11071
11155
  this.options.layoutId && this.willUpdate();
@@ -11075,6 +11159,7 @@
11075
11159
  this.parent && this.parent.children.delete(this);
11076
11160
  this.instance = undefined;
11077
11161
  this.eventHandlers.clear();
11162
+ this.hasLayoutListener = false;
11078
11163
  cancelFrame(this.updateProjection);
11079
11164
  }
11080
11165
  // only on the root
@@ -11245,7 +11330,16 @@
11245
11330
  }
11246
11331
  clearAllSnapshots() {
11247
11332
  this.nodes.forEach(clearSnapshot);
11248
- 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));
11249
11343
  }
11250
11344
  scheduleUpdateProjection() {
11251
11345
  if (!this.projectionUpdateScheduled) {
@@ -11469,6 +11563,7 @@
11469
11563
  ...options,
11470
11564
  crossfade: options.crossfade !== undefined ? options.crossfade : true,
11471
11565
  };
11566
+ this.addLayoutListener();
11472
11567
  }
11473
11568
  clearMeasurements() {
11474
11569
  this.scroll = undefined;
@@ -12398,9 +12493,6 @@
12398
12493
  function resetSkewAndRotation(node) {
12399
12494
  node.resetSkewAndRotation();
12400
12495
  }
12401
- function removeLeadSnapshots(stack) {
12402
- stack.removeLeadSnapshot();
12403
- }
12404
12496
  function mixAxisDeltaLinear(output, delta, p) {
12405
12497
  output.translate = mixNumber$1(delta.translate, 0, p);
12406
12498
  output.scale = mixNumber$1(delta.scale, 1, p);