@humanspeak/svelte-motion 0.1.23 → 0.1.25
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 +3 -1
- package/dist/html/_MotionContainer.svelte +54 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils/layoutId.d.ts +24 -0
- package/dist/utils/layoutId.js +23 -0
- package/dist/utils/motionTemplate.d.ts +21 -0
- package/dist/utils/motionTemplate.js +33 -0
- package/dist/utils/velocity.d.ts +15 -0
- package/dist/utils/velocity.js +62 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ Goal: Framer Motion API parity for Svelte where common React examples can be tra
|
|
|
44
44
|
| Drag (`drag`, constraints, momentum, controls, callbacks) | Supported |
|
|
45
45
|
| `AnimatePresence` (`initial`, `mode`, `onExitComplete`) | Supported |
|
|
46
46
|
| Layout (`layout`, `layout="position"`) | Supported (single-element FLIP) |
|
|
47
|
-
| Shared layout (`layoutId`) |
|
|
47
|
+
| Shared layout (`layoutId`) | Supported |
|
|
48
48
|
| Pan gesture API (`whilePan`, `onPan*`) | Not yet supported |
|
|
49
49
|
| `MotionConfig` parity beyond `transition` | Partial |
|
|
50
50
|
| `reducedMotion`, `features`, `transformPagePoint` | Not yet supported |
|
|
@@ -231,9 +231,11 @@ Single-element FLIP layout animation:
|
|
|
231
231
|
## Utilities
|
|
232
232
|
|
|
233
233
|
- `useAnimationFrame`
|
|
234
|
+
- `useMotionTemplate`
|
|
234
235
|
- `useSpring`
|
|
235
236
|
- `useTime`
|
|
236
237
|
- `useTransform`
|
|
238
|
+
- `useVelocity`
|
|
237
239
|
- `styleString`
|
|
238
240
|
- `stringifyStyleObject` (deprecated)
|
|
239
241
|
- `createDragControls`
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
isSVGTag,
|
|
58
58
|
SVG_NAMESPACE
|
|
59
59
|
} from '../utils/svg'
|
|
60
|
+
import { getLayoutIdRegistry } from '../utils/layoutId'
|
|
60
61
|
|
|
61
62
|
type Props = MotionProps & {
|
|
62
63
|
children?: Snippet
|
|
@@ -107,6 +108,7 @@
|
|
|
107
108
|
dragListener: dragListenerProp,
|
|
108
109
|
dragControls: dragControlsProp,
|
|
109
110
|
layout: layoutProp,
|
|
111
|
+
layoutId: layoutIdProp,
|
|
110
112
|
ref: element = $bindable(null),
|
|
111
113
|
...rest
|
|
112
114
|
}: Props = $props()
|
|
@@ -117,6 +119,9 @@
|
|
|
117
119
|
// Get presence context to check if we're inside AnimatePresence
|
|
118
120
|
const context = getAnimatePresenceContext()
|
|
119
121
|
|
|
122
|
+
// Get layoutId registry (provided by AnimatePresence or a parent LayoutGroup)
|
|
123
|
+
const layoutIdRegistry = getLayoutIdRegistry()
|
|
124
|
+
|
|
120
125
|
// Get current presence depth (0 = direct child of AnimatePresence, undefined = not in AnimatePresence)
|
|
121
126
|
const presenceDepth = getPresenceDepth()
|
|
122
127
|
|
|
@@ -163,6 +168,36 @@
|
|
|
163
168
|
})
|
|
164
169
|
}
|
|
165
170
|
|
|
171
|
+
// Keep a live snapshot of the layoutId element's rect so the next element can FLIP from it.
|
|
172
|
+
// We store the last-known-good rect and push it to the registry on cleanup,
|
|
173
|
+
// because onDestroy fires after the element is removed from DOM (rect would be zeros).
|
|
174
|
+
let layoutIdLastRect: DOMRect | null = null
|
|
175
|
+
$effect(() => {
|
|
176
|
+
if (!(element && layoutIdProp && layoutIdRegistry)) return
|
|
177
|
+
|
|
178
|
+
// Capture rect on every frame while mounted
|
|
179
|
+
let rafId: number
|
|
180
|
+
const captureRect = () => {
|
|
181
|
+
if (element) {
|
|
182
|
+
layoutIdLastRect = element.getBoundingClientRect()
|
|
183
|
+
}
|
|
184
|
+
rafId = requestAnimationFrame(captureRect)
|
|
185
|
+
}
|
|
186
|
+
rafId = requestAnimationFrame(captureRect)
|
|
187
|
+
|
|
188
|
+
// On cleanup (before DOM removal), push last-known rect to registry
|
|
189
|
+
return () => {
|
|
190
|
+
cancelAnimationFrame(rafId)
|
|
191
|
+
if (layoutIdLastRect && layoutIdProp) {
|
|
192
|
+
layoutIdRegistry.snapshot(
|
|
193
|
+
layoutIdProp,
|
|
194
|
+
layoutIdLastRect,
|
|
195
|
+
(mergedTransition ?? {}) as AnimationOptions
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
|
|
166
201
|
// Reactively update registration when element/exit/transition props change
|
|
167
202
|
$effect(() => {
|
|
168
203
|
if (element && context && resolvedExit) {
|
|
@@ -611,6 +646,25 @@
|
|
|
611
646
|
}
|
|
612
647
|
})
|
|
613
648
|
|
|
649
|
+
// Shared layout animation via layoutId.
|
|
650
|
+
// On mount, consume the previous snapshot and FLIP from its position.
|
|
651
|
+
$effect(() => {
|
|
652
|
+
if (!(element && layoutIdProp && layoutIdRegistry && isLoaded === 'ready')) return
|
|
653
|
+
|
|
654
|
+
const prev = layoutIdRegistry.consume(layoutIdProp)
|
|
655
|
+
if (!prev) return // First appearance, no animation needed
|
|
656
|
+
|
|
657
|
+
const next = measureRect(element)
|
|
658
|
+
const transforms = computeFlipTransforms(prev.rect, next, true)
|
|
659
|
+
|
|
660
|
+
setCompositorHints(element, true)
|
|
661
|
+
runFlipAnimation(
|
|
662
|
+
element,
|
|
663
|
+
transforms,
|
|
664
|
+
(prev.transition ?? mergedTransition ?? {}) as AnimationOptions
|
|
665
|
+
)
|
|
666
|
+
})
|
|
667
|
+
|
|
614
668
|
// whileTap handling via motion-dom's press()
|
|
615
669
|
$effect(() => {
|
|
616
670
|
if (!(element && isLoaded === 'ready' && isNotEmpty(whileTapProp))) return
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,9 @@ export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } f
|
|
|
8
8
|
export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, Variants } from './types';
|
|
9
9
|
export { useAnimationFrame } from './utils/animationFrame';
|
|
10
10
|
export { createDragControls } from './utils/dragControls';
|
|
11
|
+
export { useMotionTemplate } from './utils/motionTemplate';
|
|
11
12
|
export { useSpring } from './utils/spring';
|
|
13
|
+
export { useVelocity } from './utils/velocity';
|
|
12
14
|
/**
|
|
13
15
|
* @deprecated Use `styleString` instead for reactive styles with automatic unit handling.
|
|
14
16
|
*/
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,9 @@ export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cub
|
|
|
11
11
|
export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
|
|
12
12
|
export { useAnimationFrame } from './utils/animationFrame';
|
|
13
13
|
export { createDragControls } from './utils/dragControls';
|
|
14
|
+
export { useMotionTemplate } from './utils/motionTemplate';
|
|
14
15
|
export { useSpring } from './utils/spring';
|
|
16
|
+
export { useVelocity } from './utils/velocity';
|
|
15
17
|
/**
|
|
16
18
|
* @deprecated Use `styleString` instead for reactive styles with automatic unit handling.
|
|
17
19
|
*/
|
package/dist/types.d.ts
CHANGED
|
@@ -278,6 +278,8 @@ export type MotionProps = {
|
|
|
278
278
|
class?: string;
|
|
279
279
|
/** Enable FLIP layout animations; "position" limits to translation only */
|
|
280
280
|
layout?: boolean | 'position';
|
|
281
|
+
/** Shared layout animation identifier. Elements with matching layoutId animate between positions. */
|
|
282
|
+
layoutId?: string;
|
|
281
283
|
/** Ref to the element */
|
|
282
284
|
ref?: HTMLElement | null;
|
|
283
285
|
/** Enable drag gestures. true for both axes, or lock to 'x'/'y'. */
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AnimationOptions } from 'motion';
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot stored for a layoutId when an element unmounts.
|
|
4
|
+
*/
|
|
5
|
+
type LayoutIdEntry = {
|
|
6
|
+
rect: DOMRect;
|
|
7
|
+
transition?: AnimationOptions;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Registry that stores last-known rect + transition for each layoutId.
|
|
11
|
+
*
|
|
12
|
+
* - `snapshot(id, rect, transition)` — called when an element with a layoutId unmounts.
|
|
13
|
+
* - `consume(id)` — called by a newly mounted element. Returns and **deletes** the entry (one-shot).
|
|
14
|
+
*/
|
|
15
|
+
export type LayoutIdRegistry = {
|
|
16
|
+
snapshot(id: string, rect: DOMRect, transition?: AnimationOptions): void;
|
|
17
|
+
consume(id: string): LayoutIdEntry | undefined;
|
|
18
|
+
};
|
|
19
|
+
export declare const layoutIdRegistry: LayoutIdRegistry;
|
|
20
|
+
/**
|
|
21
|
+
* Get the global layoutId registry.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getLayoutIdRegistry(): LayoutIdRegistry;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level singleton registry shared across the entire component tree.
|
|
3
|
+
* This matches React Framer Motion's behavior where layoutId is shared globally
|
|
4
|
+
* (or within a LayoutGroup, which we can add later).
|
|
5
|
+
*/
|
|
6
|
+
const entries = new Map();
|
|
7
|
+
export const layoutIdRegistry = {
|
|
8
|
+
snapshot(id, rect, transition) {
|
|
9
|
+
entries.set(id, { rect, transition });
|
|
10
|
+
},
|
|
11
|
+
consume(id) {
|
|
12
|
+
const entry = entries.get(id);
|
|
13
|
+
if (entry)
|
|
14
|
+
entries.delete(id);
|
|
15
|
+
return entry;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Get the global layoutId registry.
|
|
20
|
+
*/
|
|
21
|
+
export function getLayoutIdRegistry() {
|
|
22
|
+
return layoutIdRegistry;
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Tagged template literal that creates a derived store from interpolated
|
|
4
|
+
* readable stores. When any input store changes, the resulting store
|
|
5
|
+
* recomputes the template string.
|
|
6
|
+
*
|
|
7
|
+
* SSR-safe: `derived` from svelte/store works on the server.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const blur = useSpring(0)
|
|
12
|
+
* const filter = useMotionTemplate`blur(${blur}px)`
|
|
13
|
+
* // $filter → "blur(0px)"
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @param strings Static template string parts.
|
|
17
|
+
* @param values Readable stores to interpolate.
|
|
18
|
+
* @returns A readable store of the composed string.
|
|
19
|
+
* @see https://motion.dev/docs/react-use-motion-template
|
|
20
|
+
*/
|
|
21
|
+
export declare const useMotionTemplate: (strings: TemplateStringsArray, ...values: Readable<number | string>[]) => Readable<string>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { derived } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Tagged template literal that creates a derived store from interpolated
|
|
4
|
+
* readable stores. When any input store changes, the resulting store
|
|
5
|
+
* recomputes the template string.
|
|
6
|
+
*
|
|
7
|
+
* SSR-safe: `derived` from svelte/store works on the server.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const blur = useSpring(0)
|
|
12
|
+
* const filter = useMotionTemplate`blur(${blur}px)`
|
|
13
|
+
* // $filter → "blur(0px)"
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @param strings Static template string parts.
|
|
17
|
+
* @param values Readable stores to interpolate.
|
|
18
|
+
* @returns A readable store of the composed string.
|
|
19
|
+
* @see https://motion.dev/docs/react-use-motion-template
|
|
20
|
+
*/
|
|
21
|
+
export const useMotionTemplate = (strings, ...values) => {
|
|
22
|
+
if (values.length === 0)
|
|
23
|
+
return derived([], () => strings[0] ?? '');
|
|
24
|
+
return derived(values, (current) => {
|
|
25
|
+
let result = '';
|
|
26
|
+
for (let i = 0; i < strings.length; i++) {
|
|
27
|
+
result += strings[i] ?? '';
|
|
28
|
+
if (i < current.length)
|
|
29
|
+
result += String(current[i]);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
});
|
|
33
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
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>;
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"description": "A lightweight 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",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"svelte": "^5.0.0"
|
|
112
112
|
},
|
|
113
113
|
"volta": {
|
|
114
|
-
"node": "
|
|
114
|
+
"node": "22.16.0"
|
|
115
115
|
},
|
|
116
116
|
"publishConfig": {
|
|
117
117
|
"access": "public"
|