@humanspeak/svelte-motion 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/html/_MotionContainer.svelte +8 -8
  2. package/dist/index.d.ts +17 -11
  3. package/dist/index.js +9 -9
  4. package/dist/utils/attachable.js +14 -9
  5. package/dist/utils/augmentMotionValue.svelte.d.ts +156 -0
  6. package/dist/utils/augmentMotionValue.svelte.js +193 -0
  7. package/dist/utils/booleanSnapshot.svelte.d.ts +37 -0
  8. package/dist/utils/booleanSnapshot.svelte.js +48 -0
  9. package/dist/utils/dom.d.ts +13 -0
  10. package/dist/utils/dom.js +19 -0
  11. package/dist/utils/inView.svelte.d.ts +209 -0
  12. package/dist/utils/inView.svelte.js +323 -0
  13. package/dist/utils/motionTemplate.svelte.d.ts +53 -0
  14. package/dist/utils/motionTemplate.svelte.js +78 -0
  15. package/dist/utils/motionValue.svelte.d.ts +61 -0
  16. package/dist/utils/motionValue.svelte.js +49 -0
  17. package/dist/utils/reducedMotion.svelte.d.ts +43 -0
  18. package/dist/utils/reducedMotion.svelte.js +80 -0
  19. package/dist/utils/reducedMotionConfig.svelte.d.ts +74 -0
  20. package/dist/utils/reducedMotionConfig.svelte.js +144 -0
  21. package/dist/utils/scroll.svelte.d.ts +91 -0
  22. package/dist/utils/scroll.svelte.js +259 -0
  23. package/dist/utils/spring.svelte.d.ts +2 -6
  24. package/dist/utils/spring.svelte.js +6 -70
  25. package/dist/utils/time.svelte.d.ts +47 -0
  26. package/dist/utils/time.svelte.js +128 -0
  27. package/dist/utils/transform.svelte.d.ts +170 -0
  28. package/dist/utils/transform.svelte.js +189 -0
  29. package/dist/utils/velocity.svelte.d.ts +61 -0
  30. package/dist/utils/velocity.svelte.js +132 -0
  31. package/package.json +1 -1
  32. package/dist/utils/inView.d.ts +0 -136
  33. package/dist/utils/inView.js +0 -266
  34. package/dist/utils/motionTemplate.d.ts +0 -21
  35. package/dist/utils/motionTemplate.js +0 -33
  36. package/dist/utils/motionValue.d.ts +0 -6
  37. package/dist/utils/motionValue.js +0 -13
  38. package/dist/utils/reducedMotion.d.ts +0 -20
  39. package/dist/utils/reducedMotion.js +0 -42
  40. package/dist/utils/reducedMotionConfig.d.ts +0 -39
  41. package/dist/utils/reducedMotionConfig.js +0 -92
  42. package/dist/utils/scroll.d.ts +0 -63
  43. package/dist/utils/scroll.js +0 -79
  44. package/dist/utils/time.d.ts +0 -14
  45. package/dist/utils/time.js +0 -68
  46. package/dist/utils/transform.d.ts +0 -74
  47. package/dist/utils/transform.js +0 -211
  48. package/dist/utils/velocity.d.ts +0 -15
  49. package/dist/utils/velocity.js +0 -62
@@ -1,63 +0,0 @@
1
- import { type Readable } from 'svelte/store';
2
- import { type ElementOrGetter } from './dom.js';
3
- /**
4
- * A scroll offset edge defined as a string (e.g. `"start"`, `"end"`, `"center"`)
5
- * or a number (0–1 progress). Each offset entry is a pair of `[target, container]`.
6
- */
7
- type ScrollOffset = Array<[number | string, number | string]> | string[];
8
- /**
9
- * Options accepted by `useScroll`.
10
- */
11
- type UseScrollOptions = {
12
- /** Scrollable container to track. Defaults to the page. Accepts an element or a getter function. */
13
- container?: ElementOrGetter;
14
- /** Target element to track position of within the container. Accepts an element or a getter function. */
15
- target?: ElementOrGetter;
16
- /** Scroll offset configuration for element position tracking. */
17
- offset?: ScrollOffset;
18
- /** Which axis to use for the single-axis `progress` value supplied to `scroll()`. */
19
- axis?: 'x' | 'y';
20
- };
21
- /**
22
- * Return type of `useScroll` — four readable Svelte stores representing
23
- * scroll position and normalised progress for both axes.
24
- */
25
- type UseScrollReturn = {
26
- scrollX: Readable<number>;
27
- scrollY: Readable<number>;
28
- scrollXProgress: Readable<number>;
29
- scrollYProgress: Readable<number>;
30
- };
31
- /**
32
- * Creates scroll-linked Svelte stores for building scroll-driven animations
33
- * such as progress indicators and parallax effects.
34
- *
35
- * When the returned stores are used with `opacity` / `transform` CSS properties
36
- * the animations can be hardware accelerated by the browser.
37
- *
38
- * SSR-safe: returns static `readable(0)` stores on the server.
39
- *
40
- * `container` and `target` accept either an `HTMLElement` directly or a
41
- * getter function `() => HTMLElement | undefined`. This is useful with
42
- * Svelte's `bind:this` where the element isn't available until after mount.
43
- * When a getter is provided, element resolution is deferred until the
44
- * first subscriber arrives, and if the element isn't available yet the
45
- * stores poll on each animation frame until it is.
46
- *
47
- * @example
48
- * ```svelte
49
- * <script>
50
- * import { useScroll, useSpring } from '@humanspeak/svelte-motion'
51
- *
52
- * const { scrollYProgress } = useScroll()
53
- * const scaleX = useSpring(scrollYProgress)
54
- * </script>
55
- *
56
- * <div style="transform: scaleX({$scaleX}); transform-origin: left;" />
57
- * ```
58
- *
59
- * @param options Optional scroll tracking configuration.
60
- * @returns An object with `scrollX`, `scrollY`, `scrollXProgress`, and `scrollYProgress` stores.
61
- */
62
- export declare const useScroll: (options?: UseScrollOptions) => UseScrollReturn;
63
- export {};
@@ -1,79 +0,0 @@
1
- import { scroll } from 'motion';
2
- import { readable, writable } from 'svelte/store';
3
- import { createAttachable } from './attachable.js';
4
- import {} from './dom.js';
5
- /**
6
- * Creates scroll-linked Svelte stores for building scroll-driven animations
7
- * such as progress indicators and parallax effects.
8
- *
9
- * When the returned stores are used with `opacity` / `transform` CSS properties
10
- * the animations can be hardware accelerated by the browser.
11
- *
12
- * SSR-safe: returns static `readable(0)` stores on the server.
13
- *
14
- * `container` and `target` accept either an `HTMLElement` directly or a
15
- * getter function `() => HTMLElement | undefined`. This is useful with
16
- * Svelte's `bind:this` where the element isn't available until after mount.
17
- * When a getter is provided, element resolution is deferred until the
18
- * first subscriber arrives, and if the element isn't available yet the
19
- * stores poll on each animation frame until it is.
20
- *
21
- * @example
22
- * ```svelte
23
- * <script>
24
- * import { useScroll, useSpring } from '@humanspeak/svelte-motion'
25
- *
26
- * const { scrollYProgress } = useScroll()
27
- * const scaleX = useSpring(scrollYProgress)
28
- * </script>
29
- *
30
- * <div style="transform: scaleX({$scaleX}); transform-origin: left;" />
31
- * ```
32
- *
33
- * @param options Optional scroll tracking configuration.
34
- * @returns An object with `scrollX`, `scrollY`, `scrollXProgress`, and `scrollYProgress` stores.
35
- */
36
- export const useScroll = (options) => {
37
- if (typeof window === 'undefined') {
38
- return {
39
- scrollX: readable(0),
40
- scrollY: readable(0),
41
- scrollXProgress: readable(0),
42
- scrollYProgress: readable(0)
43
- };
44
- }
45
- const stores = {
46
- scrollX: writable(0),
47
- scrollY: writable(0),
48
- scrollXProgress: writable(0),
49
- scrollYProgress: writable(0)
50
- };
51
- const attachable = createAttachable({
52
- refs: { container: options?.container, target: options?.target },
53
- onAttach: ({ container, target }) => scroll((_progress, info) => {
54
- stores.scrollX.set(info.x.current);
55
- stores.scrollY.set(info.y.current);
56
- stores.scrollXProgress.set(info.x.progress);
57
- stores.scrollYProgress.set(info.y.progress);
58
- }, {
59
- container,
60
- target,
61
- offset: options?.offset,
62
- axis: options?.axis
63
- })
64
- });
65
- const make = (key) => readable(0, (set) => {
66
- const release = attachable.subscribe();
67
- const unsub = stores[key].subscribe(set);
68
- return () => {
69
- unsub();
70
- release();
71
- };
72
- });
73
- return {
74
- scrollX: make('scrollX'),
75
- scrollY: make('scrollY'),
76
- scrollXProgress: make('scrollXProgress'),
77
- scrollYProgress: make('scrollYProgress')
78
- };
79
- };
@@ -1,14 +0,0 @@
1
- import { type Readable } from 'svelte/store';
2
- /**
3
- * Returns a time store that ticks once per animation frame.
4
- *
5
- * - Without an `id`, returns a fresh timeline per call.
6
- * - With an `id`, callers sharing the same id receive the same store/timeline,
7
- * ensuring synchronized reads across components.
8
- * - SSR-safe: Returns a static 0-valued store when `window` is unavailable.
9
- *
10
- * @param {string=} id Optional timeline identifier for sharing across calls.
11
- * @returns {Readable<number>} A readable store of elapsed milliseconds.
12
- * @see https://motion.dev/docs/react-use-time?platform=react
13
- */
14
- export declare const useTime: (id?: string) => Readable<number>;
@@ -1,68 +0,0 @@
1
- import { readable } from 'svelte/store';
2
- const SSR_ZERO = readable(0, () => { });
3
- const sharedStores = new Map();
4
- // Clear shared timelines on HMR dispose to avoid stale entries across hot reloads
5
- if (import.meta &&
6
- import.meta.hot) {
7
- ;
8
- import.meta.hot.dispose(() => {
9
- sharedStores.clear();
10
- });
11
- }
12
- /**
13
- * Creates a new time store that updates once per animation frame.
14
- *
15
- * The store value represents elapsed milliseconds since the store was created.
16
- * In SSR environments (no `window`), a static 0-valued store is returned.
17
- *
18
- * @returns {Readable<number>} A readable store of elapsed milliseconds.
19
- * @see https://motion.dev/docs/react-use-time?platform=react
20
- * @private
21
- */
22
- const createTimeStore = () => {
23
- if (typeof window === 'undefined')
24
- return SSR_ZERO;
25
- return readable(0, (set) => {
26
- const start = performance.now();
27
- let raf = 0;
28
- /* c8 ignore start */
29
- const loop = (t) => {
30
- set(t - start);
31
- raf = requestAnimationFrame(loop);
32
- };
33
- /* c8 ignore stop */
34
- raf = requestAnimationFrame(loop);
35
- return () => cancelAnimationFrame(raf);
36
- });
37
- };
38
- /**
39
- * Returns a time store that ticks once per animation frame.
40
- *
41
- * - Without an `id`, returns a fresh timeline per call.
42
- * - With an `id`, callers sharing the same id receive the same store/timeline,
43
- * ensuring synchronized reads across components.
44
- * - SSR-safe: Returns a static 0-valued store when `window` is unavailable.
45
- *
46
- * @param {string=} id Optional timeline identifier for sharing across calls.
47
- * @returns {Readable<number>} A readable store of elapsed milliseconds.
48
- * @see https://motion.dev/docs/react-use-time?platform=react
49
- */
50
- export const useTime = (id) => {
51
- if (!id)
52
- return createTimeStore();
53
- if (typeof window === 'undefined')
54
- return SSR_ZERO;
55
- const existing = sharedStores.get(id);
56
- if (existing)
57
- return existing;
58
- const base = createTimeStore();
59
- const store = readable(0, (set) => {
60
- const unsub = base.subscribe(set);
61
- return () => {
62
- unsub();
63
- sharedStores.delete(id);
64
- };
65
- });
66
- sharedStores.set(id, store);
67
- return store;
68
- };
@@ -1,74 +0,0 @@
1
- export type TransformValues = Partial<{
2
- x: number;
3
- y: number;
4
- scale: number;
5
- scaleX: number;
6
- scaleY: number;
7
- rotate: number;
8
- }>;
9
- /**
10
- * Build a CSS transform string from numeric values (no matrices).
11
- *
12
- * @param values Partial map of translate/scale/rotate values.
13
- * @returns A space-separated CSS `transform` string, or `""` when all values are defaults.
14
- */
15
- export declare const buildTransform: (values: TransformValues) => string;
16
- /**
17
- * Lightweight safety check for transform magnitudes and NaN values.
18
- *
19
- * @param values Transform values to validate.
20
- * @param opts Optional configuration; `maxScale` caps allowable absolute scale (default 8).
21
- * @returns `true` if all scale values are finite and within bounds.
22
- */
23
- export declare const isSafeTransform: (values: TransformValues, opts?: {
24
- maxScale?: number;
25
- }) => boolean;
26
- /**
27
- * Extract the uniform scale factor from a CSS `matrix()` string.
28
- *
29
- * @param matrix A CSS `matrix(...)` value, `"none"`, `null`, or `undefined`.
30
- * @returns The `a` component of the matrix (uniform scale), or `null` if unparseable.
31
- */
32
- export declare const parseMatrixScale: (matrix: string | null | undefined) => number | null;
33
- import { type Readable } from 'svelte/store';
34
- /**
35
- * Options for range-mapping transform.
36
- *
37
- * - clamp: If true, clamps the input to the active segment bounds.
38
- * - ease: A single easing function or one per segment to shape interpolation.
39
- * - mixer: Custom mixer factory to interpolate non-numeric outputs.
40
- *
41
- * @see https://motion.dev/docs/react-use-transform?platform=react
42
- */
43
- export type TransformOptions = {
44
- clamp?: boolean;
45
- ease?: ((t: number) => number) | Array<(t: number) => number>;
46
- mixer?: (from: unknown, to: unknown) => (t: number) => unknown;
47
- };
48
- /**
49
- * Clamps a numeric value between two bounds, irrespective of their order.
50
- *
51
- * @param val Current value.
52
- * @param a First bound.
53
- * @param b Second bound.
54
- * @returns Value clamped to [min(a,b), max(a,b)].
55
- */
56
- export declare const clampBidirectional: (val: number, a: number, b: number) => number;
57
- /**
58
- * Creates a derived Svelte store that transforms values.
59
- *
60
- * Two supported forms (API parity with Motion's useTransform):
61
- * - Mapping form: Map a numeric source across input/output ranges.
62
- * Example: `useTransform(src, [0, 100], [0, 1], { clamp: true })`
63
- * - Function form: Recompute from a function based on dependency stores.
64
- * Example: `useTransform(() => compute(), [depA, depB])`
65
- *
66
- * @template T
67
- * @param {Readable<number>|(() => T)} sourceOrCompute Numeric source store (mapping form), or compute function (function form).
68
- * @param {number[]|Readable<unknown>[]} inputOrDeps Input stops (mapping) or dependency stores (function form).
69
- * @param {T[]=} output Output stops (mapping form only). Must match input length.
70
- * @param {TransformOptions=} options Mapping options (mapping form only).
71
- * @returns {Readable<T>} A derived Svelte readable store.
72
- * @see https://motion.dev/docs/react-use-transform?platform=react
73
- */
74
- export declare const useTransform: <T = number>(sourceOrCompute: Readable<number> | (() => T), inputOrDeps: number[] | Readable<unknown>[], output?: T[], options?: TransformOptions) => Readable<T>;
@@ -1,211 +0,0 @@
1
- // Utilities for building and validating transform strings
2
- const DEFAULTS = {
3
- x: 0,
4
- y: 0,
5
- scale: 1,
6
- scaleX: 1,
7
- scaleY: 1,
8
- rotate: 0
9
- };
10
- /**
11
- * Build a CSS transform string from numeric values (no matrices).
12
- *
13
- * @param values Partial map of translate/scale/rotate values.
14
- * @returns A space-separated CSS `transform` string, or `""` when all values are defaults.
15
- */
16
- export const buildTransform = (values) => {
17
- const v = { ...DEFAULTS, ...values };
18
- // If explicit per-axis scales provided, use them; otherwise use uniform scale
19
- const useAxes = values.scaleX !== undefined || values.scaleY !== undefined;
20
- const parts = [];
21
- // Translate first for readability; rely on browser to optimize
22
- if (v.x !== 0 || v.y !== 0)
23
- parts.push(`translate(${round(v.x)}px, ${round(v.y)}px)`);
24
- if (v.rotate !== 0)
25
- parts.push(`rotate(${round(v.rotate)}deg)`);
26
- if (useAxes) {
27
- parts.push(`scaleX(${round(v.scaleX)})`);
28
- parts.push(`scaleY(${round(v.scaleY)})`);
29
- }
30
- else if (v.scale !== 1) {
31
- parts.push(`scale(${round(v.scale)})`);
32
- }
33
- return parts.join(' ').trim();
34
- };
35
- /**
36
- * Lightweight safety check for transform magnitudes and NaN values.
37
- *
38
- * @param values Transform values to validate.
39
- * @param opts Optional configuration; `maxScale` caps allowable absolute scale (default 8).
40
- * @returns `true` if all scale values are finite and within bounds.
41
- */
42
- export const isSafeTransform = (values, opts) => {
43
- const maxScale = opts?.maxScale ?? 8;
44
- const entries = [
45
- ['scale', values.scale],
46
- ['scaleX', values.scaleX],
47
- ['scaleY', values.scaleY]
48
- ];
49
- for (const [, val] of entries) {
50
- if (val === undefined)
51
- continue;
52
- if (!Number.isFinite(val))
53
- return false;
54
- if (Math.abs(val) > maxScale)
55
- return false;
56
- }
57
- return true;
58
- };
59
- /**
60
- * Extract the uniform scale factor from a CSS `matrix()` string.
61
- *
62
- * @param matrix A CSS `matrix(...)` value, `"none"`, `null`, or `undefined`.
63
- * @returns The `a` component of the matrix (uniform scale), or `null` if unparseable.
64
- */
65
- export const parseMatrixScale = (matrix) => {
66
- if (!matrix || matrix === 'none')
67
- return null;
68
- const m = matrix.match(/matrix\(([^)]+)\)/);
69
- if (!m)
70
- return null;
71
- const [a] = m[1].split(',').map((s) => parseFloat(s.trim()));
72
- return Number.isFinite(a) ? a : null;
73
- };
74
- /**
75
- * Round a number to six decimal places to avoid excessive precision in CSS strings.
76
- *
77
- * @param n The number to round.
78
- * @returns The rounded value.
79
- */
80
- const round = (n) => {
81
- return Math.round(n * 1e6) / 1e6;
82
- };
83
- import { derived, readable } from 'svelte/store';
84
- /**
85
- * Creates a linear mixer function for numeric values.
86
- *
87
- * @param from Starting numeric value.
88
- * @param to Ending numeric value.
89
- * @returns Function that linearly interpolates between from→to for progress t∈[0,1].
90
- * @private
91
- */
92
- const linearMix = (from, to) => (t) => from + (to - from) * t;
93
- /**
94
- * Clamps a numeric value between two bounds, irrespective of their order.
95
- *
96
- * @param val Current value.
97
- * @param a First bound.
98
- * @param b Second bound.
99
- * @returns Value clamped to [min(a,b), max(a,b)].
100
- */
101
- export const clampBidirectional = (val, a, b) => {
102
- const lower = a < b ? a : b;
103
- const upper = a < b ? b : a;
104
- return Math.min(Math.max(val, lower), upper);
105
- };
106
- /**
107
- * Finds the segment index i such that x lies between input[i] and input[i+1].
108
- * Handles both ascending and descending input ranges.
109
- *
110
- * @param input Monotonic list of input stops.
111
- * @param x Current input value.
112
- * @returns Segment index in range [0, input.length - 2].
113
- * @private
114
- */
115
- const findSegment = (input, x) => {
116
- if (input.length < 2)
117
- return 0;
118
- const first = input[0];
119
- const second = input[1];
120
- const ascending = second > first;
121
- if (ascending) {
122
- if (x <= first)
123
- return 0;
124
- for (let i = 1; i < input.length; i++) {
125
- const curr = input[i];
126
- if (x <= curr)
127
- return i - 1;
128
- }
129
- return input.length - 2;
130
- }
131
- else {
132
- if (x >= first)
133
- return 0;
134
- for (let i = 1; i < input.length; i++) {
135
- const curr = input[i];
136
- if (x >= curr)
137
- return i - 1;
138
- }
139
- return input.length - 2;
140
- }
141
- };
142
- /**
143
- * Creates a derived Svelte store that transforms values.
144
- *
145
- * Two supported forms (API parity with Motion's useTransform):
146
- * - Mapping form: Map a numeric source across input/output ranges.
147
- * Example: `useTransform(src, [0, 100], [0, 1], { clamp: true })`
148
- * - Function form: Recompute from a function based on dependency stores.
149
- * Example: `useTransform(() => compute(), [depA, depB])`
150
- *
151
- * @template T
152
- * @param {Readable<number>|(() => T)} sourceOrCompute Numeric source store (mapping form), or compute function (function form).
153
- * @param {number[]|Readable<unknown>[]} inputOrDeps Input stops (mapping) or dependency stores (function form).
154
- * @param {T[]=} output Output stops (mapping form only). Must match input length.
155
- * @param {TransformOptions=} options Mapping options (mapping form only).
156
- * @returns {Readable<T>} A derived Svelte readable store.
157
- * @see https://motion.dev/docs/react-use-transform?platform=react
158
- */
159
- export const useTransform = (sourceOrCompute, inputOrDeps, output, options = {}) => {
160
- // Function form: (compute, deps)
161
- if (typeof sourceOrCompute === 'function') {
162
- const compute = sourceOrCompute;
163
- const deps = inputOrDeps;
164
- if (!deps || deps.length === 0)
165
- return readable(compute());
166
- return derived(deps, () => compute());
167
- }
168
- // Mapping form: (source, input, output, options)
169
- const source = sourceOrCompute;
170
- const input = inputOrDeps;
171
- const out = (output ?? []);
172
- const { clamp = true, ease, mixer } = options;
173
- if (input.length !== out.length) {
174
- throw new Error(`useTransform: input and output arrays must be the same length (input: ${input.length}, output: ${out.length})`);
175
- }
176
- const easings = Array.isArray(ease)
177
- ? ease
178
- : ease
179
- ? new Array(Math.max(0, out.length - 1)).fill(ease)
180
- : [];
181
- return derived(source, (x) => {
182
- if (input.length === 0)
183
- return out[0];
184
- if (input.length === 1)
185
- return out[0];
186
- const seg = findSegment(input, x);
187
- const i0 = input[seg];
188
- const i1 = input[seg + 1];
189
- const o0 = out[seg];
190
- const o1 = out[seg + 1];
191
- // Runtime validation to avoid non-null assertions
192
- if (i0 === undefined || i1 === undefined || o0 === undefined || o1 === undefined) {
193
- console.warn('useTransform: Invalid segment bounds', {
194
- seg,
195
- inputLength: input.length,
196
- outputLength: out.length
197
- });
198
- return out[0];
199
- }
200
- const localClamp = clamp ? clampBidirectional : (val) => val;
201
- const progress = i0 === i1 ? 0 : (localClamp(x, i0, i1) - i0) / (i1 - i0);
202
- const e = easings[seg];
203
- const p = e ? e(progress) : progress;
204
- const mix = mixer
205
- ? mixer(o0, o1)
206
- : typeof o0 === 'number' && typeof o1 === 'number'
207
- ? linearMix(o0, o1)
208
- : (_t) => (p < 0.5 ? o0 : o1); // trunk-ignore(eslint/@typescript-eslint/no-unused-vars)
209
- return mix(p);
210
- });
211
- };
@@ -1,15 +0,0 @@
1
- import { type Readable } from 'svelte/store';
2
- /**
3
- * Creates a readable store that tracks the velocity of a source store's value.
4
- *
5
- * Uses `motionValue` from motion-dom for built-in velocity tracking with
6
- * timestamps. Polls velocity via `requestAnimationFrame` and settles to 0
7
- * when movement stops.
8
- *
9
- * SSR-safe: returns a static `readable(0)` on the server.
10
- *
11
- * @param source A readable store of numeric or unit-string values.
12
- * @returns A readable store of the current velocity in units/second.
13
- * @see https://motion.dev/docs/react-use-velocity
14
- */
15
- export declare const useVelocity: (source: Readable<number | string>) => Readable<number>;
@@ -1,62 +0,0 @@
1
- import { motionValue } from 'motion-dom';
2
- import { readable, writable } from 'svelte/store';
3
- /**
4
- * Parses a numeric value from a number or unit string (e.g. "100px" → 100).
5
- */
6
- const parseNumeric = (v) => {
7
- if (typeof v === 'number')
8
- return v;
9
- const parsed = Number.parseFloat(String(v));
10
- return Number.isFinite(parsed) ? parsed : 0;
11
- };
12
- /**
13
- * Creates a readable store that tracks the velocity of a source store's value.
14
- *
15
- * Uses `motionValue` from motion-dom for built-in velocity tracking with
16
- * timestamps. Polls velocity via `requestAnimationFrame` and settles to 0
17
- * when movement stops.
18
- *
19
- * SSR-safe: returns a static `readable(0)` on the server.
20
- *
21
- * @param source A readable store of numeric or unit-string values.
22
- * @returns A readable store of the current velocity in units/second.
23
- * @see https://motion.dev/docs/react-use-velocity
24
- */
25
- export const useVelocity = (source) => {
26
- if (typeof window === 'undefined')
27
- return readable(0, () => { });
28
- const mv = motionValue(0);
29
- const store = writable(0);
30
- let raf = 0;
31
- let settled = true;
32
- const poll = () => {
33
- const v = mv.getVelocity();
34
- store.set(v);
35
- if (Math.abs(v) < 0.001) {
36
- settled = true;
37
- store.set(0);
38
- raf = 0;
39
- return;
40
- }
41
- raf = requestAnimationFrame(poll);
42
- };
43
- const startPolling = () => {
44
- if (!settled)
45
- return;
46
- settled = false;
47
- raf = requestAnimationFrame(poll);
48
- };
49
- return readable(0, (set) => {
50
- const unsubStore = store.subscribe(set);
51
- const unsubSource = source.subscribe((v) => {
52
- mv.set(parseNumeric(v));
53
- startPolling();
54
- });
55
- return () => {
56
- unsubStore();
57
- unsubSource();
58
- if (raf)
59
- cancelAnimationFrame(raf);
60
- };
61
- });
62
- };