motion 12.39.0 → 12.41.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/README.md +2 -2
- package/dist/cjs/react.js +35 -11
- package/dist/es/react.mjs +16 -1
- package/dist/motion.dev.js +1671 -862
- package/dist/motion.js +1 -1
- package/dist/react.d.ts +6 -1
- package/package.json +3 -3
package/dist/motion.dev.js
CHANGED
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/*#__NO_SIDE_EFFECTS__*/
|
|
82
|
-
const noop
|
|
82
|
+
const noop = (any) => any;
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* Pipe
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
function cubicBezier(mX1, mY1, mX2, mY2) {
|
|
217
217
|
// If this is a linear gradient, return linear easing
|
|
218
218
|
if (mX1 === mY1 && mX2 === mY2)
|
|
219
|
-
return noop
|
|
219
|
+
return noop;
|
|
220
220
|
const getTForX = (aX) => binarySubdivide(aX, 0, 1, mX1, mX2);
|
|
221
221
|
// If animation is at start/end, return t without easing
|
|
222
222
|
return (t) => t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2);
|
|
@@ -277,7 +277,7 @@
|
|
|
277
277
|
const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
|
|
278
278
|
|
|
279
279
|
const easingLookup = {
|
|
280
|
-
linear: noop
|
|
280
|
+
linear: noop,
|
|
281
281
|
easeIn,
|
|
282
282
|
easeInOut,
|
|
283
283
|
easeOut,
|
|
@@ -318,12 +318,7 @@
|
|
|
318
318
|
"postRender", // Compute
|
|
319
319
|
];
|
|
320
320
|
|
|
321
|
-
|
|
322
|
-
value: null,
|
|
323
|
-
addProjectionMetrics: null,
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
function createRenderStep(runNextFrame, stepName) {
|
|
321
|
+
function createRenderStep(runNextFrame) {
|
|
327
322
|
/**
|
|
328
323
|
* We create and reuse two queues, one to queue jobs for the current frame
|
|
329
324
|
* and one for the next. We reuse to avoid triggering GC after x frames.
|
|
@@ -345,13 +340,11 @@
|
|
|
345
340
|
timestamp: 0.0,
|
|
346
341
|
isProcessing: false,
|
|
347
342
|
};
|
|
348
|
-
let numCalls = 0;
|
|
349
343
|
function triggerCallback(callback) {
|
|
350
344
|
if (toKeepAlive.has(callback)) {
|
|
351
345
|
step.schedule(callback);
|
|
352
346
|
runNextFrame();
|
|
353
347
|
}
|
|
354
|
-
numCalls++;
|
|
355
348
|
callback(latestFrameData);
|
|
356
349
|
}
|
|
357
350
|
const step = {
|
|
@@ -394,13 +387,6 @@
|
|
|
394
387
|
nextFrame = prevFrame;
|
|
395
388
|
// Execute this frame
|
|
396
389
|
thisFrame.forEach(triggerCallback);
|
|
397
|
-
/**
|
|
398
|
-
* If we're recording stats then
|
|
399
|
-
*/
|
|
400
|
-
if (stepName && statsBuffer.value) {
|
|
401
|
-
statsBuffer.value.frameloop[stepName].push(numCalls);
|
|
402
|
-
}
|
|
403
|
-
numCalls = 0;
|
|
404
390
|
// Clear the frame so no callbacks remain. This is to avoid
|
|
405
391
|
// memory leaks should this render step not run for a while.
|
|
406
392
|
thisFrame.clear();
|
|
@@ -425,7 +411,7 @@
|
|
|
425
411
|
};
|
|
426
412
|
const flagRunNextFrame = () => (runNextFrame = true);
|
|
427
413
|
const steps = stepsOrder.reduce((acc, key) => {
|
|
428
|
-
acc[key] = createRenderStep(flagRunNextFrame
|
|
414
|
+
acc[key] = createRenderStep(flagRunNextFrame);
|
|
429
415
|
return acc;
|
|
430
416
|
}, {});
|
|
431
417
|
const { setup, read, resolveKeyframes, preUpdate, update, preRender, render, postRender, } = steps;
|
|
@@ -481,7 +467,7 @@
|
|
|
481
467
|
return { schedule, cancel, state, steps };
|
|
482
468
|
}
|
|
483
469
|
|
|
484
|
-
const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop
|
|
470
|
+
const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true);
|
|
485
471
|
|
|
486
472
|
let now;
|
|
487
473
|
function clearTime() {
|
|
@@ -510,12 +496,6 @@
|
|
|
510
496
|
},
|
|
511
497
|
};
|
|
512
498
|
|
|
513
|
-
const activeAnimations = {
|
|
514
|
-
layout: 0,
|
|
515
|
-
mainThread: 0,
|
|
516
|
-
waapi: 0,
|
|
517
|
-
};
|
|
518
|
-
|
|
519
499
|
const checkStringStartsWith = (token) => (key) => typeof key === "string" && key.startsWith(token);
|
|
520
500
|
const isCSSVariableName =
|
|
521
501
|
/*@__PURE__*/ checkStringStartsWith("--");
|
|
@@ -1503,7 +1483,7 @@
|
|
|
1503
1483
|
for (let i = 0; i < numMixers; i++) {
|
|
1504
1484
|
let mixer = mixerFactory(output[i], output[i + 1]);
|
|
1505
1485
|
if (ease) {
|
|
1506
|
-
const easingFunction = Array.isArray(ease) ? ease[i] || noop
|
|
1486
|
+
const easingFunction = Array.isArray(ease) ? ease[i] || noop : ease;
|
|
1507
1487
|
mixer = pipe(easingFunction, mixer);
|
|
1508
1488
|
}
|
|
1509
1489
|
mixers.push(mixer);
|
|
@@ -1717,7 +1697,6 @@
|
|
|
1717
1697
|
this.teardown();
|
|
1718
1698
|
this.options.onStop?.();
|
|
1719
1699
|
};
|
|
1720
|
-
activeAnimations.mainThread++;
|
|
1721
1700
|
this.options = options;
|
|
1722
1701
|
this.initAnimation();
|
|
1723
1702
|
this.play();
|
|
@@ -2022,7 +2001,6 @@
|
|
|
2022
2001
|
this.state = "idle";
|
|
2023
2002
|
this.stopDriver();
|
|
2024
2003
|
this.startTime = this.holdTime = null;
|
|
2025
|
-
activeAnimations.mainThread--;
|
|
2026
2004
|
}
|
|
2027
2005
|
stopDriver() {
|
|
2028
2006
|
if (!this.driver)
|
|
@@ -2161,8 +2139,15 @@
|
|
|
2161
2139
|
];
|
|
2162
2140
|
/**
|
|
2163
2141
|
* A quick lookup for transform props.
|
|
2142
|
+
*
|
|
2143
|
+
* `pathRotation` is a transform for routing purposes (skipped from raw
|
|
2144
|
+
* style application, wired to the transform composite, flags transform
|
|
2145
|
+
* dirty) but is intentionally NOT in `transformPropOrder` — it is
|
|
2146
|
+
* composed onto `rotate` at the build sites, not serialized in its own
|
|
2147
|
+
* slot, and must stay out of the order-array consumers (parse-transform,
|
|
2148
|
+
* unit-conversion, keys-position).
|
|
2164
2149
|
*/
|
|
2165
|
-
const transformProps = /*@__PURE__*/ (() => new Set(transformPropOrder))();
|
|
2150
|
+
const transformProps = /*@__PURE__*/ (() => new Set([...transformPropOrder, "pathRotation"]))();
|
|
2166
2151
|
|
|
2167
2152
|
const isNumOrPxType = (v) => v === number || v === px;
|
|
2168
2153
|
const transformKeys = new Set(["x", "y", "z"]);
|
|
@@ -2427,9 +2412,6 @@
|
|
|
2427
2412
|
*/
|
|
2428
2413
|
if (Array.isArray(easing))
|
|
2429
2414
|
keyframeOptions.easing = easing;
|
|
2430
|
-
if (statsBuffer.value) {
|
|
2431
|
-
activeAnimations.waapi++;
|
|
2432
|
-
}
|
|
2433
2415
|
const options = {
|
|
2434
2416
|
delay,
|
|
2435
2417
|
duration,
|
|
@@ -2440,13 +2422,7 @@
|
|
|
2440
2422
|
};
|
|
2441
2423
|
if (pseudoElement)
|
|
2442
2424
|
options.pseudoElement = pseudoElement;
|
|
2443
|
-
|
|
2444
|
-
if (statsBuffer.value) {
|
|
2445
|
-
animation.finished.finally(() => {
|
|
2446
|
-
activeAnimations.waapi--;
|
|
2447
|
-
});
|
|
2448
|
-
}
|
|
2449
|
-
return animation;
|
|
2425
|
+
return element.animate(keyframeOptions, options);
|
|
2450
2426
|
}
|
|
2451
2427
|
|
|
2452
2428
|
function isGenerator(type) {
|
|
@@ -2625,7 +2601,7 @@
|
|
|
2625
2601
|
this.animation.rangeStart = rangeStart;
|
|
2626
2602
|
if (rangeEnd)
|
|
2627
2603
|
this.animation.rangeEnd = rangeEnd;
|
|
2628
|
-
return noop
|
|
2604
|
+
return noop;
|
|
2629
2605
|
}
|
|
2630
2606
|
else {
|
|
2631
2607
|
return observe(this);
|
|
@@ -2978,7 +2954,7 @@
|
|
|
2978
2954
|
}
|
|
2979
2955
|
animation.finished.then(() => {
|
|
2980
2956
|
this.notifyFinished();
|
|
2981
|
-
}).catch(noop
|
|
2957
|
+
}).catch(noop);
|
|
2982
2958
|
if (this.pendingTimeline) {
|
|
2983
2959
|
this.stopTimeline = animation.attachTimeline(this.pendingTimeline);
|
|
2984
2960
|
this.pendingTimeline = undefined;
|
|
@@ -3176,342 +3152,77 @@
|
|
|
3176
3152
|
}
|
|
3177
3153
|
|
|
3178
3154
|
/**
|
|
3179
|
-
*
|
|
3180
|
-
*
|
|
3181
|
-
* ```
|
|
3182
|
-
* `var(--foo, #fff)` => [`--foo`, '#fff']
|
|
3183
|
-
* ```
|
|
3184
|
-
*
|
|
3185
|
-
* @param current
|
|
3186
|
-
*/
|
|
3187
|
-
const splitCSSVariableRegex =
|
|
3188
|
-
// eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive, as it can match a lot of words
|
|
3189
|
-
/^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u;
|
|
3190
|
-
function parseCSSVariable(current) {
|
|
3191
|
-
const match = splitCSSVariableRegex.exec(current);
|
|
3192
|
-
if (!match)
|
|
3193
|
-
return [,];
|
|
3194
|
-
const [, token1, token2, fallback] = match;
|
|
3195
|
-
return [`--${token1 ?? token2}`, fallback];
|
|
3196
|
-
}
|
|
3197
|
-
const maxDepth = 4;
|
|
3198
|
-
function getVariableValue(current, element, depth = 1) {
|
|
3199
|
-
exports.invariant(depth <= maxDepth, `Max CSS variable fallback depth detected in property "${current}". This may indicate a circular fallback dependency.`, "max-css-var-depth");
|
|
3200
|
-
const [token, fallback] = parseCSSVariable(current);
|
|
3201
|
-
// No CSS variable detected
|
|
3202
|
-
if (!token)
|
|
3203
|
-
return;
|
|
3204
|
-
// Attempt to read this CSS variable off the element
|
|
3205
|
-
const resolved = window.getComputedStyle(element).getPropertyValue(token);
|
|
3206
|
-
if (resolved) {
|
|
3207
|
-
const trimmed = resolved.trim();
|
|
3208
|
-
return isNumericalString(trimmed) ? parseFloat(trimmed) : trimmed;
|
|
3209
|
-
}
|
|
3210
|
-
return isCSSVariableToken(fallback)
|
|
3211
|
-
? getVariableValue(fallback, element, depth + 1)
|
|
3212
|
-
: fallback;
|
|
3213
|
-
}
|
|
3214
|
-
|
|
3215
|
-
const underDampedSpring = {
|
|
3216
|
-
type: "spring",
|
|
3217
|
-
stiffness: 500,
|
|
3218
|
-
damping: 25,
|
|
3219
|
-
restSpeed: 10,
|
|
3220
|
-
};
|
|
3221
|
-
const criticallyDampedSpring = (target) => ({
|
|
3222
|
-
type: "spring",
|
|
3223
|
-
stiffness: 550,
|
|
3224
|
-
damping: target === 0 ? 2 * Math.sqrt(550) : 30,
|
|
3225
|
-
restSpeed: 10,
|
|
3226
|
-
});
|
|
3227
|
-
const keyframesTransition = {
|
|
3228
|
-
type: "keyframes",
|
|
3229
|
-
duration: 0.8,
|
|
3230
|
-
};
|
|
3231
|
-
/**
|
|
3232
|
-
* Default easing curve is a slightly shallower version of
|
|
3233
|
-
* the default browser easing curve.
|
|
3155
|
+
* Maximum time between the value of two frames, beyond which we
|
|
3156
|
+
* assume the velocity has since been 0.
|
|
3234
3157
|
*/
|
|
3235
|
-
const
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
duration: 0.3,
|
|
3158
|
+
const MAX_VELOCITY_DELTA = 30;
|
|
3159
|
+
const isFloat = (value) => {
|
|
3160
|
+
return !isNaN(parseFloat(value));
|
|
3239
3161
|
};
|
|
3240
|
-
const
|
|
3241
|
-
|
|
3242
|
-
return keyframesTransition;
|
|
3243
|
-
}
|
|
3244
|
-
else if (transformProps.has(valueKey)) {
|
|
3245
|
-
return valueKey.startsWith("scale")
|
|
3246
|
-
? criticallyDampedSpring(keyframes[1])
|
|
3247
|
-
: underDampedSpring;
|
|
3248
|
-
}
|
|
3249
|
-
return ease;
|
|
3162
|
+
const collectMotionValues = {
|
|
3163
|
+
current: undefined,
|
|
3250
3164
|
};
|
|
3251
|
-
|
|
3252
|
-
/**
|
|
3253
|
-
* If `transition` has `inherit: true`, shallow-merge it with
|
|
3254
|
-
* `parentTransition` (child keys win) and strip the `inherit` key.
|
|
3255
|
-
* Otherwise return `transition` unchanged.
|
|
3256
|
-
*/
|
|
3257
|
-
function resolveTransition(transition, parentTransition) {
|
|
3258
|
-
if (transition?.inherit && parentTransition) {
|
|
3259
|
-
const { inherit: _, ...rest } = transition;
|
|
3260
|
-
return { ...parentTransition, ...rest };
|
|
3261
|
-
}
|
|
3262
|
-
return transition;
|
|
3263
|
-
}
|
|
3264
|
-
|
|
3265
|
-
function getValueTransition$1(transition, key) {
|
|
3266
|
-
const valueTransition = transition?.[key] ??
|
|
3267
|
-
transition?.["default"] ??
|
|
3268
|
-
transition;
|
|
3269
|
-
if (valueTransition !== transition) {
|
|
3270
|
-
return resolveTransition(valueTransition, transition);
|
|
3271
|
-
}
|
|
3272
|
-
return valueTransition;
|
|
3273
|
-
}
|
|
3274
|
-
|
|
3275
|
-
const orchestrationKeys = new Set([
|
|
3276
|
-
"when",
|
|
3277
|
-
"delay",
|
|
3278
|
-
"delayChildren",
|
|
3279
|
-
"staggerChildren",
|
|
3280
|
-
"staggerDirection",
|
|
3281
|
-
"repeat",
|
|
3282
|
-
"repeatType",
|
|
3283
|
-
"repeatDelay",
|
|
3284
|
-
"from",
|
|
3285
|
-
"elapsed",
|
|
3286
|
-
]);
|
|
3287
3165
|
/**
|
|
3288
|
-
*
|
|
3289
|
-
*
|
|
3290
|
-
*
|
|
3166
|
+
* `MotionValue` is used to track the state and velocity of motion values.
|
|
3167
|
+
*
|
|
3168
|
+
* @public
|
|
3291
3169
|
*/
|
|
3292
|
-
|
|
3293
|
-
for (const key in transition) {
|
|
3294
|
-
if (!orchestrationKeys.has(key))
|
|
3295
|
-
return true;
|
|
3296
|
-
}
|
|
3297
|
-
return false;
|
|
3298
|
-
}
|
|
3299
|
-
|
|
3300
|
-
const animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => {
|
|
3301
|
-
const valueTransition = getValueTransition$1(transition, name) || {};
|
|
3302
|
-
/**
|
|
3303
|
-
* Most transition values are currently completely overwritten by value-specific
|
|
3304
|
-
* transitions. In the future it'd be nicer to blend these transitions. But for now
|
|
3305
|
-
* delay actually does inherit from the root transition if not value-specific.
|
|
3306
|
-
*/
|
|
3307
|
-
const delay = valueTransition.delay || transition.delay || 0;
|
|
3308
|
-
/**
|
|
3309
|
-
* Elapsed isn't a public transition option but can be passed through from
|
|
3310
|
-
* optimized appear effects in milliseconds.
|
|
3311
|
-
*/
|
|
3312
|
-
let { elapsed = 0 } = transition;
|
|
3313
|
-
elapsed = elapsed - secondsToMilliseconds(delay);
|
|
3314
|
-
const options = {
|
|
3315
|
-
keyframes: Array.isArray(target) ? target : [null, target],
|
|
3316
|
-
ease: "easeOut",
|
|
3317
|
-
velocity: value.getVelocity(),
|
|
3318
|
-
...valueTransition,
|
|
3319
|
-
delay: -elapsed,
|
|
3320
|
-
onUpdate: (v) => {
|
|
3321
|
-
value.set(v);
|
|
3322
|
-
valueTransition.onUpdate && valueTransition.onUpdate(v);
|
|
3323
|
-
},
|
|
3324
|
-
onComplete: () => {
|
|
3325
|
-
onComplete();
|
|
3326
|
-
valueTransition.onComplete && valueTransition.onComplete();
|
|
3327
|
-
},
|
|
3328
|
-
name,
|
|
3329
|
-
motionValue: value,
|
|
3330
|
-
element: isHandoff ? undefined : element,
|
|
3331
|
-
};
|
|
3332
|
-
/**
|
|
3333
|
-
* If there's no transition defined for this value, we can generate
|
|
3334
|
-
* unique transition settings for this value.
|
|
3335
|
-
*/
|
|
3336
|
-
if (!isTransitionDefined(valueTransition)) {
|
|
3337
|
-
Object.assign(options, getDefaultTransition(name, options));
|
|
3338
|
-
}
|
|
3339
|
-
/**
|
|
3340
|
-
* Both WAAPI and our internal animation functions use durations
|
|
3341
|
-
* as defined by milliseconds, while our external API defines them
|
|
3342
|
-
* as seconds.
|
|
3343
|
-
*/
|
|
3344
|
-
options.duration && (options.duration = secondsToMilliseconds(options.duration));
|
|
3345
|
-
options.repeatDelay && (options.repeatDelay = secondsToMilliseconds(options.repeatDelay));
|
|
3170
|
+
class MotionValue {
|
|
3346
3171
|
/**
|
|
3347
|
-
*
|
|
3172
|
+
* @param init - The initiating value
|
|
3173
|
+
* @param config - Optional configuration options
|
|
3174
|
+
*
|
|
3175
|
+
* - `transformer`: A function to transform incoming values with.
|
|
3348
3176
|
*/
|
|
3349
|
-
|
|
3350
|
-
|
|
3177
|
+
constructor(init, options = {}) {
|
|
3178
|
+
/**
|
|
3179
|
+
* Tracks whether this value can output a velocity. Currently this is only true
|
|
3180
|
+
* if the value is numerical, but we might be able to widen the scope here and support
|
|
3181
|
+
* other value types.
|
|
3182
|
+
*
|
|
3183
|
+
* @internal
|
|
3184
|
+
*/
|
|
3185
|
+
this.canTrackVelocity = null;
|
|
3186
|
+
/**
|
|
3187
|
+
* An object containing a SubscriptionManager for each active event.
|
|
3188
|
+
*/
|
|
3189
|
+
this.events = {};
|
|
3190
|
+
this.updateAndNotify = (v) => {
|
|
3191
|
+
const currentTime = time.now();
|
|
3192
|
+
/**
|
|
3193
|
+
* If we're updating the value during another frame or eventloop
|
|
3194
|
+
* than the previous frame, then the we set the previous frame value
|
|
3195
|
+
* to current.
|
|
3196
|
+
*/
|
|
3197
|
+
if (this.updatedAt !== currentTime) {
|
|
3198
|
+
this.setPrevFrameValue();
|
|
3199
|
+
}
|
|
3200
|
+
this.prev = this.current;
|
|
3201
|
+
this.setCurrent(v);
|
|
3202
|
+
// Update update subscribers
|
|
3203
|
+
if (this.current !== this.prev) {
|
|
3204
|
+
this.events.change?.notify(this.current);
|
|
3205
|
+
if (this.dependents) {
|
|
3206
|
+
for (const dependent of this.dependents) {
|
|
3207
|
+
dependent.dirty();
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
};
|
|
3212
|
+
this.hasAnimated = false;
|
|
3213
|
+
this.setCurrent(init);
|
|
3214
|
+
this.owner = options.owner;
|
|
3351
3215
|
}
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
shouldSkip = true;
|
|
3216
|
+
setCurrent(current) {
|
|
3217
|
+
this.current = current;
|
|
3218
|
+
this.updatedAt = time.now();
|
|
3219
|
+
if (this.canTrackVelocity === null && current !== undefined) {
|
|
3220
|
+
this.canTrackVelocity = isFloat(this.current);
|
|
3358
3221
|
}
|
|
3359
3222
|
}
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
valueTransition.skipAnimations) {
|
|
3364
|
-
shouldSkip = true;
|
|
3365
|
-
makeAnimationInstant(options);
|
|
3366
|
-
options.delay = 0;
|
|
3367
|
-
}
|
|
3368
|
-
/**
|
|
3369
|
-
* If the transition type or easing has been explicitly set by the user
|
|
3370
|
-
* then we don't want to allow flattening the animation.
|
|
3371
|
-
*/
|
|
3372
|
-
options.allowFlatten = !valueTransition.type && !valueTransition.ease;
|
|
3373
|
-
/**
|
|
3374
|
-
* If we can or must skip creating the animation, and apply only
|
|
3375
|
-
* the final keyframe, do so. We also check once keyframes are resolved but
|
|
3376
|
-
* this early check prevents the need to create an animation at all.
|
|
3377
|
-
*/
|
|
3378
|
-
if (shouldSkip && !isHandoff && value.get() !== undefined) {
|
|
3379
|
-
const finalKeyframe = getFinalKeyframe(options.keyframes, valueTransition);
|
|
3380
|
-
if (finalKeyframe !== undefined) {
|
|
3381
|
-
frame.update(() => {
|
|
3382
|
-
options.onUpdate(finalKeyframe);
|
|
3383
|
-
options.onComplete();
|
|
3384
|
-
});
|
|
3385
|
-
return;
|
|
3386
|
-
}
|
|
3387
|
-
}
|
|
3388
|
-
return valueTransition.isSync
|
|
3389
|
-
? new JSAnimation(options)
|
|
3390
|
-
: new AsyncMotionValueAnimation(options);
|
|
3391
|
-
};
|
|
3392
|
-
|
|
3393
|
-
function getValueState(visualElement) {
|
|
3394
|
-
const state = [{}, {}];
|
|
3395
|
-
visualElement?.values.forEach((value, key) => {
|
|
3396
|
-
state[0][key] = value.get();
|
|
3397
|
-
state[1][key] = value.getVelocity();
|
|
3398
|
-
});
|
|
3399
|
-
return state;
|
|
3400
|
-
}
|
|
3401
|
-
function resolveVariantFromProps(props, definition, custom, visualElement) {
|
|
3402
|
-
/**
|
|
3403
|
-
* If the variant definition is a function, resolve.
|
|
3404
|
-
*/
|
|
3405
|
-
if (typeof definition === "function") {
|
|
3406
|
-
const [current, velocity] = getValueState(visualElement);
|
|
3407
|
-
definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
|
|
3408
|
-
}
|
|
3409
|
-
/**
|
|
3410
|
-
* If the variant definition is a variant label, or
|
|
3411
|
-
* the function returned a variant label, resolve.
|
|
3412
|
-
*/
|
|
3413
|
-
if (typeof definition === "string") {
|
|
3414
|
-
definition = props.variants && props.variants[definition];
|
|
3415
|
-
}
|
|
3416
|
-
/**
|
|
3417
|
-
* At this point we've resolved both functions and variant labels,
|
|
3418
|
-
* but the resolved variant label might itself have been a function.
|
|
3419
|
-
* If so, resolve. This can only have returned a valid target object.
|
|
3420
|
-
*/
|
|
3421
|
-
if (typeof definition === "function") {
|
|
3422
|
-
const [current, velocity] = getValueState(visualElement);
|
|
3423
|
-
definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
|
|
3424
|
-
}
|
|
3425
|
-
return definition;
|
|
3426
|
-
}
|
|
3427
|
-
|
|
3428
|
-
function resolveVariant(visualElement, definition, custom) {
|
|
3429
|
-
const props = visualElement.getProps();
|
|
3430
|
-
return resolveVariantFromProps(props, definition, custom !== undefined ? custom : props.custom, visualElement);
|
|
3431
|
-
}
|
|
3432
|
-
|
|
3433
|
-
const positionalKeys = new Set([
|
|
3434
|
-
"width",
|
|
3435
|
-
"height",
|
|
3436
|
-
"top",
|
|
3437
|
-
"left",
|
|
3438
|
-
"right",
|
|
3439
|
-
"bottom",
|
|
3440
|
-
...transformPropOrder,
|
|
3441
|
-
]);
|
|
3442
|
-
|
|
3443
|
-
/**
|
|
3444
|
-
* Maximum time between the value of two frames, beyond which we
|
|
3445
|
-
* assume the velocity has since been 0.
|
|
3446
|
-
*/
|
|
3447
|
-
const MAX_VELOCITY_DELTA = 30;
|
|
3448
|
-
const isFloat = (value) => {
|
|
3449
|
-
return !isNaN(parseFloat(value));
|
|
3450
|
-
};
|
|
3451
|
-
const collectMotionValues = {
|
|
3452
|
-
current: undefined,
|
|
3453
|
-
};
|
|
3454
|
-
/**
|
|
3455
|
-
* `MotionValue` is used to track the state and velocity of motion values.
|
|
3456
|
-
*
|
|
3457
|
-
* @public
|
|
3458
|
-
*/
|
|
3459
|
-
class MotionValue {
|
|
3460
|
-
/**
|
|
3461
|
-
* @param init - The initiating value
|
|
3462
|
-
* @param config - Optional configuration options
|
|
3463
|
-
*
|
|
3464
|
-
* - `transformer`: A function to transform incoming values with.
|
|
3465
|
-
*/
|
|
3466
|
-
constructor(init, options = {}) {
|
|
3467
|
-
/**
|
|
3468
|
-
* Tracks whether this value can output a velocity. Currently this is only true
|
|
3469
|
-
* if the value is numerical, but we might be able to widen the scope here and support
|
|
3470
|
-
* other value types.
|
|
3471
|
-
*
|
|
3472
|
-
* @internal
|
|
3473
|
-
*/
|
|
3474
|
-
this.canTrackVelocity = null;
|
|
3475
|
-
/**
|
|
3476
|
-
* An object containing a SubscriptionManager for each active event.
|
|
3477
|
-
*/
|
|
3478
|
-
this.events = {};
|
|
3479
|
-
this.updateAndNotify = (v) => {
|
|
3480
|
-
const currentTime = time.now();
|
|
3481
|
-
/**
|
|
3482
|
-
* If we're updating the value during another frame or eventloop
|
|
3483
|
-
* than the previous frame, then the we set the previous frame value
|
|
3484
|
-
* to current.
|
|
3485
|
-
*/
|
|
3486
|
-
if (this.updatedAt !== currentTime) {
|
|
3487
|
-
this.setPrevFrameValue();
|
|
3488
|
-
}
|
|
3489
|
-
this.prev = this.current;
|
|
3490
|
-
this.setCurrent(v);
|
|
3491
|
-
// Update update subscribers
|
|
3492
|
-
if (this.current !== this.prev) {
|
|
3493
|
-
this.events.change?.notify(this.current);
|
|
3494
|
-
if (this.dependents) {
|
|
3495
|
-
for (const dependent of this.dependents) {
|
|
3496
|
-
dependent.dirty();
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
|
-
}
|
|
3500
|
-
};
|
|
3501
|
-
this.hasAnimated = false;
|
|
3502
|
-
this.setCurrent(init);
|
|
3503
|
-
this.owner = options.owner;
|
|
3504
|
-
}
|
|
3505
|
-
setCurrent(current) {
|
|
3506
|
-
this.current = current;
|
|
3507
|
-
this.updatedAt = time.now();
|
|
3508
|
-
if (this.canTrackVelocity === null && current !== undefined) {
|
|
3509
|
-
this.canTrackVelocity = isFloat(this.current);
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
setPrevFrameValue(prevFrameValue = this.current) {
|
|
3513
|
-
this.prevFrameValue = prevFrameValue;
|
|
3514
|
-
this.prevUpdatedAt = this.updatedAt;
|
|
3223
|
+
setPrevFrameValue(prevFrameValue = this.current) {
|
|
3224
|
+
this.prevFrameValue = prevFrameValue;
|
|
3225
|
+
this.prevUpdatedAt = this.updatedAt;
|
|
3515
3226
|
}
|
|
3516
3227
|
/**
|
|
3517
3228
|
* Adds a function that will be notified when the `MotionValue` is updated.
|
|
@@ -3685,79 +3396,525 @@
|
|
|
3685
3396
|
return velocityPerSecond(parseFloat(this.current) -
|
|
3686
3397
|
parseFloat(this.prevFrameValue), delta);
|
|
3687
3398
|
}
|
|
3688
|
-
/**
|
|
3689
|
-
* Registers a new animation to control this `MotionValue`. Only one
|
|
3690
|
-
* animation can drive a `MotionValue` at one time.
|
|
3691
|
-
*
|
|
3692
|
-
* ```jsx
|
|
3693
|
-
* value.start()
|
|
3694
|
-
* ```
|
|
3695
|
-
*
|
|
3696
|
-
* @param animation - A function that starts the provided animation
|
|
3697
|
-
*/
|
|
3698
|
-
start(startAnimation) {
|
|
3699
|
-
this.stop();
|
|
3700
|
-
return new Promise((resolve) => {
|
|
3701
|
-
this.hasAnimated = true;
|
|
3702
|
-
this.animation = startAnimation(resolve);
|
|
3703
|
-
if (this.events.animationStart) {
|
|
3704
|
-
this.events.animationStart.notify();
|
|
3399
|
+
/**
|
|
3400
|
+
* Registers a new animation to control this `MotionValue`. Only one
|
|
3401
|
+
* animation can drive a `MotionValue` at one time.
|
|
3402
|
+
*
|
|
3403
|
+
* ```jsx
|
|
3404
|
+
* value.start()
|
|
3405
|
+
* ```
|
|
3406
|
+
*
|
|
3407
|
+
* @param animation - A function that starts the provided animation
|
|
3408
|
+
*/
|
|
3409
|
+
start(startAnimation) {
|
|
3410
|
+
this.stop();
|
|
3411
|
+
return new Promise((resolve) => {
|
|
3412
|
+
this.hasAnimated = true;
|
|
3413
|
+
this.animation = startAnimation(resolve);
|
|
3414
|
+
if (this.events.animationStart) {
|
|
3415
|
+
this.events.animationStart.notify();
|
|
3416
|
+
}
|
|
3417
|
+
}).then(() => {
|
|
3418
|
+
if (this.events.animationComplete) {
|
|
3419
|
+
this.events.animationComplete.notify();
|
|
3420
|
+
}
|
|
3421
|
+
this.clearAnimation();
|
|
3422
|
+
});
|
|
3423
|
+
}
|
|
3424
|
+
/**
|
|
3425
|
+
* Stop the currently active animation.
|
|
3426
|
+
*
|
|
3427
|
+
* @public
|
|
3428
|
+
*/
|
|
3429
|
+
stop() {
|
|
3430
|
+
if (this.animation) {
|
|
3431
|
+
this.animation.stop();
|
|
3432
|
+
if (this.events.animationCancel) {
|
|
3433
|
+
this.events.animationCancel.notify();
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
this.clearAnimation();
|
|
3437
|
+
}
|
|
3438
|
+
/**
|
|
3439
|
+
* Returns `true` if this value is currently animating.
|
|
3440
|
+
*
|
|
3441
|
+
* @public
|
|
3442
|
+
*/
|
|
3443
|
+
isAnimating() {
|
|
3444
|
+
return !!this.animation;
|
|
3445
|
+
}
|
|
3446
|
+
clearAnimation() {
|
|
3447
|
+
delete this.animation;
|
|
3448
|
+
}
|
|
3449
|
+
/**
|
|
3450
|
+
* Destroy and clean up subscribers to this `MotionValue`.
|
|
3451
|
+
*
|
|
3452
|
+
* The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically
|
|
3453
|
+
* handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually
|
|
3454
|
+
* created a `MotionValue` via the `motionValue` function.
|
|
3455
|
+
*
|
|
3456
|
+
* @public
|
|
3457
|
+
*/
|
|
3458
|
+
destroy() {
|
|
3459
|
+
this.dependents?.clear();
|
|
3460
|
+
this.events.destroy?.notify();
|
|
3461
|
+
this.clearListeners();
|
|
3462
|
+
this.stop();
|
|
3463
|
+
if (this.stopPassiveEffect) {
|
|
3464
|
+
this.stopPassiveEffect();
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
function motionValue(init, options) {
|
|
3469
|
+
return new MotionValue(init, options);
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
/**
|
|
3473
|
+
* If `transition` has `inherit: true`, shallow-merge it with
|
|
3474
|
+
* `parentTransition` (child keys win) and strip the `inherit` key.
|
|
3475
|
+
* Otherwise return `transition` unchanged.
|
|
3476
|
+
*/
|
|
3477
|
+
function resolveTransition(transition, parentTransition) {
|
|
3478
|
+
if (transition?.inherit && parentTransition) {
|
|
3479
|
+
const { inherit: _, ...rest } = transition;
|
|
3480
|
+
return { ...parentTransition, ...rest };
|
|
3481
|
+
}
|
|
3482
|
+
return transition;
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
function getValueTransition$1(transition, key) {
|
|
3486
|
+
const valueTransition = transition?.[key] ??
|
|
3487
|
+
transition?.["default"] ??
|
|
3488
|
+
transition;
|
|
3489
|
+
if (valueTransition !== transition) {
|
|
3490
|
+
return resolveTransition(valueTransition, transition);
|
|
3491
|
+
}
|
|
3492
|
+
return valueTransition;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
const underDampedSpring = {
|
|
3496
|
+
type: "spring",
|
|
3497
|
+
stiffness: 500,
|
|
3498
|
+
damping: 25,
|
|
3499
|
+
restSpeed: 10,
|
|
3500
|
+
};
|
|
3501
|
+
const criticallyDampedSpring = (target) => ({
|
|
3502
|
+
type: "spring",
|
|
3503
|
+
stiffness: 550,
|
|
3504
|
+
damping: target === 0 ? 2 * Math.sqrt(550) : 30,
|
|
3505
|
+
restSpeed: 10,
|
|
3506
|
+
});
|
|
3507
|
+
const keyframesTransition = {
|
|
3508
|
+
type: "keyframes",
|
|
3509
|
+
duration: 0.8,
|
|
3510
|
+
};
|
|
3511
|
+
/**
|
|
3512
|
+
* Default easing curve is a slightly shallower version of
|
|
3513
|
+
* the default browser easing curve.
|
|
3514
|
+
*/
|
|
3515
|
+
const ease = {
|
|
3516
|
+
type: "keyframes",
|
|
3517
|
+
ease: [0.25, 0.1, 0.35, 1],
|
|
3518
|
+
duration: 0.3,
|
|
3519
|
+
};
|
|
3520
|
+
const getDefaultTransition = (valueKey, { keyframes }) => {
|
|
3521
|
+
if (keyframes.length > 2) {
|
|
3522
|
+
return keyframesTransition;
|
|
3523
|
+
}
|
|
3524
|
+
else if (transformProps.has(valueKey)) {
|
|
3525
|
+
return valueKey.startsWith("scale")
|
|
3526
|
+
? criticallyDampedSpring(keyframes[1])
|
|
3527
|
+
: underDampedSpring;
|
|
3528
|
+
}
|
|
3529
|
+
return ease;
|
|
3530
|
+
};
|
|
3531
|
+
|
|
3532
|
+
const orchestrationKeys = new Set([
|
|
3533
|
+
"when",
|
|
3534
|
+
"delay",
|
|
3535
|
+
"delayChildren",
|
|
3536
|
+
"staggerChildren",
|
|
3537
|
+
"staggerDirection",
|
|
3538
|
+
"repeat",
|
|
3539
|
+
"repeatType",
|
|
3540
|
+
"repeatDelay",
|
|
3541
|
+
"from",
|
|
3542
|
+
"elapsed",
|
|
3543
|
+
]);
|
|
3544
|
+
/**
|
|
3545
|
+
* Decide whether a transition is defined on a given Transition.
|
|
3546
|
+
* This filters out orchestration options and returns true
|
|
3547
|
+
* if any options are left.
|
|
3548
|
+
*/
|
|
3549
|
+
function isTransitionDefined(transition) {
|
|
3550
|
+
for (const key in transition) {
|
|
3551
|
+
if (!orchestrationKeys.has(key))
|
|
3552
|
+
return true;
|
|
3553
|
+
}
|
|
3554
|
+
return false;
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
const animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => {
|
|
3558
|
+
const valueTransition = getValueTransition$1(transition, name) || {};
|
|
3559
|
+
/**
|
|
3560
|
+
* Most transition values are currently completely overwritten by value-specific
|
|
3561
|
+
* transitions. In the future it'd be nicer to blend these transitions. But for now
|
|
3562
|
+
* delay actually does inherit from the root transition if not value-specific.
|
|
3563
|
+
*/
|
|
3564
|
+
const delay = valueTransition.delay || transition.delay || 0;
|
|
3565
|
+
/**
|
|
3566
|
+
* Elapsed isn't a public transition option but can be passed through from
|
|
3567
|
+
* optimized appear effects in milliseconds.
|
|
3568
|
+
*/
|
|
3569
|
+
let { elapsed = 0 } = transition;
|
|
3570
|
+
elapsed = elapsed - secondsToMilliseconds(delay);
|
|
3571
|
+
const options = {
|
|
3572
|
+
keyframes: Array.isArray(target) ? target : [null, target],
|
|
3573
|
+
ease: "easeOut",
|
|
3574
|
+
velocity: value.getVelocity(),
|
|
3575
|
+
...valueTransition,
|
|
3576
|
+
delay: -elapsed,
|
|
3577
|
+
onUpdate: (v) => {
|
|
3578
|
+
value.set(v);
|
|
3579
|
+
valueTransition.onUpdate && valueTransition.onUpdate(v);
|
|
3580
|
+
},
|
|
3581
|
+
onComplete: () => {
|
|
3582
|
+
onComplete();
|
|
3583
|
+
valueTransition.onComplete && valueTransition.onComplete();
|
|
3584
|
+
},
|
|
3585
|
+
name,
|
|
3586
|
+
motionValue: value,
|
|
3587
|
+
element: isHandoff ? undefined : element,
|
|
3588
|
+
};
|
|
3589
|
+
/**
|
|
3590
|
+
* If there's no transition defined for this value, we can generate
|
|
3591
|
+
* unique transition settings for this value.
|
|
3592
|
+
*/
|
|
3593
|
+
if (!isTransitionDefined(valueTransition)) {
|
|
3594
|
+
Object.assign(options, getDefaultTransition(name, options));
|
|
3595
|
+
}
|
|
3596
|
+
/**
|
|
3597
|
+
* Both WAAPI and our internal animation functions use durations
|
|
3598
|
+
* as defined by milliseconds, while our external API defines them
|
|
3599
|
+
* as seconds.
|
|
3600
|
+
*/
|
|
3601
|
+
options.duration && (options.duration = secondsToMilliseconds(options.duration));
|
|
3602
|
+
options.repeatDelay && (options.repeatDelay = secondsToMilliseconds(options.repeatDelay));
|
|
3603
|
+
/**
|
|
3604
|
+
* Support deprecated way to set initial value. Prefer keyframe syntax.
|
|
3605
|
+
*/
|
|
3606
|
+
if (options.from !== undefined) {
|
|
3607
|
+
options.keyframes[0] = options.from;
|
|
3608
|
+
}
|
|
3609
|
+
let shouldSkip = false;
|
|
3610
|
+
if (options.type === false ||
|
|
3611
|
+
(options.duration === 0 && !options.repeatDelay)) {
|
|
3612
|
+
makeAnimationInstant(options);
|
|
3613
|
+
if (options.delay === 0) {
|
|
3614
|
+
shouldSkip = true;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
if (MotionGlobalConfig.instantAnimations ||
|
|
3618
|
+
MotionGlobalConfig.skipAnimations ||
|
|
3619
|
+
element?.shouldSkipAnimations ||
|
|
3620
|
+
valueTransition.skipAnimations) {
|
|
3621
|
+
shouldSkip = true;
|
|
3622
|
+
makeAnimationInstant(options);
|
|
3623
|
+
options.delay = 0;
|
|
3624
|
+
}
|
|
3625
|
+
/**
|
|
3626
|
+
* If the transition type or easing has been explicitly set by the user
|
|
3627
|
+
* then we don't want to allow flattening the animation.
|
|
3628
|
+
*/
|
|
3629
|
+
options.allowFlatten = !valueTransition.type && !valueTransition.ease;
|
|
3630
|
+
/**
|
|
3631
|
+
* If we can or must skip creating the animation, and apply only
|
|
3632
|
+
* the final keyframe, do so. We also check once keyframes are resolved but
|
|
3633
|
+
* this early check prevents the need to create an animation at all.
|
|
3634
|
+
*/
|
|
3635
|
+
if (shouldSkip && !isHandoff && value.get() !== undefined) {
|
|
3636
|
+
const finalKeyframe = getFinalKeyframe(options.keyframes, valueTransition);
|
|
3637
|
+
if (finalKeyframe !== undefined) {
|
|
3638
|
+
frame.update(() => {
|
|
3639
|
+
options.onUpdate(finalKeyframe);
|
|
3640
|
+
options.onComplete();
|
|
3641
|
+
});
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
return valueTransition.isSync
|
|
3646
|
+
? new JSAnimation(options)
|
|
3647
|
+
: new AsyncMotionValueAnimation(options);
|
|
3648
|
+
};
|
|
3649
|
+
|
|
3650
|
+
const MIN_LAYOUT_DISTANCE = 20;
|
|
3651
|
+
function bezierPoint(t, origin, control, target) {
|
|
3652
|
+
const inv = 1 - t;
|
|
3653
|
+
return inv * inv * origin + 2 * inv * t * control + t * t * target;
|
|
3654
|
+
}
|
|
3655
|
+
function bezierTangentAngle(t, originX, controlX, targetX, originY, controlY, targetY) {
|
|
3656
|
+
const dx = 2 * (1 - t) * (controlX - originX) + 2 * t * (targetX - controlX);
|
|
3657
|
+
const dy = 2 * (1 - t) * (controlY - originY) + 2 * t * (targetY - controlY);
|
|
3658
|
+
return Math.atan2(dy, dx) * (180 / Math.PI);
|
|
3659
|
+
}
|
|
3660
|
+
function computeArcControlPoint(fromX, fromY, toX, toY, strength, peak) {
|
|
3661
|
+
const deltaX = toX - fromX;
|
|
3662
|
+
const deltaY = toY - fromY;
|
|
3663
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
3664
|
+
if (distance > 0) {
|
|
3665
|
+
const normalPerpX = -deltaY / distance;
|
|
3666
|
+
const normalPerpY = deltaX / distance;
|
|
3667
|
+
const desiredHeight = strength * distance;
|
|
3668
|
+
return {
|
|
3669
|
+
x: fromX + deltaX * peak + normalPerpX * desiredHeight,
|
|
3670
|
+
y: fromY + deltaY * peak + normalPerpY * desiredHeight,
|
|
3671
|
+
};
|
|
3672
|
+
}
|
|
3673
|
+
return { x: fromX, y: fromY };
|
|
3674
|
+
}
|
|
3675
|
+
/**
|
|
3676
|
+
* The pure sampling factory: `(from, to) => (t) => point`. Internal —
|
|
3677
|
+
* used by {@link arc} and the unit tests. Not part of the public surface.
|
|
3678
|
+
*/
|
|
3679
|
+
function createArcPath({ strength = 0.5, peak = 0.5, direction, rotate = false, } = {}) {
|
|
3680
|
+
const rotationScale = rotate === true ? 1 : typeof rotate === "number" ? rotate : 0;
|
|
3681
|
+
// Auto-direction only: persists across calls to flip the bulge back
|
|
3682
|
+
// onto the same screen side when the dominant axis changes between
|
|
3683
|
+
// calls. Reuse the factory (module scope / useMemo) to keep this alive.
|
|
3684
|
+
let prevBulgeSign;
|
|
3685
|
+
const createInterpolator = (from, to) => {
|
|
3686
|
+
const dx = to.x - from.x;
|
|
3687
|
+
const dy = to.y - from.y;
|
|
3688
|
+
let signed;
|
|
3689
|
+
if (direction === "cw") {
|
|
3690
|
+
signed = -strength;
|
|
3691
|
+
}
|
|
3692
|
+
else if (direction === "ccw") {
|
|
3693
|
+
signed = strength;
|
|
3694
|
+
}
|
|
3695
|
+
else {
|
|
3696
|
+
const dom = Math.abs(dx) >= Math.abs(dy) ? dx : dy;
|
|
3697
|
+
signed = dom < 0 ? -strength : strength;
|
|
3698
|
+
}
|
|
3699
|
+
let control = computeArcControlPoint(from.x, from.y, to.x, to.y, signed, peak);
|
|
3700
|
+
if (direction === undefined) {
|
|
3701
|
+
const isVertical = Math.abs(dx) < Math.abs(dy);
|
|
3702
|
+
const midX = from.x + dx * peak;
|
|
3703
|
+
const midY = from.y + dy * peak;
|
|
3704
|
+
const bulgeSign = isVertical
|
|
3705
|
+
? Math.sign(control.x - midX)
|
|
3706
|
+
: Math.sign(control.y - midY);
|
|
3707
|
+
if (prevBulgeSign !== undefined &&
|
|
3708
|
+
bulgeSign !== 0 &&
|
|
3709
|
+
bulgeSign !== prevBulgeSign) {
|
|
3710
|
+
signed = -signed;
|
|
3711
|
+
control = computeArcControlPoint(from.x, from.y, to.x, to.y, signed, peak);
|
|
3712
|
+
}
|
|
3713
|
+
else if (bulgeSign !== 0) {
|
|
3714
|
+
prevBulgeSign = bulgeSign;
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
const tangent0 = rotationScale
|
|
3718
|
+
? bezierTangentAngle(0, from.x, control.x, to.x, from.y, control.y, to.y)
|
|
3719
|
+
: 0;
|
|
3720
|
+
const tangent1 = rotationScale
|
|
3721
|
+
? bezierTangentAngle(1, from.x, control.x, to.x, from.y, control.y, to.y)
|
|
3722
|
+
: 0;
|
|
3723
|
+
const tangentDelta = rotationScale
|
|
3724
|
+
? wrap(-180, 180, tangent1 - tangent0)
|
|
3725
|
+
: 0;
|
|
3726
|
+
return (t) => {
|
|
3727
|
+
const out = {
|
|
3728
|
+
x: bezierPoint(t, from.x, control.x, to.x),
|
|
3729
|
+
y: bezierPoint(t, from.y, control.y, to.y),
|
|
3730
|
+
};
|
|
3731
|
+
if (rotationScale) {
|
|
3732
|
+
const raw = bezierTangentAngle(t, from.x, control.x, to.x, from.y, control.y, to.y);
|
|
3733
|
+
const baseline = tangent0 + tangentDelta * t;
|
|
3734
|
+
out.rotate = wrap(-180, 180, raw - baseline) * rotationScale;
|
|
3705
3735
|
}
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3736
|
+
return out;
|
|
3737
|
+
};
|
|
3738
|
+
};
|
|
3739
|
+
return createInterpolator;
|
|
3740
|
+
}
|
|
3741
|
+
/**
|
|
3742
|
+
* Creates a curved path for `transition.path`:
|
|
3743
|
+
*
|
|
3744
|
+
* ```ts
|
|
3745
|
+
* <motion.div animate={{ x: 200, y: 100 }} transition={{ path: arc() }} />
|
|
3746
|
+
* ```
|
|
3747
|
+
*
|
|
3748
|
+
* Reuse the returned value (module scope / useMemo / useRef) so its
|
|
3749
|
+
* continuity closure survives re-renders — a fresh `arc()` has no memory.
|
|
3750
|
+
*/
|
|
3751
|
+
function arc(options = {}) {
|
|
3752
|
+
const sample = createArcPath(options);
|
|
3753
|
+
const path = {
|
|
3754
|
+
interpolateProjection(delta) {
|
|
3755
|
+
// `from` is the current translate offset (carries any in-flight
|
|
3756
|
+
// displacement when interrupted); `to` is the new layout origin.
|
|
3757
|
+
// The distance floor avoids visible wobble on tiny shifts.
|
|
3758
|
+
const tx = delta.x.translate;
|
|
3759
|
+
const ty = delta.y.translate;
|
|
3760
|
+
if (Math.sqrt(tx * tx + ty * ty) < MIN_LAYOUT_DISTANCE) {
|
|
3761
|
+
return undefined;
|
|
3709
3762
|
}
|
|
3710
|
-
|
|
3711
|
-
}
|
|
3763
|
+
return sample({ x: tx, y: ty }, { x: 0, y: 0 });
|
|
3764
|
+
},
|
|
3765
|
+
animateVisualElement(visualElement, target, transition, delay, animations) {
|
|
3766
|
+
if (!("x" in target || "y" in target))
|
|
3767
|
+
return;
|
|
3768
|
+
const xValue = visualElement.getValue("x", visualElement.latestValues["x"] ?? 0);
|
|
3769
|
+
const yValue = visualElement.getValue("y", visualElement.latestValues["y"] ?? 0);
|
|
3770
|
+
const xRaw = target.x;
|
|
3771
|
+
const yRaw = target.y;
|
|
3772
|
+
const xFrom = (Array.isArray(xRaw) && xRaw[0] != null
|
|
3773
|
+
? xRaw[0]
|
|
3774
|
+
: xValue?.get()) ?? 0;
|
|
3775
|
+
const yFrom = (Array.isArray(yRaw) && yRaw[0] != null
|
|
3776
|
+
? yRaw[0]
|
|
3777
|
+
: yValue?.get()) ?? 0;
|
|
3778
|
+
const xTo = (Array.isArray(xRaw)
|
|
3779
|
+
? xRaw[xRaw.length - 1]
|
|
3780
|
+
: xRaw ?? xFrom);
|
|
3781
|
+
const yTo = (Array.isArray(yRaw)
|
|
3782
|
+
? yRaw[yRaw.length - 1]
|
|
3783
|
+
: yRaw ?? yFrom);
|
|
3784
|
+
// Interruption needs no flag: x/y already hold the displaced
|
|
3785
|
+
// mid-arc position, so xFrom/yFrom carry the continuity geometry.
|
|
3786
|
+
const interpolate = sample({ x: xFrom, y: yFrom }, { x: xTo, y: yTo });
|
|
3787
|
+
// Drive a dedicated `pathRotation` value (composed onto `rotate`
|
|
3788
|
+
// at the build sites) rather than `rotate` itself, so a
|
|
3789
|
+
// concurrent rotate animation composes and nothing accumulates
|
|
3790
|
+
// on interrupt.
|
|
3791
|
+
const pathRotationValue = interpolate(0).rotate !== undefined
|
|
3792
|
+
? visualElement.getValue("pathRotation", 0)
|
|
3793
|
+
: undefined;
|
|
3794
|
+
const pathTransition = {
|
|
3795
|
+
delay,
|
|
3796
|
+
...getValueTransition$1(transition || {}, "x"),
|
|
3797
|
+
};
|
|
3798
|
+
delete pathTransition.path;
|
|
3799
|
+
const progress = motionValue(0);
|
|
3800
|
+
progress.start(animateMotionValue("", progress, [0, 1000], {
|
|
3801
|
+
...pathTransition,
|
|
3802
|
+
isSync: true,
|
|
3803
|
+
velocity: 0,
|
|
3804
|
+
onUpdate: (latest) => {
|
|
3805
|
+
const point = interpolate(latest / 1000);
|
|
3806
|
+
xValue?.set(point.x);
|
|
3807
|
+
yValue?.set(point.y);
|
|
3808
|
+
if (pathRotationValue && point.rotate !== undefined) {
|
|
3809
|
+
pathRotationValue.set(point.rotate);
|
|
3810
|
+
}
|
|
3811
|
+
},
|
|
3812
|
+
onComplete: () => {
|
|
3813
|
+
xValue?.set(xTo);
|
|
3814
|
+
yValue?.set(yTo);
|
|
3815
|
+
pathRotationValue?.set(0);
|
|
3816
|
+
},
|
|
3817
|
+
// Interrupt/cancel must clear our additive contribution
|
|
3818
|
+
// so it can't linger on top of the user's `rotate`.
|
|
3819
|
+
onStop: () => pathRotationValue?.set(0),
|
|
3820
|
+
onCancel: () => pathRotationValue?.set(0),
|
|
3821
|
+
}));
|
|
3822
|
+
if (progress.animation)
|
|
3823
|
+
animations.push(progress.animation);
|
|
3824
|
+
delete target.x;
|
|
3825
|
+
delete target.y;
|
|
3826
|
+
},
|
|
3827
|
+
};
|
|
3828
|
+
return path;
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
/**
|
|
3832
|
+
* Parse Framer's special CSS variable format into a CSS token and a fallback.
|
|
3833
|
+
*
|
|
3834
|
+
* ```
|
|
3835
|
+
* `var(--foo, #fff)` => [`--foo`, '#fff']
|
|
3836
|
+
* ```
|
|
3837
|
+
*
|
|
3838
|
+
* @param current
|
|
3839
|
+
*/
|
|
3840
|
+
const splitCSSVariableRegex =
|
|
3841
|
+
// eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive, as it can match a lot of words
|
|
3842
|
+
/^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u;
|
|
3843
|
+
function parseCSSVariable(current) {
|
|
3844
|
+
const match = splitCSSVariableRegex.exec(current);
|
|
3845
|
+
if (!match)
|
|
3846
|
+
return [,];
|
|
3847
|
+
const [, token1, token2, fallback] = match;
|
|
3848
|
+
return [`--${token1 ?? token2}`, fallback];
|
|
3849
|
+
}
|
|
3850
|
+
const maxDepth = 4;
|
|
3851
|
+
function getVariableValue(current, element, depth = 1) {
|
|
3852
|
+
exports.invariant(depth <= maxDepth, `Max CSS variable fallback depth detected in property "${current}". This may indicate a circular fallback dependency.`, "max-css-var-depth");
|
|
3853
|
+
const [token, fallback] = parseCSSVariable(current);
|
|
3854
|
+
// No CSS variable detected
|
|
3855
|
+
if (!token)
|
|
3856
|
+
return;
|
|
3857
|
+
// Attempt to read this CSS variable off the element
|
|
3858
|
+
const resolved = window.getComputedStyle(element).getPropertyValue(token);
|
|
3859
|
+
if (resolved) {
|
|
3860
|
+
const trimmed = resolved.trim();
|
|
3861
|
+
return isNumericalString(trimmed) ? parseFloat(trimmed) : trimmed;
|
|
3712
3862
|
}
|
|
3863
|
+
return isCSSVariableToken(fallback)
|
|
3864
|
+
? getVariableValue(fallback, element, depth + 1)
|
|
3865
|
+
: fallback;
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
function getValueState(visualElement) {
|
|
3869
|
+
const state = [{}, {}];
|
|
3870
|
+
visualElement?.values.forEach((value, key) => {
|
|
3871
|
+
state[0][key] = value.get();
|
|
3872
|
+
state[1][key] = value.getVelocity();
|
|
3873
|
+
});
|
|
3874
|
+
return state;
|
|
3875
|
+
}
|
|
3876
|
+
function resolveVariantFromProps(props, definition, custom, visualElement) {
|
|
3713
3877
|
/**
|
|
3714
|
-
*
|
|
3715
|
-
*
|
|
3716
|
-
* @public
|
|
3878
|
+
* If the variant definition is a function, resolve.
|
|
3717
3879
|
*/
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
if (this.events.animationCancel) {
|
|
3722
|
-
this.events.animationCancel.notify();
|
|
3723
|
-
}
|
|
3724
|
-
}
|
|
3725
|
-
this.clearAnimation();
|
|
3880
|
+
if (typeof definition === "function") {
|
|
3881
|
+
const [current, velocity] = getValueState(visualElement);
|
|
3882
|
+
definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
|
|
3726
3883
|
}
|
|
3727
3884
|
/**
|
|
3728
|
-
*
|
|
3729
|
-
*
|
|
3730
|
-
* @public
|
|
3885
|
+
* If the variant definition is a variant label, or
|
|
3886
|
+
* the function returned a variant label, resolve.
|
|
3731
3887
|
*/
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
}
|
|
3735
|
-
clearAnimation() {
|
|
3736
|
-
delete this.animation;
|
|
3888
|
+
if (typeof definition === "string") {
|
|
3889
|
+
definition = props.variants && props.variants[definition];
|
|
3737
3890
|
}
|
|
3738
3891
|
/**
|
|
3739
|
-
*
|
|
3740
|
-
*
|
|
3741
|
-
*
|
|
3742
|
-
* handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually
|
|
3743
|
-
* created a `MotionValue` via the `motionValue` function.
|
|
3744
|
-
*
|
|
3745
|
-
* @public
|
|
3892
|
+
* At this point we've resolved both functions and variant labels,
|
|
3893
|
+
* but the resolved variant label might itself have been a function.
|
|
3894
|
+
* If so, resolve. This can only have returned a valid target object.
|
|
3746
3895
|
*/
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
this.clearListeners();
|
|
3751
|
-
this.stop();
|
|
3752
|
-
if (this.stopPassiveEffect) {
|
|
3753
|
-
this.stopPassiveEffect();
|
|
3754
|
-
}
|
|
3896
|
+
if (typeof definition === "function") {
|
|
3897
|
+
const [current, velocity] = getValueState(visualElement);
|
|
3898
|
+
definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
|
|
3755
3899
|
}
|
|
3900
|
+
return definition;
|
|
3756
3901
|
}
|
|
3757
|
-
|
|
3758
|
-
|
|
3902
|
+
|
|
3903
|
+
function resolveVariant(visualElement, definition, custom) {
|
|
3904
|
+
const props = visualElement.getProps();
|
|
3905
|
+
return resolveVariantFromProps(props, definition, custom !== undefined ? custom : props.custom, visualElement);
|
|
3759
3906
|
}
|
|
3760
3907
|
|
|
3908
|
+
const positionalKeys = new Set([
|
|
3909
|
+
"width",
|
|
3910
|
+
"height",
|
|
3911
|
+
"top",
|
|
3912
|
+
"left",
|
|
3913
|
+
"right",
|
|
3914
|
+
"bottom",
|
|
3915
|
+
...transformPropOrder,
|
|
3916
|
+
]);
|
|
3917
|
+
|
|
3761
3918
|
const isKeyframesTarget = (v) => {
|
|
3762
3919
|
return Array.isArray(v);
|
|
3763
3920
|
};
|
|
@@ -3846,6 +4003,11 @@
|
|
|
3846
4003
|
const animationTypeState = type &&
|
|
3847
4004
|
visualElement.animationState &&
|
|
3848
4005
|
visualElement.animationState.getState()[type];
|
|
4006
|
+
const path = transition?.path;
|
|
4007
|
+
if (path) {
|
|
4008
|
+
// path mutates `target` to claim x/y; loop below skips them.
|
|
4009
|
+
path.animateVisualElement(visualElement, target, transition, delay, animations);
|
|
4010
|
+
}
|
|
3849
4011
|
for (const key in target) {
|
|
3850
4012
|
const value = visualElement.getValue(key, visualElement.latestValues[key] ?? null);
|
|
3851
4013
|
const valueTarget = target[key];
|
|
@@ -4064,6 +4226,12 @@
|
|
|
4064
4226
|
|
|
4065
4227
|
const transformValueTypes = {
|
|
4066
4228
|
rotate: degrees,
|
|
4229
|
+
/**
|
|
4230
|
+
* Internal channel for `transition.path` orientToPath. Composed onto
|
|
4231
|
+
* `rotate` at the transform-build sites so the user's `rotate` is
|
|
4232
|
+
* never read or overwritten. Not part of `transformPropOrder`.
|
|
4233
|
+
*/
|
|
4234
|
+
pathRotation: degrees,
|
|
4067
4235
|
rotateX: degrees,
|
|
4068
4236
|
rotateY: degrees,
|
|
4069
4237
|
rotateZ: degrees,
|
|
@@ -4615,6 +4783,15 @@
|
|
|
4615
4783
|
transform += `${transformName}(${value}) `;
|
|
4616
4784
|
}
|
|
4617
4785
|
}
|
|
4786
|
+
// See build-transform.ts: additive `rotate()` so user `rotate` isn't
|
|
4787
|
+
// clobbered. Not a `transformPropOrder` slot.
|
|
4788
|
+
const pathRotation = state.latest.pathRotation;
|
|
4789
|
+
if (pathRotation) {
|
|
4790
|
+
transformIsDefault = false;
|
|
4791
|
+
transform += `rotate(${typeof pathRotation === "number"
|
|
4792
|
+
? `${pathRotation}deg`
|
|
4793
|
+
: pathRotation}) `;
|
|
4794
|
+
}
|
|
4618
4795
|
return transformIsDefault ? "none" : transform.trim();
|
|
4619
4796
|
}
|
|
4620
4797
|
|
|
@@ -4955,9 +5132,16 @@
|
|
|
4955
5132
|
claimedPointerDownEvents.add(startEvent);
|
|
4956
5133
|
}
|
|
4957
5134
|
const onPressEnd = onPressStart(target, startEvent);
|
|
5135
|
+
/**
|
|
5136
|
+
* End listeners run in the capture phase so a descendant calling
|
|
5137
|
+
* stopPropagation() in its own pointerup handler can't prevent the
|
|
5138
|
+
* press gesture from ending. This also keeps the gesture-end
|
|
5139
|
+
* ordering consistent with the drag gesture. See #2794.
|
|
5140
|
+
*/
|
|
5141
|
+
const endEventOptions = { ...eventOptions, capture: true };
|
|
4958
5142
|
const onPointerEnd = (endEvent, success) => {
|
|
4959
|
-
window.removeEventListener("pointerup", onPointerUp);
|
|
4960
|
-
window.removeEventListener("pointercancel", onPointerCancel);
|
|
5143
|
+
window.removeEventListener("pointerup", onPointerUp, endEventOptions);
|
|
5144
|
+
window.removeEventListener("pointercancel", onPointerCancel, endEventOptions);
|
|
4961
5145
|
if (isPressing.has(target)) {
|
|
4962
5146
|
isPressing.delete(target);
|
|
4963
5147
|
}
|
|
@@ -4977,8 +5161,8 @@
|
|
|
4977
5161
|
const onPointerCancel = (cancelEvent) => {
|
|
4978
5162
|
onPointerEnd(cancelEvent, false);
|
|
4979
5163
|
};
|
|
4980
|
-
window.addEventListener("pointerup", onPointerUp,
|
|
4981
|
-
window.addEventListener("pointercancel", onPointerCancel,
|
|
5164
|
+
window.addEventListener("pointerup", onPointerUp, endEventOptions);
|
|
5165
|
+
window.addEventListener("pointercancel", onPointerCancel, endEventOptions);
|
|
4982
5166
|
};
|
|
4983
5167
|
targets.forEach((target) => {
|
|
4984
5168
|
const pointerDownTarget = options.useGlobalTarget ? window : target;
|
|
@@ -5117,116 +5301,34 @@
|
|
|
5117
5301
|
return () => cancelFrame(onFrame);
|
|
5118
5302
|
}
|
|
5119
5303
|
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
}
|
|
5126
|
-
value.frameloop.rate.push(frameData.delta);
|
|
5127
|
-
value.animations.mainThread.push(activeAnimations.mainThread);
|
|
5128
|
-
value.animations.waapi.push(activeAnimations.waapi);
|
|
5129
|
-
value.animations.layout.push(activeAnimations.layout);
|
|
5130
|
-
}
|
|
5131
|
-
function mean(values) {
|
|
5132
|
-
return values.reduce((acc, value) => acc + value, 0) / values.length;
|
|
5133
|
-
}
|
|
5134
|
-
function summarise(values, calcAverage = mean) {
|
|
5135
|
-
if (values.length === 0) {
|
|
5136
|
-
return {
|
|
5137
|
-
min: 0,
|
|
5138
|
-
max: 0,
|
|
5139
|
-
avg: 0,
|
|
5140
|
-
};
|
|
5141
|
-
}
|
|
5142
|
-
return {
|
|
5143
|
-
min: Math.min(...values),
|
|
5144
|
-
max: Math.max(...values),
|
|
5145
|
-
avg: calcAverage(values),
|
|
5146
|
-
};
|
|
5147
|
-
}
|
|
5148
|
-
const msToFps = (ms) => Math.round(1000 / ms);
|
|
5304
|
+
const statsBuffer = {
|
|
5305
|
+
value: null,
|
|
5306
|
+
addProjectionMetrics: null,
|
|
5307
|
+
};
|
|
5308
|
+
|
|
5149
5309
|
function clearStatsBuffer() {
|
|
5150
5310
|
statsBuffer.value = null;
|
|
5151
5311
|
statsBuffer.addProjectionMetrics = null;
|
|
5152
5312
|
}
|
|
5153
|
-
function reportStats() {
|
|
5154
|
-
const { value } = statsBuffer;
|
|
5155
|
-
if (!value) {
|
|
5156
|
-
throw new Error("Stats are not being measured");
|
|
5157
|
-
}
|
|
5158
|
-
clearStatsBuffer();
|
|
5159
|
-
cancelFrame(record);
|
|
5160
|
-
const summary = {
|
|
5161
|
-
frameloop: {
|
|
5162
|
-
setup: summarise(value.frameloop.setup),
|
|
5163
|
-
rate: summarise(value.frameloop.rate),
|
|
5164
|
-
read: summarise(value.frameloop.read),
|
|
5165
|
-
resolveKeyframes: summarise(value.frameloop.resolveKeyframes),
|
|
5166
|
-
preUpdate: summarise(value.frameloop.preUpdate),
|
|
5167
|
-
update: summarise(value.frameloop.update),
|
|
5168
|
-
preRender: summarise(value.frameloop.preRender),
|
|
5169
|
-
render: summarise(value.frameloop.render),
|
|
5170
|
-
postRender: summarise(value.frameloop.postRender),
|
|
5171
|
-
},
|
|
5172
|
-
animations: {
|
|
5173
|
-
mainThread: summarise(value.animations.mainThread),
|
|
5174
|
-
waapi: summarise(value.animations.waapi),
|
|
5175
|
-
layout: summarise(value.animations.layout),
|
|
5176
|
-
},
|
|
5177
|
-
layoutProjection: {
|
|
5178
|
-
nodes: summarise(value.layoutProjection.nodes),
|
|
5179
|
-
calculatedTargetDeltas: summarise(value.layoutProjection.calculatedTargetDeltas),
|
|
5180
|
-
calculatedProjections: summarise(value.layoutProjection.calculatedProjections),
|
|
5181
|
-
},
|
|
5182
|
-
};
|
|
5183
|
-
/**
|
|
5184
|
-
* Convert the rate to FPS
|
|
5185
|
-
*/
|
|
5186
|
-
const { rate } = summary.frameloop;
|
|
5187
|
-
rate.min = msToFps(rate.min);
|
|
5188
|
-
rate.max = msToFps(rate.max);
|
|
5189
|
-
rate.avg = msToFps(rate.avg);
|
|
5190
|
-
[rate.min, rate.max] = [rate.max, rate.min];
|
|
5191
|
-
return summary;
|
|
5192
|
-
}
|
|
5193
5313
|
function recordStats() {
|
|
5194
5314
|
if (statsBuffer.value) {
|
|
5195
5315
|
clearStatsBuffer();
|
|
5196
5316
|
throw new Error("Stats are already being measured");
|
|
5197
5317
|
}
|
|
5198
|
-
const
|
|
5199
|
-
|
|
5200
|
-
frameloop: {
|
|
5201
|
-
setup: [],
|
|
5202
|
-
rate: [],
|
|
5203
|
-
read: [],
|
|
5204
|
-
resolveKeyframes: [],
|
|
5205
|
-
preUpdate: [],
|
|
5206
|
-
update: [],
|
|
5207
|
-
preRender: [],
|
|
5208
|
-
render: [],
|
|
5209
|
-
postRender: [],
|
|
5210
|
-
},
|
|
5211
|
-
animations: {
|
|
5212
|
-
mainThread: [],
|
|
5213
|
-
waapi: [],
|
|
5214
|
-
layout: [],
|
|
5215
|
-
},
|
|
5318
|
+
const buffer = statsBuffer;
|
|
5319
|
+
buffer.value = {
|
|
5216
5320
|
layoutProjection: {
|
|
5217
5321
|
nodes: [],
|
|
5218
5322
|
calculatedTargetDeltas: [],
|
|
5219
5323
|
calculatedProjections: [],
|
|
5220
5324
|
},
|
|
5221
5325
|
};
|
|
5222
|
-
|
|
5223
|
-
const { layoutProjection } =
|
|
5326
|
+
buffer.addProjectionMetrics = (metrics) => {
|
|
5327
|
+
const { layoutProjection } = buffer.value;
|
|
5224
5328
|
layoutProjection.nodes.push(metrics.nodes);
|
|
5225
5329
|
layoutProjection.calculatedTargetDeltas.push(metrics.calculatedTargetDeltas);
|
|
5226
5330
|
layoutProjection.calculatedProjections.push(metrics.calculatedProjections);
|
|
5227
5331
|
};
|
|
5228
|
-
frame.postRender(record, true);
|
|
5229
|
-
return reportStats;
|
|
5230
5332
|
}
|
|
5231
5333
|
|
|
5232
5334
|
/**
|
|
@@ -5512,14 +5614,126 @@
|
|
|
5512
5614
|
*/
|
|
5513
5615
|
const findValueType = (v) => valueTypes.find(testValueType(v));
|
|
5514
5616
|
|
|
5617
|
+
let nameCount = 0;
|
|
5618
|
+
/**
|
|
5619
|
+
* Generated names live in their own namespace so we can tell a name we own
|
|
5620
|
+
* (and must clean up) from an author-defined one - and so a stale generated
|
|
5621
|
+
* name left behind by an interrupted transition is re-owned, not mistaken for
|
|
5622
|
+
* the author's and leaked.
|
|
5623
|
+
*/
|
|
5624
|
+
const generatedName = () => `motion-view-${nameCount++}`;
|
|
5625
|
+
const isGeneratedName = (name) => name.startsWith("motion-view-");
|
|
5626
|
+
/**
|
|
5627
|
+
* Tag a captured element with a `view-transition-class` so authors can target
|
|
5628
|
+
* its generated layer from CSS (e.g. `::view-transition-group(.hero)`) without
|
|
5629
|
+
* the opaque generated name. Tracked in `classed` - separate from the generated
|
|
5630
|
+
* names in `assigned` - so cleanup removes the class without ever stripping an
|
|
5631
|
+
* author's own inline `view-transition-name`.
|
|
5632
|
+
*/
|
|
5633
|
+
function tagClass(element, className, classed) {
|
|
5634
|
+
if (!className)
|
|
5635
|
+
return;
|
|
5636
|
+
element.style?.setProperty("view-transition-class", className);
|
|
5637
|
+
classed.push(element);
|
|
5638
|
+
}
|
|
5639
|
+
/**
|
|
5640
|
+
* Resolve a selector/Element to elements and ensure each one carries a
|
|
5641
|
+
* `view-transition-name` we can target from script.
|
|
5642
|
+
*
|
|
5643
|
+
* Author-defined names are reused as-is. Elements that are unnamed (or use
|
|
5644
|
+
* the browser's `auto`/`match-element`, whose generated name is not exposed
|
|
5645
|
+
* to script) are given a unique generated name, set inline so it's captured,
|
|
5646
|
+
* and tracked in `assigned` for later cleanup.
|
|
5647
|
+
*
|
|
5648
|
+
* `registry` maps each Element to its name so the same element keeps its name
|
|
5649
|
+
* across both captures (before and after the update), which is what allows a
|
|
5650
|
+
* persistent element to animate as a single `group` layer.
|
|
5651
|
+
*/
|
|
5652
|
+
function assignViewTransitionNames(definition, registry, assigned, forcedNames, className, classed = []) {
|
|
5653
|
+
const elements = resolveElements(definition);
|
|
5654
|
+
/**
|
|
5655
|
+
* The new end of a paired morph: give each element the matching name from
|
|
5656
|
+
* the old end (by index) so the two share one layer and morph. If the new
|
|
5657
|
+
* end resolves to *more* elements than the old end named, the extras have no
|
|
5658
|
+
* counterpart - give them a fresh name so they animate as newcomers rather
|
|
5659
|
+
* than being silently left unnamed. We return the names actually assigned
|
|
5660
|
+
* (sized to the resolved elements), not the raw `forcedNames`, so stagger
|
|
5661
|
+
* totals and the layer set stay in step with what's on the page.
|
|
5662
|
+
*/
|
|
5663
|
+
if (forcedNames) {
|
|
5664
|
+
return elements.map((element, i) => {
|
|
5665
|
+
const existing = registry.get(element);
|
|
5666
|
+
if (existing)
|
|
5667
|
+
return existing;
|
|
5668
|
+
const name = forcedNames[i] ?? generatedName();
|
|
5669
|
+
element.style?.setProperty("view-transition-name", name);
|
|
5670
|
+
assigned.push(element);
|
|
5671
|
+
registry.set(element, name);
|
|
5672
|
+
tagClass(element, className, classed);
|
|
5673
|
+
return name;
|
|
5674
|
+
});
|
|
5675
|
+
}
|
|
5676
|
+
/**
|
|
5677
|
+
* Read every current name up front, before assigning any. Interleaving the
|
|
5678
|
+
* reads with the inline `setProperty` writes below would dirty styles
|
|
5679
|
+
* between reads and force a style recalc per element; batching the reads
|
|
5680
|
+
* keeps it to one. Elements already in the registry keep their name and
|
|
5681
|
+
* need no read.
|
|
5682
|
+
*/
|
|
5683
|
+
const currentNames = elements.map((element) => registry.has(element)
|
|
5684
|
+
? undefined
|
|
5685
|
+
: getComputedStyle(element).getPropertyValue("view-transition-name"));
|
|
5686
|
+
return elements.map((element, i) => {
|
|
5687
|
+
const existing = registry.get(element);
|
|
5688
|
+
if (existing)
|
|
5689
|
+
return existing;
|
|
5690
|
+
const current = currentNames[i];
|
|
5691
|
+
let name;
|
|
5692
|
+
if (current &&
|
|
5693
|
+
current !== "none" &&
|
|
5694
|
+
current !== "auto" &&
|
|
5695
|
+
current !== "match-element" &&
|
|
5696
|
+
!isGeneratedName(current)) {
|
|
5697
|
+
/**
|
|
5698
|
+
* The author already named this layer - target it as-is and leave
|
|
5699
|
+
* it to them to clean up. `auto`/`match-element` are overridden
|
|
5700
|
+
* because their generated name is not exposed to script, and a
|
|
5701
|
+
* stale `motion-view-*` (e.g. left by an interrupted transition) is
|
|
5702
|
+
* re-owned rather than adopted as an author name and leaked.
|
|
5703
|
+
*/
|
|
5704
|
+
name = current;
|
|
5705
|
+
}
|
|
5706
|
+
else {
|
|
5707
|
+
name = generatedName();
|
|
5708
|
+
element.style?.setProperty("view-transition-name", name);
|
|
5709
|
+
assigned.push(element);
|
|
5710
|
+
}
|
|
5711
|
+
registry.set(element, name);
|
|
5712
|
+
tagClass(element, className, classed);
|
|
5713
|
+
return name;
|
|
5714
|
+
});
|
|
5715
|
+
}
|
|
5716
|
+
/**
|
|
5717
|
+
* Remove the `view-transition-name`s we generated and the
|
|
5718
|
+
* `view-transition-class`es we applied. Author-defined names are never touched
|
|
5719
|
+
* (they're not in `assigned`). Safe to call more than once (e.g. on both a
|
|
5720
|
+
* finished and an interrupted transition).
|
|
5721
|
+
*/
|
|
5722
|
+
function releaseViewTransitionNames(assigned, classed = []) {
|
|
5723
|
+
for (const element of assigned) {
|
|
5724
|
+
element.style?.removeProperty("view-transition-name");
|
|
5725
|
+
}
|
|
5726
|
+
for (const element of classed) {
|
|
5727
|
+
element.style?.removeProperty("view-transition-class");
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
|
|
5515
5731
|
function chooseLayerType(valueName) {
|
|
5516
5732
|
if (valueName === "layout")
|
|
5517
5733
|
return "group";
|
|
5518
5734
|
if (valueName === "enter" || valueName === "new")
|
|
5519
5735
|
return "new";
|
|
5520
|
-
|
|
5521
|
-
return "old";
|
|
5522
|
-
return "group";
|
|
5736
|
+
return "old";
|
|
5523
5737
|
}
|
|
5524
5738
|
|
|
5525
5739
|
let pendingRules = {};
|
|
@@ -5554,21 +5768,21 @@
|
|
|
5554
5768
|
};
|
|
5555
5769
|
|
|
5556
5770
|
function getViewAnimationLayerInfo(pseudoElement) {
|
|
5557
|
-
const match = pseudoElement.match(
|
|
5771
|
+
const match = pseudoElement.match(
|
|
5772
|
+
// `group-children` (nested transitions) before `group` so it wins.
|
|
5773
|
+
/::view-transition-(old|new|group-children|group|image-pair)\((.*?)\)/);
|
|
5558
5774
|
if (!match)
|
|
5559
5775
|
return null;
|
|
5560
5776
|
return { layer: match[2], type: match[1] };
|
|
5561
5777
|
}
|
|
5562
5778
|
|
|
5563
|
-
function filterViewAnimations(animation) {
|
|
5564
|
-
const { effect } = animation;
|
|
5565
|
-
if (!effect)
|
|
5566
|
-
return false;
|
|
5567
|
-
return (effect.target === document.documentElement &&
|
|
5568
|
-
effect.pseudoElement?.startsWith("::view-transition"));
|
|
5569
|
-
}
|
|
5570
5779
|
function getViewAnimations() {
|
|
5571
|
-
return document.getAnimations().filter(
|
|
5780
|
+
return document.getAnimations().filter((animation) => {
|
|
5781
|
+
const { effect } = animation;
|
|
5782
|
+
return (!!effect &&
|
|
5783
|
+
effect.target === document.documentElement &&
|
|
5784
|
+
effect.pseudoElement?.startsWith("::view-transition"));
|
|
5785
|
+
});
|
|
5572
5786
|
}
|
|
5573
5787
|
|
|
5574
5788
|
function hasTarget(target, targets) {
|
|
@@ -5576,87 +5790,370 @@
|
|
|
5576
5790
|
}
|
|
5577
5791
|
|
|
5578
5792
|
const definitionNames = ["layout", "enter", "exit", "new", "old"];
|
|
5793
|
+
/**
|
|
5794
|
+
* The `ViewTransitionTarget` buckets driving each generated layer type, in
|
|
5795
|
+
* priority order - the inverse of `chooseLayerType`. The new view is driven by
|
|
5796
|
+
* `new`/`enter`, the old by `old`/`exit`. `group-children`/`image-pair` have no
|
|
5797
|
+
* bucket; they follow the default layout timing.
|
|
5798
|
+
*/
|
|
5799
|
+
const typeBuckets = {
|
|
5800
|
+
group: ["layout"],
|
|
5801
|
+
new: ["new", "enter"],
|
|
5802
|
+
old: ["old", "exit"],
|
|
5803
|
+
};
|
|
5804
|
+
/**
|
|
5805
|
+
* Default "absent" origin for a single-value keyframe, by pseudo type, so e.g.
|
|
5806
|
+
* `enter({ scale: 1 })` grows in from 0.85 and `exit({ opacity: 0 })` fades
|
|
5807
|
+
* from 1. `enter` prefers the matching `exit` value over these (see below).
|
|
5808
|
+
*/
|
|
5809
|
+
const ORIGIN_DEFAULTS = {
|
|
5810
|
+
new: { opacity: 0, scale: 0.85 },
|
|
5811
|
+
old: { opacity: 1, scale: 1 },
|
|
5812
|
+
};
|
|
5813
|
+
const cornerProps = [
|
|
5814
|
+
"borderTopLeftRadius",
|
|
5815
|
+
"borderTopRightRadius",
|
|
5816
|
+
"borderBottomRightRadius",
|
|
5817
|
+
"borderBottomLeftRadius",
|
|
5818
|
+
];
|
|
5579
5819
|
function startViewAnimation(builder) {
|
|
5580
|
-
const { update, targets, options: defaultOptions } = builder;
|
|
5820
|
+
const { update, targets, resolveDefs, noCrop, pairs, classNames, options: defaultOptions, } = builder;
|
|
5581
5821
|
if (!document.startViewTransition) {
|
|
5582
|
-
|
|
5822
|
+
// An async IIFE (not `new Promise(async …)`) so a throwing/rejecting
|
|
5823
|
+
// update rejects this promise rather than leaving it unsettled.
|
|
5824
|
+
return (async () => {
|
|
5583
5825
|
await update();
|
|
5584
|
-
|
|
5826
|
+
return new GroupAnimation([]);
|
|
5827
|
+
})();
|
|
5828
|
+
}
|
|
5829
|
+
/**
|
|
5830
|
+
* Resolve any selector/Element targets to layer names, assigning a
|
|
5831
|
+
* `view-transition-name` to each element as we go. We run this before the
|
|
5832
|
+
* update (so the elements are captured in the old snapshot) and again
|
|
5833
|
+
* after it (for the new snapshot). An element present in both keeps the
|
|
5834
|
+
* same name and animates as a single `group` layer.
|
|
5835
|
+
*/
|
|
5836
|
+
const nameRegistry = new Map();
|
|
5837
|
+
const assigned = [];
|
|
5838
|
+
/**
|
|
5839
|
+
* Elements we tagged with a `view-transition-class` (via `.class()`),
|
|
5840
|
+
* tracked separately from `assigned` so cleanup removes the class without
|
|
5841
|
+
* ever stripping an author's own inline `view-transition-name`.
|
|
5842
|
+
*/
|
|
5843
|
+
const classed = [];
|
|
5844
|
+
const layerTargets = new Map();
|
|
5845
|
+
const croppedNames = new Set();
|
|
5846
|
+
/**
|
|
5847
|
+
* Each layer's stagger position (index + total) within its subject, per
|
|
5848
|
+
* snapshot. Resolving against the snapshot the layer belongs to keeps
|
|
5849
|
+
* stagger correct when `update()` replaces the matched elements, and lets
|
|
5850
|
+
* us skip a layer that's absent from a snapshot (e.g. an exited element
|
|
5851
|
+
* has no `new` pseudo-element).
|
|
5852
|
+
*/
|
|
5853
|
+
const layerStagger = new Map();
|
|
5854
|
+
/**
|
|
5855
|
+
* Names allocated for a paired subject in the old snapshot, replayed onto
|
|
5856
|
+
* its new-snapshot target so both ends share a layer and morph.
|
|
5857
|
+
*/
|
|
5858
|
+
const pairNames = new Map();
|
|
5859
|
+
/**
|
|
5860
|
+
* The old (`from`) elements of each paired subject, so their names can be
|
|
5861
|
+
* transferred off before the new (`to`) elements inherit them.
|
|
5862
|
+
*/
|
|
5863
|
+
const pairFrom = new Map();
|
|
5864
|
+
const resolveLayers = (phase) => {
|
|
5865
|
+
targets.forEach((target, definition) => {
|
|
5866
|
+
const className = classNames.get(definition);
|
|
5867
|
+
let names;
|
|
5868
|
+
if (definition === "root" || !resolveDefs.has(definition)) {
|
|
5869
|
+
names = [definition];
|
|
5870
|
+
}
|
|
5871
|
+
else if (pairs.has(definition)) {
|
|
5872
|
+
/**
|
|
5873
|
+
* Paired morph: name the old target in the old snapshot, then
|
|
5874
|
+
* force the same name(s) onto the new target in the new one, so
|
|
5875
|
+
* two different elements morph as a single layer.
|
|
5876
|
+
*/
|
|
5877
|
+
if (phase === "old") {
|
|
5878
|
+
pairFrom.set(definition, resolveElements(definition));
|
|
5879
|
+
names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed);
|
|
5880
|
+
pairNames.set(definition, names);
|
|
5881
|
+
}
|
|
5882
|
+
else {
|
|
5883
|
+
/**
|
|
5884
|
+
* Transfer the name(s) off the `from` elements before the
|
|
5885
|
+
* `to` elements inherit them. A `from` that survives into
|
|
5886
|
+
* the new snapshot (e.g. hidden with `visibility: hidden`
|
|
5887
|
+
* rather than removed) would otherwise keep the name and
|
|
5888
|
+
* collide - "duplicate view-transition-name".
|
|
5889
|
+
*/
|
|
5890
|
+
for (const el of pairFrom.get(definition) ?? []) {
|
|
5891
|
+
el.style?.removeProperty("view-transition-name");
|
|
5892
|
+
/**
|
|
5893
|
+
* Drop the old end from the registry too, so the new
|
|
5894
|
+
* end alone supplies this name's `new` crop radii - we
|
|
5895
|
+
* neither re-measure nor get ordered by a stale element.
|
|
5896
|
+
*/
|
|
5897
|
+
nameRegistry.delete(el);
|
|
5898
|
+
}
|
|
5899
|
+
names = assignViewTransitionNames(pairs.get(definition), nameRegistry, assigned, pairNames.get(definition), className, classed);
|
|
5900
|
+
}
|
|
5901
|
+
}
|
|
5902
|
+
else {
|
|
5903
|
+
names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed);
|
|
5904
|
+
}
|
|
5905
|
+
const cropped = definition !== "root" && !noCrop.has(definition);
|
|
5906
|
+
names.forEach((name, index) => {
|
|
5907
|
+
/**
|
|
5908
|
+
* If two subjects resolve to the same element, merge their
|
|
5909
|
+
* definitions so neither subject's animations are dropped.
|
|
5910
|
+
*/
|
|
5911
|
+
const existing = layerTargets.get(name);
|
|
5912
|
+
layerTargets.set(name, existing && existing !== target
|
|
5913
|
+
? { ...existing, ...target }
|
|
5914
|
+
: target);
|
|
5915
|
+
if (cropped)
|
|
5916
|
+
croppedNames.add(name);
|
|
5917
|
+
const stagger = layerStagger.get(name) ?? {};
|
|
5918
|
+
stagger[phase] = [index, names.length];
|
|
5919
|
+
layerStagger.set(name, stagger);
|
|
5920
|
+
});
|
|
5585
5921
|
});
|
|
5586
|
-
}
|
|
5587
|
-
|
|
5922
|
+
};
|
|
5923
|
+
/**
|
|
5924
|
+
* The stagger index/total for a layer, resolved against the snapshot it
|
|
5925
|
+
* belongs to. Returns index -1 when the layer is absent from that snapshot
|
|
5926
|
+
* so the caller can skip a pseudo-element that doesn't exist.
|
|
5927
|
+
*/
|
|
5928
|
+
const staggerPosition = (name, type) => {
|
|
5929
|
+
const stagger = layerStagger.get(name);
|
|
5930
|
+
const position = type === "old"
|
|
5931
|
+
? stagger?.old
|
|
5932
|
+
: type === "new"
|
|
5933
|
+
? stagger?.new
|
|
5934
|
+
: // group / group-children / image-pair persist across both.
|
|
5935
|
+
stagger?.new ?? stagger?.old;
|
|
5936
|
+
return position ?? [-1, 1];
|
|
5937
|
+
};
|
|
5938
|
+
/**
|
|
5939
|
+
* Merge default + per-layer transition options for a generated layer and
|
|
5940
|
+
* resolve any stagger/delay function against this element's position. Used
|
|
5941
|
+
* by both the morph-retiming and crop corner-radius passes.
|
|
5942
|
+
*/
|
|
5943
|
+
const resolveLayerTransition = (target, type, transitionName, index, total) => {
|
|
5944
|
+
const transition = mergeTransition(getValueTransition$1(defaultOptions, transitionName), getValueTransition$1((layerOptions(target, type) ?? {}), transitionName));
|
|
5945
|
+
if (typeof transition.delay === "function") {
|
|
5946
|
+
transition.delay = transition.delay(index, total);
|
|
5947
|
+
}
|
|
5948
|
+
return transition;
|
|
5949
|
+
};
|
|
5950
|
+
/**
|
|
5951
|
+
* Measured corner radii per cropped layer, so the clip can animate each
|
|
5952
|
+
* corner between the old and new elements. Per-corner (rather than the
|
|
5953
|
+
* shorthand) so mismatched/individual radii interpolate cleanly.
|
|
5954
|
+
*/
|
|
5955
|
+
const cropRadii = new Map();
|
|
5956
|
+
const recordRadii = (style, name, phase) => {
|
|
5957
|
+
const corners = {};
|
|
5958
|
+
for (const corner of cornerProps)
|
|
5959
|
+
corners[corner] = style[corner];
|
|
5960
|
+
const entry = cropRadii.get(name) ?? {};
|
|
5961
|
+
entry[phase] = corners;
|
|
5962
|
+
cropRadii.set(name, entry);
|
|
5963
|
+
};
|
|
5588
5964
|
/**
|
|
5589
|
-
*
|
|
5590
|
-
*
|
|
5965
|
+
* Cropped layers all come from `.add()`, so their elements are in the
|
|
5966
|
+
* registry - read each one's corner radii directly. For a paired morph both
|
|
5967
|
+
* ends share a name; the new-snapshot element is registered last, so it
|
|
5968
|
+
* wins the `new` reading (and the old end the `old` reading).
|
|
5591
5969
|
*/
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5970
|
+
const measureCrop = (phase) => {
|
|
5971
|
+
if (!croppedNames.size)
|
|
5972
|
+
return;
|
|
5973
|
+
nameRegistry.forEach((name, element) => {
|
|
5974
|
+
if (croppedNames.has(name)) {
|
|
5975
|
+
recordRadii(getComputedStyle(element), name, phase);
|
|
5976
|
+
}
|
|
5595
5977
|
});
|
|
5596
|
-
}
|
|
5978
|
+
};
|
|
5597
5979
|
/**
|
|
5598
|
-
*
|
|
5599
|
-
*
|
|
5600
|
-
*
|
|
5980
|
+
* Write the persistent view-transition CSS: suppress root capture when the
|
|
5981
|
+
* root has no animations of its own; force linear timing (baked into the
|
|
5982
|
+
* keyframes, so we can retime later via updateTiming); and clip +
|
|
5983
|
+
* object-fit: cover every cropped morph (the UA default overflows on
|
|
5984
|
+
* aspect-ratio change), with an animated border-radius added below.
|
|
5601
5985
|
*
|
|
5602
|
-
*
|
|
5986
|
+
* `css.commit` replaces rather than appends, so we re-set the full rule set
|
|
5987
|
+
* each call - the second call (in the update callback) then picks up cropped
|
|
5988
|
+
* layers that only exist in the new snapshot.
|
|
5603
5989
|
*/
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5990
|
+
const commitViewCSS = () => {
|
|
5991
|
+
if (!hasTarget("root", targets)) {
|
|
5992
|
+
css.set(":root", { "view-transition-name": "none" });
|
|
5993
|
+
}
|
|
5994
|
+
css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
|
|
5995
|
+
croppedNames.forEach((name) => {
|
|
5996
|
+
css.set(`::view-transition-group(${name})`, { overflow: "clip" });
|
|
5997
|
+
css.set(`::view-transition-old(${name}), ::view-transition-new(${name})`, { width: "100%", height: "100%", "object-fit": "cover" });
|
|
5998
|
+
});
|
|
5999
|
+
css.commit(); // Write
|
|
6000
|
+
};
|
|
6001
|
+
const cleanup = () => {
|
|
6002
|
+
releaseViewTransitionNames(assigned, classed);
|
|
5611
6003
|
css.remove(); // Write
|
|
5612
|
-
}
|
|
5613
|
-
|
|
5614
|
-
|
|
6004
|
+
};
|
|
6005
|
+
const callback = async () => {
|
|
6006
|
+
await update();
|
|
6007
|
+
/**
|
|
6008
|
+
* Re-resolve so elements created by the update are named for the new
|
|
6009
|
+
* snapshot, then measure the cropped layers' new border-radius.
|
|
6010
|
+
*/
|
|
6011
|
+
const croppedBefore = croppedNames.size;
|
|
6012
|
+
resolveLayers("new");
|
|
6013
|
+
measureCrop("new");
|
|
6014
|
+
/**
|
|
6015
|
+
* Re-commit the crop CSS only if the new snapshot introduced cropped
|
|
6016
|
+
* layers, so a layer that exists only in the new snapshot is clipped
|
|
6017
|
+
* too - without forcing a redundant style write on the common path.
|
|
6018
|
+
*/
|
|
6019
|
+
if (croppedNames.size > croppedBefore)
|
|
6020
|
+
commitViewCSS();
|
|
6021
|
+
};
|
|
6022
|
+
let transition;
|
|
6023
|
+
try {
|
|
6024
|
+
resolveLayers("old");
|
|
6025
|
+
measureCrop("old");
|
|
6026
|
+
commitViewCSS();
|
|
6027
|
+
transition = document.startViewTransition(callback);
|
|
6028
|
+
}
|
|
6029
|
+
catch (error) {
|
|
6030
|
+
/**
|
|
6031
|
+
* The prelude writes inline names before the transition exists. If it
|
|
6032
|
+
* throws (e.g. startViewTransition rejects in a bad UA state), release
|
|
6033
|
+
* them so we neither leak DOM state nor stall the queue on a promise
|
|
6034
|
+
* that never settles - hand back a rejection it can advance past.
|
|
6035
|
+
*/
|
|
6036
|
+
cleanup();
|
|
6037
|
+
return Promise.reject(error);
|
|
6038
|
+
}
|
|
6039
|
+
transition.finished.finally(cleanup);
|
|
6040
|
+
return new Promise((resolve, reject) => {
|
|
6041
|
+
transition.ready
|
|
6042
|
+
.then(() => {
|
|
5615
6043
|
const generatedViewAnimations = getViewAnimations();
|
|
5616
6044
|
const animations = [];
|
|
5617
6045
|
/**
|
|
5618
6046
|
* Create animations for each of our explicitly-defined subjects.
|
|
6047
|
+
* `opacityAnimated` additionally tracks which `${name}:${type}`
|
|
6048
|
+
* we faded, so we can keep the UA `plus-lighter` blend only for a
|
|
6049
|
+
* genuine opacity crossfade (both sides fading) and drop it for a
|
|
6050
|
+
* slide/transform, where additive compositing would flash bright.
|
|
5619
6051
|
*/
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
6052
|
+
const explicitlyAnimated = new Set();
|
|
6053
|
+
const opacityAnimated = new Set();
|
|
6054
|
+
layerTargets.forEach((target, name) => {
|
|
6055
|
+
const stagger = layerStagger.get(name);
|
|
6056
|
+
/**
|
|
6057
|
+
* Presence: `enter` only fires for a pure newcomer (a new
|
|
6058
|
+
* view with no old), `exit` only for a pure leaver. A
|
|
6059
|
+
* survivor (both) gets neither - it just morphs.
|
|
6060
|
+
*/
|
|
6061
|
+
const enterApplies = !!stagger?.new && !stagger?.old;
|
|
6062
|
+
const exitApplies = !!stagger?.old && !stagger?.new;
|
|
5623
6063
|
for (const key of definitionNames) {
|
|
5624
|
-
if (!
|
|
6064
|
+
if (!target[key])
|
|
5625
6065
|
continue;
|
|
5626
|
-
|
|
6066
|
+
if (key === "enter" && !enterApplies)
|
|
6067
|
+
continue;
|
|
6068
|
+
if (key === "exit" && !exitApplies)
|
|
6069
|
+
continue;
|
|
6070
|
+
const type = chooseLayerType(key);
|
|
6071
|
+
const [index, total] = staggerPosition(name, type);
|
|
6072
|
+
// Skip a layer absent from its snapshot.
|
|
6073
|
+
if (index === -1)
|
|
6074
|
+
continue;
|
|
6075
|
+
const { keyframes, options } = target[key];
|
|
5627
6076
|
for (let [valueName, valueKeyframes] of Object.entries(keyframes)) {
|
|
5628
|
-
|
|
6077
|
+
// Skip only missing values - `0` (e.g. opacity: 0)
|
|
6078
|
+
// is valid and must reach the from-value inference.
|
|
6079
|
+
if (valueKeyframes == null)
|
|
5629
6080
|
continue;
|
|
5630
|
-
const valueOptions = {
|
|
5631
|
-
...getValueTransition$1(defaultOptions, valueName),
|
|
5632
|
-
...getValueTransition$1(options, valueName),
|
|
5633
|
-
};
|
|
5634
|
-
const type = chooseLayerType(key);
|
|
5635
6081
|
/**
|
|
5636
|
-
*
|
|
5637
|
-
*
|
|
6082
|
+
* The view path hands keyframes straight to WAAPI,
|
|
6083
|
+
* so Motion's `x`/`y` shorthands (compiled to
|
|
6084
|
+
* `transform` only via the value pipeline) have no
|
|
6085
|
+
* effect. Warn and skip - use `transform`/`translate`.
|
|
5638
6086
|
*/
|
|
5639
|
-
if (valueName === "
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
6087
|
+
if (valueName === "x" || valueName === "y") {
|
|
6088
|
+
warnOnce(false, `animateView() animates view-transition layers with CSS properties; the "${valueName}" shorthand has no effect - use transform, e.g. { transform: "translateX(40px)" }.`);
|
|
6089
|
+
continue;
|
|
6090
|
+
}
|
|
6091
|
+
/**
|
|
6092
|
+
* enter/exit win over new/old on a shared property -
|
|
6093
|
+
* skip it here when the gated bucket also defines it.
|
|
6094
|
+
*/
|
|
6095
|
+
if (key === "new" &&
|
|
6096
|
+
enterApplies &&
|
|
6097
|
+
target.enter?.keyframes[valueName] != null) {
|
|
6098
|
+
continue;
|
|
6099
|
+
}
|
|
6100
|
+
if (key === "old" &&
|
|
6101
|
+
exitApplies &&
|
|
6102
|
+
target.exit?.keyframes[valueName] != null) {
|
|
6103
|
+
continue;
|
|
6104
|
+
}
|
|
6105
|
+
const valueOptions = mergeTransition(getValueTransition$1(defaultOptions, valueName), getValueTransition$1(options, valueName));
|
|
6106
|
+
/**
|
|
6107
|
+
* Infer an origin for a single-value keyframe. An
|
|
6108
|
+
* `enter` mirrors the matching `exit` value (a
|
|
6109
|
+
* defined exit reverses into the enter for free);
|
|
6110
|
+
* otherwise the per-type default (opacity 0/1, scale
|
|
6111
|
+
* 0.85). No default -> left as-is (animates from the
|
|
6112
|
+
* live value).
|
|
6113
|
+
*
|
|
6114
|
+
* `new`/`old` fire for survivors too, where only the
|
|
6115
|
+
* opacity crossfade default applies - a transform
|
|
6116
|
+
* default like scale 0.85 would pop a persisting
|
|
6117
|
+
* element, so gate it on the layer actually
|
|
6118
|
+
* entering/leaving.
|
|
6119
|
+
*/
|
|
6120
|
+
if (!Array.isArray(valueKeyframes)) {
|
|
6121
|
+
const exitValue = key === "enter"
|
|
6122
|
+
? target.exit?.keyframes[valueName]
|
|
6123
|
+
: undefined;
|
|
6124
|
+
const allowDefault = valueName === "opacity" ||
|
|
6125
|
+
(type === "new" ? enterApplies : exitApplies);
|
|
6126
|
+
const from = exitValue != null
|
|
6127
|
+
? Array.isArray(exitValue)
|
|
6128
|
+
? exitValue[exitValue.length - 1]
|
|
6129
|
+
: exitValue
|
|
6130
|
+
: allowDefault
|
|
6131
|
+
? ORIGIN_DEFAULTS[type]?.[valueName]
|
|
6132
|
+
: undefined;
|
|
6133
|
+
if (from !== undefined) {
|
|
6134
|
+
valueKeyframes = [from, valueKeyframes];
|
|
6135
|
+
}
|
|
5643
6136
|
}
|
|
5644
6137
|
/**
|
|
5645
|
-
* Resolve stagger function if provided
|
|
6138
|
+
* Resolve stagger function if provided, per element
|
|
6139
|
+
* across this subject's resolved layers.
|
|
5646
6140
|
*/
|
|
5647
6141
|
if (typeof valueOptions.delay === "function") {
|
|
5648
|
-
valueOptions.delay = valueOptions.delay(
|
|
6142
|
+
valueOptions.delay = valueOptions.delay(index, total);
|
|
5649
6143
|
}
|
|
5650
6144
|
valueOptions.duration && (valueOptions.duration = secondsToMilliseconds(valueOptions.duration));
|
|
5651
6145
|
valueOptions.delay && (valueOptions.delay = secondsToMilliseconds(valueOptions.delay));
|
|
5652
|
-
|
|
6146
|
+
animations.push(new NativeAnimation({
|
|
5653
6147
|
...valueOptions,
|
|
5654
6148
|
element: document.documentElement,
|
|
5655
6149
|
name: valueName,
|
|
5656
|
-
pseudoElement: `::view-transition-${type}(${
|
|
6150
|
+
pseudoElement: `::view-transition-${type}(${name})`,
|
|
5657
6151
|
keyframes: valueKeyframes,
|
|
5658
|
-
});
|
|
5659
|
-
|
|
6152
|
+
}));
|
|
6153
|
+
explicitlyAnimated.add(`${name}:${type}`);
|
|
6154
|
+
if (valueName === "opacity") {
|
|
6155
|
+
opacityAnimated.add(`${name}:${type}`);
|
|
6156
|
+
}
|
|
5660
6157
|
}
|
|
5661
6158
|
}
|
|
5662
6159
|
});
|
|
@@ -5675,45 +6172,161 @@
|
|
|
5675
6172
|
const name = getViewAnimationLayerInfo(pseudoElement);
|
|
5676
6173
|
if (!name)
|
|
5677
6174
|
continue;
|
|
5678
|
-
const targetDefinition =
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
}
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
effect
|
|
5703
|
-
.getKeyframes()
|
|
5704
|
-
.some((keyframe) => keyframe.mixBlendMode)) {
|
|
5705
|
-
animations.push(new NativeAnimationWrapper(animation));
|
|
5706
|
-
}
|
|
5707
|
-
else {
|
|
5708
|
-
animation.cancel();
|
|
6175
|
+
const targetDefinition = layerTargets.get(name.layer);
|
|
6176
|
+
/**
|
|
6177
|
+
* We built our own animation for this layer, so drop the
|
|
6178
|
+
* browser-generated fade we're replacing. The UA
|
|
6179
|
+
* `plus-lighter` blend is a *separate* generated animation on
|
|
6180
|
+
* the same pseudo (it sets `mix-blend-mode` in its keyframes):
|
|
6181
|
+
* keep it *only* for a true opacity crossfade - both sides
|
|
6182
|
+
* fading - so a symmetric crossfade composites without
|
|
6183
|
+
* darkening, but a slide/transform (where both layers stay
|
|
6184
|
+
* opaque and overlap) doesn't flash bright from the addition.
|
|
6185
|
+
*/
|
|
6186
|
+
if (explicitlyAnimated.has(`${name.layer}:${name.type}`)) {
|
|
6187
|
+
const isCrossfade = opacityAnimated.has(`${name.layer}:new`) &&
|
|
6188
|
+
opacityAnimated.has(`${name.layer}:old`);
|
|
6189
|
+
if (isCrossfade &&
|
|
6190
|
+
effect
|
|
6191
|
+
.getKeyframes()
|
|
6192
|
+
.some((keyframe) => keyframe.mixBlendMode)) {
|
|
6193
|
+
animations.push(new NativeAnimationWrapper(animation));
|
|
6194
|
+
}
|
|
6195
|
+
else {
|
|
6196
|
+
animation.cancel();
|
|
6197
|
+
}
|
|
6198
|
+
continue;
|
|
5709
6199
|
}
|
|
6200
|
+
/**
|
|
6201
|
+
* Otherwise retime the browser-generated animation to
|
|
6202
|
+
* Motion's timing. This auto-enables the layout (group)
|
|
6203
|
+
* morph for any resolved/named target, and applies the
|
|
6204
|
+
* default timing to old/new layers we haven't explicitly
|
|
6205
|
+
* overridden.
|
|
6206
|
+
*
|
|
6207
|
+
* group + group-children both follow the layout timing so
|
|
6208
|
+
* the nesting container stays in sync with the morph.
|
|
6209
|
+
*/
|
|
6210
|
+
/**
|
|
6211
|
+
* A survivor's old + new are the two halves of one
|
|
6212
|
+
* `plus-lighter` crossfade. They must share identical timing
|
|
6213
|
+
* (so their opacities stay mirrored and sum to 1 - else the
|
|
6214
|
+
* additive blend flashes bright wherever both are partly
|
|
6215
|
+
* visible) and fade linearly (the bounce belongs on the
|
|
6216
|
+
* group's geometry, not the opacity). So time them as the
|
|
6217
|
+
* group, rather than via their own - potentially staggered,
|
|
6218
|
+
* or enter/exit-derived - old/new options.
|
|
6219
|
+
*/
|
|
6220
|
+
const stagger = layerStagger.get(name.layer);
|
|
6221
|
+
const isMorphCrossfade = (name.type === "old" || name.type === "new") &&
|
|
6222
|
+
!!stagger?.old &&
|
|
6223
|
+
!!stagger?.new;
|
|
6224
|
+
const timingType = name.type.startsWith("group") || isMorphCrossfade
|
|
6225
|
+
? "group"
|
|
6226
|
+
: name.type;
|
|
6227
|
+
const [index, total] = staggerPosition(name.layer, timingType);
|
|
6228
|
+
const transitionName = timingType === "group" ? "layout" : "";
|
|
6229
|
+
let animationTransition = resolveLayerTransition(targetDefinition, timingType, transitionName, index === -1 ? 0 : index, total);
|
|
6230
|
+
/**
|
|
6231
|
+
* The crossfade should resolve at the spring's *perceptual*
|
|
6232
|
+
* (visual) duration - the geometry can keep bouncing, but the
|
|
6233
|
+
* opacity shouldn't drag through the settle. So capture
|
|
6234
|
+
* `visualDuration` before `applyGeneratorOptions` replaces it
|
|
6235
|
+
* with the full overshoot duration, and use it for the fade.
|
|
6236
|
+
*/
|
|
6237
|
+
const visualDuration = animationTransition.visualDuration;
|
|
6238
|
+
animationTransition.duration && (animationTransition.duration = secondsToMilliseconds(animationTransition.duration));
|
|
6239
|
+
animationTransition =
|
|
6240
|
+
applyGeneratorOptions(animationTransition);
|
|
6241
|
+
const duration = isMorphCrossfade && visualDuration !== undefined
|
|
6242
|
+
? secondsToMilliseconds(visualDuration)
|
|
6243
|
+
: animationTransition.duration;
|
|
6244
|
+
const easing = isMorphCrossfade
|
|
6245
|
+
? "linear"
|
|
6246
|
+
: mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration);
|
|
6247
|
+
effect.updateTiming({
|
|
6248
|
+
delay: secondsToMilliseconds(animationTransition.delay ?? 0),
|
|
6249
|
+
duration,
|
|
6250
|
+
easing,
|
|
6251
|
+
});
|
|
6252
|
+
animations.push(new NativeAnimationWrapper(animation));
|
|
5710
6253
|
}
|
|
6254
|
+
/**
|
|
6255
|
+
* Animate each cropped layer's clip corners between the old and
|
|
6256
|
+
* new elements, so a cropped morph keeps rounded corners
|
|
6257
|
+
* (handling individual per-corner radii).
|
|
6258
|
+
*/
|
|
6259
|
+
cropRadii.forEach((radii, name) => {
|
|
6260
|
+
if (!radii.old && !radii.new)
|
|
6261
|
+
return;
|
|
6262
|
+
const target = layerTargets.get(name);
|
|
6263
|
+
const [index, total] = staggerPosition(name, "group");
|
|
6264
|
+
const radiusOptions = resolveLayerTransition(target, "group", "layout", index === -1 ? 0 : index, total);
|
|
6265
|
+
radiusOptions.duration && (radiusOptions.duration = secondsToMilliseconds(radiusOptions.duration));
|
|
6266
|
+
radiusOptions.delay && (radiusOptions.delay = secondsToMilliseconds(radiusOptions.delay));
|
|
6267
|
+
for (const corner of cornerProps) {
|
|
6268
|
+
// `||` (not `??`) so an empty measurement (e.g. an
|
|
6269
|
+
// un-rendered element) falls back rather than producing
|
|
6270
|
+
// an invalid keyframe.
|
|
6271
|
+
const from = radii.old?.[corner] || radii.new?.[corner] || "0px";
|
|
6272
|
+
const to = radii.new?.[corner] || radii.old?.[corner] || "0px";
|
|
6273
|
+
// Skip square corners - nothing to round.
|
|
6274
|
+
if (parseFloat(from) === 0 && parseFloat(to) === 0) {
|
|
6275
|
+
continue;
|
|
6276
|
+
}
|
|
6277
|
+
animations.push(new NativeAnimation({
|
|
6278
|
+
...radiusOptions,
|
|
6279
|
+
element: document.documentElement,
|
|
6280
|
+
name: corner,
|
|
6281
|
+
pseudoElement: `::view-transition-group(${name})`,
|
|
6282
|
+
keyframes: [from, to],
|
|
6283
|
+
}));
|
|
6284
|
+
}
|
|
6285
|
+
});
|
|
5711
6286
|
resolve(new GroupAnimation(animations));
|
|
5712
|
-
})
|
|
6287
|
+
})
|
|
6288
|
+
.catch(() =>
|
|
6289
|
+
/**
|
|
6290
|
+
* `ready` rejects when the transition is skipped - no visual
|
|
6291
|
+
* change, or superseded by an interrupting transition. The DOM
|
|
6292
|
+
* update still applied, so settle with no animations rather than
|
|
6293
|
+
* surfacing it as an error to an awaiting caller. A genuine
|
|
6294
|
+
* failure in `update()` rejects `updateCallbackDone` (already
|
|
6295
|
+
* settled by now), so propagate that instead.
|
|
6296
|
+
*/
|
|
6297
|
+
transition.updateCallbackDone.then(() => resolve(new GroupAnimation([])), reject));
|
|
5713
6298
|
});
|
|
5714
6299
|
}
|
|
5715
|
-
|
|
5716
|
-
|
|
6300
|
+
/**
|
|
6301
|
+
* The options that should time a given generated layer type, so a retimed
|
|
6302
|
+
* group/old/new picks up any per-target transition the user provided. Checks
|
|
6303
|
+
* the type's buckets in priority order (e.g. `new` before `enter`).
|
|
6304
|
+
*/
|
|
6305
|
+
function layerOptions(target, type) {
|
|
6306
|
+
for (const bucket of typeBuckets[type] ?? []) {
|
|
6307
|
+
const options = target?.[bucket]?.options;
|
|
6308
|
+
if (options)
|
|
6309
|
+
return options;
|
|
6310
|
+
}
|
|
6311
|
+
}
|
|
6312
|
+
/**
|
|
6313
|
+
* Merge a base transition (e.g. the default `options`) with a per-layer/value
|
|
6314
|
+
* override. An explicit `duration` on the override must win over an inherited
|
|
6315
|
+
* generator's own timing: a spring prefers `visualDuration`, and
|
|
6316
|
+
* `spring.applyToOptions` overwrites `duration` with the computed settle time -
|
|
6317
|
+
* so without this the override is silently discarded. Dropping the inherited
|
|
6318
|
+
* `type`/`visualDuration` makes the layer a plain tween of that duration, unless
|
|
6319
|
+
* it asked for its own generator `type`/`visualDuration`.
|
|
6320
|
+
*/
|
|
6321
|
+
function mergeTransition(base, override) {
|
|
6322
|
+
const merged = { ...base, ...override };
|
|
6323
|
+
if (override.duration !== undefined) {
|
|
6324
|
+
if (override.visualDuration === undefined)
|
|
6325
|
+
delete merged.visualDuration;
|
|
6326
|
+
if (override.type === undefined)
|
|
6327
|
+
delete merged.type;
|
|
6328
|
+
}
|
|
6329
|
+
return merged;
|
|
5717
6330
|
}
|
|
5718
6331
|
|
|
5719
6332
|
let builders = [];
|
|
@@ -5727,10 +6340,16 @@
|
|
|
5727
6340
|
function start(builder) {
|
|
5728
6341
|
removeItem(builders, builder);
|
|
5729
6342
|
current = builder;
|
|
5730
|
-
startViewAnimation(builder)
|
|
6343
|
+
startViewAnimation(builder)
|
|
6344
|
+
.then((animation) => {
|
|
5731
6345
|
builder.notifyReady(animation);
|
|
5732
|
-
animation.finished
|
|
5733
|
-
})
|
|
6346
|
+
return animation.finished;
|
|
6347
|
+
})
|
|
6348
|
+
// A genuinely failed transition (a throwing update) rejects the
|
|
6349
|
+
// builder; a skipped/interrupted one resolves with no animations (see
|
|
6350
|
+
// start.ts). Either way, advance the queue - else later transitions hang.
|
|
6351
|
+
.catch((error) => builder.notifyReject(error))
|
|
6352
|
+
.finally(next);
|
|
5734
6353
|
}
|
|
5735
6354
|
function processQueue() {
|
|
5736
6355
|
/**
|
|
@@ -5767,31 +6386,96 @@
|
|
|
5767
6386
|
constructor(update, options = {}) {
|
|
5768
6387
|
this.currentSubject = "root";
|
|
5769
6388
|
this.targets = new Map();
|
|
5770
|
-
|
|
5771
|
-
|
|
6389
|
+
/**
|
|
6390
|
+
* Definitions that must be resolved to elements (and assigned a
|
|
6391
|
+
* `view-transition-name`) rather than treated as pre-named layers.
|
|
6392
|
+
*/
|
|
6393
|
+
this.resolveDefs = new Set();
|
|
6394
|
+
/**
|
|
6395
|
+
* Subjects opted out of the default crop (clip + object-fit: cover +
|
|
6396
|
+
* animated corner radii) via `.crop(false)`.
|
|
6397
|
+
*/
|
|
6398
|
+
this.noCrop = new Set();
|
|
6399
|
+
/**
|
|
6400
|
+
* Subjects paired with a different new-snapshot target (the second `.add()`
|
|
6401
|
+
* argument), so two distinct elements share one name and morph into each
|
|
6402
|
+
* other - a shared-element transition.
|
|
6403
|
+
*/
|
|
6404
|
+
this.pairs = new Map();
|
|
6405
|
+
/**
|
|
6406
|
+
* A `view-transition-class` to apply to each subject's resolved elements,
|
|
6407
|
+
* so authors can target the generated layers from CSS by class rather than
|
|
6408
|
+
* the opaque generated name.
|
|
6409
|
+
*/
|
|
6410
|
+
this.classNames = new Map();
|
|
6411
|
+
this.notifyReady = noop;
|
|
6412
|
+
this.notifyReject = noop;
|
|
6413
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
5772
6414
|
this.notifyReady = resolve;
|
|
6415
|
+
this.notifyReject = reject;
|
|
5773
6416
|
});
|
|
5774
6417
|
this.update = update;
|
|
5775
6418
|
this.options = {
|
|
5776
6419
|
interrupt: "wait",
|
|
5777
6420
|
...options,
|
|
5778
6421
|
};
|
|
6422
|
+
// Avoid an unhandled rejection when a failed transition has no
|
|
6423
|
+
// `.then(_, reject)` handler attached (e.g. fire-and-forget).
|
|
6424
|
+
this.readyPromise.catch(noop);
|
|
5779
6425
|
addToQueue(this);
|
|
5780
6426
|
}
|
|
5781
|
-
|
|
6427
|
+
/**
|
|
6428
|
+
* Target elements resolved from a selector or Element, each assigned a
|
|
6429
|
+
* `view-transition-name` automatically.
|
|
6430
|
+
*
|
|
6431
|
+
* Passing a second target pairs them: the first is resolved in the old
|
|
6432
|
+
* snapshot and the second in the new, sharing one name so two *different*
|
|
6433
|
+
* elements morph into each other (e.g. `.add(card, ".modal")`). Symmetric -
|
|
6434
|
+
* pass them the other way round to morph back.
|
|
6435
|
+
*/
|
|
6436
|
+
add(subject, newSubject) {
|
|
5782
6437
|
this.currentSubject = subject;
|
|
6438
|
+
this.resolveDefs.add(subject);
|
|
6439
|
+
if (newSubject !== undefined)
|
|
6440
|
+
this.pairs.set(subject, newSubject);
|
|
6441
|
+
// Register the subject so it participates (and gets an automatic
|
|
6442
|
+
// layout/morph animation) even without an explicit enter/exit/layout.
|
|
6443
|
+
if (!this.targets.has(subject))
|
|
6444
|
+
this.targets.set(subject, {});
|
|
5783
6445
|
return this;
|
|
5784
6446
|
}
|
|
5785
|
-
|
|
5786
|
-
|
|
6447
|
+
/**
|
|
6448
|
+
* Morphs are clipped + `object-fit: cover` (and their corners animate)
|
|
6449
|
+
* by default. Call `.crop(false)` to opt this subject out and fall back
|
|
6450
|
+
* to the browser default (which overflows on aspect-ratio change).
|
|
6451
|
+
*/
|
|
6452
|
+
crop(enabled = true) {
|
|
6453
|
+
enabled
|
|
6454
|
+
? this.noCrop.delete(this.currentSubject)
|
|
6455
|
+
: this.noCrop.add(this.currentSubject);
|
|
5787
6456
|
return this;
|
|
5788
6457
|
}
|
|
5789
|
-
|
|
5790
|
-
|
|
6458
|
+
/**
|
|
6459
|
+
* Tag this subject's generated layers with a `view-transition-class`, so
|
|
6460
|
+
* they can be targeted from CSS - `::view-transition-group(.name)`,
|
|
6461
|
+
* `::view-transition-old/new(.name)`, `::view-transition-image-pair(.name)`
|
|
6462
|
+
* - without the opaque generated `view-transition-name`. Because `.add()`
|
|
6463
|
+
* can match many elements, a shared class targets them all at once (and,
|
|
6464
|
+
* for a pair, both ends). The escape hatch for z-index / custom keyframes
|
|
6465
|
+
* on a morph layer.
|
|
6466
|
+
*/
|
|
6467
|
+
class(name) {
|
|
6468
|
+
this.classNames.set(this.currentSubject, name);
|
|
5791
6469
|
return this;
|
|
5792
6470
|
}
|
|
5793
|
-
|
|
5794
|
-
|
|
6471
|
+
/**
|
|
6472
|
+
* Set the transition for this subject's morph. The morph is enabled
|
|
6473
|
+
* automatically by `.add()`; this just customises its timing (duration,
|
|
6474
|
+
* easing, a `delay`/`stagger`, …). On the implicit `root` subject it also
|
|
6475
|
+
* opts the page into the transition (the root crossfade).
|
|
6476
|
+
*/
|
|
6477
|
+
layout(options = {}) {
|
|
6478
|
+
this.updateTarget("layout", {}, options);
|
|
5795
6479
|
return this;
|
|
5796
6480
|
}
|
|
5797
6481
|
enter(keyframes, options) {
|
|
@@ -5802,9 +6486,21 @@
|
|
|
5802
6486
|
this.updateTarget("exit", keyframes, options);
|
|
5803
6487
|
return this;
|
|
5804
6488
|
}
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
6489
|
+
/**
|
|
6490
|
+
* Animate the new view directly, whether the element is appearing or
|
|
6491
|
+
* persisting (unlike `.enter()`, which only fires for a pure newcomer).
|
|
6492
|
+
* Pair with `.old()` for a crossfade or slide-through.
|
|
6493
|
+
*/
|
|
6494
|
+
new(keyframes, options) {
|
|
6495
|
+
this.updateTarget("new", keyframes, options);
|
|
6496
|
+
return this;
|
|
6497
|
+
}
|
|
6498
|
+
/**
|
|
6499
|
+
* Animate the old view directly, whether the element is leaving or
|
|
6500
|
+
* persisting (unlike `.exit()`, which only fires for a pure leaver).
|
|
6501
|
+
*/
|
|
6502
|
+
old(keyframes, options) {
|
|
6503
|
+
this.updateTarget("old", keyframes, options);
|
|
5808
6504
|
return this;
|
|
5809
6505
|
}
|
|
5810
6506
|
updateTarget(target, keyframes, options = {}) {
|
|
@@ -5819,8 +6515,8 @@
|
|
|
5819
6515
|
return this.readyPromise.then(resolve, reject);
|
|
5820
6516
|
}
|
|
5821
6517
|
}
|
|
5822
|
-
function animateView(update,
|
|
5823
|
-
return new ViewTransitionBuilder(update,
|
|
6518
|
+
function animateView(update, options = {}) {
|
|
6519
|
+
return new ViewTransitionBuilder(update, options);
|
|
5824
6520
|
}
|
|
5825
6521
|
|
|
5826
6522
|
const createAxisDelta = () => ({
|
|
@@ -6767,6 +7463,14 @@
|
|
|
6767
7463
|
}
|
|
6768
7464
|
}
|
|
6769
7465
|
}
|
|
7466
|
+
// `pathRotation` composes onto `rotate` as a separate additive term so
|
|
7467
|
+
// the user's `rotate` is never clobbered. Deliberately not a slot in
|
|
7468
|
+
// `transformPropOrder`.
|
|
7469
|
+
const pathRotation = latestValues.pathRotation;
|
|
7470
|
+
if (pathRotation) {
|
|
7471
|
+
transformIsDefault = false;
|
|
7472
|
+
transformString += `rotate(${getValueAsType(pathRotation, numberValueTypes.pathRotation)}) `;
|
|
7473
|
+
}
|
|
6770
7474
|
transformString = transformString.trim();
|
|
6771
7475
|
// If we have a custom `transform` template, pass our transform values and
|
|
6772
7476
|
// generated transformString to that before returning
|
|
@@ -7829,11 +8533,14 @@
|
|
|
7829
8533
|
transform += `scale(${1 / treeScale.x}, ${1 / treeScale.y}) `;
|
|
7830
8534
|
}
|
|
7831
8535
|
if (latestTransform) {
|
|
7832
|
-
const { transformPerspective, rotate, rotateX, rotateY, skewX, skewY } = latestTransform;
|
|
8536
|
+
const { transformPerspective, rotate, pathRotation, rotateX, rotateY, skewX, skewY, } = latestTransform;
|
|
7833
8537
|
if (transformPerspective)
|
|
7834
8538
|
transform = `perspective(${transformPerspective}px) ${transform}`;
|
|
7835
8539
|
if (rotate)
|
|
7836
8540
|
transform += `rotate(${rotate}deg) `;
|
|
8541
|
+
// Additive `rotate()` so user `rotate` isn't clobbered.
|
|
8542
|
+
if (pathRotation)
|
|
8543
|
+
transform += `rotate(${pathRotation}deg) `;
|
|
7837
8544
|
if (rotateX)
|
|
7838
8545
|
transform += `rotateX(${rotateX}deg) `;
|
|
7839
8546
|
if (rotateY)
|
|
@@ -7909,7 +8616,7 @@
|
|
|
7909
8616
|
: values.borderRadius;
|
|
7910
8617
|
}
|
|
7911
8618
|
const easeCrossfadeIn = /*@__PURE__*/ compress(0, 0.5, circOut);
|
|
7912
|
-
const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop
|
|
8619
|
+
const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop);
|
|
7913
8620
|
function compress(min, max, easing) {
|
|
7914
8621
|
return (p) => {
|
|
7915
8622
|
// Could replace ifs with clamp
|
|
@@ -7929,7 +8636,7 @@
|
|
|
7929
8636
|
|
|
7930
8637
|
function addDomEvent(target, eventName, handler, options = { passive: true }) {
|
|
7931
8638
|
target.addEventListener(eventName, handler, options);
|
|
7932
|
-
return () => target.removeEventListener(eventName, handler);
|
|
8639
|
+
return () => target.removeEventListener(eventName, handler, options);
|
|
7933
8640
|
}
|
|
7934
8641
|
|
|
7935
8642
|
const compareByDepth = (a, b) => a.depth - b.depth;
|
|
@@ -8388,7 +9095,8 @@
|
|
|
8388
9095
|
* Set animation origin after starting animation to avoid layout jump
|
|
8389
9096
|
* caused by stopping previous layout animation
|
|
8390
9097
|
*/
|
|
8391
|
-
this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged
|
|
9098
|
+
this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged, animationOptions
|
|
9099
|
+
.path);
|
|
8392
9100
|
}
|
|
8393
9101
|
else {
|
|
8394
9102
|
/**
|
|
@@ -9104,7 +9812,7 @@
|
|
|
9104
9812
|
this.projectionDelta = createDelta();
|
|
9105
9813
|
this.projectionDeltaWithTransform = createDelta();
|
|
9106
9814
|
}
|
|
9107
|
-
setAnimationOrigin(delta, hasOnlyRelativeTargetChanged = false) {
|
|
9815
|
+
setAnimationOrigin(delta, hasOnlyRelativeTargetChanged = false, pathFn) {
|
|
9108
9816
|
const snapshot = this.snapshot;
|
|
9109
9817
|
const snapshotLatestValues = snapshot ? snapshot.latestValues : {};
|
|
9110
9818
|
const mixedValues = { ...this.latestValues };
|
|
@@ -9126,10 +9834,26 @@
|
|
|
9126
9834
|
!this.path.some(hasOpacityCrossfade));
|
|
9127
9835
|
this.animationProgress = 0;
|
|
9128
9836
|
let prevRelativeTarget;
|
|
9837
|
+
// The path decides whether the layout shift is worth curving
|
|
9838
|
+
// (distance floor) and resolves the interpolator from the delta.
|
|
9839
|
+
const interpolate = pathFn?.interpolateProjection(delta);
|
|
9129
9840
|
this.mixTargetDelta = (latest) => {
|
|
9130
9841
|
const progress = latest / 1000;
|
|
9131
|
-
|
|
9132
|
-
|
|
9842
|
+
const point = interpolate?.(progress);
|
|
9843
|
+
if (point) {
|
|
9844
|
+
targetDelta.x.translate = point.x;
|
|
9845
|
+
targetDelta.x.scale = mixNumber$1(delta.x.scale, 1, progress);
|
|
9846
|
+
targetDelta.x.origin = delta.x.origin;
|
|
9847
|
+
targetDelta.x.originPoint = delta.x.originPoint;
|
|
9848
|
+
targetDelta.y.translate = point.y;
|
|
9849
|
+
targetDelta.y.scale = mixNumber$1(delta.y.scale, 1, progress);
|
|
9850
|
+
targetDelta.y.origin = delta.y.origin;
|
|
9851
|
+
targetDelta.y.originPoint = delta.y.originPoint;
|
|
9852
|
+
}
|
|
9853
|
+
else {
|
|
9854
|
+
mixAxisDeltaLinear(targetDelta.x, delta.x, progress);
|
|
9855
|
+
mixAxisDeltaLinear(targetDelta.y, delta.y, progress);
|
|
9856
|
+
}
|
|
9133
9857
|
this.setTargetDelta(targetDelta);
|
|
9134
9858
|
if (this.relativeTarget &&
|
|
9135
9859
|
this.relativeTargetOrigin &&
|
|
@@ -9154,6 +9878,13 @@
|
|
|
9154
9878
|
this.animationValues = mixedValues;
|
|
9155
9879
|
mixValues(mixedValues, snapshotLatestValues, this.latestValues, progress, shouldCrossfadeOpacity, isOnlyMember);
|
|
9156
9880
|
}
|
|
9881
|
+
if (point && point.rotate !== undefined) {
|
|
9882
|
+
// Dedicated `pathRotation` channel, not `rotate`, so an
|
|
9883
|
+
// animating `rotate` is composed with, never clobbered.
|
|
9884
|
+
if (!this.animationValues)
|
|
9885
|
+
this.animationValues = mixedValues;
|
|
9886
|
+
this.animationValues.pathRotation = point.rotate;
|
|
9887
|
+
}
|
|
9157
9888
|
this.root.scheduleUpdateProjection();
|
|
9158
9889
|
this.scheduleRender();
|
|
9159
9890
|
this.animationProgress = progress;
|
|
@@ -9175,7 +9906,6 @@
|
|
|
9175
9906
|
*/
|
|
9176
9907
|
this.pendingAnimation = frame.update(() => {
|
|
9177
9908
|
globalProjectionState.hasAnimatedSinceResize = true;
|
|
9178
|
-
activeAnimations.layout++;
|
|
9179
9909
|
this.motionValue || (this.motionValue = motionValue(0));
|
|
9180
9910
|
this.motionValue.jump(0, false);
|
|
9181
9911
|
this.currentAnimation = animateSingleValue(this.motionValue, [0, 1000], {
|
|
@@ -9186,11 +9916,7 @@
|
|
|
9186
9916
|
this.mixTargetDelta(latest);
|
|
9187
9917
|
options.onUpdate && options.onUpdate(latest);
|
|
9188
9918
|
},
|
|
9189
|
-
onStop: () => {
|
|
9190
|
-
activeAnimations.layout--;
|
|
9191
|
-
},
|
|
9192
9919
|
onComplete: () => {
|
|
9193
|
-
activeAnimations.layout--;
|
|
9194
9920
|
options.onComplete && options.onComplete();
|
|
9195
9921
|
this.completeAnimation();
|
|
9196
9922
|
},
|
|
@@ -9674,7 +10400,7 @@
|
|
|
9674
10400
|
function removeLeadSnapshots(stack) {
|
|
9675
10401
|
stack.removeLeadSnapshot();
|
|
9676
10402
|
}
|
|
9677
|
-
function
|
|
10403
|
+
function mixAxisDeltaLinear(output, delta, p) {
|
|
9678
10404
|
output.translate = mixNumber$1(delta.translate, 0, p);
|
|
9679
10405
|
output.scale = mixNumber$1(delta.scale, 1, p);
|
|
9680
10406
|
output.origin = delta.origin;
|
|
@@ -9705,7 +10431,7 @@
|
|
|
9705
10431
|
*/
|
|
9706
10432
|
const roundPoint = userAgentContains("applewebkit/") && !userAgentContains("chrome/")
|
|
9707
10433
|
? Math.round
|
|
9708
|
-
: noop
|
|
10434
|
+
: noop;
|
|
9709
10435
|
function roundAxis(axis) {
|
|
9710
10436
|
// Round to the nearest .5 pixels to support subpixel layouts
|
|
9711
10437
|
axis.min = roundPoint(axis.min);
|
|
@@ -9779,39 +10505,221 @@
|
|
|
9779
10505
|
checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
|
|
9780
10506
|
});
|
|
9781
10507
|
|
|
9782
|
-
const layoutSelector = "[data-layout],
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
9787
|
-
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
10508
|
+
const layoutSelector = "[data-layout],[data-layout-id]";
|
|
10509
|
+
/**
|
|
10510
|
+
* All imperatively-created projection nodes live in one persistent tree,
|
|
10511
|
+
* shared across animateLayout() calls (and with any React-created nodes,
|
|
10512
|
+
* via the singleton document root). Keyed by element for reuse.
|
|
10513
|
+
*/
|
|
10514
|
+
const layoutNodes = new WeakMap();
|
|
10515
|
+
/**
|
|
10516
|
+
* Builders created within the same synchronous tick are flushed together
|
|
10517
|
+
* as a single "commit": every node is snapshotted before any updateDom
|
|
10518
|
+
* runs, mirroring React batching renders from different parts of the tree.
|
|
10519
|
+
*/
|
|
10520
|
+
let pendingBuilders;
|
|
10521
|
+
function collectLayoutElements(scope) {
|
|
10522
|
+
const elements = [];
|
|
10523
|
+
if (scope instanceof HTMLElement && scope.matches(layoutSelector)) {
|
|
10524
|
+
elements.push(scope);
|
|
10525
|
+
}
|
|
10526
|
+
scope.querySelectorAll(layoutSelector).forEach((element) => {
|
|
10527
|
+
if (element instanceof HTMLElement)
|
|
10528
|
+
elements.push(element);
|
|
10529
|
+
});
|
|
10530
|
+
return elements;
|
|
10531
|
+
}
|
|
10532
|
+
/**
|
|
10533
|
+
* Process any work scheduled on the frameloop now. A previous animation
|
|
10534
|
+
* may have been seeked while paused (controls.time = x) without a frame
|
|
10535
|
+
* having rendered it - we must materialise that state into the DOM
|
|
10536
|
+
* before taking snapshots.
|
|
10537
|
+
*/
|
|
10538
|
+
function flushPendingFrame() {
|
|
10539
|
+
if (frameData.isProcessing)
|
|
10540
|
+
return;
|
|
10541
|
+
const now = time.now();
|
|
10542
|
+
frameData.delta = clamp(0, 1000 / 60, now - frameData.timestamp);
|
|
10543
|
+
frameData.timestamp = now;
|
|
10544
|
+
frameData.isProcessing = true;
|
|
10545
|
+
frameSteps.update.process(frameData);
|
|
10546
|
+
frameSteps.preRender.process(frameData);
|
|
10547
|
+
frameSteps.render.process(frameData);
|
|
10548
|
+
frameData.isProcessing = false;
|
|
10549
|
+
}
|
|
10550
|
+
function getProjectionParent(element) {
|
|
10551
|
+
let ancestor = element.parentElement;
|
|
10552
|
+
while (ancestor) {
|
|
10553
|
+
const node = layoutNodes.get(ancestor);
|
|
10554
|
+
if (node && node.instance)
|
|
10555
|
+
return node;
|
|
10556
|
+
ancestor = ancestor.parentElement;
|
|
10557
|
+
}
|
|
10558
|
+
return undefined;
|
|
10559
|
+
}
|
|
10560
|
+
function createVisualElement() {
|
|
10561
|
+
return new HTMLVisualElement({
|
|
10562
|
+
props: {},
|
|
10563
|
+
presenceContext: null,
|
|
10564
|
+
visualState: {
|
|
10565
|
+
latestValues: {},
|
|
10566
|
+
renderState: {
|
|
10567
|
+
transform: {},
|
|
10568
|
+
transformOrigin: {},
|
|
10569
|
+
style: {},
|
|
10570
|
+
vars: {},
|
|
10571
|
+
},
|
|
10572
|
+
},
|
|
10573
|
+
}, { allowProjection: true });
|
|
10574
|
+
}
|
|
10575
|
+
function readNodeOptions(element, transition) {
|
|
10576
|
+
const layoutAttr = element.getAttribute("data-layout");
|
|
10577
|
+
const layoutId = element.getAttribute("data-layout-id") ?? undefined;
|
|
9792
10578
|
return {
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
10579
|
+
layoutId,
|
|
10580
|
+
layout: layoutAttr !== null ? true : undefined,
|
|
10581
|
+
animationType: (!layoutAttr || layoutAttr === "true"
|
|
10582
|
+
? "both"
|
|
10583
|
+
: layoutAttr),
|
|
10584
|
+
transition,
|
|
10585
|
+
};
|
|
10586
|
+
}
|
|
10587
|
+
function prepareNode(element, transition) {
|
|
10588
|
+
let node = layoutNodes.get(element);
|
|
10589
|
+
if (!node) {
|
|
10590
|
+
let visualElement = visualElementStore.get(element);
|
|
10591
|
+
if (!visualElement)
|
|
10592
|
+
visualElement = createVisualElement();
|
|
10593
|
+
/**
|
|
10594
|
+
* A first-time element may carry a projection transform in its
|
|
10595
|
+
* inline style (e.g. it was cloned from an element mid-animation).
|
|
10596
|
+
* That transform isn't tracked in latestValues so the engine can't
|
|
10597
|
+
* reset it before measuring - clear it now so the first layout
|
|
10598
|
+
* measurement isn't inflated.
|
|
10599
|
+
*/
|
|
10600
|
+
if (element.style.transform &&
|
|
10601
|
+
!hasTransform(visualElement.latestValues)) {
|
|
10602
|
+
element.style.transform = "";
|
|
10603
|
+
}
|
|
10604
|
+
node = new HTMLProjectionNode(visualElement.latestValues, getProjectionParent(element));
|
|
10605
|
+
visualElement.projection = node;
|
|
10606
|
+
node.setOptions({
|
|
10607
|
+
...readNodeOptions(element, transition),
|
|
10608
|
+
visualElement,
|
|
10609
|
+
});
|
|
10610
|
+
node.mount(element);
|
|
10611
|
+
layoutNodes.set(element, node);
|
|
10612
|
+
}
|
|
10613
|
+
else {
|
|
10614
|
+
node.setOptions(readNodeOptions(element, transition));
|
|
10615
|
+
}
|
|
10616
|
+
node.isPresent = true;
|
|
10617
|
+
if (node.options.onExitComplete) {
|
|
10618
|
+
node.setOptions({ onExitComplete: undefined });
|
|
10619
|
+
}
|
|
10620
|
+
return node;
|
|
10621
|
+
}
|
|
10622
|
+
function sortDocumentOrder(elements) {
|
|
10623
|
+
return [...elements].sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
|
|
10624
|
+
}
|
|
10625
|
+
function dropNode(element, node) {
|
|
10626
|
+
node.setOptions({ onExitComplete: undefined });
|
|
10627
|
+
/**
|
|
10628
|
+
* Stop any lingering animation so it can't leak into future updates.
|
|
10629
|
+
* A follow node can share its currentAnimation with a surviving lead
|
|
10630
|
+
* (via resumingFrom), in which case it isn't ours to stop.
|
|
10631
|
+
*/
|
|
10632
|
+
const stack = node.getStack();
|
|
10633
|
+
if (!stack || node.isLead())
|
|
10634
|
+
node.currentAnimation?.stop();
|
|
10635
|
+
node.unmount();
|
|
10636
|
+
layoutNodes.delete(element);
|
|
10637
|
+
}
|
|
10638
|
+
function flushPendingBuilders() {
|
|
10639
|
+
const builders = pendingBuilders;
|
|
10640
|
+
pendingBuilders = undefined;
|
|
10641
|
+
flushPendingFrame();
|
|
10642
|
+
/**
|
|
10643
|
+
* Discover and mount every node across all builders before snapshotting
|
|
10644
|
+
* any of them. Mounting during an active update flags isLayoutDirty,
|
|
10645
|
+
* which would make that node's own willUpdate skip its snapshot.
|
|
10646
|
+
* Document order guarantees ancestors mount before descendants, even
|
|
10647
|
+
* when they're discovered by different builders.
|
|
10648
|
+
*/
|
|
10649
|
+
const targets = new Map();
|
|
10650
|
+
for (const builder of builders) {
|
|
10651
|
+
for (const element of builder.collectTargets()) {
|
|
10652
|
+
const owners = targets.get(element);
|
|
10653
|
+
owners ? owners.push(builder) : targets.set(element, [builder]);
|
|
10654
|
+
}
|
|
10655
|
+
}
|
|
10656
|
+
const union = new Map();
|
|
10657
|
+
for (const element of sortDocumentOrder(targets.keys())) {
|
|
10658
|
+
const owners = targets.get(element);
|
|
10659
|
+
const node = prepareNode(element, owners[owners.length - 1].transitionFor(element));
|
|
10660
|
+
for (const owner of owners)
|
|
10661
|
+
owner.adopt(element, node);
|
|
10662
|
+
union.set(element, node);
|
|
10663
|
+
}
|
|
10664
|
+
union.forEach((node) => {
|
|
10665
|
+
node.isLayoutDirty = false;
|
|
10666
|
+
node.willUpdate();
|
|
10667
|
+
});
|
|
10668
|
+
const updatePromises = [];
|
|
10669
|
+
for (const builder of builders) {
|
|
10670
|
+
const result = builder.runUpdate();
|
|
10671
|
+
if (result)
|
|
10672
|
+
updatePromises.push(result);
|
|
10673
|
+
}
|
|
10674
|
+
const commit = () => {
|
|
10675
|
+
/**
|
|
10676
|
+
* Process all additions before any removals so that, even across
|
|
10677
|
+
* builders, a removed member knows whether a replacement with the
|
|
10678
|
+
* same layoutId was added in this commit.
|
|
10679
|
+
*/
|
|
10680
|
+
const newMemberIds = new Set();
|
|
10681
|
+
for (const builder of builders) {
|
|
10682
|
+
builder.reconcileAdditions(newMemberIds);
|
|
10683
|
+
}
|
|
10684
|
+
for (const builder of builders) {
|
|
10685
|
+
builder.reconcileRemovals(newMemberIds);
|
|
10686
|
+
}
|
|
10687
|
+
let root;
|
|
10688
|
+
union.forEach((node) => (root || (root = node.root)));
|
|
10689
|
+
for (const builder of builders)
|
|
10690
|
+
root || (root = builder.getRoot());
|
|
10691
|
+
root?.didUpdate();
|
|
10692
|
+
/**
|
|
10693
|
+
* The root flushes the update on a microtask, synchronously
|
|
10694
|
+
* processing the frame that creates the layout animations. Collect
|
|
10695
|
+
* them in a later microtask step of the same pass.
|
|
10696
|
+
*/
|
|
10697
|
+
microtask.render(() => {
|
|
10698
|
+
for (const builder of builders)
|
|
10699
|
+
builder.finalize();
|
|
10700
|
+
});
|
|
9798
10701
|
};
|
|
10702
|
+
updatePromises.length ? Promise.all(updatePromises).then(commit) : commit();
|
|
9799
10703
|
}
|
|
9800
10704
|
class LayoutAnimationBuilder {
|
|
9801
10705
|
constructor(scope, updateDom, defaultOptions) {
|
|
9802
|
-
this.sharedTransitions = new Map();
|
|
9803
|
-
this.notifyReady = noop;
|
|
9804
|
-
this.rejectReady = noop;
|
|
9805
10706
|
this.scope = scope;
|
|
9806
10707
|
this.updateDom = updateDom;
|
|
9807
10708
|
this.defaultOptions = defaultOptions;
|
|
10709
|
+
this.sharedTransitions = new Map();
|
|
10710
|
+
this.notifyReady = () => { };
|
|
10711
|
+
this.rejectReady = () => { };
|
|
10712
|
+
this.tracked = new Map();
|
|
10713
|
+
this.restorePoints = new Map();
|
|
9808
10714
|
this.readyPromise = new Promise((resolve, reject) => {
|
|
9809
10715
|
this.notifyReady = resolve;
|
|
9810
10716
|
this.rejectReady = reject;
|
|
9811
10717
|
});
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
10718
|
+
if (!pendingBuilders) {
|
|
10719
|
+
pendingBuilders = [];
|
|
10720
|
+
queueMicrotask(flushPendingBuilders);
|
|
10721
|
+
}
|
|
10722
|
+
pendingBuilders.push(this);
|
|
9815
10723
|
}
|
|
9816
10724
|
shared(id, transition) {
|
|
9817
10725
|
this.sharedTransitions.set(id, transition);
|
|
@@ -9820,114 +10728,107 @@
|
|
|
9820
10728
|
then(resolve, reject) {
|
|
9821
10729
|
return this.readyPromise.then(resolve, reject);
|
|
9822
10730
|
}
|
|
9823
|
-
|
|
9824
|
-
const
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
}
|
|
9834
|
-
else if (projection.snapshot) {
|
|
9835
|
-
projection.snapshot = undefined;
|
|
9836
|
-
}
|
|
9837
|
-
}
|
|
9838
|
-
else if (projection.snapshot &&
|
|
9839
|
-
(projection.currentAnimation || projection.isProjecting())) {
|
|
9840
|
-
projection.snapshot = undefined;
|
|
9841
|
-
}
|
|
9842
|
-
projection.isPresent = true;
|
|
9843
|
-
projection.willUpdate();
|
|
10731
|
+
transitionFor(element) {
|
|
10732
|
+
const layoutId = element.getAttribute("data-layout-id");
|
|
10733
|
+
return ((layoutId && this.sharedTransitions.get(layoutId)) ||
|
|
10734
|
+
this.defaultOptions);
|
|
10735
|
+
}
|
|
10736
|
+
adopt(element, node) {
|
|
10737
|
+
this.tracked.set(element, node);
|
|
10738
|
+
this.restorePoints.set(element, {
|
|
10739
|
+
parent: element.parentElement,
|
|
10740
|
+
next: element.nextSibling,
|
|
9844
10741
|
});
|
|
9845
|
-
|
|
9846
|
-
|
|
9847
|
-
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
9851
|
-
const
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
return;
|
|
9857
|
-
const currentTransform = instance.style.transform;
|
|
9858
|
-
const resumeFromTransform = resumeFromInstance.style.transform;
|
|
9859
|
-
if (currentTransform &&
|
|
9860
|
-
resumeFromTransform &&
|
|
9861
|
-
currentTransform === resumeFromTransform) {
|
|
9862
|
-
instance.style.transform = "";
|
|
9863
|
-
instance.style.transformOrigin = "";
|
|
10742
|
+
}
|
|
10743
|
+
collectTargets() {
|
|
10744
|
+
return collectLayoutElements(this.scope);
|
|
10745
|
+
}
|
|
10746
|
+
runUpdate() {
|
|
10747
|
+
try {
|
|
10748
|
+
const result = this.updateDom();
|
|
10749
|
+
if (result && typeof result.then === "function") {
|
|
10750
|
+
return result.then(undefined, (error) => {
|
|
10751
|
+
this.updateError = error;
|
|
10752
|
+
});
|
|
9864
10753
|
}
|
|
9865
|
-
}
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
}
|
|
9869
|
-
|
|
9870
|
-
root?.didUpdate();
|
|
9871
|
-
await new Promise((resolve) => {
|
|
9872
|
-
frame.postRender(() => resolve());
|
|
9873
|
-
});
|
|
9874
|
-
const animations = collectAnimations(afterRecords);
|
|
9875
|
-
const animation = new GroupAnimation(animations);
|
|
9876
|
-
return animation;
|
|
10754
|
+
}
|
|
10755
|
+
catch (error) {
|
|
10756
|
+
this.updateError = error;
|
|
10757
|
+
}
|
|
10758
|
+
return undefined;
|
|
9877
10759
|
}
|
|
9878
|
-
|
|
9879
|
-
const
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
const
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
? this.sharedTransitions.get(layoutId)
|
|
9886
|
-
: undefined;
|
|
9887
|
-
const transition = override || this.defaultOptions;
|
|
9888
|
-
const record = getOrCreateRecord(element, parentRecord?.projection, {
|
|
9889
|
-
layout,
|
|
9890
|
-
layoutId,
|
|
9891
|
-
animationType: typeof layout === "string" ? layout : "both",
|
|
9892
|
-
transition: transition,
|
|
9893
|
-
});
|
|
9894
|
-
recordMap.set(element, record);
|
|
9895
|
-
records.push(record);
|
|
10760
|
+
reconcileAdditions(newMemberIds) {
|
|
10761
|
+
for (const element of collectLayoutElements(this.scope)) {
|
|
10762
|
+
if (this.tracked.has(element))
|
|
10763
|
+
continue;
|
|
10764
|
+
const node = prepareNode(element, this.transitionFor(element));
|
|
10765
|
+
this.adopt(element, node);
|
|
10766
|
+
node.options.layoutId && newMemberIds.add(node.options.layoutId);
|
|
9896
10767
|
}
|
|
9897
|
-
return records;
|
|
9898
10768
|
}
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
if (afterElementsSet.has(record.element))
|
|
10769
|
+
reconcileRemovals(newMemberIds) {
|
|
10770
|
+
this.tracked.forEach((node, element) => {
|
|
10771
|
+
if (element.isConnected)
|
|
9903
10772
|
return;
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
10773
|
+
const restore = this.restorePoints.get(element);
|
|
10774
|
+
this.restorePoints.delete(element);
|
|
10775
|
+
const { layoutId } = node.options;
|
|
10776
|
+
const stack = node.getStack();
|
|
10777
|
+
const hasSurvivor = stack &&
|
|
10778
|
+
stack.members.some((member) => member !== node &&
|
|
10779
|
+
member.instance
|
|
10780
|
+
?.isConnected);
|
|
10781
|
+
/**
|
|
10782
|
+
* A removed lead with a surviving stack member - and no
|
|
10783
|
+
* replacement member added this commit - runs an exit
|
|
10784
|
+
* crossfade: restore the element to its old position in the
|
|
10785
|
+
* DOM, relegate it and let the survivor take over. It's
|
|
10786
|
+
* removed again once the animation completes.
|
|
10787
|
+
*/
|
|
10788
|
+
if (layoutId &&
|
|
10789
|
+
node.isLead() &&
|
|
10790
|
+
hasSurvivor &&
|
|
10791
|
+
!newMemberIds.has(layoutId)) {
|
|
10792
|
+
if (restore && restore.parent.isConnected) {
|
|
10793
|
+
restore.parent.insertBefore(element, restore.next && restore.next.parentNode === restore.parent
|
|
10794
|
+
? restore.next
|
|
10795
|
+
: null);
|
|
10796
|
+
node.isPresent = false;
|
|
10797
|
+
node.setOptions({
|
|
10798
|
+
onExitComplete: () => {
|
|
10799
|
+
element.remove();
|
|
10800
|
+
dropNode(element, node);
|
|
10801
|
+
},
|
|
10802
|
+
});
|
|
10803
|
+
if (node.relegate())
|
|
10804
|
+
return;
|
|
10805
|
+
element.remove();
|
|
10806
|
+
}
|
|
10807
|
+
}
|
|
10808
|
+
dropNode(element, node);
|
|
10809
|
+
this.tracked.delete(element);
|
|
9912
10810
|
});
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
10811
|
+
}
|
|
10812
|
+
getRoot() {
|
|
10813
|
+
let root;
|
|
10814
|
+
this.tracked.forEach((node) => (root || (root = node.root)));
|
|
10815
|
+
return root;
|
|
10816
|
+
}
|
|
10817
|
+
finalize() {
|
|
10818
|
+
if (this.updateError) {
|
|
10819
|
+
this.rejectReady(this.updateError);
|
|
10820
|
+
return;
|
|
10821
|
+
}
|
|
10822
|
+
const animations = new Set();
|
|
10823
|
+
this.tracked.forEach((node) => {
|
|
10824
|
+
if (node.instance && node.currentAnimation) {
|
|
10825
|
+
animations.add(node.currentAnimation);
|
|
9925
10826
|
}
|
|
9926
10827
|
});
|
|
10828
|
+
this.notifyReady(new GroupAnimation([...animations]));
|
|
9927
10829
|
}
|
|
9928
10830
|
}
|
|
9929
10831
|
function parseAnimateLayoutArgs(scopeOrUpdateDom, updateDomOrOptions, options) {
|
|
9930
|
-
// animateLayout(updateDom)
|
|
9931
10832
|
if (typeof scopeOrUpdateDom === "function") {
|
|
9932
10833
|
return {
|
|
9933
10834
|
scope: document,
|
|
@@ -9935,107 +10836,15 @@
|
|
|
9935
10836
|
defaultOptions: updateDomOrOptions,
|
|
9936
10837
|
};
|
|
9937
10838
|
}
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
10839
|
+
const scope = scopeOrUpdateDom instanceof Document
|
|
10840
|
+
? scopeOrUpdateDom
|
|
10841
|
+
: resolveElements(scopeOrUpdateDom)[0] ?? document;
|
|
9941
10842
|
return {
|
|
9942
10843
|
scope,
|
|
9943
10844
|
updateDom: updateDomOrOptions,
|
|
9944
10845
|
defaultOptions: options,
|
|
9945
10846
|
};
|
|
9946
10847
|
}
|
|
9947
|
-
function collectLayoutElements(scope) {
|
|
9948
|
-
const elements = Array.from(scope.querySelectorAll(layoutSelector));
|
|
9949
|
-
if (scope instanceof Element && scope.matches(layoutSelector)) {
|
|
9950
|
-
if (!elements.includes(scope)) {
|
|
9951
|
-
elements.unshift(scope);
|
|
9952
|
-
}
|
|
9953
|
-
}
|
|
9954
|
-
return elements;
|
|
9955
|
-
}
|
|
9956
|
-
function readLayoutAttributes(element) {
|
|
9957
|
-
const layoutId = element.getAttribute("data-layout-id") || undefined;
|
|
9958
|
-
const rawLayout = element.getAttribute("data-layout");
|
|
9959
|
-
let layout;
|
|
9960
|
-
if (rawLayout === "" || rawLayout === "true") {
|
|
9961
|
-
layout = true;
|
|
9962
|
-
}
|
|
9963
|
-
else if (rawLayout) {
|
|
9964
|
-
layout = rawLayout;
|
|
9965
|
-
}
|
|
9966
|
-
return {
|
|
9967
|
-
layout,
|
|
9968
|
-
layoutId,
|
|
9969
|
-
};
|
|
9970
|
-
}
|
|
9971
|
-
function createVisualState() {
|
|
9972
|
-
return {
|
|
9973
|
-
latestValues: {},
|
|
9974
|
-
renderState: {
|
|
9975
|
-
transform: {},
|
|
9976
|
-
transformOrigin: {},
|
|
9977
|
-
style: {},
|
|
9978
|
-
vars: {},
|
|
9979
|
-
},
|
|
9980
|
-
};
|
|
9981
|
-
}
|
|
9982
|
-
function getOrCreateRecord(element, parentProjection, projectionOptions) {
|
|
9983
|
-
const existing = visualElementStore.get(element);
|
|
9984
|
-
const visualElement = existing ??
|
|
9985
|
-
new HTMLVisualElement({
|
|
9986
|
-
props: {},
|
|
9987
|
-
presenceContext: null,
|
|
9988
|
-
visualState: createVisualState(),
|
|
9989
|
-
}, { allowProjection: true });
|
|
9990
|
-
if (!existing || !visualElement.projection) {
|
|
9991
|
-
visualElement.projection = new HTMLProjectionNode(visualElement.latestValues, parentProjection);
|
|
9992
|
-
}
|
|
9993
|
-
visualElement.projection.setOptions({
|
|
9994
|
-
...projectionOptions,
|
|
9995
|
-
visualElement,
|
|
9996
|
-
});
|
|
9997
|
-
if (!visualElement.current) {
|
|
9998
|
-
visualElement.mount(element);
|
|
9999
|
-
}
|
|
10000
|
-
else if (!visualElement.projection.instance) {
|
|
10001
|
-
// Mount projection if VisualElement is already mounted but projection isn't
|
|
10002
|
-
// This happens when animate() was called before animateLayout()
|
|
10003
|
-
visualElement.projection.mount(element);
|
|
10004
|
-
}
|
|
10005
|
-
if (!existing) {
|
|
10006
|
-
visualElementStore.set(element, visualElement);
|
|
10007
|
-
}
|
|
10008
|
-
return {
|
|
10009
|
-
element,
|
|
10010
|
-
visualElement,
|
|
10011
|
-
projection: visualElement.projection,
|
|
10012
|
-
};
|
|
10013
|
-
}
|
|
10014
|
-
function findParentRecord(element, recordMap, scope) {
|
|
10015
|
-
let parent = element.parentElement;
|
|
10016
|
-
while (parent) {
|
|
10017
|
-
const record = recordMap.get(parent);
|
|
10018
|
-
if (record)
|
|
10019
|
-
return record;
|
|
10020
|
-
if (parent === scope)
|
|
10021
|
-
break;
|
|
10022
|
-
parent = parent.parentElement;
|
|
10023
|
-
}
|
|
10024
|
-
return undefined;
|
|
10025
|
-
}
|
|
10026
|
-
function getProjectionRoot(afterRecords, beforeRecords) {
|
|
10027
|
-
const record = afterRecords[0] || beforeRecords[0];
|
|
10028
|
-
return record?.projection.root;
|
|
10029
|
-
}
|
|
10030
|
-
function collectAnimations(afterRecords) {
|
|
10031
|
-
const animations = new Set();
|
|
10032
|
-
afterRecords.forEach((record) => {
|
|
10033
|
-
const animation = record.projection.currentAnimation;
|
|
10034
|
-
if (animation)
|
|
10035
|
-
animations.add(animation);
|
|
10036
|
-
});
|
|
10037
|
-
return Array.from(animations);
|
|
10038
|
-
}
|
|
10039
10848
|
|
|
10040
10849
|
/**
|
|
10041
10850
|
* @deprecated
|
|
@@ -11036,7 +11845,7 @@
|
|
|
11036
11845
|
const getEventTarget = (element) => element === document.scrollingElement ? window : element;
|
|
11037
11846
|
function scrollInfo(onScroll, { container = document.scrollingElement, trackContentSize = false, ...options } = {}) {
|
|
11038
11847
|
if (!container)
|
|
11039
|
-
return noop
|
|
11848
|
+
return noop;
|
|
11040
11849
|
let containerHandlers = onScrollHandlers.get(container);
|
|
11041
11850
|
/**
|
|
11042
11851
|
* Get the onScroll handlers for this container.
|
|
@@ -11320,7 +12129,7 @@
|
|
|
11320
12129
|
|
|
11321
12130
|
function scroll(onScroll, { axis = "y", container = document.scrollingElement, ...options } = {}) {
|
|
11322
12131
|
if (!container)
|
|
11323
|
-
return noop
|
|
12132
|
+
return noop;
|
|
11324
12133
|
const optionsWithDefaults = { axis, container, ...options };
|
|
11325
12134
|
return typeof onScroll === "function"
|
|
11326
12135
|
? attachToFunction(onScroll, optionsWithDefaults)
|
|
@@ -11400,7 +12209,6 @@
|
|
|
11400
12209
|
exports.ViewTransitionBuilder = ViewTransitionBuilder;
|
|
11401
12210
|
exports.VisualElement = VisualElement;
|
|
11402
12211
|
exports.acceleratedValues = acceleratedValues;
|
|
11403
|
-
exports.activeAnimations = activeAnimations;
|
|
11404
12212
|
exports.addAttrValue = addAttrValue;
|
|
11405
12213
|
exports.addDomEvent = addDomEvent;
|
|
11406
12214
|
exports.addScaleCorrector = addScaleCorrector;
|
|
@@ -11426,6 +12234,7 @@
|
|
|
11426
12234
|
exports.applyPointDelta = applyPointDelta;
|
|
11427
12235
|
exports.applyPxDefaults = applyPxDefaults;
|
|
11428
12236
|
exports.applyTreeDeltas = applyTreeDeltas;
|
|
12237
|
+
exports.arc = arc;
|
|
11429
12238
|
exports.aspectRatio = aspectRatio;
|
|
11430
12239
|
exports.attachFollow = attachFollow;
|
|
11431
12240
|
exports.attachSpring = attachSpring;
|
|
@@ -11598,7 +12407,7 @@
|
|
|
11598
12407
|
exports.motionValue = motionValue;
|
|
11599
12408
|
exports.moveItem = moveItem;
|
|
11600
12409
|
exports.nodeGroup = nodeGroup;
|
|
11601
|
-
exports.noop = noop
|
|
12410
|
+
exports.noop = noop;
|
|
11602
12411
|
exports.number = number;
|
|
11603
12412
|
exports.numberValueTypes = numberValueTypes;
|
|
11604
12413
|
exports.observeTimeline = observeTimeline;
|