@humanspeak/svelte-motion 0.6.0 → 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/html/_MotionContainer.svelte +296 -34
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/types.d.ts +85 -1
- package/dist/utils/animationControls.svelte.d.ts +63 -0
- package/dist/utils/animationControls.svelte.js +111 -0
- package/dist/utils/layout.js +19 -12
- package/dist/utils/motionDomProjection.d.ts +29 -0
- package/dist/utils/motionDomProjection.js +74 -7
- package/package.json +1 -1
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import type {
|
|
15
15
|
MotionProps,
|
|
16
16
|
MotionTransition,
|
|
17
|
+
AnimationControlsDefinition,
|
|
18
|
+
AnimationControlsSubscriber,
|
|
17
19
|
DragAxis,
|
|
18
20
|
DragConstraints,
|
|
19
21
|
DragControls,
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
import { onDestroy, untrack, type Snippet } from 'svelte'
|
|
34
36
|
import { VOID_TAGS } from '../utils/constants'
|
|
35
37
|
import { mergeTransitions, animateWithLifecycle } from '../utils/animation'
|
|
38
|
+
import { isAnimationControls } from '../utils/animationControls.svelte'
|
|
36
39
|
import { attachWhileTap } from '../utils/interaction'
|
|
37
40
|
import { attachWhileHover, computeHoverBaseline, splitHoverDefinition } from '../utils/hover'
|
|
38
41
|
import { attachWhileFocus } from '../utils/focus'
|
|
@@ -60,6 +63,7 @@
|
|
|
60
63
|
import { ProjectionNode } from '../utils/projection'
|
|
61
64
|
import { getProjectionParent, setProjectionParent } from '../components/projection.context'
|
|
62
65
|
import { MotionDomProjectionAdapter } from '../utils/motionDomProjection'
|
|
66
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
63
67
|
import {
|
|
64
68
|
getMotionDomProjectionParent,
|
|
65
69
|
setMotionDomProjectionParent
|
|
@@ -69,6 +73,7 @@
|
|
|
69
73
|
resolveAnimate,
|
|
70
74
|
resolveExit,
|
|
71
75
|
resolveWhile,
|
|
76
|
+
resolveVariantList,
|
|
72
77
|
resolveRestingValues
|
|
73
78
|
} from '../utils/variants'
|
|
74
79
|
import {
|
|
@@ -282,6 +287,11 @@
|
|
|
282
287
|
width: rect.width,
|
|
283
288
|
height: rect.height
|
|
284
289
|
})
|
|
290
|
+
const hasRectChanged = (previous: RectLike, next: RectLike): boolean =>
|
|
291
|
+
Math.abs(previous.left - next.left) > 0.5 ||
|
|
292
|
+
Math.abs(previous.top - next.top) > 0.5 ||
|
|
293
|
+
Math.abs(previous.width - next.width) > 0.5 ||
|
|
294
|
+
Math.abs(previous.height - next.height) > 0.5
|
|
285
295
|
const isViewportOffscreen = (rect: DOMRect): boolean =>
|
|
286
296
|
rect.bottom <= 0 ||
|
|
287
297
|
rect.right <= 0 ||
|
|
@@ -463,6 +473,8 @@
|
|
|
463
473
|
|
|
464
474
|
// Variant inheritance and resolution
|
|
465
475
|
const parentVariantStore = getVariantContext()
|
|
476
|
+
const animateControls = $derived(isAnimationControls(animateProp) ? animateProp : undefined)
|
|
477
|
+
const declarativeAnimateProp = $derived(animateControls ? undefined : animateProp)
|
|
466
478
|
|
|
467
479
|
// Get initial inherited variant synchronously
|
|
468
480
|
let initialInheritedVariant: string | undefined = undefined
|
|
@@ -472,8 +484,8 @@
|
|
|
472
484
|
|
|
473
485
|
// Create store with initial value so children can inherit immediately
|
|
474
486
|
const initialVariantValue =
|
|
475
|
-
typeof
|
|
476
|
-
?
|
|
487
|
+
typeof declarativeAnimateProp === 'string'
|
|
488
|
+
? declarativeAnimateProp
|
|
477
489
|
: (variantsProp && initialInheritedVariant) || undefined
|
|
478
490
|
const localVariantStore = writable<string | undefined>(initialVariantValue)
|
|
479
491
|
|
|
@@ -490,7 +502,8 @@
|
|
|
490
502
|
|
|
491
503
|
// Use the initial value first, then switch to reactive once mounted
|
|
492
504
|
const effectiveAnimate = $derived(
|
|
493
|
-
|
|
505
|
+
declarativeAnimateProp ??
|
|
506
|
+
(variantsProp ? (inheritedVariant ?? initialInheritedVariant) : undefined)
|
|
494
507
|
)
|
|
495
508
|
|
|
496
509
|
// Propagate initial={false} to children BEFORE setting variant context
|
|
@@ -547,7 +560,8 @@
|
|
|
547
560
|
|
|
548
561
|
$effect(() => {
|
|
549
562
|
if (!variantsProp) return localVariantStore.set(undefined)
|
|
550
|
-
if (typeof
|
|
563
|
+
if (typeof declarativeAnimateProp === 'string')
|
|
564
|
+
return localVariantStore.set(declarativeAnimateProp)
|
|
551
565
|
if (typeof effectiveAnimate === 'string') return localVariantStore.set(effectiveAnimate)
|
|
552
566
|
localVariantStore.set(undefined)
|
|
553
567
|
})
|
|
@@ -692,6 +706,168 @@
|
|
|
692
706
|
: null
|
|
693
707
|
}
|
|
694
708
|
|
|
709
|
+
const getAnimationPromise = (control: unknown): Promise<unknown> => {
|
|
710
|
+
const finished = getFinishedPromise(control)
|
|
711
|
+
if (finished) return finished
|
|
712
|
+
if (control && typeof (control as Promise<unknown>).then === 'function') {
|
|
713
|
+
return control as Promise<unknown>
|
|
714
|
+
}
|
|
715
|
+
return Promise.resolve()
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
type StoppableAnimationControl = {
|
|
719
|
+
stop?: () => void
|
|
720
|
+
cancel?: () => void
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const activeAnimationControls = new SvelteSet<StoppableAnimationControl>()
|
|
724
|
+
let animationControlsGeneration = 0
|
|
725
|
+
let animationControlsHasReceivedCommand = false
|
|
726
|
+
|
|
727
|
+
const isStoppableAnimationControl = (control: unknown): control is StoppableAnimationControl =>
|
|
728
|
+
!!control &&
|
|
729
|
+
typeof control === 'object' &&
|
|
730
|
+
(typeof (control as StoppableAnimationControl).stop === 'function' ||
|
|
731
|
+
typeof (control as StoppableAnimationControl).cancel === 'function')
|
|
732
|
+
|
|
733
|
+
const trackAnimationControlsControl = (control: unknown): Promise<unknown> => {
|
|
734
|
+
const promise = getAnimationPromise(control)
|
|
735
|
+
if (isStoppableAnimationControl(control)) {
|
|
736
|
+
activeAnimationControls.add(control)
|
|
737
|
+
promise.then(
|
|
738
|
+
() => activeAnimationControls.delete(control),
|
|
739
|
+
() => activeAnimationControls.delete(control)
|
|
740
|
+
)
|
|
741
|
+
}
|
|
742
|
+
return promise
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const resolveAnimationControlsDefinition = (
|
|
746
|
+
definition: AnimationControlsDefinition
|
|
747
|
+
): DOMKeyframesDefinition | undefined => {
|
|
748
|
+
const resolvedDefinition =
|
|
749
|
+
typeof definition === 'function' ? definition(effectiveCustom) : definition
|
|
750
|
+
if (typeof resolvedDefinition === 'string' || Array.isArray(resolvedDefinition)) {
|
|
751
|
+
return resolveVariantList(variantsProp, resolvedDefinition, effectiveCustom)
|
|
752
|
+
}
|
|
753
|
+
return resolvedDefinition
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const applyAnimationControlsTarget = (definition: AnimationControlsDefinition) => {
|
|
757
|
+
if (!element) return
|
|
758
|
+
const resolved = resolveAnimationControlsDefinition(definition)
|
|
759
|
+
if (!resolved) return
|
|
760
|
+
|
|
761
|
+
animationControlsHasReceivedCommand = true
|
|
762
|
+
const target = { ...(resolved as Record<string, unknown>) } as Record<string, unknown> & {
|
|
763
|
+
transition?: AnimationOptions
|
|
764
|
+
transitionEnd?: Record<string, unknown>
|
|
765
|
+
}
|
|
766
|
+
const transitionEnd = target.transitionEnd
|
|
767
|
+
delete target.transition
|
|
768
|
+
delete target.transitionEnd
|
|
769
|
+
const finalTarget = resolveRestingValues({
|
|
770
|
+
...target,
|
|
771
|
+
...(transitionEnd ?? {})
|
|
772
|
+
} as DOMKeyframesDefinition) as Record<string, unknown> | undefined
|
|
773
|
+
if (!finalTarget) return
|
|
774
|
+
|
|
775
|
+
const transformedTarget = transformSVGPathProperties(element, finalTarget) as Record<
|
|
776
|
+
string,
|
|
777
|
+
unknown
|
|
778
|
+
>
|
|
779
|
+
animate(element, transformedTarget as DOMKeyframesDefinition, { duration: 0 })
|
|
780
|
+
element.setAttribute(
|
|
781
|
+
'style',
|
|
782
|
+
mergeInlineStyles(element.getAttribute('style') ?? '', undefined, transformedTarget)
|
|
783
|
+
)
|
|
784
|
+
enterAnimationSettled = true
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const stopAnimationControlsAnimations = () => {
|
|
788
|
+
animationControlsHasReceivedCommand = true
|
|
789
|
+
animationControlsGeneration += 1
|
|
790
|
+
|
|
791
|
+
for (const control of activeAnimationControls) {
|
|
792
|
+
if (typeof control.stop === 'function') {
|
|
793
|
+
control.stop()
|
|
794
|
+
} else {
|
|
795
|
+
control.cancel?.()
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
activeAnimationControls.clear()
|
|
799
|
+
|
|
800
|
+
if (!element) return
|
|
801
|
+
if (typeof element.getAnimations !== 'function') return
|
|
802
|
+
for (const animation of element.getAnimations()) {
|
|
803
|
+
try {
|
|
804
|
+
animation.commitStyles?.()
|
|
805
|
+
} catch {
|
|
806
|
+
// Ignore unsupported commitStyles cases.
|
|
807
|
+
}
|
|
808
|
+
animation.cancel()
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const startAnimationControlsDefinition = async (
|
|
813
|
+
definition: AnimationControlsDefinition,
|
|
814
|
+
transitionOverride?: AnimationOptions
|
|
815
|
+
): Promise<unknown> => {
|
|
816
|
+
if (!element) return
|
|
817
|
+
const resolved = resolveAnimationControlsDefinition(definition)
|
|
818
|
+
if (!resolved) return
|
|
819
|
+
|
|
820
|
+
animationControlsHasReceivedCommand = true
|
|
821
|
+
const filtered = filterReducedMotionKeyframes(
|
|
822
|
+
resolved as Record<string, unknown>,
|
|
823
|
+
reducedMotion
|
|
824
|
+
) as Record<string, unknown> & {
|
|
825
|
+
transition?: AnimationOptions
|
|
826
|
+
transitionEnd?: Record<string, unknown>
|
|
827
|
+
}
|
|
828
|
+
const transition = filtered.transition
|
|
829
|
+
const target = { ...filtered }
|
|
830
|
+
delete target.transition
|
|
831
|
+
delete target.transitionEnd
|
|
832
|
+
const transitionAnimate: MotionTransition =
|
|
833
|
+
transitionOverride ?? mergeTransitions(mergedTransition ?? {}, transition ?? {})
|
|
834
|
+
const svgPathFinished =
|
|
835
|
+
isSVGPathElement(element) && hasSVGPathProperties(target)
|
|
836
|
+
? animateSVGPathAttributes(element, target, transitionAnimate, true)
|
|
837
|
+
: []
|
|
838
|
+
const payload = transformSVGPathProperties(
|
|
839
|
+
element,
|
|
840
|
+
svgPathFinished.length > 0 ? stripSVGPathKeyframes(target) : target
|
|
841
|
+
) as Record<string, unknown>
|
|
842
|
+
|
|
843
|
+
const controlsGeneration = ++animationControlsGeneration
|
|
844
|
+
enterAnimationSettled = false
|
|
845
|
+
onAnimationStartProp?.(definition as unknown as DOMKeyframesDefinition)
|
|
846
|
+
|
|
847
|
+
const promises: Promise<unknown>[] = [...svgPathFinished]
|
|
848
|
+
if (isNotEmpty(payload)) {
|
|
849
|
+
promises.push(
|
|
850
|
+
trackAnimationControlsControl(
|
|
851
|
+
animate(
|
|
852
|
+
element,
|
|
853
|
+
payload as DOMKeyframesDefinition,
|
|
854
|
+
transitionAnimate as AnimationOptions
|
|
855
|
+
)
|
|
856
|
+
)
|
|
857
|
+
)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
try {
|
|
861
|
+
await Promise.all(promises)
|
|
862
|
+
} catch (error) {
|
|
863
|
+
if (controlsGeneration !== animationControlsGeneration) return
|
|
864
|
+
throw error
|
|
865
|
+
}
|
|
866
|
+
if (controlsGeneration !== animationControlsGeneration) return
|
|
867
|
+
applyAnimationControlsTarget(definition)
|
|
868
|
+
onAnimationCompleteProp?.(definition as unknown as DOMKeyframesDefinition)
|
|
869
|
+
}
|
|
870
|
+
|
|
695
871
|
/**
|
|
696
872
|
* Animates SVG path drawing props via motion-dom's `svgEffect`, matching
|
|
697
873
|
* upstream's attribute-based pathLength/pathSpacing/pathOffset behavior.
|
|
@@ -704,7 +880,8 @@
|
|
|
704
880
|
const animateSVGPathAttributes = (
|
|
705
881
|
path: SVGPathElement,
|
|
706
882
|
keyframes: Record<string, unknown>,
|
|
707
|
-
transition: MotionTransition
|
|
883
|
+
transition: MotionTransition,
|
|
884
|
+
trackControl = false
|
|
708
885
|
): Promise<unknown>[] => {
|
|
709
886
|
if (!hasSVGPathProperties(keyframes)) return []
|
|
710
887
|
|
|
@@ -733,7 +910,9 @@
|
|
|
733
910
|
: keyframes[key]) as never,
|
|
734
911
|
transition as unknown as AnimationOptions
|
|
735
912
|
)
|
|
736
|
-
return
|
|
913
|
+
return trackControl
|
|
914
|
+
? trackAnimationControlsControl(control)
|
|
915
|
+
: getFinishedPromise(control)
|
|
737
916
|
})
|
|
738
917
|
.filter((promise): promise is Promise<unknown> => promise !== null)
|
|
739
918
|
}
|
|
@@ -831,9 +1010,13 @@
|
|
|
831
1010
|
? (resolveRestingValues(
|
|
832
1011
|
animateKeyframes as DOMKeyframesDefinition | undefined
|
|
833
1012
|
) as unknown as Record<string, unknown>)
|
|
834
|
-
:
|
|
835
|
-
|
|
836
|
-
|
|
1013
|
+
: animateControls &&
|
|
1014
|
+
!animationControlsHasReceivedCommand &&
|
|
1015
|
+
isNotEmpty(initialKeyframes)
|
|
1016
|
+
? (initialKeyframes as unknown as Record<string, unknown>)
|
|
1017
|
+
: isNotEmpty(initialKeyframes)
|
|
1018
|
+
? undefined
|
|
1019
|
+
: (animateKeyframes as unknown as Record<string, unknown>)
|
|
837
1020
|
),
|
|
838
1021
|
class: classProp
|
|
839
1022
|
})
|
|
@@ -1320,9 +1503,12 @@
|
|
|
1320
1503
|
// which shows up as a "pop" after the deferred animation completes.
|
|
1321
1504
|
pwLog('[motion] wait-unblocked: marking enter handled')
|
|
1322
1505
|
initialAnimationTriggered = true
|
|
1323
|
-
if (
|
|
1506
|
+
if (
|
|
1507
|
+
declarativeAnimateProp &&
|
|
1508
|
+
typeof declarativeAnimateProp !== 'string'
|
|
1509
|
+
) {
|
|
1324
1510
|
objectAnimateRanOnMount = true
|
|
1325
|
-
lastAnimatePropJson = JSON.stringify(
|
|
1511
|
+
lastAnimatePropJson = JSON.stringify(declarativeAnimateProp)
|
|
1326
1512
|
}
|
|
1327
1513
|
isLoaded = 'ready'
|
|
1328
1514
|
})
|
|
@@ -1360,8 +1546,8 @@
|
|
|
1360
1546
|
let lastAnimatePropJson = $state<string | undefined>(undefined)
|
|
1361
1547
|
let motionDomProjectionUpdatePending = false
|
|
1362
1548
|
const currentAnimateKey = $derived(
|
|
1363
|
-
typeof
|
|
1364
|
-
?
|
|
1549
|
+
typeof declarativeAnimateProp === 'string'
|
|
1550
|
+
? declarativeAnimateProp
|
|
1365
1551
|
: typeof effectiveAnimate === 'string'
|
|
1366
1552
|
? effectiveAnimate
|
|
1367
1553
|
: undefined
|
|
@@ -1372,6 +1558,7 @@
|
|
|
1372
1558
|
motionDomProjection.updateOptions({
|
|
1373
1559
|
layout: layoutProp,
|
|
1374
1560
|
layoutId: scopedLayoutId,
|
|
1561
|
+
layoutScroll: layoutScrollProp,
|
|
1375
1562
|
transition: mergedTransition as never,
|
|
1376
1563
|
style: styleProp
|
|
1377
1564
|
})
|
|
@@ -1380,6 +1567,13 @@
|
|
|
1380
1567
|
$effect(() => {
|
|
1381
1568
|
if (!motionDomProjection) return
|
|
1382
1569
|
if (!element) return
|
|
1570
|
+
motionDomProjection.updateOptions({
|
|
1571
|
+
layout: layoutProp,
|
|
1572
|
+
layoutId: scopedLayoutId,
|
|
1573
|
+
layoutScroll: layoutScrollProp,
|
|
1574
|
+
transition: mergedTransition as never,
|
|
1575
|
+
style: styleProp
|
|
1576
|
+
})
|
|
1383
1577
|
motionDomProjection.mount(element)
|
|
1384
1578
|
return () => {
|
|
1385
1579
|
motionDomProjection.unmount()
|
|
@@ -1423,10 +1617,18 @@
|
|
|
1423
1617
|
if (previous) {
|
|
1424
1618
|
const next = measureLayoutRect()
|
|
1425
1619
|
if (next) {
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1620
|
+
if (motionDomProjection) {
|
|
1621
|
+
lastRect = next
|
|
1622
|
+
} else {
|
|
1623
|
+
const flipLayoutMode = layoutProp === 'position' ? 'position' : true
|
|
1624
|
+
const transforms = computeFlipTransforms(previous, next, flipLayoutMode)
|
|
1625
|
+
runFlipAnimation(
|
|
1626
|
+
element!,
|
|
1627
|
+
transforms,
|
|
1628
|
+
(mergedTransition ?? {}) as AnimationOptions
|
|
1629
|
+
)
|
|
1630
|
+
lastRect = next
|
|
1631
|
+
}
|
|
1430
1632
|
}
|
|
1431
1633
|
}
|
|
1432
1634
|
projection.didUpdate()
|
|
@@ -1469,14 +1671,20 @@
|
|
|
1469
1671
|
|
|
1470
1672
|
let rafId: number | null = null
|
|
1471
1673
|
let wasViewportOffscreenSinceLastLayout = false
|
|
1674
|
+
let wasViewportScrolledSinceLastLayout = false
|
|
1472
1675
|
const flipLayoutMode = layoutProp === 'position' ? 'position' : true
|
|
1473
1676
|
motionDomProjection?.seedLayout()
|
|
1474
1677
|
lastRect = measureLayoutRect()
|
|
1475
1678
|
setCompositorHints(element!, true)
|
|
1476
1679
|
|
|
1477
1680
|
const rememberOffscreenScroll = () => {
|
|
1681
|
+
wasViewportScrolledSinceLastLayout = true
|
|
1682
|
+
if (motionDomProjection?.isAnimating()) {
|
|
1683
|
+
motionDomProjection.finishAnimation()
|
|
1684
|
+
}
|
|
1478
1685
|
if (isViewportOffscreen(element!.getBoundingClientRect())) {
|
|
1479
1686
|
wasViewportOffscreenSinceLastLayout = true
|
|
1687
|
+
motionDomProjection?.finishAnimation()
|
|
1480
1688
|
}
|
|
1481
1689
|
}
|
|
1482
1690
|
|
|
@@ -1503,18 +1711,42 @@
|
|
|
1503
1711
|
return
|
|
1504
1712
|
}
|
|
1505
1713
|
|
|
1714
|
+
if (
|
|
1715
|
+
wasViewportScrolledSinceLastLayout ||
|
|
1716
|
+
wasViewportOffscreenSinceLastLayout ||
|
|
1717
|
+
isViewportOffscreen(element!.getBoundingClientRect())
|
|
1718
|
+
) {
|
|
1719
|
+
lastRect = measureLayoutRect()
|
|
1720
|
+
motionDomProjection?.finishAnimation()
|
|
1721
|
+
wasViewportScrolledSinceLastLayout = false
|
|
1722
|
+
wasViewportOffscreenSinceLastLayout = false
|
|
1723
|
+
return
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1506
1726
|
const nextBox = projection.commitLayoutChange()
|
|
1727
|
+
let shouldCommitMotionDomLayout = false
|
|
1507
1728
|
if (nextBox && lastRect) {
|
|
1508
1729
|
const next = boxToRectLike(nextBox)
|
|
1509
|
-
|
|
1510
|
-
|
|
1730
|
+
shouldCommitMotionDomLayout = hasRectChanged(lastRect, next)
|
|
1731
|
+
if (!motionDomProjection) {
|
|
1732
|
+
const transforms = computeFlipTransforms(lastRect, next, flipLayoutMode)
|
|
1733
|
+
runFlipAnimation(
|
|
1734
|
+
element!,
|
|
1735
|
+
transforms,
|
|
1736
|
+
(mergedTransition ?? {}) as AnimationOptions
|
|
1737
|
+
)
|
|
1738
|
+
}
|
|
1511
1739
|
lastRect = next
|
|
1740
|
+
wasViewportScrolledSinceLastLayout = false
|
|
1512
1741
|
wasViewportOffscreenSinceLastLayout = false
|
|
1513
1742
|
} else if (nextBox) {
|
|
1514
1743
|
lastRect = boxToRectLike(nextBox)
|
|
1744
|
+
wasViewportScrolledSinceLastLayout = false
|
|
1515
1745
|
wasViewportOffscreenSinceLastLayout = false
|
|
1516
1746
|
}
|
|
1517
|
-
|
|
1747
|
+
if (shouldCommitMotionDomLayout) {
|
|
1748
|
+
motionDomProjection?.commitObservedLayoutChange()
|
|
1749
|
+
}
|
|
1518
1750
|
}
|
|
1519
1751
|
|
|
1520
1752
|
const commitPresenceLayoutRelease = (event: Event) => {
|
|
@@ -1532,14 +1764,20 @@
|
|
|
1532
1764
|
lastRect = next
|
|
1533
1765
|
const shouldSkipLayoutAnimation =
|
|
1534
1766
|
detail?.viewportScrolledDuringHold ||
|
|
1767
|
+
wasViewportScrolledSinceLastLayout ||
|
|
1535
1768
|
wasViewportOffscreenSinceLastLayout ||
|
|
1536
1769
|
isViewportOffscreen(viewportRect)
|
|
1537
|
-
if (!shouldSkipLayoutAnimation) {
|
|
1770
|
+
if (!shouldSkipLayoutAnimation && !motionDomProjection) {
|
|
1538
1771
|
const transforms = computeFlipTransforms(previous, next, flipLayoutMode)
|
|
1539
1772
|
runFlipAnimation(element!, transforms, (mergedTransition ?? {}) as AnimationOptions)
|
|
1540
1773
|
}
|
|
1774
|
+
wasViewportScrolledSinceLastLayout = false
|
|
1541
1775
|
wasViewportOffscreenSinceLastLayout = false
|
|
1542
|
-
|
|
1776
|
+
if (!shouldSkipLayoutAnimation && hasRectChanged(previous, next)) {
|
|
1777
|
+
motionDomProjection?.commitObservedLayoutChange()
|
|
1778
|
+
} else if (shouldSkipLayoutAnimation) {
|
|
1779
|
+
motionDomProjection?.finishAnimation()
|
|
1780
|
+
}
|
|
1543
1781
|
}
|
|
1544
1782
|
|
|
1545
1783
|
const scheduleProjectionCommit = () => {
|
|
@@ -1581,6 +1819,7 @@
|
|
|
1581
1819
|
|
|
1582
1820
|
const prev = layoutIdRegistry.consume(scopedLayoutId)
|
|
1583
1821
|
if (!prev) return // First appearance, no animation needed
|
|
1822
|
+
if (motionDomProjection) return
|
|
1584
1823
|
|
|
1585
1824
|
const next = measureRect(element, resolveLayoutScrollAncestors())
|
|
1586
1825
|
const transforms = computeFlipTransforms(prev.rect, next, true)
|
|
@@ -1690,6 +1929,22 @@
|
|
|
1690
1929
|
)
|
|
1691
1930
|
})
|
|
1692
1931
|
|
|
1932
|
+
// Legacy animation controls (`animate={controls}`) mirror upstream's
|
|
1933
|
+
// VisualElement subscription model with a small Svelte adapter. The
|
|
1934
|
+
// controls own when animations start; this component only resolves
|
|
1935
|
+
// variants/custom data and runs the resulting target on its element.
|
|
1936
|
+
$effect(() => {
|
|
1937
|
+
if (!(element && animateControls)) return
|
|
1938
|
+
|
|
1939
|
+
const subscriber: AnimationControlsSubscriber = {
|
|
1940
|
+
start: startAnimationControlsDefinition,
|
|
1941
|
+
set: applyAnimationControlsTarget,
|
|
1942
|
+
stop: stopAnimationControlsAnimations
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
return animateControls.subscribe(subscriber)
|
|
1946
|
+
})
|
|
1947
|
+
|
|
1693
1948
|
// Handle key prop changes inside AnimatePresence (simulates React's key-based remounting)
|
|
1694
1949
|
// When key changes, run exit → initial → animate sequence on the same element
|
|
1695
1950
|
$effect(() => {
|
|
@@ -1798,10 +2053,14 @@
|
|
|
1798
2053
|
// Re-run animate when animateProp changes while ready
|
|
1799
2054
|
$effect(() => {
|
|
1800
2055
|
if (!(element && isLoaded === 'ready')) return
|
|
2056
|
+
if (animateControls) return
|
|
1801
2057
|
// Skip first run if we mounted with initial={false} AND the variant hasn't changed
|
|
1802
2058
|
if (mountedWithInitialFalse) {
|
|
1803
2059
|
// Only skip if the variant is the same as what we mounted with
|
|
1804
|
-
if (
|
|
2060
|
+
if (
|
|
2061
|
+
typeof declarativeAnimateProp === 'string' &&
|
|
2062
|
+
lastRanVariantKey === declarativeAnimateProp
|
|
2063
|
+
) {
|
|
1805
2064
|
mountedWithInitialFalse = false
|
|
1806
2065
|
return
|
|
1807
2066
|
}
|
|
@@ -1813,25 +2072,28 @@
|
|
|
1813
2072
|
pwLog('[motion] effect: skipping, initial animation already triggered')
|
|
1814
2073
|
initialAnimationTriggered = false
|
|
1815
2074
|
// Also mark object animate as ran to prevent duplicate runs from effect re-triggers
|
|
1816
|
-
if (
|
|
2075
|
+
if (declarativeAnimateProp && typeof declarativeAnimateProp !== 'string') {
|
|
1817
2076
|
objectAnimateRanOnMount = true
|
|
1818
2077
|
}
|
|
1819
2078
|
return
|
|
1820
2079
|
}
|
|
1821
|
-
if (typeof
|
|
2080
|
+
if (typeof declarativeAnimateProp === 'string') {
|
|
1822
2081
|
// Compare BOTH the variant key and the resolved keyframes JSON.
|
|
1823
2082
|
// For static variants the JSON is constant per key; for
|
|
1824
2083
|
// function-form variants the JSON changes when `custom`
|
|
1825
2084
|
// changes, which we must treat as a new animation target.
|
|
1826
2085
|
const resolvedJson = resolvedAnimate ? JSON.stringify(resolvedAnimate) : undefined
|
|
1827
|
-
if (
|
|
1828
|
-
lastRanVariantKey
|
|
2086
|
+
if (
|
|
2087
|
+
lastRanVariantKey !== declarativeAnimateProp ||
|
|
2088
|
+
lastRanResolvedJson !== resolvedJson
|
|
2089
|
+
) {
|
|
2090
|
+
lastRanVariantKey = declarativeAnimateProp
|
|
1829
2091
|
lastRanResolvedJson = resolvedJson
|
|
1830
2092
|
runAnimation()
|
|
1831
2093
|
}
|
|
1832
|
-
} else if (
|
|
2094
|
+
} else if (declarativeAnimateProp) {
|
|
1833
2095
|
// Object animate props - detect if the prop actually changed
|
|
1834
|
-
const currentJson = JSON.stringify(
|
|
2096
|
+
const currentJson = JSON.stringify(declarativeAnimateProp)
|
|
1835
2097
|
const propChanged = lastAnimatePropJson !== currentJson
|
|
1836
2098
|
|
|
1837
2099
|
// Reset flag if animate prop changed
|
|
@@ -1853,7 +2115,7 @@
|
|
|
1853
2115
|
// Also run when inherited/effective variant changes
|
|
1854
2116
|
$effect(() => {
|
|
1855
2117
|
void resolvedAnimate
|
|
1856
|
-
if (!(element && isLoaded === 'ready' && !
|
|
2118
|
+
if (!(element && isLoaded === 'ready' && !declarativeAnimateProp && resolvedAnimate)) return
|
|
1857
2119
|
// Skip first run if we mounted with initial={false} AND the variant hasn't changed
|
|
1858
2120
|
if (mountedWithInitialFalse) {
|
|
1859
2121
|
// Only skip if the variant is the same as what we mounted with
|
|
@@ -1914,9 +2176,9 @@
|
|
|
1914
2176
|
dataPath = 6
|
|
1915
2177
|
isLoaded = 'initial'
|
|
1916
2178
|
initialAnimationTriggered = true
|
|
1917
|
-
if (
|
|
2179
|
+
if (declarativeAnimateProp && typeof declarativeAnimateProp !== 'string') {
|
|
1918
2180
|
objectAnimateRanOnMount = true
|
|
1919
|
-
lastAnimatePropJson = JSON.stringify(
|
|
2181
|
+
lastAnimatePropJson = JSON.stringify(declarativeAnimateProp)
|
|
1920
2182
|
}
|
|
1921
2183
|
finishOptimizedAppearAnimation(optimizedAppearId)
|
|
1922
2184
|
.then(() => {
|
|
@@ -1980,8 +2242,8 @@
|
|
|
1980
2242
|
|
|
1981
2243
|
// Mark that we're triggering the initial animation to prevent duplicate runs
|
|
1982
2244
|
initialAnimationTriggered = true
|
|
1983
|
-
if (
|
|
1984
|
-
lastAnimatePropJson = JSON.stringify(
|
|
2245
|
+
if (declarativeAnimateProp && typeof declarativeAnimateProp !== 'string') {
|
|
2246
|
+
lastAnimatePropJson = JSON.stringify(declarativeAnimateProp)
|
|
1985
2247
|
}
|
|
1986
2248
|
|
|
1987
2249
|
// IMPORTANT: Start the animation BEFORE changing isLoaded.
|
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';
|
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';
|
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
|
*
|
|
@@ -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.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { animate } from 'motion';
|
|
2
2
|
const layoutSizeAnimationAttribute = 'data-layout-size-animation';
|
|
3
|
-
const
|
|
3
|
+
const roundedPx = (value) => `${Math.max(0, Math.round(value))}px`;
|
|
4
|
+
const mix = (from, to, progress) => from + (to - from) * progress;
|
|
4
5
|
const isViewportOffscreen = (el) => {
|
|
5
6
|
if (typeof window === 'undefined')
|
|
6
7
|
return false;
|
|
@@ -26,23 +27,29 @@ const runBoxSizeAnimation = (el, transforms, transition) => {
|
|
|
26
27
|
if (child.style.willChange === 'transform')
|
|
27
28
|
child.style.willChange = '';
|
|
28
29
|
}
|
|
29
|
-
el.style.width =
|
|
30
|
-
el.style.height =
|
|
30
|
+
el.style.width = roundedPx(prevWidth);
|
|
31
|
+
el.style.height = roundedPx(prevHeight);
|
|
31
32
|
const sizedRect = el.getBoundingClientRect();
|
|
32
33
|
const residualDx = nextRect.left + dx - sizedRect.left;
|
|
33
34
|
const residualDy = nextRect.top + dy - sizedRect.top;
|
|
34
35
|
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
36
|
if (shouldTranslate) {
|
|
40
|
-
keyframes.x = [residualDx, 0];
|
|
41
|
-
keyframes.y = [residualDy, 0];
|
|
42
37
|
el.style.transformOrigin = '0 0';
|
|
43
|
-
el.style.transform = `translate(${residualDx}px, ${residualDy}px)`;
|
|
38
|
+
el.style.transform = `translate(${Math.round(residualDx)}px, ${Math.round(residualDy)}px)`;
|
|
44
39
|
}
|
|
45
|
-
const
|
|
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
|
+
});
|
|
46
53
|
let removeScrollListener;
|
|
47
54
|
let offscreenRaf = null;
|
|
48
55
|
let cleanupRan = false;
|
|
@@ -278,7 +285,7 @@ export const observeLayoutChanges = (el, onChange) => {
|
|
|
278
285
|
const attributeObserver = new MutationObserver(() => schedule());
|
|
279
286
|
attributeObserver.observe(el, {
|
|
280
287
|
attributes: true,
|
|
281
|
-
attributeFilter: ['class', '
|
|
288
|
+
attributeFilter: ['class', 'data-presence-layout-hold']
|
|
282
289
|
});
|
|
283
290
|
const childListObserver = new MutationObserver(() => schedule());
|
|
284
291
|
childListObserver.observe(el, {
|
|
@@ -16,6 +16,8 @@ export interface MotionDomProjectionUpdateOptions {
|
|
|
16
16
|
layout?: LayoutOption;
|
|
17
17
|
/** Shared layout id used by upstream projection matching. */
|
|
18
18
|
layoutId?: string;
|
|
19
|
+
/** Tracks scroll on this element for descendant layout projection. */
|
|
20
|
+
layoutScroll?: boolean;
|
|
19
21
|
/** Transition passed to the upstream layout animation builder. */
|
|
20
22
|
transition?: Transition;
|
|
21
23
|
/** Inline style props passed through to the visual element. */
|
|
@@ -29,6 +31,7 @@ export interface MotionDomProjectionUpdateOptions {
|
|
|
29
31
|
* and `HTMLVisualElement` internals Framer Motion uses.
|
|
30
32
|
*/
|
|
31
33
|
export declare class MotionDomProjectionAdapter {
|
|
34
|
+
private static adapters;
|
|
32
35
|
readonly visualElement: ProjectionVisualElement;
|
|
33
36
|
readonly projection: IProjectionNode<HTMLElement>;
|
|
34
37
|
private element;
|
|
@@ -121,6 +124,32 @@ export declare class MotionDomProjectionAdapter {
|
|
|
121
124
|
* ```
|
|
122
125
|
*/
|
|
123
126
|
commitObservedLayoutChange(): void;
|
|
127
|
+
/**
|
|
128
|
+
* Finish any active upstream layout animation in this subtree.
|
|
129
|
+
*
|
|
130
|
+
* @returns Nothing.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* adapter.finishAnimation()
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
finishAnimation(): void;
|
|
138
|
+
/**
|
|
139
|
+
* Check whether this projection subtree has an active layout animation.
|
|
140
|
+
*
|
|
141
|
+
* @returns `true` when this projection subtree is currently animating.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* if (adapter.isAnimating()) adapter.finishAnimation()
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
isAnimating(): boolean;
|
|
149
|
+
private seedCachedSnapshotsForSubtree;
|
|
150
|
+
private finishAnimationForSubtree;
|
|
151
|
+
private isAnimatingSubtree;
|
|
152
|
+
private updatePathScroll;
|
|
124
153
|
private refreshCachedLayout;
|
|
125
154
|
}
|
|
126
155
|
export {};
|
|
@@ -42,6 +42,7 @@ const animationTypeForLayout = (layout) => typeof layout === 'string' && animati
|
|
|
42
42
|
* and `HTMLVisualElement` internals Framer Motion uses.
|
|
43
43
|
*/
|
|
44
44
|
export class MotionDomProjectionAdapter {
|
|
45
|
+
static adapters = new WeakMap();
|
|
45
46
|
visualElement;
|
|
46
47
|
projection;
|
|
47
48
|
element = null;
|
|
@@ -59,6 +60,7 @@ export class MotionDomProjectionAdapter {
|
|
|
59
60
|
}, { allowProjection: true });
|
|
60
61
|
this.projection = new HTMLProjectionNode(this.visualElement.latestValues, parent?.projection);
|
|
61
62
|
this.visualElement.projection = this.projection;
|
|
63
|
+
MotionDomProjectionAdapter.adapters.set(this.projection, this);
|
|
62
64
|
}
|
|
63
65
|
/**
|
|
64
66
|
* Update projection options from current Svelte props.
|
|
@@ -82,6 +84,7 @@ export class MotionDomProjectionAdapter {
|
|
|
82
84
|
this.projection.setOptions({
|
|
83
85
|
layout: options.layout,
|
|
84
86
|
layoutId: options.layoutId,
|
|
87
|
+
layoutScroll: options.layoutScroll,
|
|
85
88
|
animationType: animationTypeForLayout(options.layout),
|
|
86
89
|
transition: options.transition,
|
|
87
90
|
visualElement: this.visualElement
|
|
@@ -104,6 +107,7 @@ export class MotionDomProjectionAdapter {
|
|
|
104
107
|
if (this.element)
|
|
105
108
|
this.unmount();
|
|
106
109
|
this.element = element;
|
|
110
|
+
MotionDomProjectionAdapter.adapters.set(this.projection, this);
|
|
107
111
|
this.visualElement.mount(element);
|
|
108
112
|
this.seedLayout();
|
|
109
113
|
}
|
|
@@ -124,6 +128,7 @@ export class MotionDomProjectionAdapter {
|
|
|
124
128
|
this.projection.scheduleCheckAfterUnmount();
|
|
125
129
|
this.visualElement.unmount();
|
|
126
130
|
visualElementStore.delete(element);
|
|
131
|
+
MotionDomProjectionAdapter.adapters.delete(this.projection);
|
|
127
132
|
this.element = null;
|
|
128
133
|
this.lastLayout = undefined;
|
|
129
134
|
}
|
|
@@ -138,7 +143,7 @@ export class MotionDomProjectionAdapter {
|
|
|
138
143
|
* ```
|
|
139
144
|
*/
|
|
140
145
|
willUpdate() {
|
|
141
|
-
if (!this.element || !this.
|
|
146
|
+
if (!this.element || !this.layout)
|
|
142
147
|
return;
|
|
143
148
|
this.projection.willUpdate();
|
|
144
149
|
}
|
|
@@ -153,7 +158,7 @@ export class MotionDomProjectionAdapter {
|
|
|
153
158
|
* ```
|
|
154
159
|
*/
|
|
155
160
|
didUpdate() {
|
|
156
|
-
if (!this.element || !this.
|
|
161
|
+
if (!this.element || !this.layout)
|
|
157
162
|
return;
|
|
158
163
|
this.projection.root?.didUpdate();
|
|
159
164
|
this.refreshCachedLayout();
|
|
@@ -171,6 +176,7 @@ export class MotionDomProjectionAdapter {
|
|
|
171
176
|
seedLayout() {
|
|
172
177
|
if (!this.element)
|
|
173
178
|
return;
|
|
179
|
+
this.updatePathScroll();
|
|
174
180
|
this.projection.isLayoutDirty = true;
|
|
175
181
|
this.projection.updateLayout();
|
|
176
182
|
this.lastLayout = cloneMeasurements(this.projection.layout);
|
|
@@ -191,19 +197,80 @@ export class MotionDomProjectionAdapter {
|
|
|
191
197
|
* ```
|
|
192
198
|
*/
|
|
193
199
|
commitObservedLayoutChange() {
|
|
194
|
-
if (!this.element || !this.
|
|
200
|
+
if (!this.element || !this.layout)
|
|
195
201
|
return;
|
|
196
|
-
|
|
197
|
-
if (!snapshot) {
|
|
202
|
+
if (!this.lastLayout) {
|
|
198
203
|
this.seedLayout();
|
|
199
204
|
return;
|
|
200
205
|
}
|
|
201
206
|
this.projection.root?.startUpdate();
|
|
202
|
-
this.projection
|
|
203
|
-
this.projection.isLayoutDirty = true;
|
|
207
|
+
this.seedCachedSnapshotsForSubtree(this.projection);
|
|
204
208
|
this.projection.root?.didUpdate();
|
|
205
209
|
this.refreshCachedLayout();
|
|
206
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Finish any active upstream layout animation in this subtree.
|
|
213
|
+
*
|
|
214
|
+
* @returns Nothing.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* adapter.finishAnimation()
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
finishAnimation() {
|
|
222
|
+
if (!this.element || !this.layout)
|
|
223
|
+
return;
|
|
224
|
+
this.finishAnimationForSubtree(this.projection);
|
|
225
|
+
this.seedLayout();
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Check whether this projection subtree has an active layout animation.
|
|
229
|
+
*
|
|
230
|
+
* @returns `true` when this projection subtree is currently animating.
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* if (adapter.isAnimating()) adapter.finishAnimation()
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
isAnimating() {
|
|
238
|
+
return this.isAnimatingSubtree(this.projection);
|
|
239
|
+
}
|
|
240
|
+
seedCachedSnapshotsForSubtree(projection) {
|
|
241
|
+
const adapter = MotionDomProjectionAdapter.adapters.get(projection);
|
|
242
|
+
const snapshot = cloneMeasurements(adapter?.lastLayout);
|
|
243
|
+
if (snapshot && projection.options.layout) {
|
|
244
|
+
projection.snapshot = snapshot;
|
|
245
|
+
projection.isLayoutDirty = true;
|
|
246
|
+
}
|
|
247
|
+
for (const child of projection.children) {
|
|
248
|
+
this.seedCachedSnapshotsForSubtree(child);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
finishAnimationForSubtree(projection) {
|
|
252
|
+
projection.finishAnimation();
|
|
253
|
+
projection.targetDelta = projection.relativeTarget = projection.target = undefined;
|
|
254
|
+
projection.isProjectionDirty = true;
|
|
255
|
+
projection.scheduleRender();
|
|
256
|
+
for (const child of projection.children) {
|
|
257
|
+
this.finishAnimationForSubtree(child);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
isAnimatingSubtree(projection) {
|
|
261
|
+
if (projection.currentAnimation)
|
|
262
|
+
return true;
|
|
263
|
+
for (const child of projection.children) {
|
|
264
|
+
if (this.isAnimatingSubtree(child))
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
updatePathScroll() {
|
|
270
|
+
for (const node of this.projection.path) {
|
|
271
|
+
node.updateScroll();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
207
274
|
refreshCachedLayout() {
|
|
208
275
|
requestAnimationFrame(() => {
|
|
209
276
|
this.lastLayout = cloneMeasurements(this.projection.layout);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Framer Motion for Svelte 5. Declarative motion.<tag> components with AnimatePresence exit animations, gestures (hover, tap, drag, focus, in-view), variants, FLIP layout animations, shared-layout transitions, spring physics, and scroll-linked motion values. The drop-in Framer Motion alternative for Svelte and SvelteKit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|