@humanspeak/svelte-motion 0.5.4 → 0.6.1
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/dist/components/motionDomProjection.context.d.ts +13 -0
- package/dist/components/motionDomProjection.context.js +18 -0
- package/dist/html/_MotionContainer.svelte +831 -81
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/types.d.ts +87 -3
- package/dist/utils/animationControls.svelte.d.ts +63 -0
- package/dist/utils/animationControls.svelte.js +111 -0
- package/dist/utils/layout.d.ts +9 -6
- package/dist/utils/layout.js +148 -14
- package/dist/utils/motionDomProjection.d.ts +155 -0
- package/dist/utils/motionDomProjection.js +279 -0
- package/dist/utils/optimizedAppear.d.ts +141 -0
- package/dist/utils/optimizedAppear.js +311 -0
- package/dist/utils/presence.d.ts +3 -2
- package/dist/utils/presence.js +49 -12
- package/dist/utils/projection.d.ts +3 -3
- package/dist/utils/projection.js +1 -1
- package/dist/utils/svg.d.ts +4 -4
- package/dist/utils/svg.js +44 -25
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -12,9 +12,10 @@ export { motion } from './motion';
|
|
|
12
12
|
export { animate, delay, hover, inView, press, resize, scroll, stagger, transform } from 'motion';
|
|
13
13
|
export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cubicBezier, easeIn, easeInOut, easeOut } from 'motion';
|
|
14
14
|
export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
|
|
15
|
-
export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionOnProjectionUpdate, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, ProjectionUpdatePayload, ReducedMotionConfig, Variants } from './types';
|
|
15
|
+
export type { AnimationControls, AnimationControlsDefinition, AnimationControlsSubscriber, DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionOnProjectionUpdate, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, ProjectionUpdatePayload, ReducedMotionConfig, Variants } from './types';
|
|
16
16
|
export { useAnimate } from './utils/animate.svelte';
|
|
17
17
|
export type { AnimationScope } from './utils/animate.svelte';
|
|
18
|
+
export { animationControls, isAnimationControls, useAnimation, useAnimationControls } from './utils/animationControls.svelte';
|
|
18
19
|
export { useAnimationFrame } from './utils/animationFrame';
|
|
19
20
|
export type { AugmentedMotionValue } from './utils/augmentMotionValue.svelte';
|
|
20
21
|
export { useCycle } from './utils/cycle.svelte';
|
|
@@ -29,6 +30,7 @@ export type { MotionTemplateInput } from './utils/motionTemplate.svelte';
|
|
|
29
30
|
export { useMotionValue } from './utils/motionValue.svelte';
|
|
30
31
|
export type { MotionValue, RawMotionValue } from './utils/motionValue.svelte';
|
|
31
32
|
export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
33
|
+
export { optimizedAppearDataAttribute, startOptimizedAppearAnimation } from './utils/optimizedAppear';
|
|
32
34
|
export { useReducedMotion } from './utils/reducedMotion.svelte';
|
|
33
35
|
export type { ReducedMotionState } from './utils/reducedMotion.svelte';
|
|
34
36
|
export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cub
|
|
|
15
15
|
// Re-export utility functions
|
|
16
16
|
export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
|
|
17
17
|
export { useAnimate } from './utils/animate.svelte';
|
|
18
|
+
export { animationControls, isAnimationControls, useAnimation, useAnimationControls } from './utils/animationControls.svelte';
|
|
18
19
|
export { useAnimationFrame } from './utils/animationFrame';
|
|
19
20
|
export { useCycle } from './utils/cycle.svelte';
|
|
20
21
|
export { createDragControls } from './utils/dragControls';
|
|
@@ -23,6 +24,7 @@ export { useInView } from './utils/inView.svelte';
|
|
|
23
24
|
export { useMotionTemplate } from './utils/motionTemplate.svelte';
|
|
24
25
|
export { useMotionValue } from './utils/motionValue.svelte';
|
|
25
26
|
export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
27
|
+
export { optimizedAppearDataAttribute, startOptimizedAppearAnimation } from './utils/optimizedAppear';
|
|
26
28
|
export { useReducedMotion } from './utils/reducedMotion.svelte';
|
|
27
29
|
export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
|
|
28
30
|
export { useScroll } from './utils/scroll.svelte';
|
package/dist/types.d.ts
CHANGED
|
@@ -74,7 +74,91 @@ export type MotionInitial = DOMKeyframesDefinition | string | string[] | false |
|
|
|
74
74
|
* <motion.div variants={myVariants} animate="visible" />
|
|
75
75
|
* ```
|
|
76
76
|
*/
|
|
77
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Definition accepted by legacy animation controls.
|
|
79
|
+
*
|
|
80
|
+
* Mirrors Motion's `AnimationDefinition`: a keyframes object, a variant
|
|
81
|
+
* label, an ordered list of variant labels, or a resolver function that
|
|
82
|
+
* receives `custom` data.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* controls.start('visible')
|
|
87
|
+
* controls.start(['visible', 'active'])
|
|
88
|
+
* controls.start({ opacity: 1, x: 0 })
|
|
89
|
+
* controls.start((custom) => ({ x: custom * 100 }))
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export type AnimationControlsDefinition = DOMKeyframesDefinition | string | string[] | ((custom: unknown) => DOMKeyframesDefinition | string);
|
|
93
|
+
/**
|
|
94
|
+
* Internal subscriber shape used by {@link AnimationControls}.
|
|
95
|
+
*
|
|
96
|
+
* Motion's upstream controls subscribe VisualElements. Svelte Motion
|
|
97
|
+
* subscribes a lightweight adapter from each `motion.*` component.
|
|
98
|
+
*/
|
|
99
|
+
export type AnimationControlsSubscriber = {
|
|
100
|
+
/** Start an animation on the subscribed component. */
|
|
101
|
+
start: (definition: AnimationControlsDefinition, transitionOverride?: AnimationOptions) => Promise<unknown>;
|
|
102
|
+
/** Synchronously set final values on the subscribed component. */
|
|
103
|
+
set: (definition: AnimationControlsDefinition) => void;
|
|
104
|
+
/** Stop currently running animations on the subscribed component. */
|
|
105
|
+
stop: () => void;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Legacy imperative controls returned by {@link useAnimationControls}.
|
|
109
|
+
*
|
|
110
|
+
* Pass the object to `animate={controls}` on one or more `motion.*`
|
|
111
|
+
* components, then call `controls.start(...)`, `controls.set(...)`, or
|
|
112
|
+
* `controls.stop()` from events or effects.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```svelte
|
|
116
|
+
* <script lang="ts">
|
|
117
|
+
* import { motion, useAnimationControls } from '@humanspeak/svelte-motion'
|
|
118
|
+
*
|
|
119
|
+
* const controls = useAnimationControls()
|
|
120
|
+
* </script>
|
|
121
|
+
*
|
|
122
|
+
* <button onclick={() => controls.start('open')}>Open</button>
|
|
123
|
+
* <motion.div animate={controls} variants={{ open: { opacity: 1 } }} />
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export type AnimationControls = {
|
|
127
|
+
/**
|
|
128
|
+
* Subscribe a motion component adapter to these controls.
|
|
129
|
+
*
|
|
130
|
+
* @param subscriber Component adapter to animate.
|
|
131
|
+
* @returns Unsubscribe callback.
|
|
132
|
+
*/
|
|
133
|
+
subscribe: (subscriber: AnimationControlsSubscriber) => () => void;
|
|
134
|
+
/**
|
|
135
|
+
* Start an animation on every subscribed component.
|
|
136
|
+
*
|
|
137
|
+
* @param definition Target keyframes, variant label(s), or resolver.
|
|
138
|
+
* @param transitionOverride Optional transition that overrides the
|
|
139
|
+
* component/default transition for this run.
|
|
140
|
+
* @returns Promise resolving when all subscribed animations complete.
|
|
141
|
+
*/
|
|
142
|
+
start: (definition: AnimationControlsDefinition, transitionOverride?: AnimationOptions) => Promise<unknown[]>;
|
|
143
|
+
/**
|
|
144
|
+
* Synchronously set every subscribed component to the target's final
|
|
145
|
+
* values.
|
|
146
|
+
*
|
|
147
|
+
* @param definition Target keyframes, variant label(s), or resolver.
|
|
148
|
+
*/
|
|
149
|
+
set: (definition: AnimationControlsDefinition) => void;
|
|
150
|
+
/** Stop animations on every subscribed component. */
|
|
151
|
+
stop: () => void;
|
|
152
|
+
/**
|
|
153
|
+
* Mark controls as mounted and return cleanup.
|
|
154
|
+
*
|
|
155
|
+
* Called automatically by `useAnimationControls()`.
|
|
156
|
+
*
|
|
157
|
+
* @returns Cleanup that marks controls unmounted and stops subscribers.
|
|
158
|
+
*/
|
|
159
|
+
mount: () => () => void;
|
|
160
|
+
};
|
|
161
|
+
export type MotionAnimate = DOMKeyframesDefinition | string | string[] | AnimationControls | undefined;
|
|
78
162
|
/**
|
|
79
163
|
* Exit animation properties for a motion component when unmounted.
|
|
80
164
|
*
|
|
@@ -431,8 +515,8 @@ export type MotionProps = {
|
|
|
431
515
|
style?: string;
|
|
432
516
|
/** CSS classes */
|
|
433
517
|
class?: string;
|
|
434
|
-
/** Enable FLIP layout animations;
|
|
435
|
-
layout?: boolean | 'position';
|
|
518
|
+
/** Enable FLIP layout animations; string values select the upstream projection animation type. */
|
|
519
|
+
layout?: boolean | 'position' | 'size' | 'preserve-aspect';
|
|
436
520
|
/**
|
|
437
521
|
* Fires after each `layout`-driven change with the FLIP delta from
|
|
438
522
|
* the element's internal projection node. Mirrors framer-motion's
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AnimationControls } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Returns true when a value looks like Motion's legacy animation controls.
|
|
4
|
+
*
|
|
5
|
+
* Upstream `motion-dom` treats any non-null object with a `start`
|
|
6
|
+
* function as animation controls. Matching that narrow check keeps
|
|
7
|
+
* `animate={controls}` detection compatible with Motion's public shape.
|
|
8
|
+
*
|
|
9
|
+
* @param value Value passed to `animate`.
|
|
10
|
+
* @returns Whether `value` is an animation controls object.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const controls = useAnimationControls()
|
|
15
|
+
* isAnimationControls(controls) // true
|
|
16
|
+
* isAnimationControls({ opacity: 1 }) // false
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare const isAnimationControls: (value: unknown) => value is AnimationControls;
|
|
20
|
+
/**
|
|
21
|
+
* Create legacy animation controls.
|
|
22
|
+
*
|
|
23
|
+
* This mirrors upstream Motion's `animationControls()`: controls collect
|
|
24
|
+
* subscribed motion components, guard `start`/`set` until mounted, fan out
|
|
25
|
+
* starts to every subscriber, and stop all subscribers on unmount.
|
|
26
|
+
*
|
|
27
|
+
* @returns Animation controls with `subscribe`, `start`, `set`, `stop`,
|
|
28
|
+
* and `mount`.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const controls = animationControls()
|
|
33
|
+
* const cleanup = controls.mount()
|
|
34
|
+
* await controls.start({ opacity: 1 })
|
|
35
|
+
* cleanup()
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const animationControls: () => AnimationControls;
|
|
39
|
+
/**
|
|
40
|
+
* Create imperative controls for one or more `motion.*` components.
|
|
41
|
+
*
|
|
42
|
+
* Pass the returned object to `animate={controls}`. Once mounted, call
|
|
43
|
+
* `controls.start(definition)`, `controls.set(definition)`, or
|
|
44
|
+
* `controls.stop()` to coordinate every subscribed component.
|
|
45
|
+
*
|
|
46
|
+
* @returns Mounted animation controls.
|
|
47
|
+
* @see https://motion.dev/docs/react-use-animation-controls
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```svelte
|
|
51
|
+
* <script lang="ts">
|
|
52
|
+
* import { motion, useAnimationControls } from '@humanspeak/svelte-motion'
|
|
53
|
+
*
|
|
54
|
+
* const controls = useAnimationControls()
|
|
55
|
+
* </script>
|
|
56
|
+
*
|
|
57
|
+
* <button onclick={() => controls.start({ scale: 1.2 })}>Pop</button>
|
|
58
|
+
* <motion.div animate={controls} />
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare const useAnimationControls: () => AnimationControls;
|
|
62
|
+
/** Alias matching Motion's legacy `useAnimation` export. */
|
|
63
|
+
export declare const useAnimation: () => AnimationControls;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
2
|
+
const mountedError = 'controls.start() should only be called after a component has mounted. Consider calling within a $effect.';
|
|
3
|
+
const setMountedError = 'controls.set() should only be called after a component has mounted. Consider calling within a $effect.';
|
|
4
|
+
/**
|
|
5
|
+
* Returns true when a value looks like Motion's legacy animation controls.
|
|
6
|
+
*
|
|
7
|
+
* Upstream `motion-dom` treats any non-null object with a `start`
|
|
8
|
+
* function as animation controls. Matching that narrow check keeps
|
|
9
|
+
* `animate={controls}` detection compatible with Motion's public shape.
|
|
10
|
+
*
|
|
11
|
+
* @param value Value passed to `animate`.
|
|
12
|
+
* @returns Whether `value` is an animation controls object.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const controls = useAnimationControls()
|
|
17
|
+
* isAnimationControls(controls) // true
|
|
18
|
+
* isAnimationControls({ opacity: 1 }) // false
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const isAnimationControls = (value) => {
|
|
22
|
+
return (value !== null &&
|
|
23
|
+
typeof value === 'object' &&
|
|
24
|
+
typeof value.start === 'function');
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Create legacy animation controls.
|
|
28
|
+
*
|
|
29
|
+
* This mirrors upstream Motion's `animationControls()`: controls collect
|
|
30
|
+
* subscribed motion components, guard `start`/`set` until mounted, fan out
|
|
31
|
+
* starts to every subscriber, and stop all subscribers on unmount.
|
|
32
|
+
*
|
|
33
|
+
* @returns Animation controls with `subscribe`, `start`, `set`, `stop`,
|
|
34
|
+
* and `mount`.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const controls = animationControls()
|
|
39
|
+
* const cleanup = controls.mount()
|
|
40
|
+
* await controls.start({ opacity: 1 })
|
|
41
|
+
* cleanup()
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export const animationControls = () => {
|
|
45
|
+
let hasMounted = false;
|
|
46
|
+
const subscribers = new SvelteSet();
|
|
47
|
+
const controls = {
|
|
48
|
+
subscribe(subscriber) {
|
|
49
|
+
subscribers.add(subscriber);
|
|
50
|
+
return () => {
|
|
51
|
+
subscribers.delete(subscriber);
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
start(definition, transitionOverride) {
|
|
55
|
+
if (!hasMounted) {
|
|
56
|
+
throw new Error(mountedError);
|
|
57
|
+
}
|
|
58
|
+
const animations = [];
|
|
59
|
+
subscribers.forEach((subscriber) => {
|
|
60
|
+
animations.push(subscriber.start(definition, transitionOverride));
|
|
61
|
+
});
|
|
62
|
+
return Promise.all(animations);
|
|
63
|
+
},
|
|
64
|
+
set(definition) {
|
|
65
|
+
if (!hasMounted) {
|
|
66
|
+
throw new Error(setMountedError);
|
|
67
|
+
}
|
|
68
|
+
subscribers.forEach((subscriber) => subscriber.set(definition));
|
|
69
|
+
},
|
|
70
|
+
stop() {
|
|
71
|
+
subscribers.forEach((subscriber) => subscriber.stop());
|
|
72
|
+
},
|
|
73
|
+
mount() {
|
|
74
|
+
hasMounted = true;
|
|
75
|
+
return () => {
|
|
76
|
+
hasMounted = false;
|
|
77
|
+
controls.stop();
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return controls;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Create imperative controls for one or more `motion.*` components.
|
|
85
|
+
*
|
|
86
|
+
* Pass the returned object to `animate={controls}`. Once mounted, call
|
|
87
|
+
* `controls.start(definition)`, `controls.set(definition)`, or
|
|
88
|
+
* `controls.stop()` to coordinate every subscribed component.
|
|
89
|
+
*
|
|
90
|
+
* @returns Mounted animation controls.
|
|
91
|
+
* @see https://motion.dev/docs/react-use-animation-controls
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```svelte
|
|
95
|
+
* <script lang="ts">
|
|
96
|
+
* import { motion, useAnimationControls } from '@humanspeak/svelte-motion'
|
|
97
|
+
*
|
|
98
|
+
* const controls = useAnimationControls()
|
|
99
|
+
* </script>
|
|
100
|
+
*
|
|
101
|
+
* <button onclick={() => controls.start({ scale: 1.2 })}>Pop</button>
|
|
102
|
+
* <motion.div animate={controls} />
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export const useAnimationControls = () => {
|
|
106
|
+
const controls = animationControls();
|
|
107
|
+
$effect(() => controls.mount());
|
|
108
|
+
return controls;
|
|
109
|
+
};
|
|
110
|
+
/** Alias matching Motion's legacy `useAnimation` export. */
|
|
111
|
+
export const useAnimation = useAnimationControls;
|
package/dist/utils/layout.d.ts
CHANGED
|
@@ -6,11 +6,13 @@ import { type AnimationOptions } from 'motion';
|
|
|
6
6
|
* immediately after reading the rect.
|
|
7
7
|
*
|
|
8
8
|
* When `scrollContainers` are provided, the returned rect is shifted by the
|
|
9
|
-
* **sum** of each container's `scrollLeft` / `scrollTop`.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* **sum** of each container's `scrollLeft` / `scrollTop`. When
|
|
10
|
+
* `includeViewportScroll` is true, the viewport's `window.scrollX` /
|
|
11
|
+
* `window.scrollY` is included too. FLIP deltas computed from two such
|
|
12
|
+
* measures stay correct even when the user scrolls between measurements —
|
|
13
|
+
* including a nested `layoutScroll` inside another `layoutScroll`. Mirrors
|
|
14
|
+
* framer-motion's `removeElementScroll`, which walks every ancestor in the
|
|
15
|
+
* path, plus root scroll compensation from the projection tree.
|
|
14
16
|
*
|
|
15
17
|
* Pass an empty array (or omit) for viewport-relative behaviour.
|
|
16
18
|
*
|
|
@@ -27,6 +29,7 @@ import { type AnimationOptions } from 'motion';
|
|
|
27
29
|
* @param el Element to measure.
|
|
28
30
|
* @param scrollContainers Optional ancestor chain with `layoutScroll` enabled.
|
|
29
31
|
* @param baseTransform Transform string applied during measurement. Defaults to `'none'`.
|
|
32
|
+
* @param includeViewportScroll Whether to include `window.scrollX/Y` in the returned rect.
|
|
30
33
|
* @returns DOMRect snapshot of the element.
|
|
31
34
|
*
|
|
32
35
|
* @example
|
|
@@ -41,7 +44,7 @@ import { type AnimationOptions } from 'motion';
|
|
|
41
44
|
* const rect = measureRect(node, [innerScroll, outerScroll])
|
|
42
45
|
* ```
|
|
43
46
|
*/
|
|
44
|
-
export declare const measureRect: (el: HTMLElement, scrollContainers?: HTMLElement[], baseTransform?: string) => DOMRect;
|
|
47
|
+
export declare const measureRect: (el: HTMLElement, scrollContainers?: HTMLElement[], baseTransform?: string, includeViewportScroll?: boolean) => DOMRect;
|
|
45
48
|
/**
|
|
46
49
|
* Minimal rectangle shape `computeFlipTransforms` reads. A `DOMRect`
|
|
47
50
|
* satisfies it structurally, and so does a projection `Box` converted to
|
package/dist/utils/layout.js
CHANGED
|
@@ -1,4 +1,109 @@
|
|
|
1
1
|
import { animate } from 'motion';
|
|
2
|
+
const layoutSizeAnimationAttribute = 'data-layout-size-animation';
|
|
3
|
+
const roundedPx = (value) => `${Math.max(0, Math.round(value))}px`;
|
|
4
|
+
const mix = (from, to, progress) => from + (to - from) * progress;
|
|
5
|
+
const isViewportOffscreen = (el) => {
|
|
6
|
+
if (typeof window === 'undefined')
|
|
7
|
+
return false;
|
|
8
|
+
const rect = el.getBoundingClientRect();
|
|
9
|
+
return (rect.bottom <= 0 ||
|
|
10
|
+
rect.right <= 0 ||
|
|
11
|
+
rect.top >= window.innerHeight ||
|
|
12
|
+
rect.left >= window.innerWidth);
|
|
13
|
+
};
|
|
14
|
+
const runBoxSizeAnimation = (el, transforms, transition) => {
|
|
15
|
+
const { dx, dy, sx, sy } = transforms;
|
|
16
|
+
const originalWidth = el.style.width;
|
|
17
|
+
const originalHeight = el.style.height;
|
|
18
|
+
const originalTransform = el.style.transform;
|
|
19
|
+
const originalTransformOrigin = el.style.transformOrigin;
|
|
20
|
+
const nextRect = el.getBoundingClientRect();
|
|
21
|
+
const prevWidth = nextRect.width * sx;
|
|
22
|
+
const prevHeight = nextRect.height * sy;
|
|
23
|
+
el.setAttribute(layoutSizeAnimationAttribute, 'true');
|
|
24
|
+
for (const child of el.querySelectorAll('[data-svelte-motion-layout]')) {
|
|
25
|
+
child.style.transform = '';
|
|
26
|
+
child.style.transformOrigin = '';
|
|
27
|
+
if (child.style.willChange === 'transform')
|
|
28
|
+
child.style.willChange = '';
|
|
29
|
+
}
|
|
30
|
+
el.style.width = roundedPx(prevWidth);
|
|
31
|
+
el.style.height = roundedPx(prevHeight);
|
|
32
|
+
const sizedRect = el.getBoundingClientRect();
|
|
33
|
+
const residualDx = nextRect.left + dx - sizedRect.left;
|
|
34
|
+
const residualDy = nextRect.top + dy - sizedRect.top;
|
|
35
|
+
const shouldTranslate = Math.abs(residualDx) > 0.5 || Math.abs(residualDy) > 0.5;
|
|
36
|
+
if (shouldTranslate) {
|
|
37
|
+
el.style.transformOrigin = '0 0';
|
|
38
|
+
el.style.transform = `translate(${Math.round(residualDx)}px, ${Math.round(residualDy)}px)`;
|
|
39
|
+
}
|
|
40
|
+
const writeBox = (progress) => {
|
|
41
|
+
el.style.width = roundedPx(mix(prevWidth, nextRect.width, progress));
|
|
42
|
+
el.style.height = roundedPx(mix(prevHeight, nextRect.height, progress));
|
|
43
|
+
if (shouldTranslate) {
|
|
44
|
+
const x = Math.round(mix(residualDx, 0, progress));
|
|
45
|
+
const y = Math.round(mix(residualDy, 0, progress));
|
|
46
|
+
el.style.transform = x === 0 && y === 0 ? '' : `translate(${x}px, ${y}px)`;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const animation = animate(0, 1, {
|
|
50
|
+
...transition,
|
|
51
|
+
onUpdate: writeBox
|
|
52
|
+
});
|
|
53
|
+
let removeScrollListener;
|
|
54
|
+
let offscreenRaf = null;
|
|
55
|
+
let cleanupRan = false;
|
|
56
|
+
const cleanup = () => {
|
|
57
|
+
if (cleanupRan)
|
|
58
|
+
return;
|
|
59
|
+
cleanupRan = true;
|
|
60
|
+
removeScrollListener?.();
|
|
61
|
+
if (offscreenRaf !== null &&
|
|
62
|
+
typeof window !== 'undefined' &&
|
|
63
|
+
typeof window.cancelAnimationFrame === 'function') {
|
|
64
|
+
window.cancelAnimationFrame(offscreenRaf);
|
|
65
|
+
offscreenRaf = null;
|
|
66
|
+
}
|
|
67
|
+
el.style.width = originalWidth;
|
|
68
|
+
el.style.height = originalHeight;
|
|
69
|
+
el.style.transformOrigin = originalTransformOrigin;
|
|
70
|
+
el.style.transform = originalTransform;
|
|
71
|
+
el.removeAttribute(layoutSizeAnimationAttribute);
|
|
72
|
+
};
|
|
73
|
+
if (typeof window !== 'undefined') {
|
|
74
|
+
const completeIfOffscreen = () => {
|
|
75
|
+
if (cleanupRan)
|
|
76
|
+
return;
|
|
77
|
+
if (isViewportOffscreen(el)) {
|
|
78
|
+
animation.complete();
|
|
79
|
+
cleanup();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const scheduleCompleteIfOffscreen = () => {
|
|
83
|
+
if (typeof window.requestAnimationFrame !== 'function') {
|
|
84
|
+
completeIfOffscreen();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (offscreenRaf !== null)
|
|
88
|
+
return;
|
|
89
|
+
offscreenRaf = window.requestAnimationFrame(() => {
|
|
90
|
+
offscreenRaf = null;
|
|
91
|
+
completeIfOffscreen();
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const handleScroll = () => {
|
|
95
|
+
completeIfOffscreen();
|
|
96
|
+
scheduleCompleteIfOffscreen();
|
|
97
|
+
};
|
|
98
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
99
|
+
removeScrollListener = () => {
|
|
100
|
+
window.removeEventListener('scroll', handleScroll);
|
|
101
|
+
};
|
|
102
|
+
completeIfOffscreen();
|
|
103
|
+
scheduleCompleteIfOffscreen();
|
|
104
|
+
}
|
|
105
|
+
animation.finished?.finally(cleanup);
|
|
106
|
+
};
|
|
2
107
|
/**
|
|
3
108
|
* Measure an element's bounding client rect without current transform.
|
|
4
109
|
*
|
|
@@ -6,11 +111,13 @@ import { animate } from 'motion';
|
|
|
6
111
|
* immediately after reading the rect.
|
|
7
112
|
*
|
|
8
113
|
* When `scrollContainers` are provided, the returned rect is shifted by the
|
|
9
|
-
* **sum** of each container's `scrollLeft` / `scrollTop`.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
114
|
+
* **sum** of each container's `scrollLeft` / `scrollTop`. When
|
|
115
|
+
* `includeViewportScroll` is true, the viewport's `window.scrollX` /
|
|
116
|
+
* `window.scrollY` is included too. FLIP deltas computed from two such
|
|
117
|
+
* measures stay correct even when the user scrolls between measurements —
|
|
118
|
+
* including a nested `layoutScroll` inside another `layoutScroll`. Mirrors
|
|
119
|
+
* framer-motion's `removeElementScroll`, which walks every ancestor in the
|
|
120
|
+
* path, plus root scroll compensation from the projection tree.
|
|
14
121
|
*
|
|
15
122
|
* Pass an empty array (or omit) for viewport-relative behaviour.
|
|
16
123
|
*
|
|
@@ -27,6 +134,7 @@ import { animate } from 'motion';
|
|
|
27
134
|
* @param el Element to measure.
|
|
28
135
|
* @param scrollContainers Optional ancestor chain with `layoutScroll` enabled.
|
|
29
136
|
* @param baseTransform Transform string applied during measurement. Defaults to `'none'`.
|
|
137
|
+
* @param includeViewportScroll Whether to include `window.scrollX/Y` in the returned rect.
|
|
30
138
|
* @returns DOMRect snapshot of the element.
|
|
31
139
|
*
|
|
32
140
|
* @example
|
|
@@ -41,19 +149,22 @@ import { animate } from 'motion';
|
|
|
41
149
|
* const rect = measureRect(node, [innerScroll, outerScroll])
|
|
42
150
|
* ```
|
|
43
151
|
*/
|
|
44
|
-
export const measureRect = (el, scrollContainers, baseTransform = 'none') => {
|
|
152
|
+
export const measureRect = (el, scrollContainers, baseTransform = 'none', includeViewportScroll = false) => {
|
|
45
153
|
const prev = el.style.transform;
|
|
46
154
|
try {
|
|
47
155
|
el.style.transform = baseTransform;
|
|
48
156
|
const rect = el.getBoundingClientRect();
|
|
49
|
-
|
|
50
|
-
|
|
157
|
+
let offsetLeft = includeViewportScroll && typeof window !== 'undefined' ? window.scrollX : 0;
|
|
158
|
+
let offsetTop = includeViewportScroll && typeof window !== 'undefined' ? window.scrollY : 0;
|
|
159
|
+
if (!scrollContainers || scrollContainers.length === 0) {
|
|
160
|
+
if (offsetLeft === 0 && offsetTop === 0)
|
|
161
|
+
return rect;
|
|
162
|
+
return new DOMRect(rect.left + offsetLeft, rect.top + offsetTop, rect.width, rect.height);
|
|
163
|
+
}
|
|
51
164
|
// Re-express the rect in the *combined* scroll-container coordinate
|
|
52
165
|
// space so a subsequent scroll on any of them doesn't show up as
|
|
53
166
|
// movement. DOMRect's left/top are read-only, so allocate a fresh
|
|
54
167
|
// one with the summed offsets applied.
|
|
55
|
-
let offsetLeft = 0;
|
|
56
|
-
let offsetTop = 0;
|
|
57
168
|
for (const container of scrollContainers) {
|
|
58
169
|
offsetLeft += container.scrollLeft;
|
|
59
170
|
offsetTop += container.scrollTop;
|
|
@@ -95,6 +206,13 @@ export const runFlipAnimation = (el, transforms, transition) => {
|
|
|
95
206
|
const { dx, dy, sx, sy, shouldTranslate, shouldScale } = transforms;
|
|
96
207
|
if (!(shouldTranslate || shouldScale))
|
|
97
208
|
return;
|
|
209
|
+
const correctionTargets = shouldScale
|
|
210
|
+
? Array.from(el.querySelectorAll('[data-svelte-motion-layout]'))
|
|
211
|
+
: [];
|
|
212
|
+
if (shouldScale && correctionTargets.length > 0) {
|
|
213
|
+
runBoxSizeAnimation(el, { dx, dy, sx, sy }, transition);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
98
216
|
const keyframes = {};
|
|
99
217
|
if (shouldTranslate) {
|
|
100
218
|
keyframes.x = [dx, 0];
|
|
@@ -140,6 +258,13 @@ export const observeLayoutChanges = (el, onChange) => {
|
|
|
140
258
|
let pendingRaf = null;
|
|
141
259
|
let releaseTimeout = null;
|
|
142
260
|
const schedule = () => {
|
|
261
|
+
if (el.closest(`[${layoutSizeAnimationAttribute}]`)) {
|
|
262
|
+
el.style.transform = '';
|
|
263
|
+
el.style.transformOrigin = '';
|
|
264
|
+
if (el.style.willChange === 'transform')
|
|
265
|
+
el.style.willChange = '';
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
143
268
|
if (pendingRaf !== null || releaseTimeout !== null)
|
|
144
269
|
return;
|
|
145
270
|
// Leading-edge: call immediately, then throttle further calls until next frame (or 50ms)
|
|
@@ -157,14 +282,23 @@ export const observeLayoutChanges = (el, onChange) => {
|
|
|
157
282
|
};
|
|
158
283
|
const ro = new ResizeObserver(() => schedule());
|
|
159
284
|
ro.observe(el);
|
|
160
|
-
const
|
|
161
|
-
|
|
285
|
+
const attributeObserver = new MutationObserver(() => schedule());
|
|
286
|
+
attributeObserver.observe(el, {
|
|
287
|
+
attributes: true,
|
|
288
|
+
attributeFilter: ['class', 'data-presence-layout-hold']
|
|
289
|
+
});
|
|
290
|
+
const childListObserver = new MutationObserver(() => schedule());
|
|
291
|
+
childListObserver.observe(el, {
|
|
292
|
+
childList: true,
|
|
293
|
+
subtree: true
|
|
294
|
+
});
|
|
162
295
|
if (el.parentElement) {
|
|
163
|
-
|
|
296
|
+
childListObserver.observe(el.parentElement, { childList: true, subtree: false });
|
|
164
297
|
}
|
|
165
298
|
return () => {
|
|
166
299
|
ro.disconnect();
|
|
167
|
-
|
|
300
|
+
attributeObserver.disconnect();
|
|
301
|
+
childListObserver.disconnect();
|
|
168
302
|
if (pendingRaf !== null && typeof cancelAnimationFrame === 'function') {
|
|
169
303
|
cancelAnimationFrame(pendingRaf);
|
|
170
304
|
pendingRaf = null;
|