Skip to content

Commit

Permalink
feat: add Spring and Tween classes (#11519)
Browse files Browse the repository at this point in the history
* feat: add Spring class

* add some docs, Spring.of static method

* add Tween class

* lint

* preserveMomentum in milliseconds

* deprecate tweened

* changeset

* wrestle with types

* more consolidation

* flesh out the distinction a bit more, deprecate `subscribe`

---------

Co-authored-by: Simon Holthausen <[email protected]>
  • Loading branch information
Rich-Harris and dummdidumm authored Dec 6, 2024
1 parent 60c0dc7 commit 80ffcc3
Show file tree
Hide file tree
Showing 7 changed files with 603 additions and 39 deletions.
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

0 comments on commit 80ffcc3

Please sign in to comment.