motion 14.1.0 → 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.
- package/dist/motion.dev.js +1199 -219
- package/dist/motion.js +1 -1
- package/package.json +3 -3
package/dist/motion.dev.js
CHANGED
|
@@ -180,48 +180,50 @@
|
|
|
180
180
|
This has been modified from Gaëtan Renaudeau's BezierEasing
|
|
181
181
|
https://github.com/gre/bezier-easing/blob/master/src/index.js
|
|
182
182
|
https://github.com/gre/bezier-easing/blob/master/LICENSE
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
183
|
+
|
|
184
|
+
t is found for a given x with Newton-Raphson, safeguarded by a bisection
|
|
185
|
+
bracket: whenever a Newton step would leave the bracket (near flat parts
|
|
186
|
+
of the curve), it bisects instead. This converges to full precision in
|
|
187
|
+
~5 iterations on typical curves, so output is smooth rather than snapped
|
|
188
|
+
to a fixed bisection grid. The slope is estimated with a forward
|
|
189
|
+
difference, and the lookup table is omitted, to save bytes.
|
|
190
|
+
|
|
191
|
+
x = 0 and x = 1 return exactly 0 and 1. x outside [0, 1] starts outside
|
|
192
|
+
the bracket, so it bisects towards the nearer end and saturates to within
|
|
193
|
+
~1e-12 of 0 or 1.
|
|
191
194
|
Usage
|
|
192
195
|
const easeOut = cubicBezier(.17,.67,.83,.67);
|
|
193
196
|
const x = easeOut(0.5); // returns 0.627...
|
|
194
197
|
*/
|
|
195
198
|
// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const subdivisionPrecision = 0.0000001;
|
|
199
|
-
const subdivisionMaxIterations = 12;
|
|
200
|
-
function binarySubdivide(x, lowerBound, upperBound, mX1, mX2) {
|
|
201
|
-
let currentX;
|
|
202
|
-
let currentT;
|
|
203
|
-
let i = 0;
|
|
204
|
-
do {
|
|
205
|
-
currentT = lowerBound + (upperBound - lowerBound) / 2.0;
|
|
206
|
-
currentX = calcBezier(currentT, mX1, mX2) - x;
|
|
207
|
-
if (currentX > 0.0) {
|
|
208
|
-
upperBound = currentT;
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
lowerBound = currentT;
|
|
212
|
-
}
|
|
213
|
-
} while (Math.abs(currentX) > subdivisionPrecision &&
|
|
214
|
-
++i < subdivisionMaxIterations);
|
|
215
|
-
return currentT;
|
|
216
|
-
}
|
|
199
|
+
// Bernstein form, so it is exactly 0 at t = 0 and exactly 1 at t = 1.
|
|
200
|
+
const calcBezier = (t, a1, a2) => t * (3 * (1 - t) * ((1 - t) * a1 + t * a2) + t * t);
|
|
217
201
|
/*#__NO_SIDE_EFFECTS__*/
|
|
218
202
|
function cubicBezier(mX1, mY1, mX2, mY2) {
|
|
219
203
|
// If this is a linear gradient, return linear easing
|
|
220
204
|
if (mX1 === mY1 && mX2 === mY2)
|
|
221
205
|
return noop;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
206
|
+
return (x) => {
|
|
207
|
+
let lowerBound = 0;
|
|
208
|
+
let upperBound = 1;
|
|
209
|
+
let t = x;
|
|
210
|
+
for (let i = 0;; i++) {
|
|
211
|
+
// If t is outside the bracket, because x is out of range or a
|
|
212
|
+
// Newton step overshot where the slope is ~0, bisect instead.
|
|
213
|
+
if (!(t >= lowerBound && t <= upperBound)) {
|
|
214
|
+
t = (lowerBound + upperBound) / 2;
|
|
215
|
+
}
|
|
216
|
+
const currentX = calcBezier(t, mX1, mX2) - x;
|
|
217
|
+
if (currentX * currentX < 1e-28 || i === 40)
|
|
218
|
+
break;
|
|
219
|
+
currentX > 0 ? (upperBound = t) : (lowerBound = t);
|
|
220
|
+
// Newton-Raphson step, with dx/dt from a forward difference
|
|
221
|
+
t -=
|
|
222
|
+
(currentX * 1e-7) /
|
|
223
|
+
(calcBezier(t + 1e-7, mX1, mX2) - x - currentX);
|
|
224
|
+
}
|
|
225
|
+
return calcBezier(t, mY1, mY2);
|
|
226
|
+
};
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
// Accepts an easing function and returns a new one that outputs mirrored values for
|
|
@@ -1110,7 +1112,20 @@
|
|
|
1110
1112
|
* Create a progress => progress easing function from a generator.
|
|
1111
1113
|
*/
|
|
1112
1114
|
function createGeneratorEasing(options, scale = 100, createGenerator) {
|
|
1113
|
-
|
|
1115
|
+
/**
|
|
1116
|
+
* The easing is generated over 0 - scale, but velocity is in the
|
|
1117
|
+
* value's own units, so scale it to match. Otherwise an interrupted
|
|
1118
|
+
* spring hands off the wrong speed, or direction. Keyframes with
|
|
1119
|
+
* units (colours, filters, mixed units) give NaN, which is falsy,
|
|
1120
|
+
* so their velocity passes through unscaled.
|
|
1121
|
+
*/
|
|
1122
|
+
const { keyframes: k, velocity: v } = options;
|
|
1123
|
+
const delta = k && k[k.length - 1] - k[0];
|
|
1124
|
+
const generator = createGenerator({
|
|
1125
|
+
...options,
|
|
1126
|
+
velocity: v && delta ? (v * scale) / delta : v,
|
|
1127
|
+
keyframes: [0, scale],
|
|
1128
|
+
});
|
|
1114
1129
|
const duration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration);
|
|
1115
1130
|
return {
|
|
1116
1131
|
type: "keyframes",
|
|
@@ -1891,6 +1906,7 @@
|
|
|
1891
1906
|
}
|
|
1892
1907
|
|
|
1893
1908
|
const percentToProgress = (percent) => percent / 100;
|
|
1909
|
+
const round = (value) => Math.round(value * 10000) / 10000;
|
|
1894
1910
|
class JSAnimation extends WithPromise {
|
|
1895
1911
|
constructor(options) {
|
|
1896
1912
|
super();
|
|
@@ -1998,10 +2014,10 @@
|
|
|
1998
2014
|
}
|
|
1999
2015
|
}
|
|
2000
2016
|
tick(timestamp, sample = false) {
|
|
2001
|
-
const { generator, totalDuration,
|
|
2017
|
+
const { generator, totalDuration, calculatedDuration } = this;
|
|
2002
2018
|
if (this.startTime === null)
|
|
2003
2019
|
return generator.next(0);
|
|
2004
|
-
const { delay = 0, keyframes,
|
|
2020
|
+
const { delay = 0, keyframes, type, onUpdate, finalKeyframe, } = this.options;
|
|
2005
2021
|
if (this.startTime === this.pendingStartTime) {
|
|
2006
2022
|
this.startTime = resolveStartTime(this.startTime, timestamp);
|
|
2007
2023
|
}
|
|
@@ -2034,7 +2050,55 @@
|
|
|
2034
2050
|
if (this.state === "finished" && this.holdTime === null) {
|
|
2035
2051
|
this.currentTime = totalDuration;
|
|
2036
2052
|
}
|
|
2037
|
-
|
|
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;
|
|
2038
2102
|
let frameGenerator = generator;
|
|
2039
2103
|
if (repeat) {
|
|
2040
2104
|
/**
|
|
@@ -2042,7 +2106,7 @@
|
|
|
2042
2106
|
* than duration we'll get values like 2.5 (midway through the
|
|
2043
2107
|
* third iteration)
|
|
2044
2108
|
*/
|
|
2045
|
-
const progress = Math.min(
|
|
2109
|
+
const progress = Math.min(currentTime, totalDuration) / resolvedDuration;
|
|
2046
2110
|
/**
|
|
2047
2111
|
* Get the current iteration (0 indexed). For instance the floor of
|
|
2048
2112
|
* 2.5 is 2.
|
|
@@ -2095,25 +2159,6 @@
|
|
|
2095
2159
|
if (mixKeyframes && !isInDelayPhase) {
|
|
2096
2160
|
state.value = mixKeyframes(state.value);
|
|
2097
2161
|
}
|
|
2098
|
-
let { done } = state;
|
|
2099
|
-
if (!isInDelayPhase && calculatedDuration !== null) {
|
|
2100
|
-
done =
|
|
2101
|
-
this.playbackSpeed >= 0
|
|
2102
|
-
? this.currentTime >= totalDuration
|
|
2103
|
-
: this.currentTime <= 0;
|
|
2104
|
-
}
|
|
2105
|
-
const isAnimationFinished = this.holdTime === null &&
|
|
2106
|
-
(this.state === "finished" || (this.state === "running" && done));
|
|
2107
|
-
// TODO: The exception for inertia could be cleaner here
|
|
2108
|
-
if (isAnimationFinished && type !== inertia) {
|
|
2109
|
-
state.value = getFinalKeyframe(keyframes, this.options, finalKeyframe, this.speed);
|
|
2110
|
-
}
|
|
2111
|
-
if (onUpdate) {
|
|
2112
|
-
onUpdate(state.value);
|
|
2113
|
-
}
|
|
2114
|
-
if (isAnimationFinished) {
|
|
2115
|
-
this.finish();
|
|
2116
|
-
}
|
|
2117
2162
|
return state;
|
|
2118
2163
|
}
|
|
2119
2164
|
/**
|
|
@@ -3122,44 +3167,71 @@
|
|
|
3122
3167
|
}
|
|
3123
3168
|
this.options = options;
|
|
3124
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
|
+
}
|
|
3125
3205
|
/**
|
|
3126
3206
|
* WAAPI doesn't natively have any interruption capabilities.
|
|
3127
3207
|
*
|
|
3128
3208
|
* Rather than read committed styles back out of the DOM, we can
|
|
3129
|
-
*
|
|
3130
|
-
*
|
|
3131
|
-
*
|
|
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.
|
|
3132
3212
|
*/
|
|
3133
3213
|
updateMotionValue(value) {
|
|
3134
|
-
const { motionValue,
|
|
3214
|
+
const { motionValue, element, name } = this.options;
|
|
3135
3215
|
if (!motionValue)
|
|
3136
3216
|
return;
|
|
3137
3217
|
if (value !== undefined) {
|
|
3138
3218
|
motionValue.set(value);
|
|
3139
3219
|
return;
|
|
3140
3220
|
}
|
|
3141
|
-
const
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
* Under CPU load, WAAPI's currentTime may not reflect actual
|
|
3148
|
-
* elapsed time, causing incorrect sampling and visual jumps.
|
|
3149
|
-
*/
|
|
3150
|
-
const sampleTime = Math.max(sampleDelta, time.now() - this.startTime);
|
|
3151
|
-
const delta = clamp(0, sampleDelta, sampleTime - sampleDelta);
|
|
3152
|
-
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);
|
|
3153
3227
|
/**
|
|
3154
3228
|
* Write the estimated value to inline style so it persists
|
|
3155
3229
|
* after cancel(), covering the async gap before the next
|
|
3156
3230
|
* animation starts.
|
|
3157
3231
|
*/
|
|
3158
|
-
const { name } = this.options;
|
|
3159
3232
|
if (element && name)
|
|
3160
3233
|
setStyle(element, name, current);
|
|
3161
|
-
motionValue.setWithVelocity(
|
|
3162
|
-
sampleAnimation.stop();
|
|
3234
|
+
motionValue.setWithVelocity(this.sample(sampleTime - delta * Math.sign(speed)), current, delta / Math.abs(speed || 1));
|
|
3163
3235
|
}
|
|
3164
3236
|
}
|
|
3165
3237
|
|
|
@@ -3253,64 +3325,973 @@
|
|
|
3253
3325
|
browserColorFunctions.test(keyframes[i])) {
|
|
3254
3326
|
return true;
|
|
3255
3327
|
}
|
|
3256
|
-
}
|
|
3257
|
-
return false;
|
|
3258
|
-
}
|
|
3259
|
-
|
|
3260
|
-
const colorProperties = new Set([
|
|
3261
|
-
"color",
|
|
3262
|
-
"backgroundColor",
|
|
3263
|
-
"outlineColor",
|
|
3264
|
-
"fill",
|
|
3265
|
-
"stroke",
|
|
3266
|
-
"borderColor",
|
|
3267
|
-
"borderTopColor",
|
|
3268
|
-
"borderRightColor",
|
|
3269
|
-
"borderBottomColor",
|
|
3270
|
-
"borderLeftColor",
|
|
3271
|
-
]);
|
|
3272
|
-
const supportsWaapi = /*@__PURE__*/ memo(() => Object.hasOwnProperty.call(Element.prototype, "animate"));
|
|
3273
|
-
function supportsBrowserAnimation(options) {
|
|
3274
|
-
const { motionValue, name, repeatDelay, repeatType, damping, type, keyframes, } = options;
|
|
3328
|
+
}
|
|
3329
|
+
return false;
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
const colorProperties = new Set([
|
|
3333
|
+
"color",
|
|
3334
|
+
"backgroundColor",
|
|
3335
|
+
"outlineColor",
|
|
3336
|
+
"fill",
|
|
3337
|
+
"stroke",
|
|
3338
|
+
"borderColor",
|
|
3339
|
+
"borderTopColor",
|
|
3340
|
+
"borderRightColor",
|
|
3341
|
+
"borderBottomColor",
|
|
3342
|
+
"borderLeftColor",
|
|
3343
|
+
]);
|
|
3344
|
+
const supportsWaapi = /*@__PURE__*/ memo(() => Object.hasOwnProperty.call(Element.prototype, "animate"));
|
|
3345
|
+
function supportsBrowserAnimation(options) {
|
|
3346
|
+
const { motionValue, name, repeatDelay, repeatType, damping, type, keyframes, } = options;
|
|
3347
|
+
/**
|
|
3348
|
+
* Most values can't be accelerated at all, so check the name before
|
|
3349
|
+
* looking at the element or its props.
|
|
3350
|
+
*/
|
|
3351
|
+
if (!name ||
|
|
3352
|
+
!(acceleratedValues.has(name) || colorProperties.has(name))) {
|
|
3353
|
+
return false;
|
|
3354
|
+
}
|
|
3355
|
+
const subject = motionValue?.owner?.current;
|
|
3356
|
+
/**
|
|
3357
|
+
* We use instanceof checks instead of isHTMLElement()/isSVGElement()
|
|
3358
|
+
* because we explicitly **don't** want elements in different timing
|
|
3359
|
+
* contexts (i.e. popups) to be accelerated, as it's not possible to sync
|
|
3360
|
+
* these animations properly with those driven from the main window
|
|
3361
|
+
* frameloop.
|
|
3362
|
+
*/
|
|
3363
|
+
if (!(subject instanceof HTMLElement) &&
|
|
3364
|
+
!(subject instanceof SVGElement)) {
|
|
3365
|
+
return false;
|
|
3366
|
+
}
|
|
3367
|
+
const { onUpdate, transformTemplate } = motionValue.owner.getProps();
|
|
3368
|
+
return (supportsWaapi() &&
|
|
3369
|
+
/**
|
|
3370
|
+
* Force WAAPI for color properties with browser-only color formats
|
|
3371
|
+
* (oklch, oklab, lab, lch, etc.) that the JS animation path can't parse.
|
|
3372
|
+
*/
|
|
3373
|
+
(acceleratedValues.has(name) ||
|
|
3374
|
+
(colorProperties.has(name) &&
|
|
3375
|
+
hasBrowserOnlyColors(keyframes))) &&
|
|
3376
|
+
(name !== "transform" || !transformTemplate) &&
|
|
3377
|
+
/**
|
|
3378
|
+
* If we're outputting values to onUpdate then we can't use WAAPI as there's
|
|
3379
|
+
* no way to read the value from WAAPI every frame.
|
|
3380
|
+
*/
|
|
3381
|
+
!onUpdate &&
|
|
3382
|
+
!repeatDelay &&
|
|
3383
|
+
repeatType !== "mirror" &&
|
|
3384
|
+
damping !== 0 &&
|
|
3385
|
+
type !== "inertia");
|
|
3386
|
+
}
|
|
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
|
+
};
|
|
3275
4011
|
/**
|
|
3276
|
-
*
|
|
3277
|
-
*
|
|
4012
|
+
* Replace the WAAPI animations with ones built from now. Returns
|
|
4013
|
+
* undefined if the animations can't be composed.
|
|
3278
4014
|
*/
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
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
|
+
};
|
|
3284
4125
|
/**
|
|
3285
|
-
*
|
|
3286
|
-
*
|
|
3287
|
-
*
|
|
3288
|
-
* these animations properly with those driven from the main window
|
|
3289
|
-
* frameloop.
|
|
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.
|
|
3290
4129
|
*/
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
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();
|
|
3297
4184
|
/**
|
|
3298
|
-
*
|
|
3299
|
-
*
|
|
4185
|
+
* Finish the animations that have reached their end, which the
|
|
4186
|
+
* WAAPI animation has already rendered.
|
|
3300
4187
|
*/
|
|
3301
|
-
(
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
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
|
+
}
|
|
3305
4222
|
/**
|
|
3306
|
-
*
|
|
3307
|
-
*
|
|
4223
|
+
* Everything that kept the group on the main thread has finished,
|
|
4224
|
+
* so move the remaining animations back to the compositor.
|
|
3308
4225
|
*/
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
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;
|
|
3314
4295
|
}
|
|
3315
4296
|
|
|
3316
4297
|
class AsyncMotionValueAnimation extends WithPromise {
|
|
@@ -3382,6 +4363,11 @@
|
|
|
3382
4363
|
* values may not be valid CSS and would trigger browser warnings.
|
|
3383
4364
|
*/
|
|
3384
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);
|
|
3385
4371
|
/**
|
|
3386
4372
|
* Resolve startTime for the animation. A startTime passed in options
|
|
3387
4373
|
* (an optimised appear handoff syncing to its WAAPI animation) takes
|
|
@@ -3397,7 +4383,10 @@
|
|
|
3397
4383
|
* animation started in that moment stays in sync. WAAPI has no frame
|
|
3398
4384
|
* of ours to do that on.
|
|
3399
4385
|
*/
|
|
3400
|
-
if (sync &&
|
|
4386
|
+
if (sync &&
|
|
4387
|
+
(useWaapi ||
|
|
4388
|
+
isGroupedTransform ||
|
|
4389
|
+
this.resolvedAt !== this.createdAt)) {
|
|
3401
4390
|
options.startTime ?? (options.startTime = resolveStartTime(this.createdAt, this.resolvedAt));
|
|
3402
4391
|
}
|
|
3403
4392
|
let animation;
|
|
@@ -3415,6 +4404,9 @@
|
|
|
3415
4404
|
animation = new JSAnimation(options);
|
|
3416
4405
|
}
|
|
3417
4406
|
}
|
|
4407
|
+
else if (isGroupedTransform) {
|
|
4408
|
+
animation = new TransformAnimation(options);
|
|
4409
|
+
}
|
|
3418
4410
|
else {
|
|
3419
4411
|
animation = new JSAnimation(options);
|
|
3420
4412
|
}
|
|
@@ -3430,6 +4422,12 @@
|
|
|
3430
4422
|
then(onResolve, _onReject) {
|
|
3431
4423
|
return this.finished.finally(onResolve).then(() => { });
|
|
3432
4424
|
}
|
|
4425
|
+
/**
|
|
4426
|
+
* The value now, if the animation runs on the compositor.
|
|
4427
|
+
*/
|
|
4428
|
+
liveValue() {
|
|
4429
|
+
return this._animation?.liveValue?.();
|
|
4430
|
+
}
|
|
3433
4431
|
get animation() {
|
|
3434
4432
|
if (!this._animation) {
|
|
3435
4433
|
this.keyframeResolver?.resume();
|
|
@@ -3867,6 +4865,18 @@
|
|
|
3867
4865
|
if (collectMotionValues.current) {
|
|
3868
4866
|
collectMotionValues.current.push(this);
|
|
3869
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() {
|
|
3870
4880
|
return this.current;
|
|
3871
4881
|
}
|
|
3872
4882
|
/**
|
|
@@ -4093,6 +5103,7 @@
|
|
|
4093
5103
|
name,
|
|
4094
5104
|
motionValue: value,
|
|
4095
5105
|
element: isHandoff ? undefined : element,
|
|
5106
|
+
hasOnUpdate: !!valueTransition.onUpdate,
|
|
4096
5107
|
};
|
|
4097
5108
|
/**
|
|
4098
5109
|
* If there's no transition defined for this value, we can generate
|
|
@@ -4692,6 +5703,30 @@
|
|
|
4692
5703
|
constructor(unresolvedKeyframes, onComplete, name, motionValue, element) {
|
|
4693
5704
|
super(unresolvedKeyframes, onComplete, name, motionValue, element, true);
|
|
4694
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
|
+
}
|
|
4695
5730
|
readKeyframes() {
|
|
4696
5731
|
const { unresolvedKeyframes, element, name } = this;
|
|
4697
5732
|
if (!element || !element.current)
|
|
@@ -5035,15 +6070,6 @@
|
|
|
5035
6070
|
return isObject(element) && "ownerSVGElement" in element;
|
|
5036
6071
|
}
|
|
5037
6072
|
|
|
5038
|
-
/**
|
|
5039
|
-
* Provided a value and a ValueType, returns the value as that value type.
|
|
5040
|
-
*/
|
|
5041
|
-
const getValueAsType = (value, type) => {
|
|
5042
|
-
return type && typeof value === "number"
|
|
5043
|
-
? type.transform(value)
|
|
5044
|
-
: value;
|
|
5045
|
-
};
|
|
5046
|
-
|
|
5047
6073
|
function resolveElements(elementOrSelector, scope, selectorCache) {
|
|
5048
6074
|
if (elementOrSelector == null) {
|
|
5049
6075
|
return [];
|
|
@@ -5217,18 +6243,12 @@
|
|
|
5217
6243
|
});
|
|
5218
6244
|
}
|
|
5219
6245
|
|
|
5220
|
-
const translateAlias$1 = {
|
|
5221
|
-
x: "translateX",
|
|
5222
|
-
y: "translateY",
|
|
5223
|
-
z: "translateZ",
|
|
5224
|
-
transformPerspective: "perspective",
|
|
5225
|
-
};
|
|
5226
6246
|
/**
|
|
5227
6247
|
* Cache of `translateX(`-style openers so each render is a few string
|
|
5228
6248
|
* concatenations rather than a template per transform.
|
|
5229
6249
|
*/
|
|
5230
6250
|
const openers = {};
|
|
5231
|
-
function buildTransform
|
|
6251
|
+
function buildTransform(state) {
|
|
5232
6252
|
let transform = "";
|
|
5233
6253
|
const { transformKeys: keys = [], transformValues: values = {} } = state;
|
|
5234
6254
|
/**
|
|
@@ -5237,7 +6257,7 @@
|
|
|
5237
6257
|
*/
|
|
5238
6258
|
for (let i = 0; i < keys.length; i++) {
|
|
5239
6259
|
const key = keys[i];
|
|
5240
|
-
const value = values[key].
|
|
6260
|
+
const value = values[key].getCurrent();
|
|
5241
6261
|
if (value === undefined)
|
|
5242
6262
|
continue;
|
|
5243
6263
|
const parsed = typeof value === "number" ? value : parseFloat(value);
|
|
@@ -5245,7 +6265,7 @@
|
|
|
5245
6265
|
transform +=
|
|
5246
6266
|
(transform && " ") +
|
|
5247
6267
|
(openers[key] ||
|
|
5248
|
-
(openers[key] = (translateAlias
|
|
6268
|
+
(openers[key] = (translateAlias[key] || key) + "(")) +
|
|
5249
6269
|
getValueAsType(value, transformValueTypes[key]) +
|
|
5250
6270
|
")";
|
|
5251
6271
|
}
|
|
@@ -5288,7 +6308,8 @@
|
|
|
5288
6308
|
addStyleValue(element, state, "transformBox", new MotionValue("fill-box"));
|
|
5289
6309
|
}
|
|
5290
6310
|
state.set("transform", new MotionValue("none"), () => {
|
|
5291
|
-
|
|
6311
|
+
transformGroups.get(element)?.check();
|
|
6312
|
+
element.style.transform = buildTransform(state);
|
|
5292
6313
|
});
|
|
5293
6314
|
}
|
|
5294
6315
|
computed = state.get("transform");
|
|
@@ -5306,12 +6327,12 @@
|
|
|
5306
6327
|
}
|
|
5307
6328
|
else if (isCSSVar(key)) {
|
|
5308
6329
|
render = () => {
|
|
5309
|
-
element.style.setProperty(key, value.
|
|
6330
|
+
element.style.setProperty(key, value.getCurrent());
|
|
5310
6331
|
};
|
|
5311
6332
|
}
|
|
5312
6333
|
else {
|
|
5313
6334
|
render = () => {
|
|
5314
|
-
element.style[key] = getValueAsType(value.
|
|
6335
|
+
element.style[key] = getValueAsType(value.getCurrent(), numberValueTypes[key]);
|
|
5315
6336
|
};
|
|
5316
6337
|
}
|
|
5317
6338
|
return state.set(key, value, render, computed);
|
|
@@ -5343,73 +6364,8 @@
|
|
|
5343
6364
|
test: isStyleSubject,
|
|
5344
6365
|
read: readStyleValue,
|
|
5345
6366
|
});
|
|
5346
|
-
const styleEffect =
|
|
5347
|
-
|
|
5348
|
-
const translateAlias = {
|
|
5349
|
-
x: "translateX",
|
|
5350
|
-
y: "translateY",
|
|
5351
|
-
z: "translateZ",
|
|
5352
|
-
transformPerspective: "perspective",
|
|
5353
|
-
};
|
|
5354
|
-
const numTransforms = transformPropOrder.length;
|
|
5355
|
-
/**
|
|
5356
|
-
* Build a CSS transform style from individual x/y/scale etc properties.
|
|
5357
|
-
*
|
|
5358
|
-
* This outputs with a default order of transforms/scales/rotations, this can be customised by
|
|
5359
|
-
* providing a transformTemplate function.
|
|
5360
|
-
*/
|
|
5361
|
-
function buildTransform(latestValues, transform, transformTemplate) {
|
|
5362
|
-
// The transform string we're going to build into.
|
|
5363
|
-
let transformString = "";
|
|
5364
|
-
let transformIsDefault = true;
|
|
5365
|
-
/**
|
|
5366
|
-
* Loop over all possible transforms in order, adding the ones that
|
|
5367
|
-
* are present to the transform string.
|
|
5368
|
-
*/
|
|
5369
|
-
for (let i = 0; i < numTransforms; i++) {
|
|
5370
|
-
const key = transformPropOrder[i];
|
|
5371
|
-
const value = latestValues[key];
|
|
5372
|
-
if (value === undefined)
|
|
5373
|
-
continue;
|
|
5374
|
-
let valueIsDefault = true;
|
|
5375
|
-
if (typeof value === "number") {
|
|
5376
|
-
valueIsDefault = value === (key.startsWith("scale") ? 1 : 0);
|
|
5377
|
-
}
|
|
5378
|
-
else {
|
|
5379
|
-
const parsed = parseFloat(value);
|
|
5380
|
-
valueIsDefault = key.startsWith("scale") ? parsed === 1 : parsed === 0;
|
|
5381
|
-
}
|
|
5382
|
-
if (!valueIsDefault || transformTemplate) {
|
|
5383
|
-
const valueAsType = getValueAsType(value, numberValueTypes[key]);
|
|
5384
|
-
if (!valueIsDefault) {
|
|
5385
|
-
transformIsDefault = false;
|
|
5386
|
-
const transformName = translateAlias[key] || key;
|
|
5387
|
-
transformString += `${transformName}(${valueAsType}) `;
|
|
5388
|
-
}
|
|
5389
|
-
if (transformTemplate) {
|
|
5390
|
-
transform[key] = valueAsType;
|
|
5391
|
-
}
|
|
5392
|
-
}
|
|
5393
|
-
}
|
|
5394
|
-
// `pathRotation` composes onto `rotate` as a separate additive term so
|
|
5395
|
-
// the user's `rotate` is never clobbered. Deliberately not a slot in
|
|
5396
|
-
// `transformPropOrder`.
|
|
5397
|
-
const pathRotation = latestValues.pathRotation;
|
|
5398
|
-
if (pathRotation) {
|
|
5399
|
-
transformIsDefault = false;
|
|
5400
|
-
transformString += `rotate(${getValueAsType(pathRotation, numberValueTypes.pathRotation)}) `;
|
|
5401
|
-
}
|
|
5402
|
-
transformString = transformString.trim();
|
|
5403
|
-
// If we have a custom `transform` template, pass our transform values and
|
|
5404
|
-
// generated transformString to that before returning
|
|
5405
|
-
if (transformTemplate) {
|
|
5406
|
-
transformString = transformTemplate(transform, transformIsDefault ? "" : transformString);
|
|
5407
|
-
}
|
|
5408
|
-
else if (transformIsDefault) {
|
|
5409
|
-
transformString = "none";
|
|
5410
|
-
}
|
|
5411
|
-
return transformString;
|
|
5412
|
-
}
|
|
6367
|
+
const styleEffect =
|
|
6368
|
+
/*@__PURE__*/ createSelectorEffect(styleSubjectEffect);
|
|
5413
6369
|
|
|
5414
6370
|
function buildHTMLStyles(state, latestValues, transformTemplate) {
|
|
5415
6371
|
const { style, vars, transformOrigin } = state;
|
|
@@ -5449,7 +6405,7 @@
|
|
|
5449
6405
|
}
|
|
5450
6406
|
if (!latestValues.transform) {
|
|
5451
6407
|
if (hasTransform || transformTemplate) {
|
|
5452
|
-
style.transform = buildTransform(latestValues, state.transform, transformTemplate);
|
|
6408
|
+
style.transform = buildTransform$1(latestValues, state.transform, transformTemplate);
|
|
5453
6409
|
}
|
|
5454
6410
|
else if (style.transform) {
|
|
5455
6411
|
/**
|
|
@@ -8353,8 +9309,11 @@
|
|
|
8353
9309
|
const removeOnChange = value.on("change", (latestValue) => {
|
|
8354
9310
|
this.latestValues[key] = latestValue;
|
|
8355
9311
|
this.props.onUpdate && frame.preRender(this.notifyUpdate);
|
|
8356
|
-
if (valueIsTransform
|
|
8357
|
-
this.projection
|
|
9312
|
+
if (valueIsTransform) {
|
|
9313
|
+
if (this.projection) {
|
|
9314
|
+
this.projection.isTransformDirty = true;
|
|
9315
|
+
}
|
|
9316
|
+
transformGroups.get(this.current)?.check();
|
|
8358
9317
|
}
|
|
8359
9318
|
this.scheduleRender();
|
|
8360
9319
|
});
|
|
@@ -8438,9 +9397,18 @@
|
|
|
8438
9397
|
* added to our map, old ones removed, and listeners updated.
|
|
8439
9398
|
*/
|
|
8440
9399
|
update(props, presenceContext) {
|
|
8441
|
-
|
|
9400
|
+
const { transformTemplate, onUpdate } = this.props;
|
|
9401
|
+
if (props.transformTemplate || transformTemplate) {
|
|
8442
9402
|
this.scheduleRender();
|
|
8443
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
|
+
}
|
|
8444
9412
|
this.prevProps = this.props;
|
|
8445
9413
|
this.props = props;
|
|
8446
9414
|
this.prevPresenceContext = this.presenceContext;
|
|
@@ -9735,6 +10703,7 @@
|
|
|
9735
10703
|
}
|
|
9736
10704
|
}
|
|
9737
10705
|
}
|
|
10706
|
+
const demoteTransforms = ({ instance }) => instance && transformGroups.get(instance)?.demote();
|
|
9738
10707
|
function cancelTreeOptimisedTransformAnimations(projectionNode) {
|
|
9739
10708
|
projectionNode.hasCheckedOptimisedAppear = true;
|
|
9740
10709
|
if (projectionNode.root === projectionNode)
|
|
@@ -10096,10 +11065,17 @@
|
|
|
10096
11065
|
!this.root.isUpdating && this.root.startUpdate();
|
|
10097
11066
|
if (this.isLayoutDirty)
|
|
10098
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);
|
|
10099
11074
|
this.isLayoutDirty = true;
|
|
10100
11075
|
for (let i = 0; i < this.path.length; i++) {
|
|
10101
11076
|
const node = this.path[i];
|
|
10102
11077
|
node.shouldResetTransform = true;
|
|
11078
|
+
demoteTransforms(node);
|
|
10103
11079
|
/**
|
|
10104
11080
|
* Percentage translates resolve against layoutBox dimensions,
|
|
10105
11081
|
* so ancestors with them must be re-measured after transform reset.
|
|
@@ -10302,6 +11278,7 @@
|
|
|
10302
11278
|
const isResetRequested = this.isLayoutDirty ||
|
|
10303
11279
|
this.shouldResetTransform ||
|
|
10304
11280
|
this.options.alwaysMeasureLayout;
|
|
11281
|
+
isResetRequested && demoteTransforms(this);
|
|
10305
11282
|
const hasProjection = this.projectionDelta && !isDeltaZero(this.projectionDelta);
|
|
10306
11283
|
const transformTemplate = this.getTransformTemplate();
|
|
10307
11284
|
const transformTemplateValue = transformTemplate
|
|
@@ -13227,7 +14204,7 @@
|
|
|
13227
14204
|
exports.buildProjectionTransform = buildProjectionTransform;
|
|
13228
14205
|
exports.buildSVGAttrs = buildSVGAttrs;
|
|
13229
14206
|
exports.buildSVGPath = buildSVGPath;
|
|
13230
|
-
exports.buildTransform = buildTransform;
|
|
14207
|
+
exports.buildTransform = buildTransform$1;
|
|
13231
14208
|
exports.calcAxisDelta = calcAxisDelta;
|
|
13232
14209
|
exports.calcBoxDelta = calcBoxDelta;
|
|
13233
14210
|
exports.calcChildStagger = calcChildStagger;
|
|
@@ -13429,6 +14406,8 @@
|
|
|
13429
14406
|
exports.rgbUnit = rgbUnit;
|
|
13430
14407
|
exports.rgba = rgba;
|
|
13431
14408
|
exports.rootProjectionNode = rootProjectionNode;
|
|
14409
|
+
exports.round = round;
|
|
14410
|
+
exports.sampleDelta = sampleDelta;
|
|
13432
14411
|
exports.scale = scale;
|
|
13433
14412
|
exports.scaleCorrectors = scaleCorrectors;
|
|
13434
14413
|
exports.scalePoint = scalePoint;
|
|
@@ -13456,6 +14435,7 @@
|
|
|
13456
14435
|
exports.supportsPartialKeyframes = supportsPartialKeyframes;
|
|
13457
14436
|
exports.supportsScrollTimeline = supportsScrollTimeline;
|
|
13458
14437
|
exports.supportsViewTimeline = supportsViewTimeline;
|
|
14438
|
+
exports.supportsWaapi = supportsWaapi;
|
|
13459
14439
|
exports.svgEffect = svgEffect;
|
|
13460
14440
|
exports.svgSubjectEffect = svgSubjectEffect;
|
|
13461
14441
|
exports.sync = sync;
|