fix: improve transition outro easing (#10190)

* fix: improve transition outro easing

* Update tests
pull/10197/head
Dominic Gannaway 11 months ago committed by GitHub
parent 86bbc83544
commit b94d72bbfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve transition outro easing

@ -256,6 +256,44 @@ function handle_raf(time) {
} }
} }
/**
* @param {{(t: number): number;(t: number): number;(arg0: number): any;}} easing_fn
* @param {((t: number, u: number) => string)} css_fn
* @param {number} duration
* @param {string} direction
* @param {boolean} reverse
*/
function create_keyframes(easing_fn, css_fn, duration, direction, reverse) {
/** @type {Keyframe[]} */
const keyframes = [];
// We need at least two frames
const frame_time = 16.666;
const max_duration = Math.max(duration, frame_time);
// Have a keyframe every fame for 60 FPS
for (let i = 0; i <= max_duration; i += frame_time) {
let time;
if (i + frame_time > max_duration) {
time = 1;
} else if (i === 0) {
time = 0;
} else {
time = i / max_duration;
}
let t = easing_fn(time);
if (reverse) {
t = 1 - t;
}
keyframes.push(css_to_keyframe(css_fn(t, 1 - t)));
}
if (direction === 'out' || reverse) {
keyframes.reverse();
}
return keyframes;
}
/** @param {number} t */
const linear = (t) => t;
/** /**
* @param {HTMLElement} dom * @param {HTMLElement} dom
* @param {() => import('./types.js').TransitionPayload} init * @param {() => import('./types.js').TransitionPayload} init
@ -286,38 +324,15 @@ function create_transition(dom, init, direction, effect) {
const delay = payload.delay ?? 0; const delay = payload.delay ?? 0;
const css_fn = payload.css; const css_fn = payload.css;
const tick_fn = payload.tick; const tick_fn = payload.tick;
/** @param {number} t */
const linear = (t) => t;
const easing_fn = payload.easing || linear; const easing_fn = payload.easing || linear;
/** @type {Keyframe[]} */
const keyframes = [];
if (typeof tick_fn === 'function') { if (typeof tick_fn === 'function') {
animation = new TickAnimation(tick_fn, duration, delay, direction === 'out'); animation = new TickAnimation(tick_fn, duration, delay, direction === 'out');
} else { } else {
if (typeof css_fn === 'function') { const keyframes =
// We need at least two frames typeof css_fn === 'function'
const frame_time = 16.666; ? create_keyframes(easing_fn, css_fn, duration, direction, false)
const max_duration = Math.max(duration, frame_time); : [];
// Have a keyframe every fame for 60 FPS
for (let i = 0; i <= max_duration; i += frame_time) {
let time;
if (i + frame_time > max_duration) {
time = 1;
} else if (i === 0) {
time = 0;
} else {
time = i / max_duration;
}
const t = easing_fn(time);
keyframes.push(css_to_keyframe(css_fn(t, 1 - t)));
}
if (direction === 'out') {
keyframes.reverse();
}
}
animation = dom.animate(keyframes, { animation = dom.animate(keyframes, {
duration, duration,
endDelay: delay, endDelay: delay,
@ -421,6 +436,26 @@ function create_transition(dom, init, direction, effect) {
} else { } else {
dispatch_event(dom, 'outrostart'); dispatch_event(dom, 'outrostart');
if (needs_reverse) { if (needs_reverse) {
const payload = transition.p;
const current_animation = /** @type {Animation} */ (animation);
// If we are working with CSS animations, then before we call reverse, we also need to ensure
// that we reverse the easing logic. To do this we need to re-create the keyframes so they're
// in reverse with easing properly reversed too.
if (
payload !== null &&
payload.css !== undefined &&
current_animation.playState === 'idle'
) {
const duration = payload.duration ?? 300;
const css_fn = payload.css;
const easing_fn = payload.easing || linear;
const keyframes = create_keyframes(easing_fn, css_fn, duration, direction, true);
const effect = current_animation.effect;
if (effect !== null) {
// @ts-ignore
effect.setKeyframes(keyframes);
}
}
/** @type {Animation | TickAnimation} */ (animation).reverse(); /** @type {Animation | TickAnimation} */ (animation).reverse();
} else { } else {
/** @type {Animation | TickAnimation} */ (animation).play(); /** @type {Animation | TickAnimation} */ (animation).play();

@ -53,11 +53,18 @@ class Animation {
this.onfinish = () => {}; this.onfinish = () => {};
this.pending = true; this.pending = true;
this.currentTime = 0; this.currentTime = 0;
this.playState = 'running';
this.effect = {
setKeyframes: (/** @type {Keyframe[]} */ keyframes) => {
this.#keyframes = keyframes;
}
};
} }
play() { play() {
this.#paused = false; this.#paused = false;
raf.animations.add(this); raf.animations.add(this);
this.playState = 'running';
this._update(); this._update();
} }
@ -107,6 +114,7 @@ class Animation {
if (this.#reversed) { if (this.#reversed) {
raf.animations.delete(this); raf.animations.delete(this);
} }
this.playState = 'idle';
} }
cancel() { cancel() {
@ -118,11 +126,13 @@ class Animation {
pause() { pause() {
this.#paused = true; this.#paused = true;
this.playState = 'paused';
} }
reverse() { reverse() {
this.#timeline_offset = this.currentTime; this.#timeline_offset = this.currentTime;
this.#reversed = !this.#reversed; this.#reversed = !this.#reversed;
this.playState = 'running';
} }
} }

@ -18,7 +18,7 @@ export default test({
raf.tick(150); raf.tick(150);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
'<p>foo</p><p class="red svelte-1yszte8 border" style="overflow: hidden; opacity: 0; border-top-width: 3.4999399975999683px; border-bottom-width: 3.4999399975999683px;">bar</p>' '<p>foo</p><p class="red svelte-1yszte8 border" style="overflow: hidden; opacity: 0; border-top-width: 0.5000600024000317px; border-bottom-width: 0.5000600024000317px;">bar</p>'
); );
component.open = true; component.open = true;
raf.tick(250); raf.tick(250);

Loading…
Cancel
Save