Skip to content

Commit b94d72b

Browse files
authored
fix: improve transition outro easing (#10190)
* fix: improve transition outro easing * Update tests
1 parent 86bbc83 commit b94d72b

File tree

4 files changed

+78
-28
lines changed

4 files changed

+78
-28
lines changed

.changeset/brave-shrimps-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: improve transition outro easing

packages/svelte/src/internal/client/transitions.js

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,44 @@ function handle_raf(time) {
256256
}
257257
}
258258

259+
/**
260+
* @param {{(t: number): number;(t: number): number;(arg0: number): any;}} easing_fn
261+
* @param {((t: number, u: number) => string)} css_fn
262+
* @param {number} duration
263+
* @param {string} direction
264+
* @param {boolean} reverse
265+
*/
266+
function create_keyframes(easing_fn, css_fn, duration, direction, reverse) {
267+
/** @type {Keyframe[]} */
268+
const keyframes = [];
269+
// We need at least two frames
270+
const frame_time = 16.666;
271+
const max_duration = Math.max(duration, frame_time);
272+
// Have a keyframe every fame for 60 FPS
273+
for (let i = 0; i <= max_duration; i += frame_time) {
274+
let time;
275+
if (i + frame_time > max_duration) {
276+
time = 1;
277+
} else if (i === 0) {
278+
time = 0;
279+
} else {
280+
time = i / max_duration;
281+
}
282+
let t = easing_fn(time);
283+
if (reverse) {
284+
t = 1 - t;
285+
}
286+
keyframes.push(css_to_keyframe(css_fn(t, 1 - t)));
287+
}
288+
if (direction === 'out' || reverse) {
289+
keyframes.reverse();
290+
}
291+
return keyframes;
292+
}
293+
294+
/** @param {number} t */
295+
const linear = (t) => t;
296+
259297
/**
260298
* @param {HTMLElement} dom
261299
* @param {() => import('./types.js').TransitionPayload} init
@@ -286,38 +324,15 @@ function create_transition(dom, init, direction, effect) {
286324
const delay = payload.delay ?? 0;
287325
const css_fn = payload.css;
288326
const tick_fn = payload.tick;
289-
290-
/** @param {number} t */
291-
const linear = (t) => t;
292327
const easing_fn = payload.easing || linear;
293328

294-
/** @type {Keyframe[]} */
295-
const keyframes = [];
296-
297329
if (typeof tick_fn === 'function') {
298330
animation = new TickAnimation(tick_fn, duration, delay, direction === 'out');
299331
} else {
300-
if (typeof css_fn === 'function') {
301-
// We need at least two frames
302-
const frame_time = 16.666;
303-
const max_duration = Math.max(duration, frame_time);
304-
// Have a keyframe every fame for 60 FPS
305-
for (let i = 0; i <= max_duration; i += frame_time) {
306-
let time;
307-
if (i + frame_time > max_duration) {
308-
time = 1;
309-
} else if (i === 0) {
310-
time = 0;
311-
} else {
312-
time = i / max_duration;
313-
}
314-
const t = easing_fn(time);
315-
keyframes.push(css_to_keyframe(css_fn(t, 1 - t)));
316-
}
317-
if (direction === 'out') {
318-
keyframes.reverse();
319-
}
320-
}
332+
const keyframes =
333+
typeof css_fn === 'function'
334+
? create_keyframes(easing_fn, css_fn, duration, direction, false)
335+
: [];
321336
animation = dom.animate(keyframes, {
322337
duration,
323338
endDelay: delay,
@@ -421,6 +436,26 @@ function create_transition(dom, init, direction, effect) {
421436
} else {
422437
dispatch_event(dom, 'outrostart');
423438
if (needs_reverse) {
439+
const payload = transition.p;
440+
const current_animation = /** @type {Animation} */ (animation);
441+
// If we are working with CSS animations, then before we call reverse, we also need to ensure
442+
// that we reverse the easing logic. To do this we need to re-create the keyframes so they're
443+
// in reverse with easing properly reversed too.
444+
if (
445+
payload !== null &&
446+
payload.css !== undefined &&
447+
current_animation.playState === 'idle'
448+
) {
449+
const duration = payload.duration ?? 300;
450+
const css_fn = payload.css;
451+
const easing_fn = payload.easing || linear;
452+
const keyframes = create_keyframes(easing_fn, css_fn, duration, direction, true);
453+
const effect = current_animation.effect;
454+
if (effect !== null) {
455+
// @ts-ignore
456+
effect.setKeyframes(keyframes);
457+
}
458+
}
424459
/** @type {Animation | TickAnimation} */ (animation).reverse();
425460
} else {
426461
/** @type {Animation | TickAnimation} */ (animation).play();

packages/svelte/tests/animation-helpers.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,18 @@ class Animation {
5353
this.onfinish = () => {};
5454
this.pending = true;
5555
this.currentTime = 0;
56+
this.playState = 'running';
57+
this.effect = {
58+
setKeyframes: (/** @type {Keyframe[]} */ keyframes) => {
59+
this.#keyframes = keyframes;
60+
}
61+
};
5662
}
5763

5864
play() {
5965
this.#paused = false;
6066
raf.animations.add(this);
67+
this.playState = 'running';
6168
this._update();
6269
}
6370

@@ -107,6 +114,7 @@ class Animation {
107114
if (this.#reversed) {
108115
raf.animations.delete(this);
109116
}
117+
this.playState = 'idle';
110118
}
111119

112120
cancel() {
@@ -118,11 +126,13 @@ class Animation {
118126

119127
pause() {
120128
this.#paused = true;
129+
this.playState = 'paused';
121130
}
122131

123132
reverse() {
124133
this.#timeline_offset = this.currentTime;
125134
this.#reversed = !this.#reversed;
135+
this.playState = 'running';
126136
}
127137
}
128138

packages/svelte/tests/runtime-legacy/samples/class-shortcut-with-transition/_config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default test({
1818
raf.tick(150);
1919
assert.htmlEqual(
2020
target.innerHTML,
21-
'<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>'
21+
'<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>'
2222
);
2323
component.open = true;
2424
raf.tick(250);

0 commit comments

Comments
 (0)