@humanspeak/svelte-motion 0.5.4 → 0.6.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.
- package/dist/components/motionDomProjection.context.d.ts +13 -0
- package/dist/components/motionDomProjection.context.js +18 -0
- package/dist/html/_MotionContainer.svelte +548 -60
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +2 -2
- package/dist/utils/layout.d.ts +9 -6
- package/dist/utils/layout.js +141 -14
- package/dist/utils/motionDomProjection.d.ts +126 -0
- package/dist/utils/motionDomProjection.js +212 -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
|
@@ -29,6 +29,7 @@ export type { MotionTemplateInput } from './utils/motionTemplate.svelte';
|
|
|
29
29
|
export { useMotionValue } from './utils/motionValue.svelte';
|
|
30
30
|
export type { MotionValue, RawMotionValue } from './utils/motionValue.svelte';
|
|
31
31
|
export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
32
|
+
export { optimizedAppearDataAttribute, startOptimizedAppearAnimation } from './utils/optimizedAppear';
|
|
32
33
|
export { useReducedMotion } from './utils/reducedMotion.svelte';
|
|
33
34
|
export type { ReducedMotionState } from './utils/reducedMotion.svelte';
|
|
34
35
|
export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ export { useInView } from './utils/inView.svelte';
|
|
|
23
23
|
export { useMotionTemplate } from './utils/motionTemplate.svelte';
|
|
24
24
|
export { useMotionValue } from './utils/motionValue.svelte';
|
|
25
25
|
export { useMotionValueEvent } from './utils/motionValueEvent';
|
|
26
|
+
export { optimizedAppearDataAttribute, startOptimizedAppearAnimation } from './utils/optimizedAppear';
|
|
26
27
|
export { useReducedMotion } from './utils/reducedMotion.svelte';
|
|
27
28
|
export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
|
|
28
29
|
export { useScroll } from './utils/scroll.svelte';
|
package/dist/types.d.ts
CHANGED
|
@@ -431,8 +431,8 @@ export type MotionProps = {
|
|
|
431
431
|
style?: string;
|
|
432
432
|
/** CSS classes */
|
|
433
433
|
class?: string;
|
|
434
|
-
/** Enable FLIP layout animations;
|
|
435
|
-
layout?: boolean | 'position';
|
|
434
|
+
/** Enable FLIP layout animations; string values select the upstream projection animation type. */
|
|
435
|
+
layout?: boolean | 'position' | 'size' | 'preserve-aspect';
|
|
436
436
|
/**
|
|
437
437
|
* Fires after each `layout`-driven change with the FLIP delta from
|
|
438
438
|
* the element's internal projection node. Mirrors framer-motion's
|
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,102 @@
|
|
|
1
1
|
import { animate } from 'motion';
|
|
2
|
+
const layoutSizeAnimationAttribute = 'data-layout-size-animation';
|
|
3
|
+
const px = (value) => `${Math.max(0, value)}px`;
|
|
4
|
+
const isViewportOffscreen = (el) => {
|
|
5
|
+
if (typeof window === 'undefined')
|
|
6
|
+
return false;
|
|
7
|
+
const rect = el.getBoundingClientRect();
|
|
8
|
+
return (rect.bottom <= 0 ||
|
|
9
|
+
rect.right <= 0 ||
|
|
10
|
+
rect.top >= window.innerHeight ||
|
|
11
|
+
rect.left >= window.innerWidth);
|
|
12
|
+
};
|
|
13
|
+
const runBoxSizeAnimation = (el, transforms, transition) => {
|
|
14
|
+
const { dx, dy, sx, sy } = transforms;
|
|
15
|
+
const originalWidth = el.style.width;
|
|
16
|
+
const originalHeight = el.style.height;
|
|
17
|
+
const originalTransform = el.style.transform;
|
|
18
|
+
const originalTransformOrigin = el.style.transformOrigin;
|
|
19
|
+
const nextRect = el.getBoundingClientRect();
|
|
20
|
+
const prevWidth = nextRect.width * sx;
|
|
21
|
+
const prevHeight = nextRect.height * sy;
|
|
22
|
+
el.setAttribute(layoutSizeAnimationAttribute, 'true');
|
|
23
|
+
for (const child of el.querySelectorAll('[data-svelte-motion-layout]')) {
|
|
24
|
+
child.style.transform = '';
|
|
25
|
+
child.style.transformOrigin = '';
|
|
26
|
+
if (child.style.willChange === 'transform')
|
|
27
|
+
child.style.willChange = '';
|
|
28
|
+
}
|
|
29
|
+
el.style.width = px(prevWidth);
|
|
30
|
+
el.style.height = px(prevHeight);
|
|
31
|
+
const sizedRect = el.getBoundingClientRect();
|
|
32
|
+
const residualDx = nextRect.left + dx - sizedRect.left;
|
|
33
|
+
const residualDy = nextRect.top + dy - sizedRect.top;
|
|
34
|
+
const shouldTranslate = Math.abs(residualDx) > 0.5 || Math.abs(residualDy) > 0.5;
|
|
35
|
+
const keyframes = {
|
|
36
|
+
width: [px(prevWidth), px(nextRect.width)],
|
|
37
|
+
height: [px(prevHeight), px(nextRect.height)]
|
|
38
|
+
};
|
|
39
|
+
if (shouldTranslate) {
|
|
40
|
+
keyframes.x = [residualDx, 0];
|
|
41
|
+
keyframes.y = [residualDy, 0];
|
|
42
|
+
el.style.transformOrigin = '0 0';
|
|
43
|
+
el.style.transform = `translate(${residualDx}px, ${residualDy}px)`;
|
|
44
|
+
}
|
|
45
|
+
const animation = animate(el, keyframes, transition);
|
|
46
|
+
let removeScrollListener;
|
|
47
|
+
let offscreenRaf = null;
|
|
48
|
+
let cleanupRan = false;
|
|
49
|
+
const cleanup = () => {
|
|
50
|
+
if (cleanupRan)
|
|
51
|
+
return;
|
|
52
|
+
cleanupRan = true;
|
|
53
|
+
removeScrollListener?.();
|
|
54
|
+
if (offscreenRaf !== null &&
|
|
55
|
+
typeof window !== 'undefined' &&
|
|
56
|
+
typeof window.cancelAnimationFrame === 'function') {
|
|
57
|
+
window.cancelAnimationFrame(offscreenRaf);
|
|
58
|
+
offscreenRaf = null;
|
|
59
|
+
}
|
|
60
|
+
el.style.width = originalWidth;
|
|
61
|
+
el.style.height = originalHeight;
|
|
62
|
+
el.style.transformOrigin = originalTransformOrigin;
|
|
63
|
+
el.style.transform = originalTransform;
|
|
64
|
+
el.removeAttribute(layoutSizeAnimationAttribute);
|
|
65
|
+
};
|
|
66
|
+
if (typeof window !== 'undefined') {
|
|
67
|
+
const completeIfOffscreen = () => {
|
|
68
|
+
if (cleanupRan)
|
|
69
|
+
return;
|
|
70
|
+
if (isViewportOffscreen(el)) {
|
|
71
|
+
animation.complete();
|
|
72
|
+
cleanup();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const scheduleCompleteIfOffscreen = () => {
|
|
76
|
+
if (typeof window.requestAnimationFrame !== 'function') {
|
|
77
|
+
completeIfOffscreen();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (offscreenRaf !== null)
|
|
81
|
+
return;
|
|
82
|
+
offscreenRaf = window.requestAnimationFrame(() => {
|
|
83
|
+
offscreenRaf = null;
|
|
84
|
+
completeIfOffscreen();
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
const handleScroll = () => {
|
|
88
|
+
completeIfOffscreen();
|
|
89
|
+
scheduleCompleteIfOffscreen();
|
|
90
|
+
};
|
|
91
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
92
|
+
removeScrollListener = () => {
|
|
93
|
+
window.removeEventListener('scroll', handleScroll);
|
|
94
|
+
};
|
|
95
|
+
completeIfOffscreen();
|
|
96
|
+
scheduleCompleteIfOffscreen();
|
|
97
|
+
}
|
|
98
|
+
animation.finished?.finally(cleanup);
|
|
99
|
+
};
|
|
2
100
|
/**
|
|
3
101
|
* Measure an element's bounding client rect without current transform.
|
|
4
102
|
*
|
|
@@ -6,11 +104,13 @@ import { animate } from 'motion';
|
|
|
6
104
|
* immediately after reading the rect.
|
|
7
105
|
*
|
|
8
106
|
* When `scrollContainers` are provided, the returned rect is shifted by the
|
|
9
|
-
* **sum** of each container's `scrollLeft` / `scrollTop`.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
107
|
+
* **sum** of each container's `scrollLeft` / `scrollTop`. When
|
|
108
|
+
* `includeViewportScroll` is true, the viewport's `window.scrollX` /
|
|
109
|
+
* `window.scrollY` is included too. FLIP deltas computed from two such
|
|
110
|
+
* measures stay correct even when the user scrolls between measurements —
|
|
111
|
+
* including a nested `layoutScroll` inside another `layoutScroll`. Mirrors
|
|
112
|
+
* framer-motion's `removeElementScroll`, which walks every ancestor in the
|
|
113
|
+
* path, plus root scroll compensation from the projection tree.
|
|
14
114
|
*
|
|
15
115
|
* Pass an empty array (or omit) for viewport-relative behaviour.
|
|
16
116
|
*
|
|
@@ -27,6 +127,7 @@ import { animate } from 'motion';
|
|
|
27
127
|
* @param el Element to measure.
|
|
28
128
|
* @param scrollContainers Optional ancestor chain with `layoutScroll` enabled.
|
|
29
129
|
* @param baseTransform Transform string applied during measurement. Defaults to `'none'`.
|
|
130
|
+
* @param includeViewportScroll Whether to include `window.scrollX/Y` in the returned rect.
|
|
30
131
|
* @returns DOMRect snapshot of the element.
|
|
31
132
|
*
|
|
32
133
|
* @example
|
|
@@ -41,19 +142,22 @@ import { animate } from 'motion';
|
|
|
41
142
|
* const rect = measureRect(node, [innerScroll, outerScroll])
|
|
42
143
|
* ```
|
|
43
144
|
*/
|
|
44
|
-
export const measureRect = (el, scrollContainers, baseTransform = 'none') => {
|
|
145
|
+
export const measureRect = (el, scrollContainers, baseTransform = 'none', includeViewportScroll = false) => {
|
|
45
146
|
const prev = el.style.transform;
|
|
46
147
|
try {
|
|
47
148
|
el.style.transform = baseTransform;
|
|
48
149
|
const rect = el.getBoundingClientRect();
|
|
49
|
-
|
|
50
|
-
|
|
150
|
+
let offsetLeft = includeViewportScroll && typeof window !== 'undefined' ? window.scrollX : 0;
|
|
151
|
+
let offsetTop = includeViewportScroll && typeof window !== 'undefined' ? window.scrollY : 0;
|
|
152
|
+
if (!scrollContainers || scrollContainers.length === 0) {
|
|
153
|
+
if (offsetLeft === 0 && offsetTop === 0)
|
|
154
|
+
return rect;
|
|
155
|
+
return new DOMRect(rect.left + offsetLeft, rect.top + offsetTop, rect.width, rect.height);
|
|
156
|
+
}
|
|
51
157
|
// Re-express the rect in the *combined* scroll-container coordinate
|
|
52
158
|
// space so a subsequent scroll on any of them doesn't show up as
|
|
53
159
|
// movement. DOMRect's left/top are read-only, so allocate a fresh
|
|
54
160
|
// one with the summed offsets applied.
|
|
55
|
-
let offsetLeft = 0;
|
|
56
|
-
let offsetTop = 0;
|
|
57
161
|
for (const container of scrollContainers) {
|
|
58
162
|
offsetLeft += container.scrollLeft;
|
|
59
163
|
offsetTop += container.scrollTop;
|
|
@@ -95,6 +199,13 @@ export const runFlipAnimation = (el, transforms, transition) => {
|
|
|
95
199
|
const { dx, dy, sx, sy, shouldTranslate, shouldScale } = transforms;
|
|
96
200
|
if (!(shouldTranslate || shouldScale))
|
|
97
201
|
return;
|
|
202
|
+
const correctionTargets = shouldScale
|
|
203
|
+
? Array.from(el.querySelectorAll('[data-svelte-motion-layout]'))
|
|
204
|
+
: [];
|
|
205
|
+
if (shouldScale && correctionTargets.length > 0) {
|
|
206
|
+
runBoxSizeAnimation(el, { dx, dy, sx, sy }, transition);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
98
209
|
const keyframes = {};
|
|
99
210
|
if (shouldTranslate) {
|
|
100
211
|
keyframes.x = [dx, 0];
|
|
@@ -140,6 +251,13 @@ export const observeLayoutChanges = (el, onChange) => {
|
|
|
140
251
|
let pendingRaf = null;
|
|
141
252
|
let releaseTimeout = null;
|
|
142
253
|
const schedule = () => {
|
|
254
|
+
if (el.closest(`[${layoutSizeAnimationAttribute}]`)) {
|
|
255
|
+
el.style.transform = '';
|
|
256
|
+
el.style.transformOrigin = '';
|
|
257
|
+
if (el.style.willChange === 'transform')
|
|
258
|
+
el.style.willChange = '';
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
143
261
|
if (pendingRaf !== null || releaseTimeout !== null)
|
|
144
262
|
return;
|
|
145
263
|
// Leading-edge: call immediately, then throttle further calls until next frame (or 50ms)
|
|
@@ -157,14 +275,23 @@ export const observeLayoutChanges = (el, onChange) => {
|
|
|
157
275
|
};
|
|
158
276
|
const ro = new ResizeObserver(() => schedule());
|
|
159
277
|
ro.observe(el);
|
|
160
|
-
const
|
|
161
|
-
|
|
278
|
+
const attributeObserver = new MutationObserver(() => schedule());
|
|
279
|
+
attributeObserver.observe(el, {
|
|
280
|
+
attributes: true,
|
|
281
|
+
attributeFilter: ['class', 'style', 'data-presence-layout-hold']
|
|
282
|
+
});
|
|
283
|
+
const childListObserver = new MutationObserver(() => schedule());
|
|
284
|
+
childListObserver.observe(el, {
|
|
285
|
+
childList: true,
|
|
286
|
+
subtree: true
|
|
287
|
+
});
|
|
162
288
|
if (el.parentElement) {
|
|
163
|
-
|
|
289
|
+
childListObserver.observe(el.parentElement, { childList: true, subtree: false });
|
|
164
290
|
}
|
|
165
291
|
return () => {
|
|
166
292
|
ro.disconnect();
|
|
167
|
-
|
|
293
|
+
attributeObserver.disconnect();
|
|
294
|
+
childListObserver.disconnect();
|
|
168
295
|
if (pendingRaf !== null && typeof cancelAnimationFrame === 'function') {
|
|
169
296
|
cancelAnimationFrame(pendingRaf);
|
|
170
297
|
pendingRaf = null;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type IProjectionNode, type ResolvedValues, type Transition, type VisualElement } from 'motion-dom';
|
|
2
|
+
type ProjectionVisualElement = VisualElement & {
|
|
3
|
+
latestValues: ResolvedValues;
|
|
4
|
+
projection?: IProjectionNode<HTMLElement>;
|
|
5
|
+
};
|
|
6
|
+
type LayoutOption = boolean | string | undefined;
|
|
7
|
+
export interface MotionDomProjectionOptions {
|
|
8
|
+
/** Parent adapter used to connect this node into the upstream projection tree. */
|
|
9
|
+
parent?: MotionDomProjectionAdapter | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Latest layout-related motion props applied to an upstream projection node.
|
|
13
|
+
*/
|
|
14
|
+
export interface MotionDomProjectionUpdateOptions {
|
|
15
|
+
/** Enables layout projection and selects the upstream animation type. */
|
|
16
|
+
layout?: LayoutOption;
|
|
17
|
+
/** Shared layout id used by upstream projection matching. */
|
|
18
|
+
layoutId?: string;
|
|
19
|
+
/** Transition passed to the upstream layout animation builder. */
|
|
20
|
+
transition?: Transition;
|
|
21
|
+
/** Inline style props passed through to the visual element. */
|
|
22
|
+
style?: unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Svelte lifecycle adapter for motion-dom's upstream projection node system.
|
|
26
|
+
*
|
|
27
|
+
* The public Svelte API stays unchanged (`layout`, `layoutId`, `transition`).
|
|
28
|
+
* This adapter only translates those props into the same `HTMLProjectionNode`
|
|
29
|
+
* and `HTMLVisualElement` internals Framer Motion uses.
|
|
30
|
+
*/
|
|
31
|
+
export declare class MotionDomProjectionAdapter {
|
|
32
|
+
readonly visualElement: ProjectionVisualElement;
|
|
33
|
+
readonly projection: IProjectionNode<HTMLElement>;
|
|
34
|
+
private element;
|
|
35
|
+
private layout;
|
|
36
|
+
private layoutId;
|
|
37
|
+
private transition;
|
|
38
|
+
private lastLayout;
|
|
39
|
+
constructor(options?: MotionDomProjectionOptions);
|
|
40
|
+
/**
|
|
41
|
+
* Update projection options from current Svelte props.
|
|
42
|
+
*
|
|
43
|
+
* @param options Current layout-related motion props.
|
|
44
|
+
* @returns Nothing.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* adapter.updateOptions({ layout, layoutId, transition, style })
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
updateOptions(options: MotionDomProjectionUpdateOptions): void;
|
|
52
|
+
/**
|
|
53
|
+
* Mount the upstream projection node to an element and seed its layout.
|
|
54
|
+
*
|
|
55
|
+
* @param element Element represented by the current motion component.
|
|
56
|
+
* @returns Nothing.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* adapter.mount(element)
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
mount(element: HTMLElement): void;
|
|
64
|
+
/**
|
|
65
|
+
* Unmount the upstream projection node and clear its visual-element store.
|
|
66
|
+
*
|
|
67
|
+
* @returns Nothing.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* adapter.unmount()
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
unmount(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Capture the upstream "before" snapshot.
|
|
77
|
+
*
|
|
78
|
+
* @returns Nothing.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* adapter.willUpdate()
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
willUpdate(): void;
|
|
86
|
+
/**
|
|
87
|
+
* Commit an upstream layout update after Svelte has patched the DOM.
|
|
88
|
+
*
|
|
89
|
+
* @returns Nothing.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* adapter.didUpdate()
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
didUpdate(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Seed the current layout without animating.
|
|
99
|
+
*
|
|
100
|
+
* @returns Nothing.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* adapter.seedLayout()
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
seedLayout(): void;
|
|
108
|
+
/**
|
|
109
|
+
* Animate from the last cached layout to the current observed layout.
|
|
110
|
+
*
|
|
111
|
+
* This covers layout changes discovered after the mutation by observers.
|
|
112
|
+
* Svelte runes mode doesn't expose the same component pre/post-update
|
|
113
|
+
* hooks Framer Motion uses in React, so this adapter reuses upstream
|
|
114
|
+
* projection while the Svelte component controls the snapshot timing.
|
|
115
|
+
*
|
|
116
|
+
* @returns Nothing.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* adapter.commitObservedLayoutChange()
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
commitObservedLayoutChange(): void;
|
|
124
|
+
private refreshCachedLayout;
|
|
125
|
+
}
|
|
126
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { HTMLProjectionNode, HTMLVisualElement, copyBoxInto, createBox, visualElementStore } from 'motion-dom';
|
|
2
|
+
const createVisualState = () => ({
|
|
3
|
+
latestValues: {},
|
|
4
|
+
renderState: {
|
|
5
|
+
transform: {},
|
|
6
|
+
transformOrigin: {},
|
|
7
|
+
style: {},
|
|
8
|
+
vars: {}
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const cloneMeasurements = (measurements) => {
|
|
12
|
+
if (!measurements)
|
|
13
|
+
return undefined;
|
|
14
|
+
const measuredBox = createBox();
|
|
15
|
+
const layoutBox = createBox();
|
|
16
|
+
copyBoxInto(measuredBox, measurements.measuredBox);
|
|
17
|
+
copyBoxInto(layoutBox, measurements.layoutBox);
|
|
18
|
+
return {
|
|
19
|
+
animationId: measurements.animationId,
|
|
20
|
+
measuredBox,
|
|
21
|
+
layoutBox,
|
|
22
|
+
latestValues: { ...measurements.latestValues },
|
|
23
|
+
source: measurements.source
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
const animationTypes = new Set([
|
|
27
|
+
'position',
|
|
28
|
+
'x',
|
|
29
|
+
'y',
|
|
30
|
+
'size',
|
|
31
|
+
'both',
|
|
32
|
+
'preserve-aspect'
|
|
33
|
+
]);
|
|
34
|
+
const animationTypeForLayout = (layout) => typeof layout === 'string' && animationTypes.has(layout)
|
|
35
|
+
? layout
|
|
36
|
+
: 'both';
|
|
37
|
+
/**
|
|
38
|
+
* Svelte lifecycle adapter for motion-dom's upstream projection node system.
|
|
39
|
+
*
|
|
40
|
+
* The public Svelte API stays unchanged (`layout`, `layoutId`, `transition`).
|
|
41
|
+
* This adapter only translates those props into the same `HTMLProjectionNode`
|
|
42
|
+
* and `HTMLVisualElement` internals Framer Motion uses.
|
|
43
|
+
*/
|
|
44
|
+
export class MotionDomProjectionAdapter {
|
|
45
|
+
visualElement;
|
|
46
|
+
projection;
|
|
47
|
+
element = null;
|
|
48
|
+
layout;
|
|
49
|
+
layoutId;
|
|
50
|
+
transition;
|
|
51
|
+
lastLayout;
|
|
52
|
+
constructor(options = {}) {
|
|
53
|
+
const parent = options.parent ?? null;
|
|
54
|
+
this.visualElement = new HTMLVisualElement({
|
|
55
|
+
parent: parent?.visualElement,
|
|
56
|
+
props: {},
|
|
57
|
+
presenceContext: null,
|
|
58
|
+
visualState: createVisualState()
|
|
59
|
+
}, { allowProjection: true });
|
|
60
|
+
this.projection = new HTMLProjectionNode(this.visualElement.latestValues, parent?.projection);
|
|
61
|
+
this.visualElement.projection = this.projection;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Update projection options from current Svelte props.
|
|
65
|
+
*
|
|
66
|
+
* @param options Current layout-related motion props.
|
|
67
|
+
* @returns Nothing.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* adapter.updateOptions({ layout, layoutId, transition, style })
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
updateOptions(options) {
|
|
75
|
+
this.layout = options.layout;
|
|
76
|
+
this.layoutId = options.layoutId;
|
|
77
|
+
this.transition = options.transition;
|
|
78
|
+
this.visualElement.update({
|
|
79
|
+
transition: options.transition,
|
|
80
|
+
style: options.style
|
|
81
|
+
}, null);
|
|
82
|
+
this.projection.setOptions({
|
|
83
|
+
layout: options.layout,
|
|
84
|
+
layoutId: options.layoutId,
|
|
85
|
+
animationType: animationTypeForLayout(options.layout),
|
|
86
|
+
transition: options.transition,
|
|
87
|
+
visualElement: this.visualElement
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Mount the upstream projection node to an element and seed its layout.
|
|
92
|
+
*
|
|
93
|
+
* @param element Element represented by the current motion component.
|
|
94
|
+
* @returns Nothing.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* adapter.mount(element)
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
mount(element) {
|
|
102
|
+
if (this.element === element)
|
|
103
|
+
return;
|
|
104
|
+
if (this.element)
|
|
105
|
+
this.unmount();
|
|
106
|
+
this.element = element;
|
|
107
|
+
this.visualElement.mount(element);
|
|
108
|
+
this.seedLayout();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Unmount the upstream projection node and clear its visual-element store.
|
|
112
|
+
*
|
|
113
|
+
* @returns Nothing.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* adapter.unmount()
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
unmount() {
|
|
121
|
+
if (!this.element)
|
|
122
|
+
return;
|
|
123
|
+
const element = this.element;
|
|
124
|
+
this.projection.scheduleCheckAfterUnmount();
|
|
125
|
+
this.visualElement.unmount();
|
|
126
|
+
visualElementStore.delete(element);
|
|
127
|
+
this.element = null;
|
|
128
|
+
this.lastLayout = undefined;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Capture the upstream "before" snapshot.
|
|
132
|
+
*
|
|
133
|
+
* @returns Nothing.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* adapter.willUpdate()
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
willUpdate() {
|
|
141
|
+
if (!this.element || !this.layoutId)
|
|
142
|
+
return;
|
|
143
|
+
this.projection.willUpdate();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Commit an upstream layout update after Svelte has patched the DOM.
|
|
147
|
+
*
|
|
148
|
+
* @returns Nothing.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```ts
|
|
152
|
+
* adapter.didUpdate()
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
didUpdate() {
|
|
156
|
+
if (!this.element || !this.layoutId)
|
|
157
|
+
return;
|
|
158
|
+
this.projection.root?.didUpdate();
|
|
159
|
+
this.refreshCachedLayout();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Seed the current layout without animating.
|
|
163
|
+
*
|
|
164
|
+
* @returns Nothing.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```ts
|
|
168
|
+
* adapter.seedLayout()
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
seedLayout() {
|
|
172
|
+
if (!this.element)
|
|
173
|
+
return;
|
|
174
|
+
this.projection.isLayoutDirty = true;
|
|
175
|
+
this.projection.updateLayout();
|
|
176
|
+
this.lastLayout = cloneMeasurements(this.projection.layout);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Animate from the last cached layout to the current observed layout.
|
|
180
|
+
*
|
|
181
|
+
* This covers layout changes discovered after the mutation by observers.
|
|
182
|
+
* Svelte runes mode doesn't expose the same component pre/post-update
|
|
183
|
+
* hooks Framer Motion uses in React, so this adapter reuses upstream
|
|
184
|
+
* projection while the Svelte component controls the snapshot timing.
|
|
185
|
+
*
|
|
186
|
+
* @returns Nothing.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* adapter.commitObservedLayoutChange()
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
commitObservedLayoutChange() {
|
|
194
|
+
if (!this.element || !this.layoutId)
|
|
195
|
+
return;
|
|
196
|
+
const snapshot = cloneMeasurements(this.lastLayout);
|
|
197
|
+
if (!snapshot) {
|
|
198
|
+
this.seedLayout();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.projection.root?.startUpdate();
|
|
202
|
+
this.projection.snapshot = snapshot;
|
|
203
|
+
this.projection.isLayoutDirty = true;
|
|
204
|
+
this.projection.root?.didUpdate();
|
|
205
|
+
this.refreshCachedLayout();
|
|
206
|
+
}
|
|
207
|
+
refreshCachedLayout() {
|
|
208
|
+
requestAnimationFrame(() => {
|
|
209
|
+
this.lastLayout = cloneMeasurements(this.projection.layout);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|