diff --git a/packages/svelte/src/motion/tweened.js b/packages/svelte/src/motion/tweened.js index cc4bcd0e526e..e247c99702b9 100644 --- a/packages/svelte/src/motion/tweened.js +++ b/packages/svelte/src/motion/tweened.js @@ -16,63 +16,63 @@ import { get, render_effect } from 'svelte/internal/client'; * @returns {(t: number) => T} */ function get_interpolator(a, b) { - if (a === b || a !== a) return () => a; - - const type = typeof a; - if (type !== typeof b || Array.isArray(a) !== Array.isArray(b)) { - throw new Error('Cannot interpolate values of different type'); - } - - if (Array.isArray(a)) { - const arr = /** @type {Array} */ (b).map((bi, i) => { - return get_interpolator(/** @type {Array} */ (a)[i], bi); - }); - - // @ts-ignore - return (t) => arr.map((fn) => fn(t)); - } - - if (type === 'object') { - if (!a || !b) { - throw new Error('Object cannot be null'); - } - - if (is_date(a) && is_date(b)) { - const an = a.getTime(); - const bn = b.getTime(); - const delta = bn - an; - - // @ts-ignore - return (t) => new Date(an + t * delta); - } - - const keys = Object.keys(b); - - /** @type {Record T>} */ - const interpolators = {}; - keys.forEach((key) => { - // @ts-ignore - interpolators[key] = get_interpolator(a[key], b[key]); - }); - - // @ts-ignore - return (t) => { - /** @type {Record} */ - const result = {}; - keys.forEach((key) => { - result[key] = interpolators[key](t); - }); - return result; - }; - } - - if (type === 'number') { - const delta = /** @type {number} */ (b) - /** @type {number} */ (a); - // @ts-ignore - return (t) => a + t * delta; - } - - throw new Error(`Cannot interpolate ${type} values`); + if (a === b || a !== a) return () => a; + + const type = typeof a; + if (type !== typeof b || Array.isArray(a) !== Array.isArray(b)) { + throw new Error('Cannot interpolate values of different type'); + } + + if (Array.isArray(a)) { + const arr = /** @type {Array} */ (b).map((bi, i) => { + return get_interpolator(/** @type {Array} */ (a)[i], bi); + }); + + // @ts-ignore + return (t) => arr.map((fn) => fn(t)); + } + + if (type === 'object') { + if (!a || !b) { + throw new Error('Object cannot be null'); + } + + if (is_date(a) && is_date(b)) { + const an = a.getTime(); + const bn = b.getTime(); + const delta = bn - an; + + // @ts-ignore + return (t) => new Date(an + t * delta); + } + + const keys = Object.keys(b); + + /** @type {Record T>} */ + const interpolators = {}; + keys.forEach((key) => { + // @ts-ignore + interpolators[key] = get_interpolator(a[key], b[key]); + }); + + // @ts-ignore + return (t) => { + /** @type {Record} */ + const result = {}; + keys.forEach((key) => { + result[key] = interpolators[key](t); + }); + return result; + }; + } + + if (type === 'number') { + const delta = /** @type {number} */ (b) - /** @type {number} */ (a); + // @ts-ignore + return (t) => a + t * delta; + } + + throw new Error(`Cannot interpolate ${type} values`); } /** @@ -85,75 +85,81 @@ function get_interpolator(a, b) { * @returns {Tweened} */ export function tweened(value, defaults = {}) { - const store = writable(value); - /** @type {Task} */ - let task; - let target_value = value; - /** - * @param {T} new_value - * @param {TweenedOptions} [opts] - */ - function set(new_value, opts) { - target_value = new_value; - - if (value == null) { - store.set((value = new_value)); - return Promise.resolve(); - } - - /** @type {Task | null} */ - let previous_task = task; - - let started = false; - let { - delay = 0, - duration = 400, - easing = linear, - interpolate = get_interpolator - } = { ...defaults, ...opts }; - - if (duration === 0) { - if (previous_task) { - previous_task.abort(); - previous_task = null; - } - store.set((value = target_value)); - return Promise.resolve(); - } - - const start = raf.now() + delay; - - /** @type {(t: number) => T} */ - let fn; - task = loop((now) => { - if (now < start) return true; - if (!started) { - fn = interpolate(/** @type {any} */ (value), new_value); - if (typeof duration === 'function') - duration = duration(/** @type {any} */ (value), new_value); - started = true; - } - if (previous_task) { - previous_task.abort(); - previous_task = null; - } - const elapsed = now - start; - if (elapsed > /** @type {number} */ (duration)) { - store.set((value = new_value)); - return false; - } - // @ts-ignore - store.set((value = fn(easing(elapsed / duration)))); - return true; - }); - return task.promise; - } - return { - set, - update: (fn, opts) => - set(fn(/** @type {any} */ (target_value), /** @type {any} */ (value)), opts), - subscribe: store.subscribe - }; + const store = writable(value); + /** @type {Task} */ + let task; + let target_value = value; + /** + * @param {T} new_value + * @param {TweenedOptions} [opts] + */ + function set(new_value, opts) { + target_value = new_value; + + if (value == null) { + store.set((value = new_value)); + return Promise.resolve(); + } + + /** @type {Task | null} */ + let previous_task = task; + + let started = false; + let { + delay = 0, + duration = 400, + easing = linear, + interpolate = get_interpolator + } = { ...defaults, ...opts }; + + if (duration === 0) { + if (previous_task) { + previous_task.abort(); + previous_task = null; + } + store.set((value = target_value)); + return Promise.resolve(); + } + + const start = raf.now() + delay; + + /** @type {(t: number) => T} */ + let fn; + task = loop((now) => { + if (now < start) return true; + if (!started) { + fn = interpolate(/** @type {any} */ (value), new_value); + if (typeof duration === 'function') + duration = duration(/** @type {any} */ (value), new_value); + started = true; + } + if (previous_task) { + previous_task.abort(); + previous_task = null; + } + const elapsed = now - start; + if (elapsed > /** @type {number} */ (duration)) { + store.set((value = new_value)); + return false; + } + // @ts-ignore + store.set((value = fn(easing(elapsed / duration)))); + return true; + }); + return task.promise; + } + return { + set, + update: (fn, opts) => + set( + fn( + /** @type {any} */ (target_value), + /** @type {any} */ (value) + ), + opts + ), + subscribe: store.subscribe + }; } /** @@ -174,119 +180,125 @@ export function tweened(value, defaults = {}) { * @since 5.8.0 */ export class Tween { - #current = source(/** @type {T | undefined} */ (undefined)); - #target = source(/** @type {T | undefined} */ (undefined)); - - /** @type {TweenedOptions} */ - #defaults; - - /** @type {import('../internal/client/types').Task | null} */ - #task = null; - - /** - * @param {T} value - * @param {TweenedOptions} options - */ - constructor(value, options = {}) { - this.#current.v = this.#target.v = value; - this.#defaults = options; - } - - /** - * Create a tween whose value is bound to the return value of `fn`. This must be called - * inside an effect root (for example, during component initialisation). - * - * ```svelte - * - * ``` - * @template {number} U - * @param {() => U} fn - * @param {TweenedOptions} [options] - */ - static of(fn, options) { - const tween = new Tween(fn(), options); - - render_effect(() => { - tween.set(fn()); - }); - - return tween; - } - - /** - * Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it. - * - * If `options` are provided, they will override the tween's defaults. - * @param {T} value - * @param {TweenedOptions} [options] - * @returns - */ - set(value, options) { - set(this.#target, value); - - let previous_value = this.#current.v; - let previous_task = this.#task; - - let started = false; - let { - delay = 0, - duration = 400, - easing = linear, - interpolate = get_interpolator - } = { ...this.#defaults, ...options }; - - const start = raf.now() + delay; - - /** @type {(t: number) => T} */ - let fn; - - this.#task = loop((now) => { - if (now < start) { - return true; - } - - if (!started) { - started = true; - - fn = interpolate(/** @type {any} */ (previous_value), value); - - if (typeof duration === 'function') { - duration = duration(/** @type {any} */ (previous_value), value); - } - - previous_task?.abort(); - } - - const elapsed = now - start; - - if (elapsed > /** @type {number} */ (duration)) { - set(this.#current, value); - return false; - } - - set(this.#current, fn(easing(elapsed / /** @type {number} */ (duration)))); - return true; - }); - - return this.#task.promise; - } - - get current() { - return get(this.#current); - } - - get target() { - return get(this.#target); - } - - set target(v) { - //@ts-ignore - this.set(v); - } + #current = source(/** @type {T | undefined} */ (undefined)); + #target = source(/** @type {T | undefined} */ (undefined)); + + /** @type {TweenedOptions} */ + #defaults; + + /** @type {import('../internal/client/types').Task | null} */ + #task = null; + + /** + * @param {T} value + * @param {TweenedOptions} options + */ + constructor(value, options = {}) { + this.#current.v = this.#target.v = value; + this.#defaults = options; + } + + /** + * Create a tween whose value is bound to the return value of `fn`. This must be called + * inside an effect root (for example, during component initialisation). + * + * ```svelte + * + * ``` + * @template {number} U + * @param {() => U} fn + * @param {TweenedOptions} [options] + */ + static of(fn, options) { + const tween = new Tween(fn(), options); + + render_effect(() => { + tween.set(fn()); + }); + + return tween; + } + + /** + * Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it. + * + * If `options` are provided, they will override the tween's defaults. + * @param {T} value + * @param {TweenedOptions} [options] + * @returns + */ + set(value, options) { + set(this.#target, value); + + let previous_value = this.#current.v; + let previous_task = this.#task; + + let started = false; + let { + delay = 0, + duration = 400, + easing = linear, + interpolate = get_interpolator + } = { ...this.#defaults, ...options }; + + const start = raf.now() + delay; + + /** @type {(t: number) => T} */ + let fn; + + this.#task = loop((now) => { + if (now < start) { + return true; + } + + if (!started) { + started = true; + + fn = interpolate(/** @type {any} */ (previous_value), value); + + if (typeof duration === 'function') { + duration = duration( + /** @type {any} */ (previous_value), + value + ); + } + + previous_task?.abort(); + } + + const elapsed = now - start; + + if (elapsed > /** @type {number} */ (duration)) { + set(this.#current, value); + return false; + } + + set( + this.#current, + fn(easing(elapsed / /** @type {number} */ (duration))) + ); + return true; + }); + + return this.#task.promise; + } + + get current() { + return get(this.#current); + } + + get target() { + return get(this.#target); + } + + set target(v) { + //@ts-ignore + this.set(v); + } }