fix: make animations more robust to quick shuffling (#12496)

Previously, if transitions/animations were playing in quick succession, overlapping each other, it could have disastrous outcomes, leading to elements jumping all over the place.
This PR gets that into much better state (not completely fixed, but close) by applying a few fixes:
- destructure style object from `getComputedStyles`, because it's a live object with getters and we're interested in the fixed values at the beginning
- `unfix` for animations didn't reset the transition styles
- don't apply `fix` when we detect already-running animations on the element. That means it's already away from its original position, and doesn't need fixing. Worse, applying an absolute position can lead to the element jumping to the top left if the running animation also applies a transition style - those take precedence over the one we would apply

fixes #10252
pull/12504/head
Simon H 1 year ago committed by GitHub
parent 20e6508c47
commit e02c90209b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make animations more robust to quick shuffling

@ -75,7 +75,7 @@ export function animation(element, get_fn, get_params) {
/** @type {import('#client').Animation | undefined} */ /** @type {import('#client').Animation | undefined} */
var animation; var animation;
/** @type {null | { position: string, width: string, height: string }} */ /** @type {null | { position: string, width: string, height: string, transform: string }} */
var original_styles = null; var original_styles = null;
item.a ??= { item.a ??= {
@ -110,20 +110,29 @@ export function animation(element, get_fn, get_params) {
} }
}, },
fix() { fix() {
var computed_style = getComputedStyle(element); // If an animation is already running, transforming the element is likely to fail,
// because the styles applied by the animation take precedence. In the case of crossfade,
// that means the `translate(...)` of the crossfade transition overrules the `translate(...)`
// we would apply below, leading to the element jumping somewhere to the top left.
if (element.getAnimations().length) return;
if (computed_style.position !== 'absolute' && computed_style.position !== 'fixed') { // It's important to destructure these to get fixed values - the object itself has getters,
// and changing the style to 'absolute' can for example influence the width.
var { position, width, height } = getComputedStyle(element);
if (position !== 'absolute' && position !== 'fixed') {
var style = /** @type {HTMLElement | SVGElement} */ (element).style; var style = /** @type {HTMLElement | SVGElement} */ (element).style;
original_styles = { original_styles = {
position: style.position, position: style.position,
width: style.width, width: style.width,
height: style.height height: style.height,
transform: style.transform
}; };
style.position = 'absolute'; style.position = 'absolute';
style.width = computed_style.width; style.width = width;
style.height = computed_style.height; style.height = height;
var to = element.getBoundingClientRect(); var to = element.getBoundingClientRect();
if (from.left !== to.left || from.top !== to.top) { if (from.left !== to.left || from.top !== to.top) {
@ -139,6 +148,7 @@ export function animation(element, get_fn, get_params) {
style.position = original_styles.position; style.position = original_styles.position;
style.width = original_styles.width; style.width = original_styles.width;
style.height = original_styles.height; style.height = original_styles.height;
style.transform = original_styles.transform;
} }
} }
}; };

@ -32,7 +32,6 @@ function tick(time) {
} }
class Animation { class Animation {
#target;
#keyframes; #keyframes;
#duration; #duration;
#delay; #delay;
@ -42,6 +41,7 @@ class Animation {
#finished = () => {}; #finished = () => {};
#cancelled = () => {}; #cancelled = () => {};
target;
currentTime = 0; currentTime = 0;
startTime = 0; startTime = 0;
@ -51,7 +51,7 @@ class Animation {
* @param {{ duration: number, delay: number }} options * @param {{ duration: number, delay: number }} options
*/ */
constructor(target, keyframes, { duration, delay }) { constructor(target, keyframes, { duration, delay }) {
this.#target = target; this.target = target;
this.#keyframes = keyframes; this.#keyframes = keyframes;
this.#duration = duration; this.#duration = duration;
this.#delay = delay ?? 0; this.#delay = delay ?? 0;
@ -111,14 +111,14 @@ class Animation {
for (let prop in frame) { for (let prop in frame) {
// @ts-ignore // @ts-ignore
this.#target.style[prop] = frame[prop]; this.target.style[prop] = frame[prop];
} }
if (this.currentTime >= this.#duration) { if (this.currentTime >= this.#duration) {
this.currentTime = this.#duration; this.currentTime = this.#duration;
for (let prop in frame) { for (let prop in frame) {
// @ts-ignore // @ts-ignore
this.#target.style[prop] = null; this.target.style[prop] = null;
} }
} }
} }
@ -181,3 +181,7 @@ HTMLElement.prototype.animate = function (keyframes, options) {
// @ts-ignore // @ts-ignore
return animation; return animation;
}; };
HTMLElement.prototype.getAnimations = function () {
return Array.from(raf.animations).filter((animation) => animation.target === this);
};

Loading…
Cancel
Save