diff --git a/.changeset/brave-shrimps-kiss.md b/.changeset/brave-shrimps-kiss.md new file mode 100644 index 0000000000..096a156794 --- /dev/null +++ b/.changeset/brave-shrimps-kiss.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve transition outro easing diff --git a/packages/svelte/src/internal/client/transitions.js b/packages/svelte/src/internal/client/transitions.js index 96ad2fd35a..254b06e0dd 100644 --- a/packages/svelte/src/internal/client/transitions.js +++ b/packages/svelte/src/internal/client/transitions.js @@ -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 {() => import('./types.js').TransitionPayload} init @@ -286,38 +324,15 @@ function create_transition(dom, init, direction, effect) { const delay = payload.delay ?? 0; const css_fn = payload.css; const tick_fn = payload.tick; - - /** @param {number} t */ - const linear = (t) => t; const easing_fn = payload.easing || linear; - /** @type {Keyframe[]} */ - const keyframes = []; - if (typeof tick_fn === 'function') { animation = new TickAnimation(tick_fn, duration, delay, direction === 'out'); } else { - if (typeof css_fn === 'function') { - // 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; - } - const t = easing_fn(time); - keyframes.push(css_to_keyframe(css_fn(t, 1 - t))); - } - if (direction === 'out') { - keyframes.reverse(); - } - } + const keyframes = + typeof css_fn === 'function' + ? create_keyframes(easing_fn, css_fn, duration, direction, false) + : []; animation = dom.animate(keyframes, { duration, endDelay: delay, @@ -421,6 +436,26 @@ function create_transition(dom, init, direction, effect) { } else { dispatch_event(dom, 'outrostart'); 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(); } else { /** @type {Animation | TickAnimation} */ (animation).play(); diff --git a/packages/svelte/tests/animation-helpers.js b/packages/svelte/tests/animation-helpers.js index 21635a05bd..880442244e 100644 --- a/packages/svelte/tests/animation-helpers.js +++ b/packages/svelte/tests/animation-helpers.js @@ -53,11 +53,18 @@ class Animation { this.onfinish = () => {}; this.pending = true; this.currentTime = 0; + this.playState = 'running'; + this.effect = { + setKeyframes: (/** @type {Keyframe[]} */ keyframes) => { + this.#keyframes = keyframes; + } + }; } play() { this.#paused = false; raf.animations.add(this); + this.playState = 'running'; this._update(); } @@ -107,6 +114,7 @@ class Animation { if (this.#reversed) { raf.animations.delete(this); } + this.playState = 'idle'; } cancel() { @@ -118,11 +126,13 @@ class Animation { pause() { this.#paused = true; + this.playState = 'paused'; } reverse() { this.#timeline_offset = this.currentTime; this.#reversed = !this.#reversed; + this.playState = 'running'; } } diff --git a/packages/svelte/tests/runtime-legacy/samples/class-shortcut-with-transition/_config.js b/packages/svelte/tests/runtime-legacy/samples/class-shortcut-with-transition/_config.js index 8e2013fc54..3df138e99a 100644 --- a/packages/svelte/tests/runtime-legacy/samples/class-shortcut-with-transition/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/class-shortcut-with-transition/_config.js @@ -18,7 +18,7 @@ export default test({ raf.tick(150); assert.htmlEqual( target.innerHTML, - '

foo

bar

' + '

foo

bar

' ); component.open = true; raf.tick(250);