Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Spring and Tween classes #11519

Merged
merged 13 commits into from
Dec 6, 2024
5 changes: 5 additions & 0 deletions .changeset/tame-bottles-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: add `Spring` and `Tween` classes to `svelte/motion`
21 changes: 21 additions & 0 deletions packages/svelte/src/internal/shared/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,27 @@ export function run_all(arr) {
}
}

/**
* 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
* @param {V} value
Expand Down
24 changes: 20 additions & 4 deletions packages/svelte/src/motion/private.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
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;
}

Expand All @@ -14,8 +16,22 @@ export interface SpringOpts {
}

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;
}

export type Updater<T> = (target_value: T, value: T) => T;
Expand Down
78 changes: 74 additions & 4 deletions packages/svelte/src/motion/public.d.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,87 @@
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;
damping: number;
stiffness: 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> {
set(value: T, opts?: TweenedOptions<T>): Promise<void>;
update(updater: Updater<T>, opts?: TweenedOptions<T>): Promise<void>;
}

export * from './index.js';
export { spring, tweened, Tween } from './index.js';
Loading
Loading