motion 14.1.1 → 14.2.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.
@@ -1906,6 +1906,7 @@
1906
1906
  }
1907
1907
 
1908
1908
  const percentToProgress = (percent) => percent / 100;
1909
+ const round = (value) => Math.round(value * 10000) / 10000;
1909
1910
  class JSAnimation extends WithPromise {
1910
1911
  constructor(options) {
1911
1912
  super();
@@ -2013,10 +2014,10 @@
2013
2014
  }
2014
2015
  }
2015
2016
  tick(timestamp, sample = false) {
2016
- const { generator, totalDuration, mixKeyframes, mirroredGenerator, resolvedDuration, calculatedDuration, } = this;
2017
+ const { generator, totalDuration, calculatedDuration } = this;
2017
2018
  if (this.startTime === null)
2018
2019
  return generator.next(0);
2019
- const { delay = 0, keyframes, repeat, repeatType, repeatDelay, type, onUpdate, finalKeyframe, } = this.options;
2020
+ const { delay = 0, keyframes, type, onUpdate, finalKeyframe, } = this.options;
2020
2021
  if (this.startTime === this.pendingStartTime) {
2021
2022
  this.startTime = resolveStartTime(this.startTime, timestamp);
2022
2023
  }
@@ -2049,7 +2050,55 @@
2049
2050
  if (this.state === "finished" && this.holdTime === null) {
2050
2051
  this.currentTime = totalDuration;
2051
2052
  }
2052
- let elapsed = this.currentTime;
2053
+ const state = this.generate(this.currentTime, isInDelayPhase);
2054
+ let { done } = state;
2055
+ if (!isInDelayPhase && calculatedDuration !== null) {
2056
+ done =
2057
+ this.playbackSpeed >= 0
2058
+ ? this.currentTime >= totalDuration
2059
+ : this.currentTime <= 0;
2060
+ }
2061
+ const isAnimationFinished = this.holdTime === null &&
2062
+ (this.state === "finished" || (this.state === "running" && done));
2063
+ // TODO: The exception for inertia could be cleaner here
2064
+ if (isAnimationFinished && type !== inertia) {
2065
+ state.value = getFinalKeyframe(keyframes, this.options, finalKeyframe, this.speed);
2066
+ }
2067
+ if (onUpdate) {
2068
+ onUpdate(state.value);
2069
+ }
2070
+ if (isAnimationFinished) {
2071
+ this.finish();
2072
+ }
2073
+ return state;
2074
+ }
2075
+ /**
2076
+ * This animation's time, including delay, at a timestamp.
2077
+ */
2078
+ at(timestamp) {
2079
+ return (this.holdTime ??
2080
+ round((timestamp - this.startTime) * this.playbackSpeed));
2081
+ }
2082
+ /**
2083
+ * The value at a timestamp, without changing this animation's state.
2084
+ * If resolved, it ends at the last resolved keyframe, which is in the
2085
+ * same unit as the others, rather than at finalKeyframe.
2086
+ */
2087
+ sampleAt(timestamp, resolved) {
2088
+ const { options, totalDuration } = this;
2089
+ const t = this.at(timestamp) - (options.delay || 0);
2090
+ return t >= totalDuration && options.type !== inertia
2091
+ ? getFinalKeyframe(options.keyframes, options, resolved ? undefined : options.finalKeyframe)
2092
+ : this.generate(Math.max(t, 0), t < 0).value;
2093
+ }
2094
+ /**
2095
+ * The generator's state `currentTime` ms after the delay, without
2096
+ * changing the animation's own state.
2097
+ */
2098
+ generate(currentTime, isInDelayPhase) {
2099
+ const { generator, totalDuration, mixKeyframes, mirroredGenerator, resolvedDuration, } = this;
2100
+ const { keyframes, repeat, repeatType, repeatDelay } = this.options;
2101
+ let elapsed = currentTime;
2053
2102
  let frameGenerator = generator;
2054
2103
  if (repeat) {
2055
2104
  /**
@@ -2057,7 +2106,7 @@
2057
2106
  * than duration we'll get values like 2.5 (midway through the
2058
2107
  * third iteration)
2059
2108
  */
2060
- const progress = Math.min(this.currentTime, totalDuration) / resolvedDuration;
2109
+ const progress = Math.min(currentTime, totalDuration) / resolvedDuration;
2061
2110
  /**
2062
2111
  * Get the current iteration (0 indexed). For instance the floor of
2063
2112
  * 2.5 is 2.
@@ -2110,25 +2159,6 @@
2110
2159
  if (mixKeyframes && !isInDelayPhase) {
2111
2160
  state.value = mixKeyframes(state.value);
2112
2161
  }
2113
- let { done } = state;
2114
- if (!isInDelayPhase && calculatedDuration !== null) {
2115
- done =
2116
- this.playbackSpeed >= 0
2117
- ? this.currentTime >= totalDuration
2118
- : this.currentTime <= 0;
2119
- }
2120
- const isAnimationFinished = this.holdTime === null &&
2121
- (this.state === "finished" || (this.state === "running" && done));
2122
- // TODO: The exception for inertia could be cleaner here
2123
- if (isAnimationFinished && type !== inertia) {
2124
- state.value = getFinalKeyframe(keyframes, this.options, finalKeyframe, this.speed);
2125
- }
2126
- if (onUpdate) {
2127
- onUpdate(state.value);
2128
- }
2129
- if (isAnimationFinished) {
2130
- this.finish();
2131
- }
2132
2162
  return state;
2133
2163
  }
2134
2164
  /**
@@ -3137,44 +3167,71 @@
3137
3167
  }
3138
3168
  this.options = options;
3139
3169
  }
3170
+ /**
3171
+ * The animation's time now, in ms. Wall-clock rather than WAAPI's
3172
+ * currentTime while running, as under CPU load currentTime may not
3173
+ * reflect the actual elapsed time, causing incorrect sampling and
3174
+ * visual jumps.
3175
+ */
3176
+ elapsed() {
3177
+ return this.state === "running"
3178
+ ? (time.now() - this.startTime) * this.speed
3179
+ : this.time * 1000;
3180
+ }
3181
+ /**
3182
+ * The value at a time, from a renderless JS animation.
3183
+ */
3184
+ sample(sampleTime) {
3185
+ if (!this.sampler) {
3186
+ const { motionValue, onUpdate, onComplete, onPlay, onStop, element, ...options } = this.options;
3187
+ /**
3188
+ * Stop it straight away so its driver doesn't run every frame.
3189
+ * It can still be sampled.
3190
+ */
3191
+ this.sampler = new JSAnimation({ ...options, autoplay: false });
3192
+ this.sampler.stop();
3193
+ }
3194
+ return this.sampler.sample(Math.max(0, sampleTime)).value;
3195
+ }
3196
+ /**
3197
+ * The value now, as WAAPI doesn't set the motion value while it runs.
3198
+ */
3199
+ liveValue() {
3200
+ const { state } = this;
3201
+ if (state === "running" || state === "paused") {
3202
+ return this.sample(this.elapsed());
3203
+ }
3204
+ }
3140
3205
  /**
3141
3206
  * WAAPI doesn't natively have any interruption capabilities.
3142
3207
  *
3143
3208
  * Rather than read committed styles back out of the DOM, we can
3144
- * create a renderless JS animation and sample it twice to calculate
3145
- * its current value, "previous" value, and therefore allow
3146
- * Motion to calculate velocity for any subsequent animation.
3209
+ * sample a renderless JS animation twice to calculate its current
3210
+ * value, "previous" value, and therefore allow Motion to calculate
3211
+ * velocity for any subsequent animation.
3147
3212
  */
3148
3213
  updateMotionValue(value) {
3149
- const { motionValue, onUpdate, onComplete, element, ...options } = this.options;
3214
+ const { motionValue, element, name } = this.options;
3150
3215
  if (!motionValue)
3151
3216
  return;
3152
3217
  if (value !== undefined) {
3153
3218
  motionValue.set(value);
3154
3219
  return;
3155
3220
  }
3156
- const sampleAnimation = new JSAnimation({
3157
- ...options,
3158
- autoplay: false,
3159
- });
3160
- /**
3161
- * Use wall-clock elapsed time for sampling.
3162
- * Under CPU load, WAAPI's currentTime may not reflect actual
3163
- * elapsed time, causing incorrect sampling and visual jumps.
3164
- */
3165
- const sampleTime = Math.max(sampleDelta, time.now() - this.startTime);
3166
- const delta = clamp(0, sampleDelta, sampleTime - sampleDelta);
3167
- const current = sampleAnimation.sample(sampleTime).value;
3221
+ const { speed } = this;
3222
+ const sampleTime = this.elapsed();
3223
+ const delta = this.state === "running" && speed
3224
+ ? clamp(0, sampleDelta, sampleTime)
3225
+ : 0;
3226
+ const current = this.sample(sampleTime);
3168
3227
  /**
3169
3228
  * Write the estimated value to inline style so it persists
3170
3229
  * after cancel(), covering the async gap before the next
3171
3230
  * animation starts.
3172
3231
  */
3173
- const { name } = this.options;
3174
3232
  if (element && name)
3175
3233
  setStyle(element, name, current);
3176
- motionValue.setWithVelocity(sampleAnimation.sample(Math.max(0, sampleTime - delta)).value, current, delta);
3177
- sampleAnimation.stop();
3234
+ motionValue.setWithVelocity(this.sample(sampleTime - delta * Math.sign(speed)), current, delta / Math.abs(speed || 1));
3178
3235
  }
3179
3236
  }
3180
3237
 
@@ -3328,6 +3385,915 @@
3328
3385
  type !== "inertia");
3329
3386
  }
3330
3387
 
3388
+ /**
3389
+ * Provided a value and a ValueType, returns the value as that value type.
3390
+ */
3391
+ const getValueAsType = (value, type) => {
3392
+ return type && typeof value === "number"
3393
+ ? type.transform(value)
3394
+ : value;
3395
+ };
3396
+
3397
+ const translateAlias = {
3398
+ x: "translateX",
3399
+ y: "translateY",
3400
+ z: "translateZ",
3401
+ transformPerspective: "perspective",
3402
+ };
3403
+ const numTransforms = transformPropOrder.length;
3404
+ /**
3405
+ * Build a CSS transform style from individual x/y/scale etc properties.
3406
+ *
3407
+ * This outputs with a default order of transforms/scales/rotations, this can be customised by
3408
+ * providing a transformTemplate function.
3409
+ */
3410
+ function buildTransform$1(latestValues, transform, transformTemplate) {
3411
+ // The transform string we're going to build into.
3412
+ let transformString = "";
3413
+ let transformIsDefault = true;
3414
+ /**
3415
+ * Loop over all possible transforms in order, adding the ones that
3416
+ * are present to the transform string.
3417
+ */
3418
+ for (let i = 0; i < numTransforms; i++) {
3419
+ const key = transformPropOrder[i];
3420
+ const value = latestValues[key];
3421
+ if (value === undefined)
3422
+ continue;
3423
+ let valueIsDefault = true;
3424
+ if (typeof value === "number") {
3425
+ valueIsDefault = value === (key.startsWith("scale") ? 1 : 0);
3426
+ }
3427
+ else {
3428
+ const parsed = parseFloat(value);
3429
+ valueIsDefault = key.startsWith("scale") ? parsed === 1 : parsed === 0;
3430
+ }
3431
+ if (!valueIsDefault || transformTemplate) {
3432
+ const valueAsType = getValueAsType(value, numberValueTypes[key]);
3433
+ if (!valueIsDefault) {
3434
+ transformIsDefault = false;
3435
+ const transformName = translateAlias[key] || key;
3436
+ transformString += `${transformName}(${valueAsType}) `;
3437
+ }
3438
+ if (transformTemplate) {
3439
+ transform[key] = valueAsType;
3440
+ }
3441
+ }
3442
+ }
3443
+ // `pathRotation` composes onto `rotate` as a separate additive term so
3444
+ // the user's `rotate` is never clobbered. Deliberately not a slot in
3445
+ // `transformPropOrder`.
3446
+ const pathRotation = latestValues.pathRotation;
3447
+ if (pathRotation) {
3448
+ transformIsDefault = false;
3449
+ transformString += `rotate(${getValueAsType(pathRotation, numberValueTypes.pathRotation)}) `;
3450
+ }
3451
+ transformString = transformString.trim();
3452
+ // If we have a custom `transform` template, pass our transform values and
3453
+ // generated transformString to that before returning
3454
+ if (transformTemplate) {
3455
+ transformString = transformTemplate(transform, transformIsDefault ? "" : transformString);
3456
+ }
3457
+ else if (transformIsDefault) {
3458
+ transformString = "none";
3459
+ }
3460
+ return transformString;
3461
+ }
3462
+
3463
+ const transformGroups = new WeakMap();
3464
+
3465
+ /**
3466
+ * Sample at the interruption sampling rate, up to a cap for long
3467
+ * animations.
3468
+ */
3469
+ const maxSamples = 200;
3470
+ /**
3471
+ * How far values may stray from the WAAPI animation: as a fraction of
3472
+ * each value's change when they share an easing, otherwise in px or deg,
3473
+ * or for scale.
3474
+ */
3475
+ const easingTolerance = 0.001;
3476
+ const sampleTolerance = 0.1;
3477
+ const scaleTolerance = 0.001;
3478
+ /**
3479
+ * If an outside write, or a seek the WAAPI animation can't follow,
3480
+ * happens again within this window, the group is being driven every
3481
+ * frame (scrubbing, drag), which is cheaper on the main thread than
3482
+ * rebuilding the WAAPI animation every frame.
3483
+ */
3484
+ const churnWindow = 100;
3485
+ /**
3486
+ * Named easings that are the same cubic-bezier in JS and CSS.
3487
+ */
3488
+ const isNativeEasing = (ease) => /^(linear|ease(In|Out|InOut))$/.test(ease);
3489
+ const byTime = (a, b) => a - b;
3490
+ /**
3491
+ * n + 1 evenly spaced timestamps, from one timestamp to another.
3492
+ */
3493
+ const spread = (from, to, n) => {
3494
+ const result = [];
3495
+ for (let i = 0; i <= n; i++)
3496
+ result.push(mixNumber$1(from, to, i / n));
3497
+ return result;
3498
+ };
3499
+ const getOwner = (options) => options.motionValue.owner;
3500
+ const getGroup = (element) => transformGroups.get(element);
3501
+ const read = (owner, key) => owner.latestValues ? owner.latestValues[key] : owner.getValue(key)?.get();
3502
+ /**
3503
+ * Whether this animation should join its element's transform group.
3504
+ * Animations that can't be accelerated still join, so the group knows
3505
+ * to stay on the main thread while they run. Browsers without linear()
3506
+ * easing stay on the main thread.
3507
+ */
3508
+ const canGroupTransform = ({ name, motionValue, }) => name &&
3509
+ transformPropOrder.includes(name) &&
3510
+ motionValue?.owner?.current instanceof HTMLElement &&
3511
+ supportsWaapi() &&
3512
+ supportsLinearEasing();
3513
+ function canAccelerateTransform(options) {
3514
+ const owner = getOwner(options);
3515
+ const { onUpdate, transformTemplate } = owner.getProps();
3516
+ return (
3517
+ /**
3518
+ * A handoff from an optimised appear animation is already running
3519
+ * on its own WAAPI animation. onUpdate needs every value, every
3520
+ * frame.
3521
+ */
3522
+ !options.isHandoff &&
3523
+ !onUpdate &&
3524
+ !options.hasOnUpdate &&
3525
+ /**
3526
+ * transformTemplate could output anything.
3527
+ */
3528
+ !transformTemplate &&
3529
+ options.type !== "inertia" &&
3530
+ /**
3531
+ * A literal transform, or pathRotation, replaces or adds to the
3532
+ * transform we'd build. styleEffect builds its own as "none".
3533
+ */
3534
+ (read(owner, "transform") || "none") === "none" &&
3535
+ !read(owner, "pathRotation") &&
3536
+ /**
3537
+ * Layout animations write transform every frame.
3538
+ */
3539
+ !owner.projection?.isProjecting());
3540
+ }
3541
+ class TransformAnimation extends JSAnimation {
3542
+ constructor(options) {
3543
+ const element = getOwner(options).current;
3544
+ const { driver = frameloopDriver } = options;
3545
+ /**
3546
+ * Before JSAnimation replaces the type with its generator.
3547
+ */
3548
+ const canAccelerate = canAccelerateTransform(options);
3549
+ /**
3550
+ * The driver ticks on the main thread only once the group has
3551
+ * moved there. Until then, starting or stopping tells the group to
3552
+ * update its WAAPI animation.
3553
+ */
3554
+ options.driver = (update) => {
3555
+ const js = driver(update);
3556
+ return {
3557
+ js,
3558
+ now: js.now,
3559
+ start: (keepAlive = true) => {
3560
+ const group = getGroup(element);
3561
+ const isJS = !group || group.isJS;
3562
+ /**
3563
+ * keepAlive is false when seeking. Seeking the WAAPI
3564
+ * animation is free, but if a seek needs a rebuild and
3565
+ * keeps happening, the group moves to the main thread.
3566
+ */
3567
+ isJS
3568
+ ? js.start(keepAlive)
3569
+ : group.schedule(false, !keepAlive);
3570
+ },
3571
+ stop: () => {
3572
+ js.stop();
3573
+ const group = getGroup(element);
3574
+ group?.schedule(!group.isFinishing);
3575
+ },
3576
+ };
3577
+ };
3578
+ super(options);
3579
+ this.canAccelerate = canAccelerate;
3580
+ /**
3581
+ * stop is bound in JSAnimation, so wrap it rather than override.
3582
+ * Before stopping, set the motion value to where the WAAPI
3583
+ * animation is now, with velocity, for the next animation to
3584
+ * start from.
3585
+ */
3586
+ const { stop } = this;
3587
+ this.stop = () => {
3588
+ this.isAccelerated() && this.sync();
3589
+ stop();
3590
+ };
3591
+ }
3592
+ get group() {
3593
+ return getGroup(getOwner(this.options).current);
3594
+ }
3595
+ get js() {
3596
+ return this.driver?.js;
3597
+ }
3598
+ get name() {
3599
+ return this.options.name;
3600
+ }
3601
+ /**
3602
+ * When this stops moving, or, if it repeats forever, when it starts
3603
+ * repeating, and how long each repeat takes. Held (paused, or at
3604
+ * speed 0) animations never stop moving.
3605
+ */
3606
+ timing() {
3607
+ const { startTime, speed, totalDuration } = this;
3608
+ const { delay = 0, repeatType } = this.options;
3609
+ return this.holdTime !== null
3610
+ ? [Infinity, 0]
3611
+ : totalDuration === Infinity
3612
+ ? [
3613
+ startTime + delay / speed,
3614
+ (this.resolvedDuration * (repeatType === "loop" ? 1 : 2)) /
3615
+ speed || 1,
3616
+ ]
3617
+ : [startTime + (delay + totalDuration) / speed, 0];
3618
+ }
3619
+ /**
3620
+ * Whether this is running, or paused, on the compositor.
3621
+ */
3622
+ isAccelerated() {
3623
+ const { group, state } = this;
3624
+ return (group?.tracks.has(this) &&
3625
+ !group.isJS &&
3626
+ this.canAccelerate &&
3627
+ (state === "running" || state === "paused"));
3628
+ }
3629
+ /**
3630
+ * The value now, while this runs on the compositor and so doesn't
3631
+ * set its motion value every frame.
3632
+ */
3633
+ liveValue() {
3634
+ return this.isAccelerated() ? this.sampleAt(time.now()) : undefined;
3635
+ }
3636
+ /**
3637
+ * If this is a two-keyframe tween whose easing WAAPI runs exactly,
3638
+ * when it starts and stops moving, and that easing.
3639
+ */
3640
+ tween() {
3641
+ const { keyframes: keyframes$1, type, ease, times, repeat, delay = 0 } = this.options;
3642
+ const easing = isBezierDefinition(ease)
3643
+ ? cubicBezierAsString(ease)
3644
+ : isNativeEasing(ease) && supportedWaapiEasing[ease];
3645
+ if (easing &&
3646
+ !repeat &&
3647
+ !times &&
3648
+ keyframes$1.length === 2 &&
3649
+ type === keyframes &&
3650
+ this.holdTime === null) {
3651
+ const start = this.startTime + delay / this.speed;
3652
+ return [start, start + this.resolvedDuration / this.speed, easing];
3653
+ }
3654
+ }
3655
+ /**
3656
+ * The timestamps between two timestamps where this reaches a
3657
+ * keyframe or starts a repeat, where it can turn or jump.
3658
+ */
3659
+ boundaries(from, to) {
3660
+ const { startTime, speed, resolvedDuration, options } = this;
3661
+ const { delay = 0, repeat = 0, repeatDelay = 0, repeatType, times, keyframes: keyframes$1, type, } = options;
3662
+ const offsets = (type || keyframes) === keyframes
3663
+ ? times || defaultOffset$1(keyframes$1)
3664
+ : [0, 1];
3665
+ const start = startTime + delay / speed;
3666
+ const result = [];
3667
+ if (this.holdTime !== null || !(resolvedDuration > 0))
3668
+ return result;
3669
+ for (let i = Math.max(0, Math.floor(((from - start) * speed) / resolvedDuration)); i <= repeat; i++) {
3670
+ const iterationStart = start + (i * resolvedDuration) / speed;
3671
+ if (iterationStart > to)
3672
+ break;
3673
+ for (const offset of offsets) {
3674
+ const t = iterationStart +
3675
+ ((i % 2 && repeatType && repeatType !== "loop"
3676
+ ? 1 - offset
3677
+ : offset) *
3678
+ (resolvedDuration - repeatDelay)) /
3679
+ speed;
3680
+ t > from && t < to && result.push(t);
3681
+ }
3682
+ }
3683
+ return result;
3684
+ }
3685
+ /**
3686
+ * The value at a timestamp, as a number.
3687
+ */
3688
+ numberAt(timestamp) {
3689
+ return parseFloat(this.sampleAt(timestamp, true));
3690
+ }
3691
+ /**
3692
+ * Write the current value, and velocity, to the motion value.
3693
+ */
3694
+ sync() {
3695
+ const now = time.now();
3696
+ this.options.motionValue.setWithVelocity(this.sampleAt(now - sampleDelta, true), this.sampleAt(now, true), sampleDelta);
3697
+ }
3698
+ play() {
3699
+ /**
3700
+ * A stopped animation (one that was interrupted) can't play again,
3701
+ * so it mustn't join the group.
3702
+ */
3703
+ if (this.isStopped)
3704
+ return;
3705
+ (this.group || createGroup(getOwner(this.options))).tracks.add(this);
3706
+ super.play();
3707
+ }
3708
+ pause() {
3709
+ super.pause();
3710
+ if (this.isAccelerated()) {
3711
+ this.sync();
3712
+ this.group.schedule(false);
3713
+ }
3714
+ }
3715
+ /**
3716
+ * JSAnimation finishes on its next tick, which only happens on the
3717
+ * main thread, so finish now.
3718
+ */
3719
+ complete() {
3720
+ super.complete();
3721
+ const { group } = this;
3722
+ group && !group.isJS && this.canAccelerate && this.tick(time.now());
3723
+ }
3724
+ /**
3725
+ * Without a tick to rebase it, a held time still includes the delay,
3726
+ * so take it off here, as a JS animation does on its next tick.
3727
+ */
3728
+ get time() {
3729
+ if (this.isAccelerated()) {
3730
+ return millisecondsToSeconds(clamp(0, this.totalDuration, this.at(time.now()) - (this.options.delay || 0)));
3731
+ }
3732
+ return super.time;
3733
+ }
3734
+ set time(newTime) {
3735
+ super.time = newTime;
3736
+ }
3737
+ /**
3738
+ * Scroll timelines set time every frame, so run these on the main
3739
+ * thread.
3740
+ */
3741
+ attachTimeline(timeline) {
3742
+ this.canAccelerate = false;
3743
+ this.group?.demote(true);
3744
+ return super.attachTimeline(timeline);
3745
+ }
3746
+ }
3747
+ function createGroup(owner) {
3748
+ const element = owner.current;
3749
+ const tracks = new Set();
3750
+ const byKey = new Map();
3751
+ /**
3752
+ * Transforms that aren't animating, as built into the WAAPI animation.
3753
+ */
3754
+ let statics = {};
3755
+ /**
3756
+ * The transforms in the WAAPI animation, in order.
3757
+ */
3758
+ let keys = [];
3759
+ let built = new Map();
3760
+ let segments = [];
3761
+ let needsBuild = false;
3762
+ let isScheduled = false;
3763
+ let hasChurned = false;
3764
+ let lastChurn = -Infinity;
3765
+ const has = (key) => {
3766
+ for (const track of tracks)
3767
+ if (track.name === key)
3768
+ return true;
3769
+ };
3770
+ /**
3771
+ * The transform values at a timestamp or, without one, from the
3772
+ * motion values. Animations that don't repeat forever are sampled no
3773
+ * earlier than settled, so a loop that starts from an earlier cycle
3774
+ * boundary holds them at their end.
3775
+ */
3776
+ const valuesAt = (timestamp, settled) => {
3777
+ const values = {};
3778
+ for (const key of timestamp === undefined ? transformPropOrder : keys) {
3779
+ const track = byKey.get(key);
3780
+ const value = timestamp === undefined
3781
+ ? read(owner, key)
3782
+ : track
3783
+ ? track.sampleAt(settled > timestamp && !track.timing()[1]
3784
+ ? settled
3785
+ : timestamp)
3786
+ : statics[key];
3787
+ if (value !== undefined)
3788
+ values[key] = value;
3789
+ }
3790
+ return values;
3791
+ };
3792
+ /**
3793
+ * Build the transform at a timestamp. Unlike buildTransform, every key
3794
+ * is written, even at its default, so each keyframe has the same list
3795
+ * of functions and the browser interpolates them one by one.
3796
+ */
3797
+ const compose = (timestamp, settled) => {
3798
+ let transform = "";
3799
+ const values = valuesAt(timestamp, settled);
3800
+ for (const key in values) {
3801
+ transform +=
3802
+ (translateAlias[key] || key) +
3803
+ "(" +
3804
+ getValueAsType(values[key], numberValueTypes[key]) +
3805
+ ") ";
3806
+ }
3807
+ return transform || "none";
3808
+ };
3809
+ /**
3810
+ * Write the transform as an inline style, as the renderer would.
3811
+ */
3812
+ const commit = (values = valuesAt()) => {
3813
+ element.style.transform = buildTransform$1(values, {});
3814
+ };
3815
+ /**
3816
+ * Cancel the WAAPI animations, by default first committing the
3817
+ * transform. The renderer writes its own version on its next render.
3818
+ */
3819
+ const stop = (shouldCommit = true) => {
3820
+ shouldCommit && commit();
3821
+ segments.forEach(({ animation }) => animation.cancel());
3822
+ segments = [];
3823
+ };
3824
+ const add = (animation, offset, end = Infinity) => {
3825
+ const segment = { animation, offset, end };
3826
+ animation.startTime = time.now() + offset;
3827
+ /**
3828
+ * Animations finish when the WAAPI animation reaches their end.
3829
+ * Once a loop has taken over, the finished animation isn't needed.
3830
+ */
3831
+ if (end < Infinity) {
3832
+ animation.onfinish = () => {
3833
+ segment.isDone = true;
3834
+ if (segments.some((s) => s.end === Infinity)) {
3835
+ animation.cancel();
3836
+ segments = segments.filter((s) => s !== segment);
3837
+ }
3838
+ group.schedule(false);
3839
+ };
3840
+ }
3841
+ /**
3842
+ * Something else cancelled it, so carry on on the main thread.
3843
+ */
3844
+ animation.oncancel = () => segments.includes(segment) && group.demote();
3845
+ segments.push(segment);
3846
+ };
3847
+ /**
3848
+ * Where a value jumps between two timestamps, as when a repeat starts
3849
+ * again: the timestamps either side of the jump, or none if it moves
3850
+ * there continuously.
3851
+ */
3852
+ const jumpAt = (value, a, b) => {
3853
+ const change = (x, y) => Math.abs(value(y) - value(x));
3854
+ const size = change(a, b);
3855
+ while (b - a > 0.01) {
3856
+ const mid = (a + b) / 2;
3857
+ change(a, mid) > change(mid, b) ? (b = mid) : (a = mid);
3858
+ }
3859
+ return change(a, b) > size / 2 ? [a, b] : [];
3860
+ };
3861
+ /**
3862
+ * If every moving animation follows the same eased progress between
3863
+ * two timestamps, the easing that describes it.
3864
+ */
3865
+ const getEasing = (moving, from, to, bounds) => {
3866
+ const samples = Math.min(maxSamples, Math.max(2, Math.ceil((to - from) / sampleDelta)));
3867
+ /**
3868
+ * Too many keyframes or repeats to follow with this many samples.
3869
+ */
3870
+ if (bounds.length * 4 > samples)
3871
+ return;
3872
+ const at = spread(from, to, samples).concat(bounds).sort(byTime);
3873
+ const position = (t) => progress(from, to, t);
3874
+ let shared;
3875
+ let progressAt;
3876
+ for (const track of moving) {
3877
+ const values = at.map((t) => track.numberAt(t));
3878
+ const [first] = values;
3879
+ const range = values[values.length - 1] - first;
3880
+ const progress = values.map((value) => range ? (value - first) / range : value - first);
3881
+ /**
3882
+ * A value that doesn't change, like one still in its delay,
3883
+ * fits any easing. One that changes but ends where it started
3884
+ * fits none.
3885
+ */
3886
+ if (!range) {
3887
+ if (progress.some((p) => !(Math.abs(p) <= easingTolerance))) {
3888
+ return;
3889
+ }
3890
+ }
3891
+ else if (!shared) {
3892
+ shared = progress;
3893
+ progressAt = (t) => (track.numberAt(t) - first) / range;
3894
+ }
3895
+ else if (progress.some((p, i) => !(Math.abs(p - shared[i]) <= easingTolerance))) {
3896
+ return;
3897
+ }
3898
+ }
3899
+ if (!shared ||
3900
+ shared.every((p, i) => Math.abs(p - position(at[i])) <= easingTolerance)) {
3901
+ return "linear";
3902
+ }
3903
+ /**
3904
+ * A repeat that starts again jumps back, which takes two points at
3905
+ * the same position.
3906
+ */
3907
+ const points = [];
3908
+ const point = (t, p = progressAt(t)) => points.push(round(p) + " " + round(position(t) * 100) + "%");
3909
+ shared.forEach((p, i) => {
3910
+ i &&
3911
+ Math.abs(p - shared[i - 1]) > 0.2 &&
3912
+ jumpAt(progressAt, at[i - 1], at[i]).forEach((t) => point(t));
3913
+ point(at[i], p);
3914
+ });
3915
+ return "linear(" + points + ")";
3916
+ };
3917
+ /**
3918
+ * Sample timestamps between two timestamps, closer together where the
3919
+ * moving animations curve.
3920
+ */
3921
+ const sample = (moving, from, to, bounds) => {
3922
+ const times = [from];
3923
+ /**
3924
+ * Values that aren't numbers, like calc(), are left to the
3925
+ * browser to interpolate.
3926
+ */
3927
+ const isStraight = (a, b) => moving.every((track) => !(Math.abs(track.numberAt((a + b) / 2) -
3928
+ (track.numberAt(a) + track.numberAt(b)) / 2) >
3929
+ (track.name.startsWith("scale")
3930
+ ? scaleTolerance
3931
+ : sampleTolerance)));
3932
+ const split = (a, b, depth) => {
3933
+ if (b - a < 1 || isStraight(a, b)) {
3934
+ times.push(b);
3935
+ }
3936
+ else if (depth < 4) {
3937
+ split(a, (a + b) / 2, depth + 1);
3938
+ split((a + b) / 2, b, depth + 1);
3939
+ }
3940
+ else {
3941
+ /**
3942
+ * Keyframes either side of a jump, like a repeat starting
3943
+ * again, so it doesn't play as a quick sweep.
3944
+ */
3945
+ for (const track of moving) {
3946
+ times.push(...jumpAt((t) => track.numberAt(t), a, b));
3947
+ }
3948
+ times.push(b);
3949
+ }
3950
+ };
3951
+ /**
3952
+ * Split at least every 100ms, and at every keyframe and repeat,
3953
+ * either side of where a value can jump.
3954
+ */
3955
+ const ends = spread(from, to, Math.ceil((to - from) / 100));
3956
+ for (const t of bounds)
3957
+ ends.push(t, Math.min(t + 0.01, to));
3958
+ let start = from;
3959
+ for (const end of ends.sort(byTime)) {
3960
+ end > start && split(start, end, 0);
3961
+ start = end;
3962
+ }
3963
+ return times.sort(byTime);
3964
+ };
3965
+ /**
3966
+ * If every moving animation is the same tween, started together, as
3967
+ * happens for the values of one animate() call or transition, when
3968
+ * they move and their native easing.
3969
+ */
3970
+ const getTween = (moving) => {
3971
+ let shared;
3972
+ for (const track of moving) {
3973
+ const tween = track.tween();
3974
+ if (!tween ||
3975
+ shared?.some((v, i) => i < 2
3976
+ ? Math.abs(tween[i] - v) > 1
3977
+ : tween[i] !== v)) {
3978
+ return;
3979
+ }
3980
+ shared = tween;
3981
+ }
3982
+ return shared;
3983
+ };
3984
+ /**
3985
+ * A WAAPI animation of the composed transform between two timestamps.
3986
+ */
3987
+ const run = (moving, from, to, options = { fill: "both" }, settled, nativeEasing) => {
3988
+ const bounds = moving.flatMap((track) => track.boundaries(from, to));
3989
+ const easing = nativeEasing || getEasing(moving, from, to, bounds);
3990
+ const times = easing ? [from, to] : sample(moving, from, to, bounds);
3991
+ const transform = times.map((t) => compose(t, settled));
3992
+ let keyframes = { transform };
3993
+ if (!easing) {
3994
+ keyframes.offset = times.map((t) => progress(from, to, t));
3995
+ }
3996
+ /**
3997
+ * If nothing moves, don't hold a transform, which, even at its
3998
+ * default, would make the element a containing block. The empty
3999
+ * animation still finishes the animations.
4000
+ */
4001
+ if (transform.every((t) => t === transform[0])) {
4002
+ commit(valuesAt(from, settled));
4003
+ keyframes = null;
4004
+ }
4005
+ return element.animate(keyframes, {
4006
+ ...options,
4007
+ duration: to - from,
4008
+ easing: easing || "linear",
4009
+ });
4010
+ };
4011
+ /**
4012
+ * Replace the WAAPI animations with ones built from now. Returns
4013
+ * undefined if the animations can't be composed.
4014
+ */
4015
+ const animate = (now) => {
4016
+ /**
4017
+ * The finite part runs until the last finite animation (or the
4018
+ * last delay) ends. Infinitely repeating animations then continue
4019
+ * as one looping WAAPI animation, which needs them to share a
4020
+ * cycle.
4021
+ */
4022
+ let end = now;
4023
+ let cycle = 0;
4024
+ let cycleStart = 0;
4025
+ const moving = [];
4026
+ const looping = [];
4027
+ const ends = new Set();
4028
+ byKey.clear();
4029
+ built = new Map();
4030
+ for (const track of tracks) {
4031
+ byKey.set(track.name, track);
4032
+ const [trackEnd, trackCycle] = track.timing();
4033
+ const isHeld = trackEnd === Infinity;
4034
+ built.set(track, [track.at(now), track.speed, isHeld]);
4035
+ /**
4036
+ * Held animations don't move, so keep their motion values up
4037
+ * to date, as a JSAnimation would on a seek.
4038
+ */
4039
+ if (isHeld) {
4040
+ track.sync();
4041
+ continue;
4042
+ }
4043
+ if (track.speed < 0)
4044
+ return;
4045
+ if (trackCycle) {
4046
+ if (cycle) {
4047
+ const phase = (trackEnd - cycleStart) / trackCycle;
4048
+ if (Math.abs(trackCycle - cycle) > 1 ||
4049
+ Math.abs(phase - Math.round(phase)) * cycle > 1) {
4050
+ return;
4051
+ }
4052
+ }
4053
+ else {
4054
+ cycle = trackCycle;
4055
+ cycleStart = trackEnd;
4056
+ }
4057
+ looping.push(track);
4058
+ }
4059
+ else {
4060
+ ends.add(trackEnd);
4061
+ }
4062
+ moving.push(track);
4063
+ end = Math.max(end, trackEnd);
4064
+ }
4065
+ statics = {};
4066
+ /**
4067
+ * Transforms that aren't animating are left out at their default.
4068
+ */
4069
+ keys = transformPropOrder.filter((key) => {
4070
+ if (byKey.has(key))
4071
+ return true;
4072
+ const value = (statics[key] = read(owner, key));
4073
+ return (value !== undefined &&
4074
+ parseFloat(value) !==
4075
+ (key.startsWith("scale") ? 1 : 0));
4076
+ });
4077
+ stop(false);
4078
+ /**
4079
+ * Values that share one tween are one WAAPI animation of it, with
4080
+ * two keyframes and its own easing, from when they started.
4081
+ */
4082
+ const tween = !cycle && getTween(moving);
4083
+ try {
4084
+ if (tween && tween[1] > now) {
4085
+ const [from, to, easing] = tween;
4086
+ add(run(moving, from, to, undefined, undefined, easing), from - now, to - from);
4087
+ }
4088
+ else if (end > now) {
4089
+ add(run(moving, now, end), 0, end - now);
4090
+ /**
4091
+ * Animations that end sooner finish on the events of
4092
+ * empty animations that end with them.
4093
+ */
4094
+ ends.forEach((trackEnd) => trackEnd < end - 1 &&
4095
+ add(element.animate(null, {
4096
+ duration: trackEnd - now,
4097
+ }), 0, trackEnd - now));
4098
+ }
4099
+ if (cycle) {
4100
+ /**
4101
+ * Build one cycle from a cycle boundary, so it repeats
4102
+ * seamlessly, and start it part way through.
4103
+ */
4104
+ const from = cycleStart + Math.floor((end - cycleStart) / cycle) * cycle;
4105
+ add(run(looping, from, from + cycle, {
4106
+ iterations: Infinity,
4107
+ iterationStart: (end - from) / cycle,
4108
+ /**
4109
+ * It must not fill backwards over the finite part
4110
+ * that runs before it.
4111
+ */
4112
+ fill: "forwards",
4113
+ }, end), end - now);
4114
+ }
4115
+ }
4116
+ catch {
4117
+ return;
4118
+ }
4119
+ /**
4120
+ * Everything is held, so hold it with an inline style instead.
4121
+ */
4122
+ segments.length || stop();
4123
+ return true;
4124
+ };
4125
+ /**
4126
+ * If every animation has been paused, played, seeked or sped up
4127
+ * together since the WAAPI animation was built, do the same to it.
4128
+ * Returns false if it needs rebuilding instead.
4129
+ */
4130
+ const retime = (now) => {
4131
+ let elapsed;
4132
+ let rate = 0;
4133
+ for (const track of tracks) {
4134
+ const state = built.get(track);
4135
+ if (!state)
4136
+ return false;
4137
+ const [builtTime, builtSpeed, wasHeld] = state;
4138
+ const trackTime = track.at(now);
4139
+ const isHeld = track.timing()[0] === Infinity;
4140
+ if (wasHeld) {
4141
+ if (isHeld && trackTime === builtTime)
4142
+ continue;
4143
+ return false;
4144
+ }
4145
+ const trackElapsed = (trackTime - builtTime) / builtSpeed;
4146
+ const trackRate = isHeld ? 0 : track.speed / builtSpeed;
4147
+ if (elapsed === undefined) {
4148
+ elapsed = trackElapsed;
4149
+ rate = trackRate;
4150
+ }
4151
+ else if (Math.abs(trackElapsed - elapsed) > 1 ||
4152
+ trackRate !== rate) {
4153
+ return false;
4154
+ }
4155
+ }
4156
+ if (elapsed === undefined)
4157
+ return true;
4158
+ if (!segments.length)
4159
+ return false;
4160
+ for (const { offset, end, isDone } of segments) {
4161
+ const local = elapsed - offset;
4162
+ if (end < Infinity &&
4163
+ (isDone ? local < end - 1 : local < 0 || local >= end)) {
4164
+ return false;
4165
+ }
4166
+ }
4167
+ for (const { animation, offset, isDone } of segments) {
4168
+ if (isDone)
4169
+ continue;
4170
+ const local = elapsed - offset;
4171
+ if (rate) {
4172
+ animation.playbackRate = rate;
4173
+ animation.startTime = now - local / rate;
4174
+ }
4175
+ else {
4176
+ animation.pause();
4177
+ animation.currentTime = local;
4178
+ }
4179
+ }
4180
+ return true;
4181
+ };
4182
+ const build = () => {
4183
+ const now = time.now();
4184
+ /**
4185
+ * Finish the animations that have reached their end, which the
4186
+ * WAAPI animation has already rendered.
4187
+ */
4188
+ if (!group.isJS) {
4189
+ group.isFinishing = true;
4190
+ tracks.forEach((track) => {
4191
+ const [trackEnd, trackCycle] = track.timing();
4192
+ if (track.state === "running" &&
4193
+ track.speed > 0 &&
4194
+ !trackCycle &&
4195
+ trackEnd <= now + 1) {
4196
+ track.complete();
4197
+ /**
4198
+ * From now it's checked for outside writes like any
4199
+ * other static transform. If the WAAPI animation never
4200
+ * rendered it (it was instant), rebuild with its value.
4201
+ */
4202
+ statics[track.name] = read(owner, track.name);
4203
+ built.has(track) || (needsBuild = true);
4204
+ }
4205
+ });
4206
+ group.isFinishing = false;
4207
+ }
4208
+ isScheduled = false;
4209
+ tracks.forEach(({ state }, track) => {
4210
+ if (state === "idle" || state === "finished")
4211
+ tracks.delete(track);
4212
+ });
4213
+ if (!tracks.size) {
4214
+ transformGroups.delete(element);
4215
+ group.isJS || stop();
4216
+ return;
4217
+ }
4218
+ for (const track of tracks) {
4219
+ if (!track.canAccelerate)
4220
+ return group.demote(true);
4221
+ }
4222
+ /**
4223
+ * Everything that kept the group on the main thread has finished,
4224
+ * so move the remaining animations back to the compositor.
4225
+ */
4226
+ if (group.isJS) {
4227
+ group.isJS = false;
4228
+ needsBuild = true;
4229
+ tracks.forEach((track) => track.js?.stop());
4230
+ }
4231
+ const shouldBuild = needsBuild;
4232
+ const churned = hasChurned;
4233
+ needsBuild = hasChurned = false;
4234
+ if (shouldBuild || !retime(now)) {
4235
+ if (churned) {
4236
+ if (now - lastChurn < churnWindow)
4237
+ return group.demote();
4238
+ lastChurn = now;
4239
+ }
4240
+ if (!animate(now))
4241
+ return group.demote();
4242
+ }
4243
+ };
4244
+ const group = {
4245
+ tracks,
4246
+ isJS: false,
4247
+ isFinishing: false,
4248
+ schedule(build_, isChurn) {
4249
+ needsBuild || (needsBuild = build_);
4250
+ hasChurned || (hasChurned = !!isChurn);
4251
+ /**
4252
+ * Build in this frame's render step, so it's in place before
4253
+ * anything reads the DOM in postRender, or straight after the
4254
+ * current task outside of a frame.
4255
+ */
4256
+ if (!isScheduled) {
4257
+ isScheduled = true;
4258
+ frameData.isProcessing
4259
+ ? frame.render(build, false, true)
4260
+ : queueMicrotask(build);
4261
+ }
4262
+ },
4263
+ /**
4264
+ * An outside write. Rebuild once, but if it keeps happening, move
4265
+ * to the main thread.
4266
+ */
4267
+ check() {
4268
+ if (group.isJS)
4269
+ return;
4270
+ for (const key in statics) {
4271
+ if (read(owner, key) !== statics[key] && !has(key)) {
4272
+ return group.schedule(true, true);
4273
+ }
4274
+ }
4275
+ },
4276
+ demote(canReturn) {
4277
+ if (group.isJS)
4278
+ return;
4279
+ /**
4280
+ * Unless only another animation is holding the group on the
4281
+ * main thread, these animations stay there until they end.
4282
+ */
4283
+ tracks.forEach((track) => {
4284
+ track.isAccelerated() && track.sync();
4285
+ canReturn || (track.canAccelerate = false);
4286
+ });
4287
+ group.isJS = true;
4288
+ built = new Map();
4289
+ stop();
4290
+ tracks.forEach((track) => track.state === "running" && track.js?.start());
4291
+ },
4292
+ };
4293
+ transformGroups.set(element, group);
4294
+ return group;
4295
+ }
4296
+
3331
4297
  class AsyncMotionValueAnimation extends WithPromise {
3332
4298
  constructor(options) {
3333
4299
  super();
@@ -3397,6 +4363,11 @@
3397
4363
  * values may not be valid CSS and would trigger browser warnings.
3398
4364
  */
3399
4365
  const useWaapi = canAnimateValue && !isHandoff && supportsBrowserAnimation(options);
4366
+ /**
4367
+ * Independent transforms (x, scale etc) on an HTML element are
4368
+ * composed into one WAAPI transform animation per element.
4369
+ */
4370
+ const isGroupedTransform = !useWaapi && canGroupTransform(options);
3400
4371
  /**
3401
4372
  * Resolve startTime for the animation. A startTime passed in options
3402
4373
  * (an optimised appear handoff syncing to its WAAPI animation) takes
@@ -3412,7 +4383,10 @@
3412
4383
  * animation started in that moment stays in sync. WAAPI has no frame
3413
4384
  * of ours to do that on.
3414
4385
  */
3415
- if (sync && (useWaapi || this.resolvedAt !== this.createdAt)) {
4386
+ if (sync &&
4387
+ (useWaapi ||
4388
+ isGroupedTransform ||
4389
+ this.resolvedAt !== this.createdAt)) {
3416
4390
  options.startTime ?? (options.startTime = resolveStartTime(this.createdAt, this.resolvedAt));
3417
4391
  }
3418
4392
  let animation;
@@ -3430,6 +4404,9 @@
3430
4404
  animation = new JSAnimation(options);
3431
4405
  }
3432
4406
  }
4407
+ else if (isGroupedTransform) {
4408
+ animation = new TransformAnimation(options);
4409
+ }
3433
4410
  else {
3434
4411
  animation = new JSAnimation(options);
3435
4412
  }
@@ -3445,6 +4422,12 @@
3445
4422
  then(onResolve, _onReject) {
3446
4423
  return this.finished.finally(onResolve).then(() => { });
3447
4424
  }
4425
+ /**
4426
+ * The value now, if the animation runs on the compositor.
4427
+ */
4428
+ liveValue() {
4429
+ return this._animation?.liveValue?.();
4430
+ }
3448
4431
  get animation() {
3449
4432
  if (!this._animation) {
3450
4433
  this.keyframeResolver?.resume();
@@ -3882,6 +4865,18 @@
3882
4865
  if (collectMotionValues.current) {
3883
4866
  collectMotionValues.current.push(this);
3884
4867
  }
4868
+ /**
4869
+ * A value animating on the compositor isn't updated every frame,
4870
+ * so read it from its animation.
4871
+ */
4872
+ const sampled = this.animation?.liveValue?.();
4873
+ return sampled === undefined ? this.current : sampled;
4874
+ }
4875
+ /**
4876
+ * The value as last set. Unlike get(), this doesn't read an animation
4877
+ * running on the compositor, which renders the value itself.
4878
+ */
4879
+ getCurrent() {
3885
4880
  return this.current;
3886
4881
  }
3887
4882
  /**
@@ -4108,6 +5103,7 @@
4108
5103
  name,
4109
5104
  motionValue: value,
4110
5105
  element: isHandoff ? undefined : element,
5106
+ hasOnUpdate: !!valueTransition.onUpdate,
4111
5107
  };
4112
5108
  /**
4113
5109
  * If there's no transition defined for this value, we can generate
@@ -4707,6 +5703,30 @@
4707
5703
  constructor(unresolvedKeyframes, onComplete, name, motionValue, element) {
4708
5704
  super(unresolvedKeyframes, onComplete, name, motionValue, element, true);
4709
5705
  }
5706
+ /**
5707
+ * Keyframes that don't need reading from the DOM, or measuring,
5708
+ * resolve straight away rather than in the frame loop's batched read.
5709
+ */
5710
+ scheduleResolve() {
5711
+ if (this.canReadNow()) {
5712
+ this.readKeyframes();
5713
+ if (!this.needsMeasurement)
5714
+ return this.complete();
5715
+ }
5716
+ super.scheduleResolve();
5717
+ }
5718
+ canReadNow() {
5719
+ const { unresolvedKeyframes, element, motionValue } = this;
5720
+ const origin = unresolvedKeyframes[0] ?? motionValue?.get();
5721
+ if (!element?.current || origin === undefined)
5722
+ return false;
5723
+ for (let i = 0; i < unresolvedKeyframes.length; i++) {
5724
+ if (containsCSSVariable(i ? unresolvedKeyframes[i] : origin)) {
5725
+ return false;
5726
+ }
5727
+ }
5728
+ return true;
5729
+ }
4710
5730
  readKeyframes() {
4711
5731
  const { unresolvedKeyframes, element, name } = this;
4712
5732
  if (!element || !element.current)
@@ -5050,15 +6070,6 @@
5050
6070
  return isObject(element) && "ownerSVGElement" in element;
5051
6071
  }
5052
6072
 
5053
- /**
5054
- * Provided a value and a ValueType, returns the value as that value type.
5055
- */
5056
- const getValueAsType = (value, type) => {
5057
- return type && typeof value === "number"
5058
- ? type.transform(value)
5059
- : value;
5060
- };
5061
-
5062
6073
  function resolveElements(elementOrSelector, scope, selectorCache) {
5063
6074
  if (elementOrSelector == null) {
5064
6075
  return [];
@@ -5232,18 +6243,12 @@
5232
6243
  });
5233
6244
  }
5234
6245
 
5235
- const translateAlias$1 = {
5236
- x: "translateX",
5237
- y: "translateY",
5238
- z: "translateZ",
5239
- transformPerspective: "perspective",
5240
- };
5241
6246
  /**
5242
6247
  * Cache of `translateX(`-style openers so each render is a few string
5243
6248
  * concatenations rather than a template per transform.
5244
6249
  */
5245
6250
  const openers = {};
5246
- function buildTransform$1(state) {
6251
+ function buildTransform(state) {
5247
6252
  let transform = "";
5248
6253
  const { transformKeys: keys = [], transformValues: values = {} } = state;
5249
6254
  /**
@@ -5252,7 +6257,7 @@
5252
6257
  */
5253
6258
  for (let i = 0; i < keys.length; i++) {
5254
6259
  const key = keys[i];
5255
- const value = values[key].get();
6260
+ const value = values[key].getCurrent();
5256
6261
  if (value === undefined)
5257
6262
  continue;
5258
6263
  const parsed = typeof value === "number" ? value : parseFloat(value);
@@ -5260,7 +6265,7 @@
5260
6265
  transform +=
5261
6266
  (transform && " ") +
5262
6267
  (openers[key] ||
5263
- (openers[key] = (translateAlias$1[key] || key) + "(")) +
6268
+ (openers[key] = (translateAlias[key] || key) + "(")) +
5264
6269
  getValueAsType(value, transformValueTypes[key]) +
5265
6270
  ")";
5266
6271
  }
@@ -5303,7 +6308,8 @@
5303
6308
  addStyleValue(element, state, "transformBox", new MotionValue("fill-box"));
5304
6309
  }
5305
6310
  state.set("transform", new MotionValue("none"), () => {
5306
- element.style.transform = buildTransform$1(state);
6311
+ transformGroups.get(element)?.check();
6312
+ element.style.transform = buildTransform(state);
5307
6313
  });
5308
6314
  }
5309
6315
  computed = state.get("transform");
@@ -5321,12 +6327,12 @@
5321
6327
  }
5322
6328
  else if (isCSSVar(key)) {
5323
6329
  render = () => {
5324
- element.style.setProperty(key, value.get());
6330
+ element.style.setProperty(key, value.getCurrent());
5325
6331
  };
5326
6332
  }
5327
6333
  else {
5328
6334
  render = () => {
5329
- element.style[key] = getValueAsType(value.get(), numberValueTypes[key]);
6335
+ element.style[key] = getValueAsType(value.getCurrent(), numberValueTypes[key]);
5330
6336
  };
5331
6337
  }
5332
6338
  return state.set(key, value, render, computed);
@@ -5358,73 +6364,8 @@
5358
6364
  test: isStyleSubject,
5359
6365
  read: readStyleValue,
5360
6366
  });
5361
- const styleEffect = /*@__PURE__*/ createSelectorEffect(styleSubjectEffect);
5362
-
5363
- const translateAlias = {
5364
- x: "translateX",
5365
- y: "translateY",
5366
- z: "translateZ",
5367
- transformPerspective: "perspective",
5368
- };
5369
- const numTransforms = transformPropOrder.length;
5370
- /**
5371
- * Build a CSS transform style from individual x/y/scale etc properties.
5372
- *
5373
- * This outputs with a default order of transforms/scales/rotations, this can be customised by
5374
- * providing a transformTemplate function.
5375
- */
5376
- function buildTransform(latestValues, transform, transformTemplate) {
5377
- // The transform string we're going to build into.
5378
- let transformString = "";
5379
- let transformIsDefault = true;
5380
- /**
5381
- * Loop over all possible transforms in order, adding the ones that
5382
- * are present to the transform string.
5383
- */
5384
- for (let i = 0; i < numTransforms; i++) {
5385
- const key = transformPropOrder[i];
5386
- const value = latestValues[key];
5387
- if (value === undefined)
5388
- continue;
5389
- let valueIsDefault = true;
5390
- if (typeof value === "number") {
5391
- valueIsDefault = value === (key.startsWith("scale") ? 1 : 0);
5392
- }
5393
- else {
5394
- const parsed = parseFloat(value);
5395
- valueIsDefault = key.startsWith("scale") ? parsed === 1 : parsed === 0;
5396
- }
5397
- if (!valueIsDefault || transformTemplate) {
5398
- const valueAsType = getValueAsType(value, numberValueTypes[key]);
5399
- if (!valueIsDefault) {
5400
- transformIsDefault = false;
5401
- const transformName = translateAlias[key] || key;
5402
- transformString += `${transformName}(${valueAsType}) `;
5403
- }
5404
- if (transformTemplate) {
5405
- transform[key] = valueAsType;
5406
- }
5407
- }
5408
- }
5409
- // `pathRotation` composes onto `rotate` as a separate additive term so
5410
- // the user's `rotate` is never clobbered. Deliberately not a slot in
5411
- // `transformPropOrder`.
5412
- const pathRotation = latestValues.pathRotation;
5413
- if (pathRotation) {
5414
- transformIsDefault = false;
5415
- transformString += `rotate(${getValueAsType(pathRotation, numberValueTypes.pathRotation)}) `;
5416
- }
5417
- transformString = transformString.trim();
5418
- // If we have a custom `transform` template, pass our transform values and
5419
- // generated transformString to that before returning
5420
- if (transformTemplate) {
5421
- transformString = transformTemplate(transform, transformIsDefault ? "" : transformString);
5422
- }
5423
- else if (transformIsDefault) {
5424
- transformString = "none";
5425
- }
5426
- return transformString;
5427
- }
6367
+ const styleEffect =
6368
+ /*@__PURE__*/ createSelectorEffect(styleSubjectEffect);
5428
6369
 
5429
6370
  function buildHTMLStyles(state, latestValues, transformTemplate) {
5430
6371
  const { style, vars, transformOrigin } = state;
@@ -5464,7 +6405,7 @@
5464
6405
  }
5465
6406
  if (!latestValues.transform) {
5466
6407
  if (hasTransform || transformTemplate) {
5467
- style.transform = buildTransform(latestValues, state.transform, transformTemplate);
6408
+ style.transform = buildTransform$1(latestValues, state.transform, transformTemplate);
5468
6409
  }
5469
6410
  else if (style.transform) {
5470
6411
  /**
@@ -8368,8 +9309,11 @@
8368
9309
  const removeOnChange = value.on("change", (latestValue) => {
8369
9310
  this.latestValues[key] = latestValue;
8370
9311
  this.props.onUpdate && frame.preRender(this.notifyUpdate);
8371
- if (valueIsTransform && this.projection) {
8372
- this.projection.isTransformDirty = true;
9312
+ if (valueIsTransform) {
9313
+ if (this.projection) {
9314
+ this.projection.isTransformDirty = true;
9315
+ }
9316
+ transformGroups.get(this.current)?.check();
8373
9317
  }
8374
9318
  this.scheduleRender();
8375
9319
  });
@@ -8453,9 +9397,18 @@
8453
9397
  * added to our map, old ones removed, and listeners updated.
8454
9398
  */
8455
9399
  update(props, presenceContext) {
8456
- if (props.transformTemplate || this.props.transformTemplate) {
9400
+ const { transformTemplate, onUpdate } = this.props;
9401
+ if (props.transformTemplate || transformTemplate) {
8457
9402
  this.scheduleRender();
8458
9403
  }
9404
+ /**
9405
+ * Transforms on the compositor can't use a new transformTemplate
9406
+ * or onUpdate, so move them to the main thread.
9407
+ */
9408
+ if ((props.transformTemplate && !transformTemplate) ||
9409
+ (props.onUpdate && !onUpdate)) {
9410
+ transformGroups.get(this.current)?.demote();
9411
+ }
8459
9412
  this.prevProps = this.props;
8460
9413
  this.props = props;
8461
9414
  this.prevPresenceContext = this.presenceContext;
@@ -9750,6 +10703,7 @@
9750
10703
  }
9751
10704
  }
9752
10705
  }
10706
+ const demoteTransforms = ({ instance }) => instance && transformGroups.get(instance)?.demote();
9753
10707
  function cancelTreeOptimisedTransformAnimations(projectionNode) {
9754
10708
  projectionNode.hasCheckedOptimisedAppear = true;
9755
10709
  if (projectionNode.root === projectionNode)
@@ -10111,10 +11065,17 @@
10111
11065
  !this.root.isUpdating && this.root.startUpdate();
10112
11066
  if (this.isLayoutDirty)
10113
11067
  return;
11068
+ /**
11069
+ * Accelerated transforms aren't reflected in latestValues or
11070
+ * removed by resetTransform, so move them back to the main
11071
+ * thread before this node, or its ancestors, are measured.
11072
+ */
11073
+ demoteTransforms(this);
10114
11074
  this.isLayoutDirty = true;
10115
11075
  for (let i = 0; i < this.path.length; i++) {
10116
11076
  const node = this.path[i];
10117
11077
  node.shouldResetTransform = true;
11078
+ demoteTransforms(node);
10118
11079
  /**
10119
11080
  * Percentage translates resolve against layoutBox dimensions,
10120
11081
  * so ancestors with them must be re-measured after transform reset.
@@ -10317,6 +11278,7 @@
10317
11278
  const isResetRequested = this.isLayoutDirty ||
10318
11279
  this.shouldResetTransform ||
10319
11280
  this.options.alwaysMeasureLayout;
11281
+ isResetRequested && demoteTransforms(this);
10320
11282
  const hasProjection = this.projectionDelta && !isDeltaZero(this.projectionDelta);
10321
11283
  const transformTemplate = this.getTransformTemplate();
10322
11284
  const transformTemplateValue = transformTemplate
@@ -13242,7 +14204,7 @@
13242
14204
  exports.buildProjectionTransform = buildProjectionTransform;
13243
14205
  exports.buildSVGAttrs = buildSVGAttrs;
13244
14206
  exports.buildSVGPath = buildSVGPath;
13245
- exports.buildTransform = buildTransform;
14207
+ exports.buildTransform = buildTransform$1;
13246
14208
  exports.calcAxisDelta = calcAxisDelta;
13247
14209
  exports.calcBoxDelta = calcBoxDelta;
13248
14210
  exports.calcChildStagger = calcChildStagger;
@@ -13444,6 +14406,8 @@
13444
14406
  exports.rgbUnit = rgbUnit;
13445
14407
  exports.rgba = rgba;
13446
14408
  exports.rootProjectionNode = rootProjectionNode;
14409
+ exports.round = round;
14410
+ exports.sampleDelta = sampleDelta;
13447
14411
  exports.scale = scale;
13448
14412
  exports.scaleCorrectors = scaleCorrectors;
13449
14413
  exports.scalePoint = scalePoint;
@@ -13471,6 +14435,7 @@
13471
14435
  exports.supportsPartialKeyframes = supportsPartialKeyframes;
13472
14436
  exports.supportsScrollTimeline = supportsScrollTimeline;
13473
14437
  exports.supportsViewTimeline = supportsViewTimeline;
14438
+ exports.supportsWaapi = supportsWaapi;
13474
14439
  exports.svgEffect = svgEffect;
13475
14440
  exports.svgSubjectEffect = svgSubjectEffect;
13476
14441
  exports.sync = sync;