From 032b739e98307f6846f5dee56e163bb27c3f2b1c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 21 Feb 2024 10:42:49 -0500 Subject: [PATCH] WIP transitions --- .../client/reactivity/computations.js | 61 ++++++- .../svelte/src/internal/client/transitions.js | 161 ++++++------------ packages/svelte/tests/animation-helpers.js | 10 ++ .../samples/if-transition-inert/_config.js | 2 +- 4 files changed, 113 insertions(+), 121 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/computations.js b/packages/svelte/src/internal/client/reactivity/computations.js index cec1ce6799..316a660191 100644 --- a/packages/svelte/src/internal/client/reactivity/computations.js +++ b/packages/svelte/src/internal/client/reactivity/computations.js @@ -265,26 +265,73 @@ export function derived_safe_equal(fn) { * @param {() => void} done */ export function pause_effect(effect, done) { - if (effect.r) { - for (const child of effect.r) { - pause_effect(child, noop); + const transitions = []; + + pause_children(effect, transitions); + + let remaining = transitions.length; + + if (remaining > 0) { + const check = () => { + if (!--remaining) { + destroy_effect(effect); + done(); + } + }; + + for (const transition of transitions) { + transition.to(0, check); } + } else { + destroy_effect(effect); + done(); } +} +/** + * @param {import('../types.js').ComputationSignal} effect + * @param {TODO[]} transitions + */ +function pause_children(effect, transitions) { effect.f |= INERT; + if (effect.out) { + transitions.push(...effect.out); // TODO differentiate between global and local + } + + if (effect.r) { + for (const child of effect.r) { + pause_children(child, transitions); + } + } +} + +/** + * @param {import('../types.js').ComputationSignal} effect + */ +function destroy_effect(effect) { // TODO distinguish between 'block effects' (?) which own their own DOM // and other render effects if (effect.dom) { remove(effect.dom); } - done(); // TODO defer until transitions have completed + if (effect.r) { + for (const child of effect.r) { + destroy_effect(child); + } + } } /** - * @param {import('../types.js').ComputationSignal} signal + * @param {import('../types.js').ComputationSignal} effect */ -export function resume_effect(signal) { - // TODO +export function resume_effect(effect) { + if (effect.r) { + for (const child of effect.r) { + resume_effect(child); + } + } + + effect.f ^= INERT; } diff --git a/packages/svelte/src/internal/client/transitions.js b/packages/svelte/src/internal/client/transitions.js index 59eb3ad60d..938618ae89 100644 --- a/packages/svelte/src/internal/client/transitions.js +++ b/packages/svelte/src/internal/client/transitions.js @@ -495,133 +495,68 @@ function is_transition_block(block) { /** * @template P - * @param {HTMLElement} dom - * @param {() => import('./types.js').TransitionFn

| import('./types.js').AnimateFn

} get_transition_fn - * @param {(() => P) | null} props_fn - * @param {'in' | 'out' | 'both' | 'key'} direction + * @param {HTMLElement} element + * @param {() => import('./types.js').TransitionFn

} get_fn + * @param {(() => P) | null} get_params + * @param {'in' | 'out' | 'both'} direction * @param {boolean} global * @returns {void} */ -export function bind_transition(dom, get_transition_fn, props_fn, direction, global) { - const transition_effect = /** @type {import('./types.js').EffectSignal} */ (current_effect); - const block = current_block; - const is_keyed_transition = direction === 'key'; +export function bind_transition(element, get_fn, get_params, direction, global) { + const effect = /** @type {import('./types.js').EffectSignal} */ (current_effect); - let can_show_intro_on_mount = true; - let can_apply_lazy_transitions = false; + let p = direction === 'out' ? 1 : 0; - if (is_keyed_transition) { - // @ts-ignore - dom.__animate = true; - } - /** @type {import('./types.js').Block | null} */ - let transition_block = block; - main: while (transition_block !== null) { - if (is_transition_block(transition_block)) { - if (transition_block.t === EACH_ITEM_BLOCK) { - // Lazily apply the each block transition - transition_block.r = each_item_transition; - transition_block.a = each_item_animate; - transition_block = transition_block.p; - } else if (transition_block.t === AWAIT_BLOCK && transition_block.n /* pending */) { - can_show_intro_on_mount = true; - } else if (transition_block.t === IF_BLOCK) { - transition_block.r = if_block_transition; - if (can_show_intro_on_mount) { - /** @type {import('./types.js').Block | null} */ - let if_block = transition_block; - while (if_block.t === IF_BLOCK) { - // If we have an if block parent that is currently falsy then - // we can show the intro on mount as long as that block is mounted - if (if_block.e !== null && !if_block.v) { - can_show_intro_on_mount = true; - break main; - } - if_block = if_block.p; - } - } - } - if (!can_apply_lazy_transitions && can_show_intro_on_mount) { - can_show_intro_on_mount = transition_block.e !== null; - } - if (can_show_intro_on_mount || !global) { - can_apply_lazy_transitions = true; + /** @type {Animation | null} */ + let current_animation; + + /** @type {TODO} */ + let current_options; + + const transition = { + global, + to(target, callback) { + if (current_animation) { + // TODO get `p` from current_animation? + current_animation.cancel(); } - } else if (transition_block.t === ROOT_BLOCK && !can_apply_lazy_transitions) { - can_show_intro_on_mount = transition_block.e !== null || transition_block.i; - } - transition_block = transition_block.p; - } - /** @type {import('./types.js').Transition} */ - let transition; - - effect(() => { - let already_mounted = false; - if (transition !== undefined) { - already_mounted = true; - // Destroy any existing transitions first - transition.x(); - } - const transition_fn = get_transition_fn(); - /** @param {DOMRect} [from] */ - const init = (from) => - untrack(() => { - const props = props_fn === null ? {} : props_fn(); - return is_keyed_transition - ? /** @type {import('./types.js').AnimateFn} */ (transition_fn)( - dom, - { from: /** @type {DOMRect} */ (from), to: dom.getBoundingClientRect() }, - props, - {} - ) - : /** @type {import('./types.js').TransitionFn} */ (transition_fn)(dom, props, { - direction - }); - }); + current_options ??= get_fn()(element, get_params?.(), { direction }); - transition = create_transition(dom, init, direction, transition_effect); - const is_intro = direction === 'in'; - const show_intro = can_show_intro_on_mount && (is_intro || direction === 'both'); + if (current_options.css) { + // WAAPI + const keyframes = []; + const n = current_options.duration / (1000 / 60); - if (show_intro && !already_mounted) { - transition.p = transition.i(); - } + for (let i = 0; i <= n; i += 1) { + const t = current_options.easing(p + ((target - p) * i) / n); + const css = current_options.css(t); + keyframes.push(css_to_keyframe(css)); + } - const effect = managed_pre_effect(() => { - destroy_signal(effect); - dom.inert = false; + current_animation = element.animate(keyframes, { + duration: current_options.duration, + easing: 'linear' + }); - if (show_intro && !already_mounted) { - transition.in(); + current_animation.finished.then(() => { + console.log('done'); + current_animation = null; + callback(); + }); + } else { + // TODO timer } + } + }; - /** @type {import('./types.js').Block | null} */ - let transition_block = block; - while (!is_intro && transition_block !== null) { - const parent = transition_block.p; - if (is_transition_block(transition_block)) { - if (transition_block.r !== null) { - transition_block.r(transition); - } - if ( - parent === null || - (!global && (transition_block.t !== IF_BLOCK || parent.t !== IF_BLOCK || parent.v)) - ) { - break; - } - } - transition_block = parent; - } - }, false); - }); + // TODO don't pass strings around like this, it's silly + if (direction === 'in' || direction === 'both') { + (effect.in ??= []).push(transition); + } - if (direction === 'key') { - effect(() => { - return () => { - transition.x(); - }; - }); + if (direction === 'out' || direction === 'both') { + (effect.out ??= []).push(transition); } } diff --git a/packages/svelte/tests/animation-helpers.js b/packages/svelte/tests/animation-helpers.js index 94bb1839a0..51d1f6bae3 100644 --- a/packages/svelte/tests/animation-helpers.js +++ b/packages/svelte/tests/animation-helpers.js @@ -37,6 +37,7 @@ class Animation { #reversed; #target; #paused; + #finished; /** * @param {HTMLElement} target @@ -59,6 +60,10 @@ class Animation { this.#keyframes = keyframes; } }; + + this.finished = new Promise((fulfil) => { + this.#finished = fulfil; + }); } play() { @@ -78,8 +83,13 @@ class Animation { } else { this.currentTime = raf.time - this.#timeline_offset; } + const target_frame = this.currentTime / this.#duration; this._applyKeyFrame(target_frame); + + if (this.currentTime >= this.#duration) { + this.#finished(); + } } /** diff --git a/packages/svelte/tests/runtime-runes/samples/if-transition-inert/_config.js b/packages/svelte/tests/runtime-runes/samples/if-transition-inert/_config.js index 7a1673786c..37b75b195f 100644 --- a/packages/svelte/tests/runtime-runes/samples/if-transition-inert/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/if-transition-inert/_config.js @@ -11,6 +11,6 @@ export default test({ btn1.click(); }); - assert.htmlEqual(target.innerHTML, `

hello
`); + assert.htmlEqual(target.innerHTML, `
hello
`); } });