motion 14.1.1 → 14.3.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.
- package/dist/motion.dev.js +1205 -181
- package/dist/motion.js +1 -1
- package/package.json +3 -3
package/dist/motion.dev.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
@@ -1906,6 +1907,7 @@
|
|
|
1906
1907
|
}
|
|
1907
1908
|
|
|
1908
1909
|
const percentToProgress = (percent) => percent / 100;
|
|
1910
|
+
const round = (value) => Math.round(value * 10000) / 10000;
|
|
1909
1911
|
class JSAnimation extends WithPromise {
|
|
1910
1912
|
constructor(options) {
|
|
1911
1913
|
super();
|
|
@@ -2013,10 +2015,10 @@
|
|
|
2013
2015
|
}
|
|
2014
2016
|
}
|
|
2015
2017
|
tick(timestamp, sample = false) {
|
|
2016
|
-
const { generator, totalDuration,
|
|
2018
|
+
const { generator, totalDuration, calculatedDuration } = this;
|
|
2017
2019
|
if (this.startTime === null)
|
|
2018
2020
|
return generator.next(0);
|
|
2019
|
-
const { delay = 0, keyframes,
|
|
2021
|
+
const { delay = 0, keyframes, type, onUpdate, finalKeyframe, } = this.options;
|
|
2020
2022
|
if (this.startTime === this.pendingStartTime) {
|
|
2021
2023
|
this.startTime = resolveStartTime(this.startTime, timestamp);
|
|
2022
2024
|
}
|
|
@@ -2049,7 +2051,55 @@
|
|
|
2049
2051
|
if (this.state === "finished" && this.holdTime === null) {
|
|
2050
2052
|
this.currentTime = totalDuration;
|
|
2051
2053
|
}
|
|
2052
|
-
|
|
2054
|
+
const state = this.generate(this.currentTime, isInDelayPhase);
|
|
2055
|
+
let { done } = state;
|
|
2056
|
+
if (!isInDelayPhase && calculatedDuration !== null) {
|
|
2057
|
+
done =
|
|
2058
|
+
this.playbackSpeed >= 0
|
|
2059
|
+
? this.currentTime >= totalDuration
|
|
2060
|
+
: this.currentTime <= 0;
|
|
2061
|
+
}
|
|
2062
|
+
const isAnimationFinished = this.holdTime === null &&
|
|
2063
|
+
(this.state === "finished" || (this.state === "running" && done));
|
|
2064
|
+
// TODO: The exception for inertia could be cleaner here
|
|
2065
|
+
if (isAnimationFinished && type !== inertia) {
|
|
2066
|
+
state.value = getFinalKeyframe(keyframes, this.options, finalKeyframe, this.speed);
|
|
2067
|
+
}
|
|
2068
|
+
if (onUpdate) {
|
|
2069
|
+
onUpdate(state.value);
|
|
2070
|
+
}
|
|
2071
|
+
if (isAnimationFinished) {
|
|
2072
|
+
this.finish();
|
|
2073
|
+
}
|
|
2074
|
+
return state;
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* This animation's time, including delay, at a timestamp.
|
|
2078
|
+
*/
|
|
2079
|
+
at(timestamp) {
|
|
2080
|
+
return (this.holdTime ??
|
|
2081
|
+
round((timestamp - this.startTime) * this.playbackSpeed));
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* The value at a timestamp, without changing this animation's state.
|
|
2085
|
+
* If resolved, it ends at the last resolved keyframe, which is in the
|
|
2086
|
+
* same unit as the others, rather than at finalKeyframe.
|
|
2087
|
+
*/
|
|
2088
|
+
sampleAt(timestamp, resolved) {
|
|
2089
|
+
const { options, totalDuration } = this;
|
|
2090
|
+
const t = this.at(timestamp) - (options.delay || 0);
|
|
2091
|
+
return t >= totalDuration && options.type !== inertia
|
|
2092
|
+
? getFinalKeyframe(options.keyframes, options, resolved ? undefined : options.finalKeyframe)
|
|
2093
|
+
: this.generate(Math.max(t, 0), t < 0).value;
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* The generator's state `currentTime` ms after the delay, without
|
|
2097
|
+
* changing the animation's own state.
|
|
2098
|
+
*/
|
|
2099
|
+
generate(currentTime, isInDelayPhase) {
|
|
2100
|
+
const { generator, totalDuration, mixKeyframes, mirroredGenerator, resolvedDuration, } = this;
|
|
2101
|
+
const { keyframes, repeat, repeatType, repeatDelay } = this.options;
|
|
2102
|
+
let elapsed = currentTime;
|
|
2053
2103
|
let frameGenerator = generator;
|
|
2054
2104
|
if (repeat) {
|
|
2055
2105
|
/**
|
|
@@ -2057,7 +2107,7 @@
|
|
|
2057
2107
|
* than duration we'll get values like 2.5 (midway through the
|
|
2058
2108
|
* third iteration)
|
|
2059
2109
|
*/
|
|
2060
|
-
const progress = Math.min(
|
|
2110
|
+
const progress = Math.min(currentTime, totalDuration) / resolvedDuration;
|
|
2061
2111
|
/**
|
|
2062
2112
|
* Get the current iteration (0 indexed). For instance the floor of
|
|
2063
2113
|
* 2.5 is 2.
|
|
@@ -2110,25 +2160,6 @@
|
|
|
2110
2160
|
if (mixKeyframes && !isInDelayPhase) {
|
|
2111
2161
|
state.value = mixKeyframes(state.value);
|
|
2112
2162
|
}
|
|
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
2163
|
return state;
|
|
2133
2164
|
}
|
|
2134
2165
|
/**
|
|
@@ -2469,6 +2500,7 @@
|
|
|
2469
2500
|
translateY: 5,
|
|
2470
2501
|
scaleX: 0,
|
|
2471
2502
|
scaleY: 3,
|
|
2503
|
+
scaleZ: () => 1,
|
|
2472
2504
|
scale: (v) => (Math.abs(v[0]) + Math.abs(v[3])) / 2,
|
|
2473
2505
|
rotate,
|
|
2474
2506
|
rotateZ: rotate,
|
|
@@ -2483,8 +2515,8 @@
|
|
|
2483
2515
|
return angle;
|
|
2484
2516
|
};
|
|
2485
2517
|
const rotateZ = rotate;
|
|
2486
|
-
const scaleX = (v) => Math.
|
|
2487
|
-
const scaleY = (v) => Math.
|
|
2518
|
+
const scaleX = (v) => Math.hypot(v[0], v[1]);
|
|
2519
|
+
const scaleY = (v) => Math.hypot(v[4], v[5]);
|
|
2488
2520
|
const matrix3dParsers = {
|
|
2489
2521
|
x: 12,
|
|
2490
2522
|
y: 13,
|
|
@@ -2494,6 +2526,7 @@
|
|
|
2494
2526
|
translateZ: 14,
|
|
2495
2527
|
scaleX,
|
|
2496
2528
|
scaleY,
|
|
2529
|
+
scaleZ: (v) => Math.hypot(v[8], v[9], v[10]),
|
|
2497
2530
|
scale: (v) => (scaleX(v) + scaleY(v)) / 2,
|
|
2498
2531
|
rotateX: (v) => rebaseAngle(radToDeg(Math.atan2(v[6], v[5]))),
|
|
2499
2532
|
rotateY: (v) => rebaseAngle(radToDeg(Math.atan2(-v[2], v[0]))),
|
|
@@ -2553,6 +2586,7 @@
|
|
|
2553
2586
|
"scale",
|
|
2554
2587
|
"scaleX",
|
|
2555
2588
|
"scaleY",
|
|
2589
|
+
"scaleZ",
|
|
2556
2590
|
"rotate",
|
|
2557
2591
|
"rotateX",
|
|
2558
2592
|
"rotateY",
|
|
@@ -3137,44 +3171,71 @@
|
|
|
3137
3171
|
}
|
|
3138
3172
|
this.options = options;
|
|
3139
3173
|
}
|
|
3174
|
+
/**
|
|
3175
|
+
* The animation's time now, in ms. Wall-clock rather than WAAPI's
|
|
3176
|
+
* currentTime while running, as under CPU load currentTime may not
|
|
3177
|
+
* reflect the actual elapsed time, causing incorrect sampling and
|
|
3178
|
+
* visual jumps.
|
|
3179
|
+
*/
|
|
3180
|
+
elapsed() {
|
|
3181
|
+
return this.state === "running"
|
|
3182
|
+
? (time.now() - this.startTime) * this.speed
|
|
3183
|
+
: this.time * 1000;
|
|
3184
|
+
}
|
|
3185
|
+
/**
|
|
3186
|
+
* The value at a time, from a renderless JS animation.
|
|
3187
|
+
*/
|
|
3188
|
+
sample(sampleTime) {
|
|
3189
|
+
if (!this.sampler) {
|
|
3190
|
+
const { motionValue, onUpdate, onComplete, onPlay, onStop, element, ...options } = this.options;
|
|
3191
|
+
/**
|
|
3192
|
+
* Stop it straight away so its driver doesn't run every frame.
|
|
3193
|
+
* It can still be sampled.
|
|
3194
|
+
*/
|
|
3195
|
+
this.sampler = new JSAnimation({ ...options, autoplay: false });
|
|
3196
|
+
this.sampler.stop();
|
|
3197
|
+
}
|
|
3198
|
+
return this.sampler.sample(Math.max(0, sampleTime)).value;
|
|
3199
|
+
}
|
|
3200
|
+
/**
|
|
3201
|
+
* The value now, as WAAPI doesn't set the motion value while it runs.
|
|
3202
|
+
*/
|
|
3203
|
+
liveValue() {
|
|
3204
|
+
const { state } = this;
|
|
3205
|
+
if (state === "running" || state === "paused") {
|
|
3206
|
+
return this.sample(this.elapsed());
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3140
3209
|
/**
|
|
3141
3210
|
* WAAPI doesn't natively have any interruption capabilities.
|
|
3142
3211
|
*
|
|
3143
3212
|
* Rather than read committed styles back out of the DOM, we can
|
|
3144
|
-
*
|
|
3145
|
-
*
|
|
3146
|
-
*
|
|
3213
|
+
* sample a renderless JS animation twice to calculate its current
|
|
3214
|
+
* value, "previous" value, and therefore allow Motion to calculate
|
|
3215
|
+
* velocity for any subsequent animation.
|
|
3147
3216
|
*/
|
|
3148
3217
|
updateMotionValue(value) {
|
|
3149
|
-
const { motionValue,
|
|
3218
|
+
const { motionValue, element, name } = this.options;
|
|
3150
3219
|
if (!motionValue)
|
|
3151
3220
|
return;
|
|
3152
3221
|
if (value !== undefined) {
|
|
3153
3222
|
motionValue.set(value);
|
|
3154
3223
|
return;
|
|
3155
3224
|
}
|
|
3156
|
-
const
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
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;
|
|
3225
|
+
const { speed } = this;
|
|
3226
|
+
const sampleTime = this.elapsed();
|
|
3227
|
+
const delta = this.state === "running" && speed
|
|
3228
|
+
? clamp(0, sampleDelta, sampleTime)
|
|
3229
|
+
: 0;
|
|
3230
|
+
const current = this.sample(sampleTime);
|
|
3168
3231
|
/**
|
|
3169
3232
|
* Write the estimated value to inline style so it persists
|
|
3170
3233
|
* after cancel(), covering the async gap before the next
|
|
3171
3234
|
* animation starts.
|
|
3172
3235
|
*/
|
|
3173
|
-
const { name } = this.options;
|
|
3174
3236
|
if (element && name)
|
|
3175
3237
|
setStyle(element, name, current);
|
|
3176
|
-
motionValue.setWithVelocity(
|
|
3177
|
-
sampleAnimation.stop();
|
|
3238
|
+
motionValue.setWithVelocity(this.sample(sampleTime - delta * Math.sign(speed)), current, delta / Math.abs(speed || 1));
|
|
3178
3239
|
}
|
|
3179
3240
|
}
|
|
3180
3241
|
|
|
@@ -3288,44 +3349,953 @@
|
|
|
3288
3349
|
function supportsBrowserAnimation(options) {
|
|
3289
3350
|
const { motionValue, name, repeatDelay, repeatType, damping, type, keyframes, } = options;
|
|
3290
3351
|
/**
|
|
3291
|
-
* Most values can't be accelerated at all, so check the name before
|
|
3292
|
-
* looking at the element or its props.
|
|
3352
|
+
* Most values can't be accelerated at all, so check the name before
|
|
3353
|
+
* looking at the element or its props.
|
|
3354
|
+
*/
|
|
3355
|
+
if (!name ||
|
|
3356
|
+
!(acceleratedValues.has(name) || colorProperties.has(name))) {
|
|
3357
|
+
return false;
|
|
3358
|
+
}
|
|
3359
|
+
const subject = motionValue?.owner?.current;
|
|
3360
|
+
/**
|
|
3361
|
+
* We use instanceof checks instead of isHTMLElement()/isSVGElement()
|
|
3362
|
+
* because we explicitly **don't** want elements in different timing
|
|
3363
|
+
* contexts (i.e. popups) to be accelerated, as it's not possible to sync
|
|
3364
|
+
* these animations properly with those driven from the main window
|
|
3365
|
+
* frameloop.
|
|
3366
|
+
*/
|
|
3367
|
+
if (!(subject instanceof HTMLElement) &&
|
|
3368
|
+
!(subject instanceof SVGElement)) {
|
|
3369
|
+
return false;
|
|
3370
|
+
}
|
|
3371
|
+
const { onUpdate, transformTemplate } = motionValue.owner.getProps();
|
|
3372
|
+
return (supportsWaapi() &&
|
|
3373
|
+
/**
|
|
3374
|
+
* Force WAAPI for color properties with browser-only color formats
|
|
3375
|
+
* (oklch, oklab, lab, lch, etc.) that the JS animation path can't parse.
|
|
3376
|
+
*/
|
|
3377
|
+
(acceleratedValues.has(name) ||
|
|
3378
|
+
(colorProperties.has(name) &&
|
|
3379
|
+
hasBrowserOnlyColors(keyframes))) &&
|
|
3380
|
+
(name !== "transform" || !transformTemplate) &&
|
|
3381
|
+
/**
|
|
3382
|
+
* If we're outputting values to onUpdate then we can't use WAAPI as there's
|
|
3383
|
+
* no way to read the value from WAAPI every frame.
|
|
3384
|
+
*/
|
|
3385
|
+
!onUpdate &&
|
|
3386
|
+
!repeatDelay &&
|
|
3387
|
+
repeatType !== "mirror" &&
|
|
3388
|
+
damping !== 0 &&
|
|
3389
|
+
type !== "inertia");
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
/**
|
|
3393
|
+
* Provided a value and a ValueType, returns the value as that value type.
|
|
3394
|
+
*/
|
|
3395
|
+
const getValueAsType = (value, type) => {
|
|
3396
|
+
return type && typeof value === "number"
|
|
3397
|
+
? type.transform(value)
|
|
3398
|
+
: value;
|
|
3399
|
+
};
|
|
3400
|
+
|
|
3401
|
+
const translateAlias = {
|
|
3402
|
+
x: "translateX",
|
|
3403
|
+
y: "translateY",
|
|
3404
|
+
z: "translateZ",
|
|
3405
|
+
transformPerspective: "perspective",
|
|
3406
|
+
};
|
|
3407
|
+
const numTransforms = transformPropOrder.length;
|
|
3408
|
+
/**
|
|
3409
|
+
* Build a CSS transform style from individual x/y/scale etc properties.
|
|
3410
|
+
*
|
|
3411
|
+
* This outputs with a default order of transforms/scales/rotations, this can be customised by
|
|
3412
|
+
* providing a transformTemplate function.
|
|
3413
|
+
*/
|
|
3414
|
+
function buildTransform$1(latestValues, transform, transformTemplate) {
|
|
3415
|
+
// The transform string we're going to build into.
|
|
3416
|
+
let transformString = "";
|
|
3417
|
+
let transformIsDefault = true;
|
|
3418
|
+
/**
|
|
3419
|
+
* Loop over all possible transforms in order, adding the ones that
|
|
3420
|
+
* are present to the transform string.
|
|
3421
|
+
*/
|
|
3422
|
+
for (let i = 0; i < numTransforms; i++) {
|
|
3423
|
+
const key = transformPropOrder[i];
|
|
3424
|
+
const value = latestValues[key];
|
|
3425
|
+
if (value === undefined)
|
|
3426
|
+
continue;
|
|
3427
|
+
let valueIsDefault = true;
|
|
3428
|
+
if (typeof value === "number") {
|
|
3429
|
+
valueIsDefault = value === (key.startsWith("scale") ? 1 : 0);
|
|
3430
|
+
}
|
|
3431
|
+
else {
|
|
3432
|
+
const parsed = parseFloat(value);
|
|
3433
|
+
valueIsDefault = key.startsWith("scale") ? parsed === 1 : parsed === 0;
|
|
3434
|
+
}
|
|
3435
|
+
if (!valueIsDefault || transformTemplate) {
|
|
3436
|
+
const valueAsType = getValueAsType(value, numberValueTypes[key]);
|
|
3437
|
+
if (!valueIsDefault) {
|
|
3438
|
+
transformIsDefault = false;
|
|
3439
|
+
const transformName = translateAlias[key] || key;
|
|
3440
|
+
transformString += `${transformName}(${valueAsType}) `;
|
|
3441
|
+
}
|
|
3442
|
+
if (transformTemplate) {
|
|
3443
|
+
transform[key] = valueAsType;
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
// `pathRotation` composes onto `rotate` as a separate additive term so
|
|
3448
|
+
// the user's `rotate` is never clobbered. Deliberately not a slot in
|
|
3449
|
+
// `transformPropOrder`.
|
|
3450
|
+
const pathRotation = latestValues.pathRotation;
|
|
3451
|
+
if (pathRotation) {
|
|
3452
|
+
transformIsDefault = false;
|
|
3453
|
+
transformString += `rotate(${getValueAsType(pathRotation, numberValueTypes.pathRotation)}) `;
|
|
3454
|
+
}
|
|
3455
|
+
transformString = transformString.trim();
|
|
3456
|
+
// If we have a custom `transform` template, pass our transform values and
|
|
3457
|
+
// generated transformString to that before returning
|
|
3458
|
+
if (transformTemplate) {
|
|
3459
|
+
transformString = transformTemplate(transform, transformIsDefault ? "" : transformString);
|
|
3460
|
+
}
|
|
3461
|
+
else if (transformIsDefault) {
|
|
3462
|
+
transformString = "none";
|
|
3463
|
+
}
|
|
3464
|
+
return transformString;
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
const transformGroups = new WeakMap();
|
|
3468
|
+
|
|
3469
|
+
/**
|
|
3470
|
+
* Sample at the interruption sampling rate, up to a cap for long
|
|
3471
|
+
* animations.
|
|
3472
|
+
*/
|
|
3473
|
+
const maxSamples = 200;
|
|
3474
|
+
/**
|
|
3475
|
+
* How far values may stray from the WAAPI animation: as a fraction of
|
|
3476
|
+
* each value's change when they share an easing, otherwise in px or deg,
|
|
3477
|
+
* or for scale.
|
|
3478
|
+
*/
|
|
3479
|
+
const easingTolerance = 0.001;
|
|
3480
|
+
const sampleTolerance = 0.1;
|
|
3481
|
+
const scaleTolerance = 0.001;
|
|
3482
|
+
/**
|
|
3483
|
+
* If an outside write, or a seek the WAAPI animation can't follow,
|
|
3484
|
+
* happens again within this window, the group is being driven every
|
|
3485
|
+
* frame (scrubbing, drag), which is cheaper on the main thread than
|
|
3486
|
+
* rebuilding the WAAPI animation every frame.
|
|
3487
|
+
*/
|
|
3488
|
+
const churnWindow = 100;
|
|
3489
|
+
/**
|
|
3490
|
+
* Named easings that are the same cubic-bezier in JS and CSS.
|
|
3491
|
+
*/
|
|
3492
|
+
const isNativeEasing = (ease) => /^(linear|ease(In|Out|InOut))$/.test(ease);
|
|
3493
|
+
const byTime = (a, b) => a - b;
|
|
3494
|
+
/**
|
|
3495
|
+
* n + 1 evenly spaced timestamps, from one timestamp to another.
|
|
3496
|
+
*/
|
|
3497
|
+
const spread = (from, to, n) => {
|
|
3498
|
+
const result = [];
|
|
3499
|
+
for (let i = 0; i <= n; i++)
|
|
3500
|
+
result.push(mixNumber$1(from, to, i / n));
|
|
3501
|
+
return result;
|
|
3502
|
+
};
|
|
3503
|
+
const getOwner = (options) => options.motionValue.owner;
|
|
3504
|
+
const getGroup = (element) => transformGroups.get(element);
|
|
3505
|
+
const read = (owner, key) => owner.latestValues ? owner.latestValues[key] : owner.getValue(key)?.get();
|
|
3506
|
+
/**
|
|
3507
|
+
* Whether this animation should join its element's transform group.
|
|
3508
|
+
* Animations that can't be accelerated still join, so the group knows
|
|
3509
|
+
* to stay on the main thread while they run. Browsers without linear()
|
|
3510
|
+
* easing stay on the main thread.
|
|
3511
|
+
*/
|
|
3512
|
+
const canGroupTransform = ({ name, motionValue, }) => name &&
|
|
3513
|
+
transformPropOrder.includes(name) &&
|
|
3514
|
+
motionValue?.owner?.current instanceof HTMLElement &&
|
|
3515
|
+
supportsWaapi() &&
|
|
3516
|
+
supportsLinearEasing();
|
|
3517
|
+
function canAccelerateTransform(options) {
|
|
3518
|
+
const owner = getOwner(options);
|
|
3519
|
+
const { onUpdate, transformTemplate } = owner.getProps();
|
|
3520
|
+
return (
|
|
3521
|
+
/**
|
|
3522
|
+
* A handoff from an optimised appear animation is already running
|
|
3523
|
+
* on its own WAAPI animation. onUpdate needs every value, every
|
|
3524
|
+
* frame.
|
|
3525
|
+
*/
|
|
3526
|
+
!options.isHandoff &&
|
|
3527
|
+
!onUpdate &&
|
|
3528
|
+
!options.hasOnUpdate &&
|
|
3529
|
+
/**
|
|
3530
|
+
* transformTemplate could output anything.
|
|
3531
|
+
*/
|
|
3532
|
+
!transformTemplate &&
|
|
3533
|
+
options.type !== "inertia" &&
|
|
3534
|
+
/**
|
|
3535
|
+
* A literal transform, or pathRotation, replaces or adds to the
|
|
3536
|
+
* transform we'd build. styleEffect builds its own as "none".
|
|
3537
|
+
*/
|
|
3538
|
+
(read(owner, "transform") || "none") === "none" &&
|
|
3539
|
+
!read(owner, "pathRotation") &&
|
|
3540
|
+
/**
|
|
3541
|
+
* Layout animations write transform every frame.
|
|
3542
|
+
*/
|
|
3543
|
+
!owner.projection?.isProjecting());
|
|
3544
|
+
}
|
|
3545
|
+
class TransformAnimation extends JSAnimation {
|
|
3546
|
+
constructor(options) {
|
|
3547
|
+
const element = getOwner(options).current;
|
|
3548
|
+
const { driver = frameloopDriver } = options;
|
|
3549
|
+
/**
|
|
3550
|
+
* Before JSAnimation replaces the type with its generator.
|
|
3551
|
+
*/
|
|
3552
|
+
const canAccelerate = canAccelerateTransform(options);
|
|
3553
|
+
/**
|
|
3554
|
+
* The driver ticks on the main thread only once the group has
|
|
3555
|
+
* moved there. Until then, starting or stopping tells the group to
|
|
3556
|
+
* update its WAAPI animation.
|
|
3557
|
+
*/
|
|
3558
|
+
options.driver = (update) => {
|
|
3559
|
+
const js = driver(update);
|
|
3560
|
+
return {
|
|
3561
|
+
js,
|
|
3562
|
+
now: js.now,
|
|
3563
|
+
start: (keepAlive = true) => {
|
|
3564
|
+
const group = getGroup(element);
|
|
3565
|
+
const isJS = !group || group.isJS;
|
|
3566
|
+
/**
|
|
3567
|
+
* keepAlive is false when seeking. Seeking the WAAPI
|
|
3568
|
+
* animation is free, but if a seek needs a rebuild and
|
|
3569
|
+
* keeps happening, the group moves to the main thread.
|
|
3570
|
+
*/
|
|
3571
|
+
isJS
|
|
3572
|
+
? js.start(keepAlive)
|
|
3573
|
+
: group.schedule(false, !keepAlive);
|
|
3574
|
+
},
|
|
3575
|
+
stop: () => {
|
|
3576
|
+
js.stop();
|
|
3577
|
+
const group = getGroup(element);
|
|
3578
|
+
group?.schedule(!group.isFinishing);
|
|
3579
|
+
},
|
|
3580
|
+
};
|
|
3581
|
+
};
|
|
3582
|
+
super(options);
|
|
3583
|
+
this.canAccelerate = canAccelerate;
|
|
3584
|
+
/**
|
|
3585
|
+
* stop is bound in JSAnimation, so wrap it rather than override.
|
|
3586
|
+
* Before stopping, set the motion value to where the WAAPI
|
|
3587
|
+
* animation is now, with velocity, for the next animation to
|
|
3588
|
+
* start from.
|
|
3589
|
+
*/
|
|
3590
|
+
const { stop } = this;
|
|
3591
|
+
this.stop = () => {
|
|
3592
|
+
this.isAccelerated() && this.sync();
|
|
3593
|
+
stop();
|
|
3594
|
+
};
|
|
3595
|
+
}
|
|
3596
|
+
get group() {
|
|
3597
|
+
return getGroup(getOwner(this.options).current);
|
|
3598
|
+
}
|
|
3599
|
+
get js() {
|
|
3600
|
+
return this.driver?.js;
|
|
3601
|
+
}
|
|
3602
|
+
get name() {
|
|
3603
|
+
return this.options.name;
|
|
3604
|
+
}
|
|
3605
|
+
/**
|
|
3606
|
+
* When this stops moving, or, if it repeats forever, when it starts
|
|
3607
|
+
* repeating, and how long each repeat takes. Held (paused, or at
|
|
3608
|
+
* speed 0) animations never stop moving.
|
|
3609
|
+
*/
|
|
3610
|
+
timing() {
|
|
3611
|
+
const { startTime, speed, totalDuration } = this;
|
|
3612
|
+
const { delay = 0, repeatType } = this.options;
|
|
3613
|
+
return this.holdTime !== null
|
|
3614
|
+
? [Infinity, 0]
|
|
3615
|
+
: totalDuration === Infinity
|
|
3616
|
+
? [
|
|
3617
|
+
startTime + delay / speed,
|
|
3618
|
+
(this.resolvedDuration * (repeatType === "loop" ? 1 : 2)) /
|
|
3619
|
+
speed || 1,
|
|
3620
|
+
]
|
|
3621
|
+
: [startTime + (delay + totalDuration) / speed, 0];
|
|
3622
|
+
}
|
|
3623
|
+
/**
|
|
3624
|
+
* Whether this is running, or paused, on the compositor.
|
|
3625
|
+
*/
|
|
3626
|
+
isAccelerated() {
|
|
3627
|
+
const { group, state } = this;
|
|
3628
|
+
return (group?.tracks.has(this) &&
|
|
3629
|
+
!group.isJS &&
|
|
3630
|
+
this.canAccelerate &&
|
|
3631
|
+
(state === "running" || state === "paused"));
|
|
3632
|
+
}
|
|
3633
|
+
/**
|
|
3634
|
+
* The value now, while this runs on the compositor and so doesn't
|
|
3635
|
+
* set its motion value every frame.
|
|
3636
|
+
*/
|
|
3637
|
+
liveValue() {
|
|
3638
|
+
return this.isAccelerated() ? this.sampleAt(time.now()) : undefined;
|
|
3639
|
+
}
|
|
3640
|
+
/**
|
|
3641
|
+
* If this is a two-keyframe tween whose easing WAAPI runs exactly,
|
|
3642
|
+
* when it starts and stops moving, and that easing.
|
|
3643
|
+
*/
|
|
3644
|
+
tween() {
|
|
3645
|
+
const { keyframes: keyframes$1, type, ease, times, repeat, delay = 0 } = this.options;
|
|
3646
|
+
const easing = isBezierDefinition(ease)
|
|
3647
|
+
? cubicBezierAsString(ease)
|
|
3648
|
+
: isNativeEasing(ease) && supportedWaapiEasing[ease];
|
|
3649
|
+
if (easing &&
|
|
3650
|
+
!repeat &&
|
|
3651
|
+
!times &&
|
|
3652
|
+
keyframes$1.length === 2 &&
|
|
3653
|
+
type === keyframes &&
|
|
3654
|
+
this.holdTime === null) {
|
|
3655
|
+
const start = this.startTime + delay / this.speed;
|
|
3656
|
+
return [start, start + this.resolvedDuration / this.speed, easing];
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
/**
|
|
3660
|
+
* The timestamps between two timestamps where this reaches a
|
|
3661
|
+
* keyframe or starts a repeat, where it can turn or jump.
|
|
3662
|
+
*/
|
|
3663
|
+
boundaries(from, to) {
|
|
3664
|
+
const { startTime, speed, resolvedDuration, options } = this;
|
|
3665
|
+
const { delay = 0, repeat = 0, repeatDelay = 0, repeatType, times, keyframes: keyframes$1, type, } = options;
|
|
3666
|
+
const offsets = (type || keyframes) === keyframes
|
|
3667
|
+
? times || defaultOffset$1(keyframes$1)
|
|
3668
|
+
: [0, 1];
|
|
3669
|
+
const start = startTime + delay / speed;
|
|
3670
|
+
const result = [];
|
|
3671
|
+
if (this.holdTime !== null || !(resolvedDuration > 0))
|
|
3672
|
+
return result;
|
|
3673
|
+
for (let i = Math.max(0, Math.floor(((from - start) * speed) / resolvedDuration)); i <= repeat; i++) {
|
|
3674
|
+
const iterationStart = start + (i * resolvedDuration) / speed;
|
|
3675
|
+
if (iterationStart > to)
|
|
3676
|
+
break;
|
|
3677
|
+
for (const offset of offsets) {
|
|
3678
|
+
const t = iterationStart +
|
|
3679
|
+
((i % 2 && repeatType && repeatType !== "loop"
|
|
3680
|
+
? 1 - offset
|
|
3681
|
+
: offset) *
|
|
3682
|
+
(resolvedDuration - repeatDelay)) /
|
|
3683
|
+
speed;
|
|
3684
|
+
t > from && t < to && result.push(t);
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
return result;
|
|
3688
|
+
}
|
|
3689
|
+
/**
|
|
3690
|
+
* The value at a timestamp, as a number.
|
|
3691
|
+
*/
|
|
3692
|
+
numberAt(timestamp) {
|
|
3693
|
+
return parseFloat(this.sampleAt(timestamp, true));
|
|
3694
|
+
}
|
|
3695
|
+
/**
|
|
3696
|
+
* Write the current value, and velocity, to the motion value.
|
|
3697
|
+
*/
|
|
3698
|
+
sync() {
|
|
3699
|
+
const now = time.now();
|
|
3700
|
+
this.options.motionValue.setWithVelocity(this.sampleAt(now - sampleDelta, true), this.sampleAt(now, true), sampleDelta);
|
|
3701
|
+
}
|
|
3702
|
+
play() {
|
|
3703
|
+
/**
|
|
3704
|
+
* A stopped animation (one that was interrupted) can't play again,
|
|
3705
|
+
* so it mustn't join the group.
|
|
3706
|
+
*/
|
|
3707
|
+
if (this.isStopped)
|
|
3708
|
+
return;
|
|
3709
|
+
(this.group || createGroup(getOwner(this.options))).tracks.add(this);
|
|
3710
|
+
super.play();
|
|
3711
|
+
}
|
|
3712
|
+
pause() {
|
|
3713
|
+
super.pause();
|
|
3714
|
+
if (this.isAccelerated()) {
|
|
3715
|
+
this.sync();
|
|
3716
|
+
this.group.schedule(false);
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
/**
|
|
3720
|
+
* JSAnimation finishes on its next tick, which only happens on the
|
|
3721
|
+
* main thread, so finish now.
|
|
3722
|
+
*/
|
|
3723
|
+
complete() {
|
|
3724
|
+
super.complete();
|
|
3725
|
+
const { group } = this;
|
|
3726
|
+
group && !group.isJS && this.canAccelerate && this.tick(time.now());
|
|
3727
|
+
}
|
|
3728
|
+
/**
|
|
3729
|
+
* Without a tick to rebase it, a held time still includes the delay,
|
|
3730
|
+
* so take it off here, as a JS animation does on its next tick.
|
|
3731
|
+
*/
|
|
3732
|
+
get time() {
|
|
3733
|
+
if (this.isAccelerated()) {
|
|
3734
|
+
return millisecondsToSeconds(clamp(0, this.totalDuration, this.at(time.now()) - (this.options.delay || 0)));
|
|
3735
|
+
}
|
|
3736
|
+
return super.time;
|
|
3737
|
+
}
|
|
3738
|
+
set time(newTime) {
|
|
3739
|
+
super.time = newTime;
|
|
3740
|
+
}
|
|
3741
|
+
/**
|
|
3742
|
+
* Scroll timelines set time every frame, so run these on the main
|
|
3743
|
+
* thread.
|
|
3744
|
+
*/
|
|
3745
|
+
attachTimeline(timeline) {
|
|
3746
|
+
this.canAccelerate = false;
|
|
3747
|
+
this.group?.demote(true);
|
|
3748
|
+
return super.attachTimeline(timeline);
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
function createGroup(owner) {
|
|
3752
|
+
const element = owner.current;
|
|
3753
|
+
const tracks = new Set();
|
|
3754
|
+
const byKey = new Map();
|
|
3755
|
+
/**
|
|
3756
|
+
* Transforms that aren't animating, as built into the WAAPI animation.
|
|
3757
|
+
*/
|
|
3758
|
+
let statics = {};
|
|
3759
|
+
/**
|
|
3760
|
+
* The transforms in the WAAPI animation, in order.
|
|
3761
|
+
*/
|
|
3762
|
+
let keys = [];
|
|
3763
|
+
let built = new Map();
|
|
3764
|
+
let segments = [];
|
|
3765
|
+
let needsBuild = false;
|
|
3766
|
+
let isScheduled = false;
|
|
3767
|
+
let hasChurned = false;
|
|
3768
|
+
let lastChurn = -Infinity;
|
|
3769
|
+
const has = (key) => {
|
|
3770
|
+
for (const track of tracks)
|
|
3771
|
+
if (track.name === key)
|
|
3772
|
+
return true;
|
|
3773
|
+
};
|
|
3774
|
+
/**
|
|
3775
|
+
* The transform values at a timestamp or, without one, from the
|
|
3776
|
+
* motion values. Animations that don't repeat forever are sampled no
|
|
3777
|
+
* earlier than settled, so a loop that starts from an earlier cycle
|
|
3778
|
+
* boundary holds them at their end.
|
|
3779
|
+
*/
|
|
3780
|
+
const valuesAt = (timestamp, settled) => {
|
|
3781
|
+
const values = {};
|
|
3782
|
+
for (const key of timestamp === undefined ? transformPropOrder : keys) {
|
|
3783
|
+
const track = byKey.get(key);
|
|
3784
|
+
const value = timestamp === undefined
|
|
3785
|
+
? read(owner, key)
|
|
3786
|
+
: track
|
|
3787
|
+
? track.sampleAt(settled > timestamp && !track.timing()[1]
|
|
3788
|
+
? settled
|
|
3789
|
+
: timestamp)
|
|
3790
|
+
: statics[key];
|
|
3791
|
+
if (value !== undefined)
|
|
3792
|
+
values[key] = value;
|
|
3793
|
+
}
|
|
3794
|
+
return values;
|
|
3795
|
+
};
|
|
3796
|
+
/**
|
|
3797
|
+
* Build the transform at a timestamp. Unlike buildTransform, every key
|
|
3798
|
+
* is written, even at its default, so each keyframe has the same list
|
|
3799
|
+
* of functions and the browser interpolates them one by one.
|
|
3800
|
+
*/
|
|
3801
|
+
const compose = (timestamp, settled) => {
|
|
3802
|
+
let transform = "";
|
|
3803
|
+
const values = valuesAt(timestamp, settled);
|
|
3804
|
+
for (const key in values) {
|
|
3805
|
+
transform +=
|
|
3806
|
+
(translateAlias[key] || key) +
|
|
3807
|
+
"(" +
|
|
3808
|
+
getValueAsType(values[key], numberValueTypes[key]) +
|
|
3809
|
+
") ";
|
|
3810
|
+
}
|
|
3811
|
+
return transform || "none";
|
|
3812
|
+
};
|
|
3813
|
+
/**
|
|
3814
|
+
* Write the transform as an inline style, as the renderer would.
|
|
3815
|
+
*/
|
|
3816
|
+
const commit = (values = valuesAt()) => {
|
|
3817
|
+
element.style.transform = buildTransform$1(values, {});
|
|
3818
|
+
};
|
|
3819
|
+
/**
|
|
3820
|
+
* Cancel the WAAPI animations, by default first committing the
|
|
3821
|
+
* transform. The renderer writes its own version on its next render.
|
|
3822
|
+
*/
|
|
3823
|
+
const stop = (shouldCommit = true) => {
|
|
3824
|
+
shouldCommit && commit();
|
|
3825
|
+
segments.forEach(({ animation }) => animation.cancel());
|
|
3826
|
+
segments = [];
|
|
3827
|
+
};
|
|
3828
|
+
const add = (animation, offset, end = Infinity) => {
|
|
3829
|
+
const segment = { animation, offset, end };
|
|
3830
|
+
animation.startTime = time.now() + offset;
|
|
3831
|
+
/**
|
|
3832
|
+
* Animations finish when the WAAPI animation reaches their end.
|
|
3833
|
+
* Once a loop has taken over, the finished animation isn't needed.
|
|
3834
|
+
*/
|
|
3835
|
+
if (end < Infinity) {
|
|
3836
|
+
animation.onfinish = () => {
|
|
3837
|
+
segment.isDone = true;
|
|
3838
|
+
if (segments.some((s) => s.end === Infinity)) {
|
|
3839
|
+
animation.cancel();
|
|
3840
|
+
segments = segments.filter((s) => s !== segment);
|
|
3841
|
+
}
|
|
3842
|
+
group.schedule(false);
|
|
3843
|
+
};
|
|
3844
|
+
}
|
|
3845
|
+
/**
|
|
3846
|
+
* Something else cancelled it, so carry on on the main thread.
|
|
3847
|
+
*/
|
|
3848
|
+
animation.oncancel = () => segments.includes(segment) && group.demote();
|
|
3849
|
+
segments.push(segment);
|
|
3850
|
+
};
|
|
3851
|
+
/**
|
|
3852
|
+
* Where a value jumps between two timestamps, as when a repeat starts
|
|
3853
|
+
* again: the timestamps either side of the jump, or none if it moves
|
|
3854
|
+
* there continuously.
|
|
3855
|
+
*/
|
|
3856
|
+
const jumpAt = (value, a, b) => {
|
|
3857
|
+
const change = (x, y) => Math.abs(value(y) - value(x));
|
|
3858
|
+
const size = change(a, b);
|
|
3859
|
+
while (b - a > 0.01) {
|
|
3860
|
+
const mid = (a + b) / 2;
|
|
3861
|
+
change(a, mid) > change(mid, b) ? (b = mid) : (a = mid);
|
|
3862
|
+
}
|
|
3863
|
+
return change(a, b) > size / 2 ? [a, b] : [];
|
|
3864
|
+
};
|
|
3865
|
+
/**
|
|
3866
|
+
* If every moving animation follows the same eased progress between
|
|
3867
|
+
* two timestamps, the easing that describes it.
|
|
3868
|
+
*/
|
|
3869
|
+
const getEasing = (moving, from, to, bounds) => {
|
|
3870
|
+
const samples = Math.min(maxSamples, Math.max(2, Math.ceil((to - from) / sampleDelta)));
|
|
3871
|
+
/**
|
|
3872
|
+
* Too many keyframes or repeats to follow with this many samples.
|
|
3873
|
+
*/
|
|
3874
|
+
if (bounds.length * 4 > samples)
|
|
3875
|
+
return;
|
|
3876
|
+
const at = spread(from, to, samples).concat(bounds).sort(byTime);
|
|
3877
|
+
const position = (t) => progress(from, to, t);
|
|
3878
|
+
let shared;
|
|
3879
|
+
let progressAt;
|
|
3880
|
+
for (const track of moving) {
|
|
3881
|
+
const values = at.map((t) => track.numberAt(t));
|
|
3882
|
+
const [first] = values;
|
|
3883
|
+
const range = values[values.length - 1] - first;
|
|
3884
|
+
const progress = values.map((value) => range ? (value - first) / range : value - first);
|
|
3885
|
+
/**
|
|
3886
|
+
* A value that doesn't change, like one still in its delay,
|
|
3887
|
+
* fits any easing. One that changes but ends where it started
|
|
3888
|
+
* fits none.
|
|
3889
|
+
*/
|
|
3890
|
+
if (!range) {
|
|
3891
|
+
if (progress.some((p) => !(Math.abs(p) <= easingTolerance))) {
|
|
3892
|
+
return;
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
else if (!shared) {
|
|
3896
|
+
shared = progress;
|
|
3897
|
+
progressAt = (t) => (track.numberAt(t) - first) / range;
|
|
3898
|
+
}
|
|
3899
|
+
else if (progress.some((p, i) => !(Math.abs(p - shared[i]) <= easingTolerance))) {
|
|
3900
|
+
return;
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
if (!shared ||
|
|
3904
|
+
shared.every((p, i) => Math.abs(p - position(at[i])) <= easingTolerance)) {
|
|
3905
|
+
return "linear";
|
|
3906
|
+
}
|
|
3907
|
+
/**
|
|
3908
|
+
* A repeat that starts again jumps back, which takes two points at
|
|
3909
|
+
* the same position.
|
|
3910
|
+
*/
|
|
3911
|
+
const points = [];
|
|
3912
|
+
const point = (t, p = progressAt(t)) => points.push(round(p) + " " + round(position(t) * 100) + "%");
|
|
3913
|
+
shared.forEach((p, i) => {
|
|
3914
|
+
i &&
|
|
3915
|
+
Math.abs(p - shared[i - 1]) > 0.2 &&
|
|
3916
|
+
jumpAt(progressAt, at[i - 1], at[i]).forEach((t) => point(t));
|
|
3917
|
+
point(at[i], p);
|
|
3918
|
+
});
|
|
3919
|
+
return "linear(" + points + ")";
|
|
3920
|
+
};
|
|
3921
|
+
/**
|
|
3922
|
+
* Sample timestamps between two timestamps, closer together where the
|
|
3923
|
+
* moving animations curve.
|
|
3924
|
+
*/
|
|
3925
|
+
const sample = (moving, from, to, bounds) => {
|
|
3926
|
+
const times = [from];
|
|
3927
|
+
/**
|
|
3928
|
+
* Values that aren't numbers, like calc(), are left to the
|
|
3929
|
+
* browser to interpolate.
|
|
3930
|
+
*/
|
|
3931
|
+
const isStraight = (a, b) => moving.every((track) => !(Math.abs(track.numberAt((a + b) / 2) -
|
|
3932
|
+
(track.numberAt(a) + track.numberAt(b)) / 2) >
|
|
3933
|
+
(track.name.startsWith("scale")
|
|
3934
|
+
? scaleTolerance
|
|
3935
|
+
: sampleTolerance)));
|
|
3936
|
+
const split = (a, b, depth) => {
|
|
3937
|
+
if (b - a < 1 || isStraight(a, b)) {
|
|
3938
|
+
times.push(b);
|
|
3939
|
+
}
|
|
3940
|
+
else if (depth < 4) {
|
|
3941
|
+
split(a, (a + b) / 2, depth + 1);
|
|
3942
|
+
split((a + b) / 2, b, depth + 1);
|
|
3943
|
+
}
|
|
3944
|
+
else {
|
|
3945
|
+
/**
|
|
3946
|
+
* Keyframes either side of a jump, like a repeat starting
|
|
3947
|
+
* again, so it doesn't play as a quick sweep.
|
|
3948
|
+
*/
|
|
3949
|
+
for (const track of moving) {
|
|
3950
|
+
times.push(...jumpAt((t) => track.numberAt(t), a, b));
|
|
3951
|
+
}
|
|
3952
|
+
times.push(b);
|
|
3953
|
+
}
|
|
3954
|
+
};
|
|
3955
|
+
/**
|
|
3956
|
+
* Split at least every 100ms, and at every keyframe and repeat,
|
|
3957
|
+
* either side of where a value can jump.
|
|
3958
|
+
*/
|
|
3959
|
+
const ends = spread(from, to, Math.ceil((to - from) / 100));
|
|
3960
|
+
for (const t of bounds)
|
|
3961
|
+
ends.push(t, Math.min(t + 0.01, to));
|
|
3962
|
+
let start = from;
|
|
3963
|
+
for (const end of ends.sort(byTime)) {
|
|
3964
|
+
end > start && split(start, end, 0);
|
|
3965
|
+
start = end;
|
|
3966
|
+
}
|
|
3967
|
+
return times.sort(byTime);
|
|
3968
|
+
};
|
|
3969
|
+
/**
|
|
3970
|
+
* If every moving animation is the same tween, started together, as
|
|
3971
|
+
* happens for the values of one animate() call or transition, when
|
|
3972
|
+
* they move and their native easing.
|
|
3973
|
+
*/
|
|
3974
|
+
const getTween = (moving) => {
|
|
3975
|
+
let shared;
|
|
3976
|
+
for (const track of moving) {
|
|
3977
|
+
const tween = track.tween();
|
|
3978
|
+
if (!tween ||
|
|
3979
|
+
shared?.some((v, i) => i < 2
|
|
3980
|
+
? Math.abs(tween[i] - v) > 1
|
|
3981
|
+
: tween[i] !== v)) {
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3984
|
+
shared = tween;
|
|
3985
|
+
}
|
|
3986
|
+
return shared;
|
|
3987
|
+
};
|
|
3988
|
+
/**
|
|
3989
|
+
* A WAAPI animation of the composed transform between two timestamps.
|
|
3293
3990
|
*/
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3991
|
+
const run = (moving, from, to, options = { fill: "both" }, settled, nativeEasing) => {
|
|
3992
|
+
const bounds = moving.flatMap((track) => track.boundaries(from, to));
|
|
3993
|
+
const easing = nativeEasing || getEasing(moving, from, to, bounds);
|
|
3994
|
+
const times = easing ? [from, to] : sample(moving, from, to, bounds);
|
|
3995
|
+
const transform = times.map((t) => compose(t, settled));
|
|
3996
|
+
let keyframes = { transform };
|
|
3997
|
+
if (!easing) {
|
|
3998
|
+
keyframes.offset = times.map((t) => progress(from, to, t));
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* If nothing moves, don't hold a transform, which, even at its
|
|
4002
|
+
* default, would make the element a containing block. The empty
|
|
4003
|
+
* animation still finishes the animations.
|
|
4004
|
+
*/
|
|
4005
|
+
if (transform.every((t) => t === transform[0])) {
|
|
4006
|
+
commit(valuesAt(from, settled));
|
|
4007
|
+
keyframes = null;
|
|
4008
|
+
}
|
|
4009
|
+
return element.animate(keyframes, {
|
|
4010
|
+
...options,
|
|
4011
|
+
duration: to - from,
|
|
4012
|
+
easing: easing || "linear",
|
|
4013
|
+
});
|
|
4014
|
+
};
|
|
3299
4015
|
/**
|
|
3300
|
-
*
|
|
3301
|
-
*
|
|
3302
|
-
* contexts (i.e. popups) to be accelerated, as it's not possible to sync
|
|
3303
|
-
* these animations properly with those driven from the main window
|
|
3304
|
-
* frameloop.
|
|
4016
|
+
* Replace the WAAPI animations with ones built from now. Returns
|
|
4017
|
+
* undefined if the animations can't be composed.
|
|
3305
4018
|
*/
|
|
3306
|
-
|
|
3307
|
-
!(subject instanceof SVGElement)) {
|
|
3308
|
-
return false;
|
|
3309
|
-
}
|
|
3310
|
-
const { onUpdate, transformTemplate } = motionValue.owner.getProps();
|
|
3311
|
-
return (supportsWaapi() &&
|
|
4019
|
+
const animate = (now) => {
|
|
3312
4020
|
/**
|
|
3313
|
-
*
|
|
3314
|
-
*
|
|
4021
|
+
* The finite part runs until the last finite animation (or the
|
|
4022
|
+
* last delay) ends. Infinitely repeating animations then continue
|
|
4023
|
+
* as one looping WAAPI animation, which needs them to share a
|
|
4024
|
+
* cycle.
|
|
3315
4025
|
*/
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
4026
|
+
let end = now;
|
|
4027
|
+
let cycle = 0;
|
|
4028
|
+
let cycleStart = 0;
|
|
4029
|
+
const moving = [];
|
|
4030
|
+
const looping = [];
|
|
4031
|
+
const ends = new Set();
|
|
4032
|
+
byKey.clear();
|
|
4033
|
+
built = new Map();
|
|
4034
|
+
for (const track of tracks) {
|
|
4035
|
+
byKey.set(track.name, track);
|
|
4036
|
+
const [trackEnd, trackCycle] = track.timing();
|
|
4037
|
+
const isHeld = trackEnd === Infinity;
|
|
4038
|
+
built.set(track, [track.at(now), track.speed, isHeld]);
|
|
4039
|
+
/**
|
|
4040
|
+
* Held animations don't move, so keep their motion values up
|
|
4041
|
+
* to date, as a JSAnimation would on a seek.
|
|
4042
|
+
*/
|
|
4043
|
+
if (isHeld) {
|
|
4044
|
+
track.sync();
|
|
4045
|
+
continue;
|
|
4046
|
+
}
|
|
4047
|
+
if (track.speed < 0)
|
|
4048
|
+
return;
|
|
4049
|
+
if (trackCycle) {
|
|
4050
|
+
if (cycle) {
|
|
4051
|
+
const phase = (trackEnd - cycleStart) / trackCycle;
|
|
4052
|
+
if (Math.abs(trackCycle - cycle) > 1 ||
|
|
4053
|
+
Math.abs(phase - Math.round(phase)) * cycle > 1) {
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
else {
|
|
4058
|
+
cycle = trackCycle;
|
|
4059
|
+
cycleStart = trackEnd;
|
|
4060
|
+
}
|
|
4061
|
+
looping.push(track);
|
|
4062
|
+
}
|
|
4063
|
+
else {
|
|
4064
|
+
ends.add(trackEnd);
|
|
4065
|
+
}
|
|
4066
|
+
moving.push(track);
|
|
4067
|
+
end = Math.max(end, trackEnd);
|
|
4068
|
+
}
|
|
4069
|
+
statics = {};
|
|
3320
4070
|
/**
|
|
3321
|
-
*
|
|
3322
|
-
* no way to read the value from WAAPI every frame.
|
|
4071
|
+
* Transforms that aren't animating are left out at their default.
|
|
3323
4072
|
*/
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
4073
|
+
keys = transformPropOrder.filter((key) => {
|
|
4074
|
+
if (byKey.has(key))
|
|
4075
|
+
return true;
|
|
4076
|
+
const value = (statics[key] = read(owner, key));
|
|
4077
|
+
return (value !== undefined &&
|
|
4078
|
+
parseFloat(value) !==
|
|
4079
|
+
(key.startsWith("scale") ? 1 : 0));
|
|
4080
|
+
});
|
|
4081
|
+
stop(false);
|
|
4082
|
+
/**
|
|
4083
|
+
* Values that share one tween are one WAAPI animation of it, with
|
|
4084
|
+
* two keyframes and its own easing, from when they started.
|
|
4085
|
+
*/
|
|
4086
|
+
const tween = !cycle && getTween(moving);
|
|
4087
|
+
try {
|
|
4088
|
+
if (tween && tween[1] > now) {
|
|
4089
|
+
const [from, to, easing] = tween;
|
|
4090
|
+
add(run(moving, from, to, undefined, undefined, easing), from - now, to - from);
|
|
4091
|
+
}
|
|
4092
|
+
else if (end > now) {
|
|
4093
|
+
add(run(moving, now, end), 0, end - now);
|
|
4094
|
+
/**
|
|
4095
|
+
* Animations that end sooner finish on the events of
|
|
4096
|
+
* empty animations that end with them.
|
|
4097
|
+
*/
|
|
4098
|
+
ends.forEach((trackEnd) => trackEnd < end - 1 &&
|
|
4099
|
+
add(element.animate(null, {
|
|
4100
|
+
duration: trackEnd - now,
|
|
4101
|
+
}), 0, trackEnd - now));
|
|
4102
|
+
}
|
|
4103
|
+
if (cycle) {
|
|
4104
|
+
/**
|
|
4105
|
+
* Build one cycle from a cycle boundary, so it repeats
|
|
4106
|
+
* seamlessly, and start it part way through.
|
|
4107
|
+
*/
|
|
4108
|
+
const from = cycleStart + Math.floor((end - cycleStart) / cycle) * cycle;
|
|
4109
|
+
add(run(looping, from, from + cycle, {
|
|
4110
|
+
iterations: Infinity,
|
|
4111
|
+
iterationStart: (end - from) / cycle,
|
|
4112
|
+
/**
|
|
4113
|
+
* It must not fill backwards over the finite part
|
|
4114
|
+
* that runs before it.
|
|
4115
|
+
*/
|
|
4116
|
+
fill: "forwards",
|
|
4117
|
+
}, end), end - now);
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
catch {
|
|
4121
|
+
return;
|
|
4122
|
+
}
|
|
4123
|
+
/**
|
|
4124
|
+
* Everything is held, so hold it with an inline style instead.
|
|
4125
|
+
*/
|
|
4126
|
+
segments.length || stop();
|
|
4127
|
+
return true;
|
|
4128
|
+
};
|
|
4129
|
+
/**
|
|
4130
|
+
* If every animation has been paused, played, seeked or sped up
|
|
4131
|
+
* together since the WAAPI animation was built, do the same to it.
|
|
4132
|
+
* Returns false if it needs rebuilding instead.
|
|
4133
|
+
*/
|
|
4134
|
+
const retime = (now) => {
|
|
4135
|
+
let elapsed;
|
|
4136
|
+
let rate = 0;
|
|
4137
|
+
for (const track of tracks) {
|
|
4138
|
+
const state = built.get(track);
|
|
4139
|
+
if (!state)
|
|
4140
|
+
return false;
|
|
4141
|
+
const [builtTime, builtSpeed, wasHeld] = state;
|
|
4142
|
+
const trackTime = track.at(now);
|
|
4143
|
+
const isHeld = track.timing()[0] === Infinity;
|
|
4144
|
+
if (wasHeld) {
|
|
4145
|
+
if (isHeld && trackTime === builtTime)
|
|
4146
|
+
continue;
|
|
4147
|
+
return false;
|
|
4148
|
+
}
|
|
4149
|
+
const trackElapsed = (trackTime - builtTime) / builtSpeed;
|
|
4150
|
+
const trackRate = isHeld ? 0 : track.speed / builtSpeed;
|
|
4151
|
+
if (elapsed === undefined) {
|
|
4152
|
+
elapsed = trackElapsed;
|
|
4153
|
+
rate = trackRate;
|
|
4154
|
+
}
|
|
4155
|
+
else if (Math.abs(trackElapsed - elapsed) > 1 ||
|
|
4156
|
+
trackRate !== rate) {
|
|
4157
|
+
return false;
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
if (elapsed === undefined)
|
|
4161
|
+
return true;
|
|
4162
|
+
if (!segments.length)
|
|
4163
|
+
return false;
|
|
4164
|
+
for (const { offset, end, isDone } of segments) {
|
|
4165
|
+
const local = elapsed - offset;
|
|
4166
|
+
if (end < Infinity &&
|
|
4167
|
+
(isDone ? local < end - 1 : local < 0 || local >= end)) {
|
|
4168
|
+
return false;
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
for (const { animation, offset, isDone } of segments) {
|
|
4172
|
+
if (isDone)
|
|
4173
|
+
continue;
|
|
4174
|
+
const local = elapsed - offset;
|
|
4175
|
+
if (rate) {
|
|
4176
|
+
animation.playbackRate = rate;
|
|
4177
|
+
animation.startTime = now - local / rate;
|
|
4178
|
+
}
|
|
4179
|
+
else {
|
|
4180
|
+
animation.pause();
|
|
4181
|
+
animation.currentTime = local;
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
return true;
|
|
4185
|
+
};
|
|
4186
|
+
const build = () => {
|
|
4187
|
+
const now = time.now();
|
|
4188
|
+
/**
|
|
4189
|
+
* Finish the animations that have reached their end, which the
|
|
4190
|
+
* WAAPI animation has already rendered.
|
|
4191
|
+
*/
|
|
4192
|
+
if (!group.isJS) {
|
|
4193
|
+
group.isFinishing = true;
|
|
4194
|
+
tracks.forEach((track) => {
|
|
4195
|
+
const [trackEnd, trackCycle] = track.timing();
|
|
4196
|
+
if (track.state === "running" &&
|
|
4197
|
+
track.speed > 0 &&
|
|
4198
|
+
!trackCycle &&
|
|
4199
|
+
trackEnd <= now + 1) {
|
|
4200
|
+
track.complete();
|
|
4201
|
+
/**
|
|
4202
|
+
* From now it's checked for outside writes like any
|
|
4203
|
+
* other static transform. If the WAAPI animation never
|
|
4204
|
+
* rendered it (it was instant), rebuild with its value.
|
|
4205
|
+
*/
|
|
4206
|
+
statics[track.name] = read(owner, track.name);
|
|
4207
|
+
built.has(track) || (needsBuild = true);
|
|
4208
|
+
}
|
|
4209
|
+
});
|
|
4210
|
+
group.isFinishing = false;
|
|
4211
|
+
}
|
|
4212
|
+
isScheduled = false;
|
|
4213
|
+
tracks.forEach(({ state }, track) => {
|
|
4214
|
+
if (state === "idle" || state === "finished")
|
|
4215
|
+
tracks.delete(track);
|
|
4216
|
+
});
|
|
4217
|
+
if (!tracks.size) {
|
|
4218
|
+
transformGroups.delete(element);
|
|
4219
|
+
group.isJS || stop();
|
|
4220
|
+
return;
|
|
4221
|
+
}
|
|
4222
|
+
for (const track of tracks) {
|
|
4223
|
+
if (!track.canAccelerate)
|
|
4224
|
+
return group.demote(true);
|
|
4225
|
+
}
|
|
4226
|
+
/**
|
|
4227
|
+
* Everything that kept the group on the main thread has finished,
|
|
4228
|
+
* so move the remaining animations back to the compositor.
|
|
4229
|
+
*/
|
|
4230
|
+
if (group.isJS) {
|
|
4231
|
+
group.isJS = false;
|
|
4232
|
+
needsBuild = true;
|
|
4233
|
+
tracks.forEach((track) => track.js?.stop());
|
|
4234
|
+
}
|
|
4235
|
+
const shouldBuild = needsBuild;
|
|
4236
|
+
const churned = hasChurned;
|
|
4237
|
+
needsBuild = hasChurned = false;
|
|
4238
|
+
if (shouldBuild || !retime(now)) {
|
|
4239
|
+
if (churned) {
|
|
4240
|
+
if (now - lastChurn < churnWindow)
|
|
4241
|
+
return group.demote();
|
|
4242
|
+
lastChurn = now;
|
|
4243
|
+
}
|
|
4244
|
+
if (!animate(now))
|
|
4245
|
+
return group.demote();
|
|
4246
|
+
}
|
|
4247
|
+
};
|
|
4248
|
+
const group = {
|
|
4249
|
+
tracks,
|
|
4250
|
+
isJS: false,
|
|
4251
|
+
isFinishing: false,
|
|
4252
|
+
schedule(build_, isChurn) {
|
|
4253
|
+
needsBuild || (needsBuild = build_);
|
|
4254
|
+
hasChurned || (hasChurned = !!isChurn);
|
|
4255
|
+
/**
|
|
4256
|
+
* Build in this frame's render step, so it's in place before
|
|
4257
|
+
* anything reads the DOM in postRender, or straight after the
|
|
4258
|
+
* current task outside of a frame.
|
|
4259
|
+
*/
|
|
4260
|
+
if (!isScheduled) {
|
|
4261
|
+
isScheduled = true;
|
|
4262
|
+
frameData.isProcessing
|
|
4263
|
+
? frame.render(build, false, true)
|
|
4264
|
+
: queueMicrotask(build);
|
|
4265
|
+
}
|
|
4266
|
+
},
|
|
4267
|
+
/**
|
|
4268
|
+
* An outside write. Rebuild once, but if it keeps happening, move
|
|
4269
|
+
* to the main thread.
|
|
4270
|
+
*/
|
|
4271
|
+
check() {
|
|
4272
|
+
if (group.isJS)
|
|
4273
|
+
return;
|
|
4274
|
+
for (const key in statics) {
|
|
4275
|
+
if (read(owner, key) !== statics[key] && !has(key)) {
|
|
4276
|
+
return group.schedule(true, true);
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
},
|
|
4280
|
+
demote(canReturn) {
|
|
4281
|
+
if (group.isJS)
|
|
4282
|
+
return;
|
|
4283
|
+
/**
|
|
4284
|
+
* Unless only another animation is holding the group on the
|
|
4285
|
+
* main thread, these animations stay there until they end.
|
|
4286
|
+
*/
|
|
4287
|
+
tracks.forEach((track) => {
|
|
4288
|
+
track.isAccelerated() && track.sync();
|
|
4289
|
+
canReturn || (track.canAccelerate = false);
|
|
4290
|
+
});
|
|
4291
|
+
group.isJS = true;
|
|
4292
|
+
built = new Map();
|
|
4293
|
+
stop();
|
|
4294
|
+
tracks.forEach((track) => track.state === "running" && track.js?.start());
|
|
4295
|
+
},
|
|
4296
|
+
};
|
|
4297
|
+
transformGroups.set(element, group);
|
|
4298
|
+
return group;
|
|
3329
4299
|
}
|
|
3330
4300
|
|
|
3331
4301
|
class AsyncMotionValueAnimation extends WithPromise {
|
|
@@ -3397,6 +4367,11 @@
|
|
|
3397
4367
|
* values may not be valid CSS and would trigger browser warnings.
|
|
3398
4368
|
*/
|
|
3399
4369
|
const useWaapi = canAnimateValue && !isHandoff && supportsBrowserAnimation(options);
|
|
4370
|
+
/**
|
|
4371
|
+
* Independent transforms (x, scale etc) on an HTML element are
|
|
4372
|
+
* composed into one WAAPI transform animation per element.
|
|
4373
|
+
*/
|
|
4374
|
+
const isGroupedTransform = !useWaapi && canGroupTransform(options);
|
|
3400
4375
|
/**
|
|
3401
4376
|
* Resolve startTime for the animation. A startTime passed in options
|
|
3402
4377
|
* (an optimised appear handoff syncing to its WAAPI animation) takes
|
|
@@ -3412,7 +4387,10 @@
|
|
|
3412
4387
|
* animation started in that moment stays in sync. WAAPI has no frame
|
|
3413
4388
|
* of ours to do that on.
|
|
3414
4389
|
*/
|
|
3415
|
-
if (sync &&
|
|
4390
|
+
if (sync &&
|
|
4391
|
+
(useWaapi ||
|
|
4392
|
+
isGroupedTransform ||
|
|
4393
|
+
this.resolvedAt !== this.createdAt)) {
|
|
3416
4394
|
options.startTime ?? (options.startTime = resolveStartTime(this.createdAt, this.resolvedAt));
|
|
3417
4395
|
}
|
|
3418
4396
|
let animation;
|
|
@@ -3430,6 +4408,9 @@
|
|
|
3430
4408
|
animation = new JSAnimation(options);
|
|
3431
4409
|
}
|
|
3432
4410
|
}
|
|
4411
|
+
else if (isGroupedTransform) {
|
|
4412
|
+
animation = new TransformAnimation(options);
|
|
4413
|
+
}
|
|
3433
4414
|
else {
|
|
3434
4415
|
animation = new JSAnimation(options);
|
|
3435
4416
|
}
|
|
@@ -3445,6 +4426,12 @@
|
|
|
3445
4426
|
then(onResolve, _onReject) {
|
|
3446
4427
|
return this.finished.finally(onResolve).then(() => { });
|
|
3447
4428
|
}
|
|
4429
|
+
/**
|
|
4430
|
+
* The value now, if the animation runs on the compositor.
|
|
4431
|
+
*/
|
|
4432
|
+
liveValue() {
|
|
4433
|
+
return this._animation?.liveValue?.();
|
|
4434
|
+
}
|
|
3448
4435
|
get animation() {
|
|
3449
4436
|
if (!this._animation) {
|
|
3450
4437
|
this.keyframeResolver?.resume();
|
|
@@ -3882,6 +4869,18 @@
|
|
|
3882
4869
|
if (collectMotionValues.current) {
|
|
3883
4870
|
collectMotionValues.current.push(this);
|
|
3884
4871
|
}
|
|
4872
|
+
/**
|
|
4873
|
+
* A value animating on the compositor isn't updated every frame,
|
|
4874
|
+
* so read it from its animation.
|
|
4875
|
+
*/
|
|
4876
|
+
const sampled = this.animation?.liveValue?.();
|
|
4877
|
+
return sampled === undefined ? this.current : sampled;
|
|
4878
|
+
}
|
|
4879
|
+
/**
|
|
4880
|
+
* The value as last set. Unlike get(), this doesn't read an animation
|
|
4881
|
+
* running on the compositor, which renders the value itself.
|
|
4882
|
+
*/
|
|
4883
|
+
getCurrent() {
|
|
3885
4884
|
return this.current;
|
|
3886
4885
|
}
|
|
3887
4886
|
/**
|
|
@@ -4108,6 +5107,7 @@
|
|
|
4108
5107
|
name,
|
|
4109
5108
|
motionValue: value,
|
|
4110
5109
|
element: isHandoff ? undefined : element,
|
|
5110
|
+
hasOnUpdate: !!valueTransition.onUpdate,
|
|
4111
5111
|
};
|
|
4112
5112
|
/**
|
|
4113
5113
|
* If there's no transition defined for this value, we can generate
|
|
@@ -4707,6 +5707,30 @@
|
|
|
4707
5707
|
constructor(unresolvedKeyframes, onComplete, name, motionValue, element) {
|
|
4708
5708
|
super(unresolvedKeyframes, onComplete, name, motionValue, element, true);
|
|
4709
5709
|
}
|
|
5710
|
+
/**
|
|
5711
|
+
* Keyframes that don't need reading from the DOM, or measuring,
|
|
5712
|
+
* resolve straight away rather than in the frame loop's batched read.
|
|
5713
|
+
*/
|
|
5714
|
+
scheduleResolve() {
|
|
5715
|
+
if (this.canReadNow()) {
|
|
5716
|
+
this.readKeyframes();
|
|
5717
|
+
if (!this.needsMeasurement)
|
|
5718
|
+
return this.complete();
|
|
5719
|
+
}
|
|
5720
|
+
super.scheduleResolve();
|
|
5721
|
+
}
|
|
5722
|
+
canReadNow() {
|
|
5723
|
+
const { unresolvedKeyframes, element, motionValue } = this;
|
|
5724
|
+
const origin = unresolvedKeyframes[0] ?? motionValue?.get();
|
|
5725
|
+
if (!element?.current || origin === undefined)
|
|
5726
|
+
return false;
|
|
5727
|
+
for (let i = 0; i < unresolvedKeyframes.length; i++) {
|
|
5728
|
+
if (containsCSSVariable(i ? unresolvedKeyframes[i] : origin)) {
|
|
5729
|
+
return false;
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
return true;
|
|
5733
|
+
}
|
|
4710
5734
|
readKeyframes() {
|
|
4711
5735
|
const { unresolvedKeyframes, element, name } = this;
|
|
4712
5736
|
if (!element || !element.current)
|
|
@@ -5050,15 +6074,6 @@
|
|
|
5050
6074
|
return isObject(element) && "ownerSVGElement" in element;
|
|
5051
6075
|
}
|
|
5052
6076
|
|
|
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
6077
|
function resolveElements(elementOrSelector, scope, selectorCache) {
|
|
5063
6078
|
if (elementOrSelector == null) {
|
|
5064
6079
|
return [];
|
|
@@ -5232,18 +6247,12 @@
|
|
|
5232
6247
|
});
|
|
5233
6248
|
}
|
|
5234
6249
|
|
|
5235
|
-
const translateAlias$1 = {
|
|
5236
|
-
x: "translateX",
|
|
5237
|
-
y: "translateY",
|
|
5238
|
-
z: "translateZ",
|
|
5239
|
-
transformPerspective: "perspective",
|
|
5240
|
-
};
|
|
5241
6250
|
/**
|
|
5242
6251
|
* Cache of `translateX(`-style openers so each render is a few string
|
|
5243
6252
|
* concatenations rather than a template per transform.
|
|
5244
6253
|
*/
|
|
5245
6254
|
const openers = {};
|
|
5246
|
-
function buildTransform
|
|
6255
|
+
function buildTransform(state) {
|
|
5247
6256
|
let transform = "";
|
|
5248
6257
|
const { transformKeys: keys = [], transformValues: values = {} } = state;
|
|
5249
6258
|
/**
|
|
@@ -5252,7 +6261,7 @@
|
|
|
5252
6261
|
*/
|
|
5253
6262
|
for (let i = 0; i < keys.length; i++) {
|
|
5254
6263
|
const key = keys[i];
|
|
5255
|
-
const value = values[key].
|
|
6264
|
+
const value = values[key].getCurrent();
|
|
5256
6265
|
if (value === undefined)
|
|
5257
6266
|
continue;
|
|
5258
6267
|
const parsed = typeof value === "number" ? value : parseFloat(value);
|
|
@@ -5260,7 +6269,7 @@
|
|
|
5260
6269
|
transform +=
|
|
5261
6270
|
(transform && " ") +
|
|
5262
6271
|
(openers[key] ||
|
|
5263
|
-
(openers[key] = (translateAlias
|
|
6272
|
+
(openers[key] = (translateAlias[key] || key) + "(")) +
|
|
5264
6273
|
getValueAsType(value, transformValueTypes[key]) +
|
|
5265
6274
|
")";
|
|
5266
6275
|
}
|
|
@@ -5303,7 +6312,8 @@
|
|
|
5303
6312
|
addStyleValue(element, state, "transformBox", new MotionValue("fill-box"));
|
|
5304
6313
|
}
|
|
5305
6314
|
state.set("transform", new MotionValue("none"), () => {
|
|
5306
|
-
|
|
6315
|
+
transformGroups.get(element)?.check();
|
|
6316
|
+
element.style.transform = buildTransform(state);
|
|
5307
6317
|
});
|
|
5308
6318
|
}
|
|
5309
6319
|
computed = state.get("transform");
|
|
@@ -5321,12 +6331,12 @@
|
|
|
5321
6331
|
}
|
|
5322
6332
|
else if (isCSSVar(key)) {
|
|
5323
6333
|
render = () => {
|
|
5324
|
-
element.style.setProperty(key, value.
|
|
6334
|
+
element.style.setProperty(key, value.getCurrent());
|
|
5325
6335
|
};
|
|
5326
6336
|
}
|
|
5327
6337
|
else {
|
|
5328
6338
|
render = () => {
|
|
5329
|
-
element.style[key] = getValueAsType(value.
|
|
6339
|
+
element.style[key] = getValueAsType(value.getCurrent(), numberValueTypes[key]);
|
|
5330
6340
|
};
|
|
5331
6341
|
}
|
|
5332
6342
|
return state.set(key, value, render, computed);
|
|
@@ -5358,73 +6368,8 @@
|
|
|
5358
6368
|
test: isStyleSubject,
|
|
5359
6369
|
read: readStyleValue,
|
|
5360
6370
|
});
|
|
5361
|
-
const styleEffect =
|
|
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
|
-
}
|
|
6371
|
+
const styleEffect =
|
|
6372
|
+
/*@__PURE__*/ createSelectorEffect(styleSubjectEffect);
|
|
5428
6373
|
|
|
5429
6374
|
function buildHTMLStyles(state, latestValues, transformTemplate) {
|
|
5430
6375
|
const { style, vars, transformOrigin } = state;
|
|
@@ -5464,7 +6409,7 @@
|
|
|
5464
6409
|
}
|
|
5465
6410
|
if (!latestValues.transform) {
|
|
5466
6411
|
if (hasTransform || transformTemplate) {
|
|
5467
|
-
style.transform = buildTransform(latestValues, state.transform, transformTemplate);
|
|
6412
|
+
style.transform = buildTransform$1(latestValues, state.transform, transformTemplate);
|
|
5468
6413
|
}
|
|
5469
6414
|
else if (style.transform) {
|
|
5470
6415
|
/**
|
|
@@ -6426,13 +7371,46 @@
|
|
|
6426
7371
|
return from === "last" ? lastIndex : lastIndex / 2;
|
|
6427
7372
|
}
|
|
6428
7373
|
}
|
|
6429
|
-
function stagger(duration = 0.1, { startDelay = 0, from = 0, ease } = {}) {
|
|
7374
|
+
function stagger(duration = 0.1, { startDelay = 0, from = 0, ease, grid } = {}) {
|
|
7375
|
+
/**
|
|
7376
|
+
* Distances from the origin for each element count. The final entry
|
|
7377
|
+
* holds the distance used to normalise easing.
|
|
7378
|
+
*/
|
|
7379
|
+
const cache = {};
|
|
6430
7380
|
return (i, total) => {
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
const
|
|
7381
|
+
let distances = cache[total];
|
|
7382
|
+
if (!distances) {
|
|
7383
|
+
const [columns, rows] = grid || [Infinity, 1];
|
|
7384
|
+
const lastX = Math.min(columns, total) - 1;
|
|
7385
|
+
const lastY = Math.max(rows, Math.ceil(total / columns)) - 1;
|
|
7386
|
+
const isIndex = typeof from === "number";
|
|
7387
|
+
const scale = from === "first" ? 0 : from === "last" ? 1 : 0.5;
|
|
7388
|
+
const originX = isIndex ? from % columns : lastX * scale;
|
|
7389
|
+
const originY = isIndex ? Math.floor(from / columns) : lastY * scale;
|
|
7390
|
+
distances = cache[total] = [];
|
|
7391
|
+
for (let j = 0; j < total; j++) {
|
|
7392
|
+
const x = j % columns;
|
|
7393
|
+
const y = Math.floor(j / columns);
|
|
7394
|
+
const edgeX = Math.min(x, lastX - x);
|
|
7395
|
+
const edgeY = Math.min(y, lastY - y);
|
|
7396
|
+
if (from === "random") {
|
|
7397
|
+
// Inside-out shuffle, so each element gets a distinct step
|
|
7398
|
+
const k = (Math.random() * (j + 1)) | 0;
|
|
7399
|
+
distances[j] = distances[k];
|
|
7400
|
+
distances[k] = j;
|
|
7401
|
+
}
|
|
7402
|
+
else {
|
|
7403
|
+
distances[j] =
|
|
7404
|
+
from === "edges"
|
|
7405
|
+
? Math.min(lastX ? edgeX : edgeY, lastY ? edgeY : edgeX)
|
|
7406
|
+
: Math.hypot(x - originX, y - originY);
|
|
7407
|
+
}
|
|
7408
|
+
}
|
|
7409
|
+
distances.push(grid ? Math.max(...distances) : total);
|
|
7410
|
+
}
|
|
7411
|
+
let delay = duration * distances[i];
|
|
7412
|
+
const maxDelay = duration * distances[total];
|
|
7413
|
+
if (ease && maxDelay) {
|
|
6436
7414
|
const easingFunction = easingDefinitionToFunction(ease);
|
|
6437
7415
|
delay = easingFunction(delay / maxDelay) * maxDelay;
|
|
6438
7416
|
}
|
|
@@ -6666,7 +7644,7 @@
|
|
|
6666
7644
|
}, stopAnimation);
|
|
6667
7645
|
if (isMotionValue(source)) {
|
|
6668
7646
|
let skipNextAnimation = options.skipInitialAnimation === true;
|
|
6669
|
-
const
|
|
7647
|
+
const onSourceChange = (v) => {
|
|
6670
7648
|
if (skipNextAnimation) {
|
|
6671
7649
|
skipNextAnimation = false;
|
|
6672
7650
|
value.jump(parseValue(v, unit), false);
|
|
@@ -6674,11 +7652,28 @@
|
|
|
6674
7652
|
else {
|
|
6675
7653
|
value.set(parseValue(v, unit));
|
|
6676
7654
|
}
|
|
6677
|
-
}
|
|
7655
|
+
};
|
|
7656
|
+
const removeSourceOnChange = source.on("change", onSourceChange);
|
|
7657
|
+
/**
|
|
7658
|
+
* Catch up on a change made before we subscribed, for instance
|
|
7659
|
+
* between a React render reading the source and the effect
|
|
7660
|
+
* attaching to it. Subtracting is falsy for equal or non-numeric
|
|
7661
|
+
* (NaN) values.
|
|
7662
|
+
*/
|
|
7663
|
+
const latest = source.get();
|
|
7664
|
+
if (asNumber(latest) - asNumber(value.get()))
|
|
7665
|
+
onSourceChange(latest);
|
|
6678
7666
|
const removeValueOnDestroy = value.on("destroy", removeSourceOnChange);
|
|
7667
|
+
/**
|
|
7668
|
+
* Stop our animation on detach, otherwise a re-attach (new
|
|
7669
|
+
* options, Strict Mode) starts a second animation that fights
|
|
7670
|
+
* this orphaned one. The catch-up above resumes from the
|
|
7671
|
+
* current value and velocity.
|
|
7672
|
+
*/
|
|
6679
7673
|
return () => {
|
|
6680
7674
|
removeSourceOnChange();
|
|
6681
7675
|
removeValueOnDestroy();
|
|
7676
|
+
stopAnimation();
|
|
6682
7677
|
};
|
|
6683
7678
|
}
|
|
6684
7679
|
return stopAnimation;
|
|
@@ -8368,8 +9363,11 @@
|
|
|
8368
9363
|
const removeOnChange = value.on("change", (latestValue) => {
|
|
8369
9364
|
this.latestValues[key] = latestValue;
|
|
8370
9365
|
this.props.onUpdate && frame.preRender(this.notifyUpdate);
|
|
8371
|
-
if (valueIsTransform
|
|
8372
|
-
this.projection
|
|
9366
|
+
if (valueIsTransform) {
|
|
9367
|
+
if (this.projection) {
|
|
9368
|
+
this.projection.isTransformDirty = true;
|
|
9369
|
+
}
|
|
9370
|
+
transformGroups.get(this.current)?.check();
|
|
8373
9371
|
}
|
|
8374
9372
|
this.scheduleRender();
|
|
8375
9373
|
});
|
|
@@ -8453,9 +9451,18 @@
|
|
|
8453
9451
|
* added to our map, old ones removed, and listeners updated.
|
|
8454
9452
|
*/
|
|
8455
9453
|
update(props, presenceContext) {
|
|
8456
|
-
|
|
9454
|
+
const { transformTemplate, onUpdate } = this.props;
|
|
9455
|
+
if (props.transformTemplate || transformTemplate) {
|
|
8457
9456
|
this.scheduleRender();
|
|
8458
9457
|
}
|
|
9458
|
+
/**
|
|
9459
|
+
* Transforms on the compositor can't use a new transformTemplate
|
|
9460
|
+
* or onUpdate, so move them to the main thread.
|
|
9461
|
+
*/
|
|
9462
|
+
if ((props.transformTemplate && !transformTemplate) ||
|
|
9463
|
+
(props.onUpdate && !onUpdate)) {
|
|
9464
|
+
transformGroups.get(this.current)?.demote();
|
|
9465
|
+
}
|
|
8459
9466
|
this.prevProps = this.props;
|
|
8460
9467
|
this.props = props;
|
|
8461
9468
|
this.prevPresenceContext = this.presenceContext;
|
|
@@ -8829,8 +9836,13 @@
|
|
|
8829
9836
|
function scrapeMotionValuesFromProps(props, prevProps, visualElement) {
|
|
8830
9837
|
const newValues = scrapeMotionValuesFromProps$1(props, prevProps, visualElement);
|
|
8831
9838
|
for (const key in props) {
|
|
8832
|
-
|
|
8833
|
-
|
|
9839
|
+
/**
|
|
9840
|
+
* A MotionValue child is rendered as text content by
|
|
9841
|
+
* DOMVisualElement, so it mustn't also be written as an attribute.
|
|
9842
|
+
*/
|
|
9843
|
+
if (key !== "children" &&
|
|
9844
|
+
(isMotionValue(props[key]) ||
|
|
9845
|
+
isMotionValue(prevProps[key]))) {
|
|
8834
9846
|
const targetKey = transformPropOrder.indexOf(key) !== -1
|
|
8835
9847
|
? "attr" + key.charAt(0).toUpperCase() + key.substring(1)
|
|
8836
9848
|
: key;
|
|
@@ -9750,6 +10762,7 @@
|
|
|
9750
10762
|
}
|
|
9751
10763
|
}
|
|
9752
10764
|
}
|
|
10765
|
+
const demoteTransforms = ({ instance }) => instance && transformGroups.get(instance)?.demote();
|
|
9753
10766
|
function cancelTreeOptimisedTransformAnimations(projectionNode) {
|
|
9754
10767
|
projectionNode.hasCheckedOptimisedAppear = true;
|
|
9755
10768
|
if (projectionNode.root === projectionNode)
|
|
@@ -10111,10 +11124,17 @@
|
|
|
10111
11124
|
!this.root.isUpdating && this.root.startUpdate();
|
|
10112
11125
|
if (this.isLayoutDirty)
|
|
10113
11126
|
return;
|
|
11127
|
+
/**
|
|
11128
|
+
* Accelerated transforms aren't reflected in latestValues or
|
|
11129
|
+
* removed by resetTransform, so move them back to the main
|
|
11130
|
+
* thread before this node, or its ancestors, are measured.
|
|
11131
|
+
*/
|
|
11132
|
+
demoteTransforms(this);
|
|
10114
11133
|
this.isLayoutDirty = true;
|
|
10115
11134
|
for (let i = 0; i < this.path.length; i++) {
|
|
10116
11135
|
const node = this.path[i];
|
|
10117
11136
|
node.shouldResetTransform = true;
|
|
11137
|
+
demoteTransforms(node);
|
|
10118
11138
|
/**
|
|
10119
11139
|
* Percentage translates resolve against layoutBox dimensions,
|
|
10120
11140
|
* so ancestors with them must be re-measured after transform reset.
|
|
@@ -10317,6 +11337,7 @@
|
|
|
10317
11337
|
const isResetRequested = this.isLayoutDirty ||
|
|
10318
11338
|
this.shouldResetTransform ||
|
|
10319
11339
|
this.options.alwaysMeasureLayout;
|
|
11340
|
+
isResetRequested && demoteTransforms(this);
|
|
10320
11341
|
const hasProjection = this.projectionDelta && !isDeltaZero(this.projectionDelta);
|
|
10321
11342
|
const transformTemplate = this.getTransformTemplate();
|
|
10322
11343
|
const transformTemplateValue = transformTemplate
|
|
@@ -13242,7 +14263,7 @@
|
|
|
13242
14263
|
exports.buildProjectionTransform = buildProjectionTransform;
|
|
13243
14264
|
exports.buildSVGAttrs = buildSVGAttrs;
|
|
13244
14265
|
exports.buildSVGPath = buildSVGPath;
|
|
13245
|
-
exports.buildTransform = buildTransform;
|
|
14266
|
+
exports.buildTransform = buildTransform$1;
|
|
13246
14267
|
exports.calcAxisDelta = calcAxisDelta;
|
|
13247
14268
|
exports.calcBoxDelta = calcBoxDelta;
|
|
13248
14269
|
exports.calcChildStagger = calcChildStagger;
|
|
@@ -13444,6 +14465,8 @@
|
|
|
13444
14465
|
exports.rgbUnit = rgbUnit;
|
|
13445
14466
|
exports.rgba = rgba;
|
|
13446
14467
|
exports.rootProjectionNode = rootProjectionNode;
|
|
14468
|
+
exports.round = round;
|
|
14469
|
+
exports.sampleDelta = sampleDelta;
|
|
13447
14470
|
exports.scale = scale;
|
|
13448
14471
|
exports.scaleCorrectors = scaleCorrectors;
|
|
13449
14472
|
exports.scalePoint = scalePoint;
|
|
@@ -13471,6 +14494,7 @@
|
|
|
13471
14494
|
exports.supportsPartialKeyframes = supportsPartialKeyframes;
|
|
13472
14495
|
exports.supportsScrollTimeline = supportsScrollTimeline;
|
|
13473
14496
|
exports.supportsViewTimeline = supportsViewTimeline;
|
|
14497
|
+
exports.supportsWaapi = supportsWaapi;
|
|
13474
14498
|
exports.svgEffect = svgEffect;
|
|
13475
14499
|
exports.svgSubjectEffect = svgSubjectEffect;
|
|
13476
14500
|
exports.sync = sync;
|