Comparing version 5.7.1 to 5.8.0
@@ -5,3 +5,3 @@ { | ||
"license": "MIT", | ||
"version": "5.7.1", | ||
"version": "5.8.0", | ||
"type": "module", | ||
@@ -8,0 +8,0 @@ "types": "./types/index.d.ts", |
@@ -48,2 +48,23 @@ // Store the references to globals in case someone tries to monkey patch these, causing the below | ||
/** | ||
* TODO replace with Promise.withResolvers once supported widely enough | ||
* @template T | ||
*/ | ||
export function deferred() { | ||
/** @type {(value: T) => void} */ | ||
var resolve; | ||
/** @type {(reason: any) => void} */ | ||
var reject; | ||
/** @type {Promise<T>} */ | ||
var promise = new Promise((res, rej) => { | ||
resolve = res; | ||
reject = rej; | ||
}); | ||
// @ts-expect-error | ||
return { promise, resolve, reject }; | ||
} | ||
/** | ||
* @template V | ||
@@ -50,0 +71,0 @@ * @param {V} value |
@@ -1,7 +0,9 @@ | ||
import { Spring } from './public.js'; | ||
export interface TickContext<T> { | ||
export interface TickContext { | ||
inv_mass: number; | ||
dt: number; | ||
opts: Spring<T>; | ||
opts: { | ||
stiffness: number; | ||
damping: number; | ||
precision: number; | ||
}; | ||
settled: boolean; | ||
@@ -17,4 +19,18 @@ } | ||
export interface SpringUpdateOpts { | ||
/** | ||
* @deprecated Only use this for the spring store; does nothing when set on the Spring class | ||
*/ | ||
hard?: any; | ||
/** | ||
* @deprecated Only use this for the spring store; does nothing when set on the Spring class | ||
*/ | ||
soft?: string | number | boolean; | ||
/** | ||
* Only use this for the Spring class; does nothing when set on the spring store | ||
*/ | ||
instant?: boolean; | ||
/** | ||
* Only use this for the Spring class; does nothing when set on the spring store | ||
*/ | ||
preserveMomentum?: number; | ||
} | ||
@@ -21,0 +37,0 @@ |
@@ -1,7 +0,18 @@ | ||
import { Readable } from '../store/public.js'; | ||
import { SpringUpdateOpts, TweenedOptions, Updater } from './private.js'; | ||
import { Readable, type Unsubscriber } from '../store/public.js'; | ||
import { SpringUpdateOpts, TweenedOptions, Updater, SpringOpts } from './private.js'; | ||
// TODO we do declaration merging here in order to not have a breaking change (renaming the Spring interface) | ||
// this means both the Spring class and the Spring interface are merged into one with some things only | ||
// existing on one side. In Svelte 6, remove the type definition and move the jsdoc onto the class in spring.js | ||
export interface Spring<T> extends Readable<T> { | ||
set: (new_value: T, opts?: SpringUpdateOpts) => Promise<void>; | ||
set(new_value: T, opts?: SpringUpdateOpts): Promise<void>; | ||
/** | ||
* @deprecated Only exists on the legacy `spring` store, not the `Spring` class | ||
*/ | ||
update: (fn: Updater<T>, opts?: SpringUpdateOpts) => Promise<void>; | ||
/** | ||
* @deprecated Only exists on the legacy `spring` store, not the `Spring` class | ||
*/ | ||
subscribe(fn: (value: T) => void): Unsubscriber; | ||
precision: number; | ||
@@ -12,2 +23,61 @@ damping: number; | ||
/** | ||
* A wrapper for a value that behaves in a spring-like fashion. Changes to `spring.target` will cause `spring.current` to | ||
* move towards it over time, taking account of the `spring.stiffness` and `spring.damping` parameters. | ||
* | ||
* ```svelte | ||
* <script> | ||
* import { Spring } from 'svelte/motion'; | ||
* | ||
* const spring = new Spring(0); | ||
* </script> | ||
* | ||
* <input type="range" bind:value={spring.target} /> | ||
* <input type="range" bind:value={spring.current} disabled /> | ||
* ``` | ||
*/ | ||
export class Spring<T> { | ||
constructor(value: T, options?: SpringOpts); | ||
/** | ||
* Create a spring whose value is bound to the return value of `fn`. This must be called | ||
* inside an effect root (for example, during component initialisation). | ||
* | ||
* ```svelte | ||
* <script> | ||
* import { Spring } from 'svelte/motion'; | ||
* | ||
* let { number } = $props(); | ||
* | ||
* const spring = Spring.of(() => number); | ||
* </script> | ||
* ``` | ||
*/ | ||
static of<U>(fn: () => U, options?: SpringOpts): Spring<U>; | ||
/** | ||
* Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it. | ||
* | ||
* If `options.instant` is `true`, `spring.current` immediately matches `spring.target`. | ||
* | ||
* If `options.preserveMomentum` is provided, the spring will continue on its current trajectory for | ||
* the specified number of milliseconds. This is useful for things like 'fling' gestures. | ||
*/ | ||
set(value: T, options?: SpringUpdateOpts): Promise<void>; | ||
damping: number; | ||
precision: number; | ||
stiffness: number; | ||
/** | ||
* The end value of the spring. | ||
* This property only exists on the `Spring` class, not the legacy `spring` store. | ||
*/ | ||
target: T; | ||
/** | ||
* The current value of the spring. | ||
* This property only exists on the `Spring` class, not the legacy `spring` store. | ||
*/ | ||
get current(): T; | ||
} | ||
export interface Tweened<T> extends Readable<T> { | ||
@@ -18,2 +88,2 @@ set(value: T, opts?: TweenedOptions<T>): Promise<void>; | ||
export * from './index.js'; | ||
export { spring, tweened, Tween } from './index.js'; |
/** @import { Task } from '#client' */ | ||
/** @import { SpringOpts, SpringUpdateOpts, TickContext } from './private.js' */ | ||
/** @import { Spring } from './public.js' */ | ||
/** @import { Spring as SpringStore } from './public.js' */ | ||
import { writable } from '../store/shared/index.js'; | ||
@@ -8,6 +8,10 @@ import { loop } from '../internal/client/loop.js'; | ||
import { is_date } from './utils.js'; | ||
import { set, source } from '../internal/client/reactivity/sources.js'; | ||
import { render_effect } from '../internal/client/reactivity/effects.js'; | ||
import { get } from '../internal/client/runtime.js'; | ||
import { deferred, noop } from '../internal/shared/utils.js'; | ||
/** | ||
* @template T | ||
* @param {TickContext<T>} ctx | ||
* @param {TickContext} ctx | ||
* @param {T} last_value | ||
@@ -57,6 +61,7 @@ * @param {T} current_value | ||
* | ||
* @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead | ||
* @template [T=any] | ||
* @param {T} [value] | ||
* @param {SpringOpts} [opts] | ||
* @returns {Spring<T>} | ||
* @returns {SpringStore<T>} | ||
*/ | ||
@@ -108,3 +113,3 @@ export function spring(value, opts = {}) { | ||
inv_mass = Math.min(inv_mass + inv_mass_recovery_rate, 1); | ||
/** @type {TickContext<T>} */ | ||
/** @type {TickContext} */ | ||
const ctx = { | ||
@@ -133,3 +138,4 @@ inv_mass, | ||
} | ||
/** @type {Spring<T>} */ | ||
/** @type {SpringStore<T>} */ | ||
// @ts-expect-error - class-only properties are missing | ||
const spring = { | ||
@@ -145,1 +151,203 @@ set, | ||
} | ||
/** | ||
* A wrapper for a value that behaves in a spring-like fashion. Changes to `spring.target` will cause `spring.current` to | ||
* move towards it over time, taking account of the `spring.stiffness` and `spring.damping` parameters. | ||
* | ||
* ```svelte | ||
* <script> | ||
* import { Spring } from 'svelte/motion'; | ||
* | ||
* const spring = new Spring(0); | ||
* </script> | ||
* | ||
* <input type="range" bind:value={spring.target} /> | ||
* <input type="range" bind:value={spring.current} disabled /> | ||
* ``` | ||
* @template T | ||
* @since 5.8.0 | ||
*/ | ||
export class Spring { | ||
#stiffness = source(0.15); | ||
#damping = source(0.8); | ||
#precision = source(0.01); | ||
#current = source(/** @type {T} */ (undefined)); | ||
#target = source(/** @type {T} */ (undefined)); | ||
#last_value = /** @type {T} */ (undefined); | ||
#last_time = 0; | ||
#inverse_mass = 1; | ||
#momentum = 0; | ||
/** @type {import('../internal/client/types').Task | null} */ | ||
#task = null; | ||
/** @type {ReturnType<typeof deferred> | null} */ | ||
#deferred = null; | ||
/** | ||
* @param {T} value | ||
* @param {SpringOpts} [options] | ||
*/ | ||
constructor(value, options = {}) { | ||
this.#current.v = this.#target.v = value; | ||
if (typeof options.stiffness === 'number') this.#stiffness.v = clamp(options.stiffness, 0, 1); | ||
if (typeof options.damping === 'number') this.#damping.v = clamp(options.damping, 0, 1); | ||
if (typeof options.precision === 'number') this.#precision.v = options.precision; | ||
} | ||
/** | ||
* Create a spring whose value is bound to the return value of `fn`. This must be called | ||
* inside an effect root (for example, during component initialisation). | ||
* | ||
* ```svelte | ||
* <script> | ||
* import { Spring } from 'svelte/motion'; | ||
* | ||
* let { number } = $props(); | ||
* | ||
* const spring = Spring.of(() => number); | ||
* </script> | ||
* ``` | ||
* @template U | ||
* @param {() => U} fn | ||
* @param {SpringOpts} [options] | ||
*/ | ||
static of(fn, options) { | ||
const spring = new Spring(fn(), options); | ||
render_effect(() => { | ||
spring.set(fn()); | ||
}); | ||
return spring; | ||
} | ||
/** @param {T} value */ | ||
#update(value) { | ||
set(this.#target, value); | ||
this.#current.v ??= value; | ||
this.#last_value ??= this.#current.v; | ||
if (!this.#task) { | ||
this.#last_time = raf.now(); | ||
var inv_mass_recovery_rate = 1000 / (this.#momentum * 60); | ||
this.#task ??= loop((now) => { | ||
this.#inverse_mass = Math.min(this.#inverse_mass + inv_mass_recovery_rate, 1); | ||
/** @type {import('./private').TickContext} */ | ||
const ctx = { | ||
inv_mass: this.#inverse_mass, | ||
opts: { | ||
stiffness: this.#stiffness.v, | ||
damping: this.#damping.v, | ||
precision: this.#precision.v | ||
}, | ||
settled: true, | ||
dt: ((now - this.#last_time) * 60) / 1000 | ||
}; | ||
var next = tick_spring(ctx, this.#last_value, this.#current.v, this.#target.v); | ||
this.#last_value = this.#current.v; | ||
this.#last_time = now; | ||
set(this.#current, next); | ||
if (ctx.settled) { | ||
this.#task = null; | ||
} | ||
return !ctx.settled; | ||
}); | ||
} | ||
return this.#task.promise; | ||
} | ||
/** | ||
* Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it. | ||
* | ||
* If `options.instant` is `true`, `spring.current` immediately matches `spring.target`. | ||
* | ||
* If `options.preserveMomentum` is provided, the spring will continue on its current trajectory for | ||
* the specified number of milliseconds. This is useful for things like 'fling' gestures. | ||
* | ||
* @param {T} value | ||
* @param {SpringUpdateOpts} [options] | ||
*/ | ||
set(value, options) { | ||
this.#deferred?.reject(new Error('Aborted')); | ||
if (options?.instant || this.#current.v === undefined) { | ||
this.#task?.abort(); | ||
this.#task = null; | ||
set(this.#current, set(this.#target, value)); | ||
return Promise.resolve(); | ||
} | ||
if (options?.preserveMomentum) { | ||
this.#inverse_mass = 0; | ||
this.#momentum = options.preserveMomentum; | ||
} | ||
var d = (this.#deferred = deferred()); | ||
d.promise.catch(noop); | ||
this.#update(value).then(() => { | ||
if (d !== this.#deferred) return; | ||
d.resolve(undefined); | ||
}); | ||
return d.promise; | ||
} | ||
get current() { | ||
return get(this.#current); | ||
} | ||
get damping() { | ||
return get(this.#damping); | ||
} | ||
set damping(v) { | ||
set(this.#damping, clamp(v, 0, 1)); | ||
} | ||
get precision() { | ||
return get(this.#precision); | ||
} | ||
set precision(v) { | ||
set(this.#precision, v); | ||
} | ||
get stiffness() { | ||
return get(this.#stiffness); | ||
} | ||
set stiffness(v) { | ||
set(this.#stiffness, clamp(v, 0, 1)); | ||
} | ||
get target() { | ||
return get(this.#target); | ||
} | ||
set target(v) { | ||
this.set(v); | ||
} | ||
} | ||
/** | ||
* @param {number} n | ||
* @param {number} min | ||
* @param {number} max | ||
*/ | ||
function clamp(n, min, max) { | ||
return Math.max(min, Math.min(max, n)); | ||
} |
@@ -9,2 +9,4 @@ /** @import { Task } from '../internal/client/types' */ | ||
import { is_date } from './utils.js'; | ||
import { set, source } from '../internal/client/reactivity/sources.js'; | ||
import { get, render_effect } from 'svelte/internal/client'; | ||
@@ -80,2 +82,3 @@ /** | ||
* | ||
* @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead | ||
* @template T | ||
@@ -157,1 +160,135 @@ * @param {T} [value] | ||
} | ||
/** | ||
* A wrapper for a value that tweens smoothly to its target value. Changes to `tween.target` will cause `tween.current` to | ||
* move towards it over time, taking account of the `delay`, `duration` and `easing` options. | ||
* | ||
* ```svelte | ||
* <script> | ||
* import { Tween } from 'svelte/motion'; | ||
* | ||
* const tween = new Tween(0); | ||
* </script> | ||
* | ||
* <input type="range" bind:value={tween.target} /> | ||
* <input type="range" bind:value={tween.current} disabled /> | ||
* ``` | ||
* @template T | ||
* @since 5.8.0 | ||
*/ | ||
export class Tween { | ||
#current = source(/** @type {T} */ (undefined)); | ||
#target = source(/** @type {T} */ (undefined)); | ||
/** @type {TweenedOptions<T>} */ | ||
#defaults; | ||
/** @type {import('../internal/client/types').Task | null} */ | ||
#task = null; | ||
/** | ||
* @param {T} value | ||
* @param {TweenedOptions<T>} 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 | ||
* <script> | ||
* import { Tween } from 'svelte/motion'; | ||
* | ||
* let { number } = $props(); | ||
* | ||
* const tween = Tween.of(() => number); | ||
* </script> | ||
* ``` | ||
* @template U | ||
* @param {() => U} fn | ||
* @param {TweenedOptions<U>} [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<T>} [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) { | ||
this.set(v); | ||
} | ||
} |
@@ -9,3 +9,3 @@ // generated during release, do not modify | ||
*/ | ||
export const VERSION = '5.7.1'; | ||
export const VERSION = '5.8.0'; | ||
export const PUBLIC_VERSION = '5'; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2390677
53031