@humanspeak/svelte-motion 0.1.26 → 0.1.28
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.
- package/README.md +2 -2
- package/dist/components/motionConfig.context.d.ts +1 -1
- package/dist/components/motionConfig.context.js +1 -1
- package/dist/html/_MotionContainer.svelte +21 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/utils/dragMath.d.ts +15 -0
- package/dist/utils/dragMath.js +15 -0
- package/dist/utils/inertia.d.ts +10 -0
- package/dist/utils/inertia.js +10 -0
- package/dist/utils/layoutId.d.ts +7 -0
- package/dist/utils/layoutId.js +7 -0
- package/dist/utils/motionValue.d.ts +6 -0
- package/dist/utils/motionValue.js +13 -0
- package/dist/utils/motionValueEvent.d.ts +29 -0
- package/dist/utils/motionValueEvent.js +38 -0
- package/dist/utils/presence.d.ts +2 -0
- package/dist/utils/presence.js +19 -0
- package/dist/utils/scroll.d.ts +68 -0
- package/dist/utils/scroll.js +119 -0
- package/dist/utils/testing.d.ts +5 -0
- package/dist/utils/testing.js +5 -0
- package/package.json +24 -13
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Svelte Motion — Framer Motion API for Svelte 5
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@humanspeak/svelte-motion)
|
|
4
4
|
[](https://github.com/humanspeak/svelte-motion/actions/workflows/npm-publish.yml)
|
|
@@ -32,7 +32,7 @@ npm install @humanspeak/svelte-motion
|
|
|
32
32
|
</motion.button>
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
##
|
|
35
|
+
## Framer Motion API Parity
|
|
36
36
|
|
|
37
37
|
Goal: Framer Motion API parity for Svelte where common React examples can be translated with minimal changes.
|
|
38
38
|
|
|
@@ -8,7 +8,7 @@ export declare const getMotionConfig: () => MotionConfigProps | undefined;
|
|
|
8
8
|
/**
|
|
9
9
|
* Provide motion configuration to descendant components via Svelte context.
|
|
10
10
|
*
|
|
11
|
-
* @param motionConfig The configuration to propagate (e.g. `
|
|
11
|
+
* @param motionConfig The configuration to propagate (e.g. `transition`).
|
|
12
12
|
* @returns The same `MotionConfigProps` that was set.
|
|
13
13
|
*/
|
|
14
14
|
export declare const createMotionConfig: (motionConfig: MotionConfigProps) => MotionConfigProps;
|
|
@@ -11,7 +11,7 @@ export const getMotionConfig = () => {
|
|
|
11
11
|
/**
|
|
12
12
|
* Provide motion configuration to descendant components via Svelte context.
|
|
13
13
|
*
|
|
14
|
-
* @param motionConfig The configuration to propagate (e.g. `
|
|
14
|
+
* @param motionConfig The configuration to propagate (e.g. `transition`).
|
|
15
15
|
* @returns The same `MotionConfigProps` that was set.
|
|
16
16
|
*/
|
|
17
17
|
export const createMotionConfig = (motionConfig) => {
|
|
@@ -168,6 +168,24 @@
|
|
|
168
168
|
})
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// Capture mid-animation computed styles via rAF so exit clones can start
|
|
172
|
+
// from the correct visual state. Without this, interrupting an enter animation
|
|
173
|
+
// causes the exit to snap (the element is disconnected before onDestroy, so
|
|
174
|
+
// getAnimations()/commitStyles() can't work at clone time).
|
|
175
|
+
$effect(() => {
|
|
176
|
+
if (!(element && context)) return
|
|
177
|
+
let rafId: number
|
|
178
|
+
const capture = () => {
|
|
179
|
+
if (element && element.isConnected && element.getAnimations().length > 0) {
|
|
180
|
+
const cs = getComputedStyle(element)
|
|
181
|
+
context.updateChildAnimatedStyle(presenceKey, cs.opacity, cs.transform)
|
|
182
|
+
}
|
|
183
|
+
rafId = requestAnimationFrame(capture)
|
|
184
|
+
}
|
|
185
|
+
rafId = requestAnimationFrame(capture)
|
|
186
|
+
return () => cancelAnimationFrame(rafId)
|
|
187
|
+
})
|
|
188
|
+
|
|
171
189
|
// Keep a live snapshot of the layoutId element's rect so the next element can FLIP from it.
|
|
172
190
|
// We store the last-known-good rect and push it to the registry on cleanup,
|
|
173
191
|
// because onDestroy fires after the element is removed from DOM (rect would be zeros).
|
|
@@ -985,6 +1003,9 @@
|
|
|
985
1003
|
|
|
986
1004
|
// Mark that we're triggering the initial animation to prevent duplicate runs
|
|
987
1005
|
initialAnimationTriggered = true
|
|
1006
|
+
if (animateProp && typeof animateProp !== 'string') {
|
|
1007
|
+
lastAnimatePropJson = JSON.stringify(animateProp)
|
|
1008
|
+
}
|
|
988
1009
|
|
|
989
1010
|
// IMPORTANT: Start the animation BEFORE changing isLoaded.
|
|
990
1011
|
// When isLoaded changes to 'ready', Svelte will reactively remove the
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition,
|
|
|
9
9
|
export { useAnimationFrame } from './utils/animationFrame';
|
|
10
10
|
export { createDragControls } from './utils/dragControls';
|
|
11
11
|
export { useMotionTemplate } from './utils/motionTemplate';
|
|
12
|
+
export { useMotionValue } from './utils/motionValue';
|
|
13
|
+
export type { MotionValue } from './utils/motionValue';
|
|
14
|
+
export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
15
|
+
export { useScroll } from './utils/scroll';
|
|
12
16
|
export { useSpring } from './utils/spring';
|
|
13
17
|
export { useVelocity } from './utils/velocity';
|
|
14
18
|
/**
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,9 @@ export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } f
|
|
|
12
12
|
export { useAnimationFrame } from './utils/animationFrame';
|
|
13
13
|
export { createDragControls } from './utils/dragControls';
|
|
14
14
|
export { useMotionTemplate } from './utils/motionTemplate';
|
|
15
|
+
export { useMotionValue } from './utils/motionValue';
|
|
16
|
+
export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
17
|
+
export { useScroll } from './utils/scroll';
|
|
15
18
|
export { useSpring } from './utils/spring';
|
|
16
19
|
export { useVelocity } from './utils/velocity';
|
|
17
20
|
/**
|
package/dist/utils/dragMath.d.ts
CHANGED
|
@@ -17,5 +17,20 @@ export type ConstraintElastic = {
|
|
|
17
17
|
* Apply float-safe constraints with optional elastic mixing.
|
|
18
18
|
* Mirrors Framer Motion behavior: clamp via Math.min/Math.max with no rounding.
|
|
19
19
|
* If `elastic` provided, blends toward the bound using its side-specific factor.
|
|
20
|
+
*
|
|
21
|
+
* @param point The unconstrained value to clamp.
|
|
22
|
+
* @param range Min/max boundaries. Either or both may be omitted.
|
|
23
|
+
* @param elastic Optional per-side elastic factor(s) in [0,1]. When provided,
|
|
24
|
+
* the value is interpolated toward the boundary instead of hard-clamped.
|
|
25
|
+
* @returns The constrained (and optionally elastically blended) value.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* // Hard clamp to [0, 100]
|
|
30
|
+
* applyConstraints(120, { min: 0, max: 100 }) // 100
|
|
31
|
+
*
|
|
32
|
+
* // Elastic overshoot (50 % blend toward max)
|
|
33
|
+
* applyConstraints(120, { min: 0, max: 100 }, 0.5) // 110
|
|
34
|
+
* ```
|
|
20
35
|
*/
|
|
21
36
|
export declare const applyConstraints: (point: number, range: ConstraintRange, elastic?: ConstraintElastic) => number;
|
package/dist/utils/dragMath.js
CHANGED
|
@@ -7,6 +7,21 @@ export const mixNumber = (from, to, t) => from + (to - from) * t;
|
|
|
7
7
|
* Apply float-safe constraints with optional elastic mixing.
|
|
8
8
|
* Mirrors Framer Motion behavior: clamp via Math.min/Math.max with no rounding.
|
|
9
9
|
* If `elastic` provided, blends toward the bound using its side-specific factor.
|
|
10
|
+
*
|
|
11
|
+
* @param point The unconstrained value to clamp.
|
|
12
|
+
* @param range Min/max boundaries. Either or both may be omitted.
|
|
13
|
+
* @param elastic Optional per-side elastic factor(s) in [0,1]. When provided,
|
|
14
|
+
* the value is interpolated toward the boundary instead of hard-clamped.
|
|
15
|
+
* @returns The constrained (and optionally elastically blended) value.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* // Hard clamp to [0, 100]
|
|
20
|
+
* applyConstraints(120, { min: 0, max: 100 }) // 100
|
|
21
|
+
*
|
|
22
|
+
* // Elastic overshoot (50 % blend toward max)
|
|
23
|
+
* applyConstraints(120, { min: 0, max: 100 }, 0.5) // 110
|
|
24
|
+
* ```
|
|
10
25
|
*/
|
|
11
26
|
export const applyConstraints = (point, range, elastic) => {
|
|
12
27
|
const hasMin = range.min !== undefined;
|
package/dist/utils/inertia.d.ts
CHANGED
|
@@ -42,5 +42,15 @@ export type StepResult = {
|
|
|
42
42
|
* @param bounds Min/max boundaries for the axis.
|
|
43
43
|
* @param opts Physics parameters for decay and boundary spring.
|
|
44
44
|
* @returns A function that accepts elapsed time in ms and returns the current `StepResult`.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* const step = createInertiaToBoundary(
|
|
49
|
+
* { value: 50, velocity: 200 },
|
|
50
|
+
* { min: 0, max: 300 },
|
|
51
|
+
* { timeConstantMs: 350, restDelta: 0.5, restSpeed: 10, bounceStiffness: 500, bounceDamping: 25 }
|
|
52
|
+
* )
|
|
53
|
+
* const { value, done } = step(16) // advance 16 ms
|
|
54
|
+
* ```
|
|
45
55
|
*/
|
|
46
56
|
export declare const createInertiaToBoundary: (initial: AxisState, bounds: Bounds, opts: InertiaHandoffOptions) => ((tMs: number) => StepResult);
|
package/dist/utils/inertia.js
CHANGED
|
@@ -56,6 +56,16 @@ const solveCrossTimeMs = (x0, v0, tauMs, boundary) => {
|
|
|
56
56
|
* @param bounds Min/max boundaries for the axis.
|
|
57
57
|
* @param opts Physics parameters for decay and boundary spring.
|
|
58
58
|
* @returns A function that accepts elapsed time in ms and returns the current `StepResult`.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const step = createInertiaToBoundary(
|
|
63
|
+
* { value: 50, velocity: 200 },
|
|
64
|
+
* { min: 0, max: 300 },
|
|
65
|
+
* { timeConstantMs: 350, restDelta: 0.5, restSpeed: 10, bounceStiffness: 500, bounceDamping: 25 }
|
|
66
|
+
* )
|
|
67
|
+
* const { value, done } = step(16) // advance 16 ms
|
|
68
|
+
* ```
|
|
59
69
|
*/
|
|
60
70
|
export const createInertiaToBoundary = (initial, bounds, opts) => {
|
|
61
71
|
const min = bounds.min;
|
package/dist/utils/layoutId.d.ts
CHANGED
|
@@ -21,6 +21,13 @@ export declare const layoutIdRegistry: LayoutIdRegistry;
|
|
|
21
21
|
* Get the global layoutId registry.
|
|
22
22
|
*
|
|
23
23
|
* @returns The singleton `LayoutIdRegistry` instance.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const registry = getLayoutIdRegistry()
|
|
28
|
+
* registry.snapshot('hero', element.getBoundingClientRect())
|
|
29
|
+
* const entry = registry.consume('hero') // one-shot: returns and deletes
|
|
30
|
+
* ```
|
|
24
31
|
*/
|
|
25
32
|
export declare const getLayoutIdRegistry: () => LayoutIdRegistry;
|
|
26
33
|
export {};
|
package/dist/utils/layoutId.js
CHANGED
|
@@ -19,6 +19,13 @@ export const layoutIdRegistry = {
|
|
|
19
19
|
* Get the global layoutId registry.
|
|
20
20
|
*
|
|
21
21
|
* @returns The singleton `LayoutIdRegistry` instance.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const registry = getLayoutIdRegistry()
|
|
26
|
+
* registry.snapshot('hero', element.getBoundingClientRect())
|
|
27
|
+
* const entry = registry.consume('hero') // one-shot: returns and deletes
|
|
28
|
+
* ```
|
|
22
29
|
*/
|
|
23
30
|
export const getLayoutIdRegistry = () => {
|
|
24
31
|
return layoutIdRegistry;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
export const useMotionValue = (initial) => {
|
|
3
|
+
let current = initial;
|
|
4
|
+
const store = writable(initial);
|
|
5
|
+
return {
|
|
6
|
+
subscribe: store.subscribe,
|
|
7
|
+
set: (v) => {
|
|
8
|
+
current = v;
|
|
9
|
+
store.set(v);
|
|
10
|
+
},
|
|
11
|
+
get: () => current
|
|
12
|
+
};
|
|
13
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Readable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Subscribes to a Svelte store and fires a callback on every *change*,
|
|
4
|
+
* skipping the initial synchronous emission that Svelte stores produce
|
|
5
|
+
* on subscribe.
|
|
6
|
+
*
|
|
7
|
+
* Returns an unsubscribe function. Use inside `$effect` or `onDestroy`
|
|
8
|
+
* for automatic cleanup.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <script>
|
|
13
|
+
* import { useMotionValueEvent, useSpring } from '@humanspeak/svelte-motion'
|
|
14
|
+
* import { onDestroy } from 'svelte'
|
|
15
|
+
*
|
|
16
|
+
* const x = useSpring(0)
|
|
17
|
+
* const unsub = useMotionValueEvent(x, 'change', (latest) => {
|
|
18
|
+
* console.log('x changed to', latest)
|
|
19
|
+
* })
|
|
20
|
+
* onDestroy(unsub)
|
|
21
|
+
* </script>
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @param store A readable Svelte store to observe.
|
|
25
|
+
* @param event The event type — currently only `'change'` is supported.
|
|
26
|
+
* @param callback Invoked with the latest value on each change after the initial emission.
|
|
27
|
+
* @returns An unsubscribe function.
|
|
28
|
+
*/
|
|
29
|
+
export declare const useMotionValueEvent: <T>(store: Readable<T>, event: "change", callback: (latest: T) => void) => (() => void);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscribes to a Svelte store and fires a callback on every *change*,
|
|
3
|
+
* skipping the initial synchronous emission that Svelte stores produce
|
|
4
|
+
* on subscribe.
|
|
5
|
+
*
|
|
6
|
+
* Returns an unsubscribe function. Use inside `$effect` or `onDestroy`
|
|
7
|
+
* for automatic cleanup.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```svelte
|
|
11
|
+
* <script>
|
|
12
|
+
* import { useMotionValueEvent, useSpring } from '@humanspeak/svelte-motion'
|
|
13
|
+
* import { onDestroy } from 'svelte'
|
|
14
|
+
*
|
|
15
|
+
* const x = useSpring(0)
|
|
16
|
+
* const unsub = useMotionValueEvent(x, 'change', (latest) => {
|
|
17
|
+
* console.log('x changed to', latest)
|
|
18
|
+
* })
|
|
19
|
+
* onDestroy(unsub)
|
|
20
|
+
* </script>
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @param store A readable Svelte store to observe.
|
|
24
|
+
* @param event The event type — currently only `'change'` is supported.
|
|
25
|
+
* @param callback Invoked with the latest value on each change after the initial emission.
|
|
26
|
+
* @returns An unsubscribe function.
|
|
27
|
+
*/
|
|
28
|
+
export const useMotionValueEvent = (store, event, callback) => {
|
|
29
|
+
let initialized = false;
|
|
30
|
+
const unsub = store.subscribe((value) => {
|
|
31
|
+
if (!initialized) {
|
|
32
|
+
initialized = true;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
callback(value);
|
|
36
|
+
});
|
|
37
|
+
return unsub;
|
|
38
|
+
};
|
package/dist/utils/presence.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export type AnimatePresenceContext = {
|
|
|
31
31
|
registerChild: (key: string, element: HTMLElement, exit?: MotionExit, mergedTransition?: MotionTransition) => void;
|
|
32
32
|
/** Update the last known rect/style snapshot for a registered child. */
|
|
33
33
|
updateChildState: (key: string, rect: DOMRect, computedStyle: CSSStyleDeclaration) => void;
|
|
34
|
+
/** Update the last captured mid-animation style values for a child. */
|
|
35
|
+
updateChildAnimatedStyle: (key: string, opacity: string, transform: string) => void;
|
|
34
36
|
/** Unregister a child. If it has an exit, clone and animate it out. */
|
|
35
37
|
unregisterChild: (key: string) => void;
|
|
36
38
|
};
|
package/dist/utils/presence.js
CHANGED
|
@@ -239,6 +239,17 @@ export const createAnimatePresenceContext = (context) => {
|
|
|
239
239
|
child.lastComputedStyle = computedStyle;
|
|
240
240
|
}
|
|
241
241
|
};
|
|
242
|
+
/**
|
|
243
|
+
* Update the last captured mid-animation style values for a child.
|
|
244
|
+
* Called from a rAF loop while WAAPI animations are running.
|
|
245
|
+
*/
|
|
246
|
+
const updateChildAnimatedStyle = (key, opacity, transform) => {
|
|
247
|
+
const child = children.get(key);
|
|
248
|
+
if (child) {
|
|
249
|
+
child.lastAnimatedOpacity = opacity;
|
|
250
|
+
child.lastAnimatedTransform = transform;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
242
253
|
/**
|
|
243
254
|
* Unregister a child. If it has an `exit` definition, create a styled
|
|
244
255
|
* clone and run the exit animation using Motion. Cleans up after finish.
|
|
@@ -316,6 +327,13 @@ export const createAnimatePresenceContext = (context) => {
|
|
|
316
327
|
catch {
|
|
317
328
|
// Ignore
|
|
318
329
|
}
|
|
330
|
+
// Apply last captured mid-animation values (from rAF polling) so that
|
|
331
|
+
// exit clones start from the correct visual state when interrupting
|
|
332
|
+
// an enter animation. The element is disconnected by now so
|
|
333
|
+
// getComputedStyle/getAnimations won't reflect in-flight values.
|
|
334
|
+
if (child.lastAnimatedOpacity != null) {
|
|
335
|
+
clone.style.opacity = child.lastAnimatedOpacity;
|
|
336
|
+
}
|
|
319
337
|
// Attach to original parent and position absolutely at the last known rect
|
|
320
338
|
// Find the nearest positioned ancestor that isn't display: contents
|
|
321
339
|
let parent = child.element.parentElement ?? document.body;
|
|
@@ -464,6 +482,7 @@ export const createAnimatePresenceContext = (context) => {
|
|
|
464
482
|
onExitComplete: context.onExitComplete,
|
|
465
483
|
registerChild,
|
|
466
484
|
updateChildState,
|
|
485
|
+
updateChildAnimatedStyle,
|
|
467
486
|
unregisterChild
|
|
468
487
|
};
|
|
469
488
|
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* A scroll offset edge defined as a string (e.g. `"start"`, `"end"`, `"center"`)
|
|
4
|
+
* or a number (0–1 progress). Each offset entry is a pair of `[target, container]`.
|
|
5
|
+
*/
|
|
6
|
+
type ScrollOffset = Array<[number | string, number | string]> | string[];
|
|
7
|
+
/**
|
|
8
|
+
* An element reference — either an element directly or a getter function
|
|
9
|
+
* that returns one (useful with Svelte's `bind:this` where the element
|
|
10
|
+
* isn't available until after mount).
|
|
11
|
+
*/
|
|
12
|
+
type ElementOrGetter = HTMLElement | (() => HTMLElement | undefined);
|
|
13
|
+
/**
|
|
14
|
+
* Options accepted by `useScroll`.
|
|
15
|
+
*/
|
|
16
|
+
type UseScrollOptions = {
|
|
17
|
+
/** Scrollable container to track. Defaults to the page. Accepts an element or a getter function. */
|
|
18
|
+
container?: ElementOrGetter;
|
|
19
|
+
/** Target element to track position of within the container. Accepts an element or a getter function. */
|
|
20
|
+
target?: ElementOrGetter;
|
|
21
|
+
/** Scroll offset configuration for element position tracking. */
|
|
22
|
+
offset?: ScrollOffset;
|
|
23
|
+
/** Which axis to use for the single-axis `progress` value supplied to `scroll()`. */
|
|
24
|
+
axis?: 'x' | 'y';
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Return type of `useScroll` — four readable Svelte stores representing
|
|
28
|
+
* scroll position and normalised progress for both axes.
|
|
29
|
+
*/
|
|
30
|
+
type UseScrollReturn = {
|
|
31
|
+
scrollX: Readable<number>;
|
|
32
|
+
scrollY: Readable<number>;
|
|
33
|
+
scrollXProgress: Readable<number>;
|
|
34
|
+
scrollYProgress: Readable<number>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Creates scroll-linked Svelte stores for building scroll-driven animations
|
|
38
|
+
* such as progress indicators and parallax effects.
|
|
39
|
+
*
|
|
40
|
+
* When the returned stores are used with `opacity` / `transform` CSS properties
|
|
41
|
+
* the animations can be hardware accelerated by the browser.
|
|
42
|
+
*
|
|
43
|
+
* SSR-safe: returns static `readable(0)` stores on the server.
|
|
44
|
+
*
|
|
45
|
+
* `container` and `target` accept either an `HTMLElement` directly or a
|
|
46
|
+
* getter function `() => HTMLElement | undefined`. This is useful with
|
|
47
|
+
* Svelte's `bind:this` where the element isn't available until after mount.
|
|
48
|
+
* When a getter is provided, element resolution is deferred until the
|
|
49
|
+
* first subscriber arrives, and if the element isn't available yet the
|
|
50
|
+
* stores poll on each animation frame until it is.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```svelte
|
|
54
|
+
* <script>
|
|
55
|
+
* import { useScroll, useSpring } from '@humanspeak/svelte-motion'
|
|
56
|
+
*
|
|
57
|
+
* const { scrollYProgress } = useScroll()
|
|
58
|
+
* const scaleX = useSpring(scrollYProgress)
|
|
59
|
+
* </script>
|
|
60
|
+
*
|
|
61
|
+
* <div style="transform: scaleX({$scaleX}); transform-origin: left;" />
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @param options Optional scroll tracking configuration.
|
|
65
|
+
* @returns An object with `scrollX`, `scrollY`, `scrollXProgress`, and `scrollYProgress` stores.
|
|
66
|
+
*/
|
|
67
|
+
export declare const useScroll: (options?: UseScrollOptions) => UseScrollReturn;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { scroll } from 'motion';
|
|
2
|
+
import { readable, writable } from 'svelte/store';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves an element-or-getter to an HTMLElement (or undefined).
|
|
5
|
+
*/
|
|
6
|
+
const resolveElement = (ref) => {
|
|
7
|
+
if (!ref)
|
|
8
|
+
return undefined;
|
|
9
|
+
return typeof ref === 'function' ? ref() : ref;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Creates scroll-linked Svelte stores for building scroll-driven animations
|
|
13
|
+
* such as progress indicators and parallax effects.
|
|
14
|
+
*
|
|
15
|
+
* When the returned stores are used with `opacity` / `transform` CSS properties
|
|
16
|
+
* the animations can be hardware accelerated by the browser.
|
|
17
|
+
*
|
|
18
|
+
* SSR-safe: returns static `readable(0)` stores on the server.
|
|
19
|
+
*
|
|
20
|
+
* `container` and `target` accept either an `HTMLElement` directly or a
|
|
21
|
+
* getter function `() => HTMLElement | undefined`. This is useful with
|
|
22
|
+
* Svelte's `bind:this` where the element isn't available until after mount.
|
|
23
|
+
* When a getter is provided, element resolution is deferred until the
|
|
24
|
+
* first subscriber arrives, and if the element isn't available yet the
|
|
25
|
+
* stores poll on each animation frame until it is.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```svelte
|
|
29
|
+
* <script>
|
|
30
|
+
* import { useScroll, useSpring } from '@humanspeak/svelte-motion'
|
|
31
|
+
*
|
|
32
|
+
* const { scrollYProgress } = useScroll()
|
|
33
|
+
* const scaleX = useSpring(scrollYProgress)
|
|
34
|
+
* </script>
|
|
35
|
+
*
|
|
36
|
+
* <div style="transform: scaleX({$scaleX}); transform-origin: left;" />
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @param options Optional scroll tracking configuration.
|
|
40
|
+
* @returns An object with `scrollX`, `scrollY`, `scrollXProgress`, and `scrollYProgress` stores.
|
|
41
|
+
*/
|
|
42
|
+
export const useScroll = (options) => {
|
|
43
|
+
if (typeof window === 'undefined') {
|
|
44
|
+
return {
|
|
45
|
+
scrollX: readable(0),
|
|
46
|
+
scrollY: readable(0),
|
|
47
|
+
scrollXProgress: readable(0),
|
|
48
|
+
scrollYProgress: readable(0)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const stores = {
|
|
52
|
+
scrollX: writable(0),
|
|
53
|
+
scrollY: writable(0),
|
|
54
|
+
scrollXProgress: writable(0),
|
|
55
|
+
scrollYProgress: writable(0)
|
|
56
|
+
};
|
|
57
|
+
let cleanup;
|
|
58
|
+
let pollRaf = 0;
|
|
59
|
+
let subscriberCount = 0;
|
|
60
|
+
const attach = () => {
|
|
61
|
+
if (cleanup)
|
|
62
|
+
return;
|
|
63
|
+
// Resolve elements — they may not be available yet when using getters
|
|
64
|
+
const container = resolveElement(options?.container);
|
|
65
|
+
const target = resolveElement(options?.target);
|
|
66
|
+
// If a getter was provided but returned undefined, the element isn't
|
|
67
|
+
// mounted yet. Poll on the next frame until it appears.
|
|
68
|
+
const needsContainer = options?.container && !container;
|
|
69
|
+
const needsTarget = options?.target && !target;
|
|
70
|
+
if (needsContainer || needsTarget) {
|
|
71
|
+
if (!pollRaf) {
|
|
72
|
+
pollRaf = requestAnimationFrame(() => {
|
|
73
|
+
pollRaf = 0;
|
|
74
|
+
attach();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
cleanup = scroll((_progress, info) => {
|
|
80
|
+
stores.scrollX.set(info.x.current);
|
|
81
|
+
stores.scrollY.set(info.y.current);
|
|
82
|
+
stores.scrollXProgress.set(info.x.progress);
|
|
83
|
+
stores.scrollYProgress.set(info.y.progress);
|
|
84
|
+
}, {
|
|
85
|
+
container,
|
|
86
|
+
target,
|
|
87
|
+
offset: options?.offset,
|
|
88
|
+
axis: options?.axis
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
const detach = () => {
|
|
92
|
+
if (subscriberCount <= 0) {
|
|
93
|
+
if (pollRaf) {
|
|
94
|
+
cancelAnimationFrame(pollRaf);
|
|
95
|
+
pollRaf = 0;
|
|
96
|
+
}
|
|
97
|
+
if (cleanup) {
|
|
98
|
+
cleanup();
|
|
99
|
+
cleanup = undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const make = (key) => readable(0, (set) => {
|
|
104
|
+
subscriberCount++;
|
|
105
|
+
const unsub = stores[key].subscribe(set);
|
|
106
|
+
attach();
|
|
107
|
+
return () => {
|
|
108
|
+
unsub();
|
|
109
|
+
subscriberCount--;
|
|
110
|
+
detach();
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
scrollX: make('scrollX'),
|
|
115
|
+
scrollY: make('scrollY'),
|
|
116
|
+
scrollXProgress: make('scrollXProgress'),
|
|
117
|
+
scrollYProgress: make('scrollYProgress')
|
|
118
|
+
};
|
|
119
|
+
};
|
package/dist/utils/testing.d.ts
CHANGED
package/dist/utils/testing.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A
|
|
3
|
+
"version": "0.1.28",
|
|
4
|
+
"description": "A Framer Motion-compatible animation library for Svelte 5 that provides smooth, hardware-accelerated animations. Features include spring physics, custom easing, and fluid transitions. Built on top of the motion library, it offers a simple API for creating complex animations with minimal code. Perfect for interactive UIs, micro-interactions, and engaging user experiences.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
7
7
|
"animation",
|
|
@@ -12,7 +12,18 @@
|
|
|
12
12
|
"svelte5",
|
|
13
13
|
"hardware-accelerated",
|
|
14
14
|
"micro-interactions",
|
|
15
|
-
"performance"
|
|
15
|
+
"performance",
|
|
16
|
+
"framer-motion",
|
|
17
|
+
"framer-motion-svelte",
|
|
18
|
+
"gestures",
|
|
19
|
+
"exit-animation",
|
|
20
|
+
"animate-presence",
|
|
21
|
+
"layout-animation",
|
|
22
|
+
"drag",
|
|
23
|
+
"variants",
|
|
24
|
+
"sveltekit",
|
|
25
|
+
"typescript",
|
|
26
|
+
"svelte-animation"
|
|
16
27
|
],
|
|
17
28
|
"homepage": "https://motion.svelte.page",
|
|
18
29
|
"bugs": {
|
|
@@ -62,20 +73,19 @@
|
|
|
62
73
|
"@eslint/js": "^10.0.1",
|
|
63
74
|
"@playwright/test": "^1.58.2",
|
|
64
75
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
65
|
-
"@sveltejs/kit": "^2.53.
|
|
76
|
+
"@sveltejs/kit": "^2.53.3",
|
|
66
77
|
"@sveltejs/package": "^2.5.7",
|
|
67
78
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
68
79
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
|
69
80
|
"@tailwindcss/container-queries": "^0.1.1",
|
|
70
81
|
"@tailwindcss/forms": "^0.5.11",
|
|
71
|
-
"@tailwindcss/postcss": "^4.2.
|
|
82
|
+
"@tailwindcss/postcss": "^4.2.1",
|
|
72
83
|
"@tailwindcss/typography": "^0.5.19",
|
|
73
84
|
"@testing-library/jest-dom": "^6.9.1",
|
|
74
85
|
"@testing-library/svelte": "^5.3.1",
|
|
75
|
-
"@types/node": "^25.3.
|
|
86
|
+
"@types/node": "^25.3.2",
|
|
76
87
|
"@vitest/coverage-v8": "^4.0.18",
|
|
77
|
-
"
|
|
78
|
-
"eslint": "^10.0.1",
|
|
88
|
+
"eslint": "^10.0.2",
|
|
79
89
|
"eslint-config-prettier": "10.1.8",
|
|
80
90
|
"eslint-plugin-import": "2.32.0",
|
|
81
91
|
"eslint-plugin-svelte": "3.15.0",
|
|
@@ -86,6 +96,7 @@
|
|
|
86
96
|
"html-void-elements": "^3.0.0",
|
|
87
97
|
"husky": "^9.1.7",
|
|
88
98
|
"jsdom": "^28.1.0",
|
|
99
|
+
"mprocs": "^0.8.3",
|
|
89
100
|
"prettier": "^3.8.1",
|
|
90
101
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
91
102
|
"prettier-plugin-sort-json": "^4.2.0",
|
|
@@ -93,16 +104,16 @@
|
|
|
93
104
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
94
105
|
"publint": "^0.3.17",
|
|
95
106
|
"runed": "0.37.1",
|
|
96
|
-
"svelte": "^5.53.
|
|
97
|
-
"svelte-check": "^4.4.
|
|
107
|
+
"svelte": "^5.53.5",
|
|
108
|
+
"svelte-check": "^4.4.4",
|
|
98
109
|
"svg-tags": "^1.0.0",
|
|
99
110
|
"tailwind-merge": "^3.5.0",
|
|
100
111
|
"tailwind-variants": "^3.2.2",
|
|
101
|
-
"tailwindcss": "^4.2.
|
|
112
|
+
"tailwindcss": "^4.2.1",
|
|
102
113
|
"tailwindcss-animate": "^1.0.7",
|
|
103
114
|
"tsx": "^4.21.0",
|
|
104
115
|
"typescript": "^5.9.3",
|
|
105
|
-
"typescript-eslint": "^8.56.
|
|
116
|
+
"typescript-eslint": "^8.56.1",
|
|
106
117
|
"vite": "^7.3.1",
|
|
107
118
|
"vite-tsconfig-paths": "^6.1.1",
|
|
108
119
|
"vitest": "^4.0.18"
|
|
@@ -135,7 +146,7 @@
|
|
|
135
146
|
"cs:publish": "pnpm run build && changeset publish",
|
|
136
147
|
"cs:version": "changeset version && pnpm i --lockfile-only",
|
|
137
148
|
"dev": "vite dev",
|
|
138
|
-
"dev:all": "
|
|
149
|
+
"dev:all": "mprocs",
|
|
139
150
|
"dev:pkg": "svelte-kit sync && svelte-package --watch",
|
|
140
151
|
"format": "prettier --write .",
|
|
141
152
|
"generate": "tsx scripts/generate-html.ts",
|