Skip to content

Commit 992bb83

Browse files
committed
add Tween class
1 parent a3535fb commit 992bb83

File tree

4 files changed

+188
-8
lines changed

4 files changed

+188
-8
lines changed

packages/svelte/src/motion/private.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export interface TickContext<T> {
1+
export interface TickContext {
22
inv_mass: number;
33
dt: number;
44
opts: {

packages/svelte/src/motion/spring.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { deferred, noop } from '../internal/shared/utils.js';
1212

1313
/**
1414
* @template T
15-
* @param {TickContext<T>} ctx
15+
* @param {TickContext} ctx
1616
* @param {T} last_value
1717
* @param {T} current_value
1818
* @param {T} target_value
@@ -108,7 +108,7 @@ export function spring(value, opts = {}) {
108108
return false;
109109
}
110110
inv_mass = Math.min(inv_mass + inv_mass_recovery_rate, 1);
111-
/** @type {TickContext<T>} */
111+
/** @type {TickContext} */
112112
const ctx = {
113113
inv_mass,
114114
opts: spring,
@@ -152,7 +152,7 @@ export function spring(value, opts = {}) {
152152
* <script>
153153
* import { Spring } from 'svelte/motion';
154154
*
155-
* const spring = new Spring({ x: 0, y: 0 });
155+
* const spring = new Spring(0);
156156
* </script>
157157
*
158158
* <input type="range" bind:value={spring.target} />
@@ -234,7 +234,7 @@ export class Spring {
234234
this.#task ??= loop((now) => {
235235
this.#inverse_mass = Math.min(this.#inverse_mass + inv_mass_recovery_rate, 1);
236236

237-
/** @type {import('./private').TickContext<T>} */
237+
/** @type {import('./private').TickContext} */
238238
const ctx = {
239239
inv_mass: this.#inverse_mass,
240240
opts: {
@@ -263,7 +263,7 @@ export class Spring {
263263
}
264264

265265
/**
266-
* Sets `spring.target` to `value` and returns a `Promise` if and when `spring.current` catches up to it.
266+
* Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it.
267267
*
268268
* If `options.instant` is `true`, `spring.current` immediately matches `spring.target`.
269269
*

packages/svelte/src/motion/tweened.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { raf } from '../internal/client/timing.js';
66
import { loop } from '../internal/client/loop.js';
77
import { linear } from '../easing/index.js';
88
import { is_date } from './utils.js';
9+
import { set, source } from '../internal/client/reactivity/sources.js';
10+
import { get, render_effect } from 'svelte/internal/client';
911

1012
/**
1113
* @template T
@@ -152,3 +154,136 @@ export function tweened(value, defaults = {}) {
152154
subscribe: store.subscribe
153155
};
154156
}
157+
158+
/**
159+
* A wrapper for a value that tweens smoothly to its target value. Changes to `tween.target` will cause `tween.current` to
160+
* move towards it over time, taking account of the `delay`, `duration` and `easing` options.
161+
*
162+
* ```svelte
163+
* <script>
164+
* import { Tween } from 'svelte/motion';
165+
*
166+
* const tween = new Tween(0);
167+
* </script>
168+
*
169+
* <input type="range" bind:value={tween.target} />
170+
* <input type="range" bind:value={tween.current} disabled />
171+
* ```
172+
* @template T
173+
*/
174+
export class Tween {
175+
#current = source(/** @type {T} */ (undefined));
176+
#target = source(/** @type {T} */ (undefined));
177+
178+
/** @type {TweenedOptions<T>} */
179+
#defaults;
180+
181+
/** @type {import('../internal/client/types').Task | null} */
182+
#task = null;
183+
184+
/**
185+
* @param {T} value
186+
* @param {TweenedOptions<T>} options
187+
*/
188+
constructor(value, options = {}) {
189+
this.#current.v = this.#target.v = value;
190+
this.#defaults = options;
191+
}
192+
193+
/**
194+
* Create a tween whose value is bound to the return value of `fn`. This must be called
195+
* inside an effect root (for example, during component initialisation).
196+
*
197+
* ```svelte
198+
* <script>
199+
* import { Tween } from 'svelte/motion';
200+
*
201+
* let { number } = $props();
202+
*
203+
* const tween = Tween.of(() => number);
204+
* </script>
205+
* ```
206+
* @template U
207+
* @param {() => U} fn
208+
* @param {TweenedOptions<U>} [options]
209+
*/
210+
static of(fn, options) {
211+
const tween = new Tween(fn(), options);
212+
213+
render_effect(() => {
214+
tween.set(fn());
215+
});
216+
217+
return tween;
218+
}
219+
220+
/**
221+
* Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it.
222+
*
223+
* If `options` are provided, they will override the tween's defaults.
224+
* @param {T} value
225+
* @param {TweenedOptions<T>} [options]
226+
* @returns
227+
*/
228+
set(value, options) {
229+
set(this.#target, value);
230+
231+
let previous_value = this.#current.v;
232+
let previous_task = this.#task;
233+
234+
let started = false;
235+
let {
236+
delay = 0,
237+
duration = 400,
238+
easing = linear,
239+
interpolate = get_interpolator
240+
} = { ...this.#defaults, ...options };
241+
242+
const start = raf.now() + delay;
243+
244+
/** @type {(t: number) => T} */
245+
let fn;
246+
247+
this.#task = loop((now) => {
248+
if (now < start) {
249+
return true;
250+
}
251+
252+
if (!started) {
253+
started = true;
254+
255+
fn = interpolate(/** @type {any} */ (previous_value), value);
256+
257+
if (typeof duration === 'function') {
258+
duration = duration(/** @type {any} */ (previous_value), value);
259+
}
260+
261+
previous_task?.abort();
262+
}
263+
264+
const elapsed = now - start;
265+
266+
if (elapsed > /** @type {number} */ (duration)) {
267+
set(this.#current, value);
268+
return false;
269+
}
270+
271+
set(this.#current, fn(easing(elapsed / /** @type {number} */ (duration))));
272+
return true;
273+
});
274+
275+
return this.#task.promise;
276+
}
277+
278+
get current() {
279+
return get(this.#current);
280+
}
281+
282+
get target() {
283+
return get(this.#target);
284+
}
285+
286+
set target(v) {
287+
this.set(v);
288+
}
289+
}

packages/svelte/types/index.d.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1697,7 +1697,7 @@ declare module 'svelte/motion' {
16971697
* <script>
16981698
* import { Spring } from 'svelte/motion';
16991699
*
1700-
* const spring = new Spring({ x: 0, y: 0 });
1700+
* const spring = new Spring(0);
17011701
* </script>
17021702
*
17031703
* <input type="range" bind:value={spring.target} />
@@ -1732,7 +1732,7 @@ declare module 'svelte/motion' {
17321732
precision?: number;
17331733
} | undefined);
17341734
/**
1735-
* Sets `spring.target` to `value` and returns a `Promise` if and when `spring.current` catches up to it.
1735+
* Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it.
17361736
*
17371737
* If `options.instant` is `true`, `spring.current` immediately matches `spring.target`.
17381738
*
@@ -1761,6 +1761,51 @@ declare module 'svelte/motion' {
17611761
*
17621762
* */
17631763
export function tweened<T>(value?: T | undefined, defaults?: TweenedOptions<T> | undefined): TweenedStore<T>;
1764+
/**
1765+
* A wrapper for a value that tweens smoothly to its target value. Changes to `tween.target` will cause `tween.current` to
1766+
* move towards it over time, taking account of the `delay`, `duration` and `easing` options.
1767+
*
1768+
* ```svelte
1769+
* <script>
1770+
* import { Tween } from 'svelte/motion';
1771+
*
1772+
* const tween = new Tween(0);
1773+
* </script>
1774+
*
1775+
* <input type="range" bind:value={tween.target} />
1776+
* <input type="range" bind:value={tween.current} disabled />
1777+
* ```
1778+
* */
1779+
export class Tween<T> {
1780+
/**
1781+
* Create a tween whose value is bound to the return value of `fn`. This must be called
1782+
* inside an effect root (for example, during component initialisation).
1783+
*
1784+
* ```svelte
1785+
* <script>
1786+
* import { Tween } from 'svelte/motion';
1787+
*
1788+
* let { number } = $props();
1789+
*
1790+
* const tween = Tween.of(() => number);
1791+
* </script>
1792+
* ```
1793+
*
1794+
*/
1795+
static of<U>(fn: () => U, options?: TweenedOptions<U> | undefined): Tween<U>;
1796+
1797+
constructor(value: T, options?: TweenedOptions<T>);
1798+
/**
1799+
* Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it.
1800+
*
1801+
* If `options` are provided, they will override the tween's defaults.
1802+
* */
1803+
set(value: T, options?: TweenedOptions<T> | undefined): Promise<void>;
1804+
get current(): T;
1805+
set target(v: T);
1806+
get target(): T;
1807+
#private;
1808+
}
17641809

17651810
export {};
17661811
}

0 commit comments

Comments
 (0)