-
-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement animation controls and enhance animation features
- Loading branch information
Showing
20 changed files
with
545 additions
and
215 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
packages/motion/src/animation/hooks/animation-controls.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import type { AnimationControls } from '@/animation/types' | ||
import { type MotionState, mountedStates } from '@/state' | ||
import type { Options } from '@/types' | ||
import { invariant } from 'hey-listen' | ||
import { setTarget } from 'framer-motion/dist/es/render/utils/setters.mjs' | ||
import type { VisualElement } from 'framer-motion' | ||
|
||
function stopAnimation(visualElement: VisualElement) { | ||
visualElement.values.forEach(value => value.stop()) | ||
} | ||
|
||
/** | ||
* @public | ||
*/ | ||
export function animationControls(): AnimationControls { | ||
/** | ||
* Track whether the host component has mounted. | ||
*/ | ||
let hasMounted = false | ||
|
||
/** | ||
* A collection of linked component animation controls. | ||
*/ | ||
const subscribers = new Set<MotionState>() | ||
|
||
const controls: AnimationControls = { | ||
subscribe(state) { | ||
subscribers.add(state) | ||
return () => void subscribers.delete(state) | ||
}, | ||
|
||
start(definition, transitionOverride) { | ||
invariant( | ||
hasMounted, | ||
'controls.start() should only be called after a component has mounted. Consider calling within a useEffect hook.', | ||
) | ||
|
||
const animations: Array<Promise<any>> = [] | ||
subscribers.forEach((state) => { | ||
animations.push( | ||
state.animateUpdates({ | ||
directAnimate: definition, | ||
directTransition: transitionOverride, | ||
}) as Promise<any>, | ||
) | ||
}) | ||
|
||
return Promise.all(animations) | ||
}, | ||
|
||
set(definition) { | ||
invariant( | ||
hasMounted, | ||
'controls.set() should only be called after a component has mounted. Consider calling within a useEffect hook.', | ||
) | ||
return subscribers.forEach((state) => { | ||
setValues(state, definition) | ||
}) | ||
}, | ||
|
||
stop() { | ||
subscribers.forEach((state) => { | ||
stopAnimation(state.visualElement) | ||
}) | ||
}, | ||
|
||
mount() { | ||
hasMounted = true | ||
|
||
return () => { | ||
hasMounted = false | ||
controls.stop() | ||
} | ||
}, | ||
} | ||
|
||
return controls | ||
} | ||
|
||
export function setValues( | ||
state: MotionState, | ||
definition: Options['animate'], | ||
) { | ||
if (typeof definition === 'string') { | ||
return setVariants(state, [definition]) | ||
} | ||
else { | ||
setTarget(state.visualElement, definition as any) | ||
} | ||
} | ||
|
||
function setVariants(state: MotionState, variantLabels: string[]) { | ||
const reversedLabels = [...variantLabels].reverse() | ||
const visualElement = state.visualElement | ||
reversedLabels.forEach((key) => { | ||
const variant = visualElement.getVariant(key) | ||
variant && setTarget(visualElement, variant) | ||
|
||
if (visualElement.variantChildren) { | ||
visualElement.variantChildren.forEach((child) => { | ||
setVariants(mountedStates.get(child.current as HTMLElement), variantLabels) | ||
}) | ||
} | ||
}) | ||
} |
File renamed without changes.
46 changes: 46 additions & 0 deletions
46
packages/motion/src/animation/hooks/use-animation-controls.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { animationControls } from '@/animation/hooks/animation-controls' | ||
import { onMounted, onUnmounted } from 'vue' | ||
|
||
/** | ||
* Creates `AnimationControls`, which can be used to manually start, stop | ||
* and sequence animations on one or more components. | ||
* | ||
* The returned `AnimationControls` should be passed to the `animate` property | ||
* of the components you want to animate. | ||
* | ||
* These components can then be animated with the `start` method. | ||
* | ||
* ```jsx | ||
* import { motion, useAnimationControls } from 'motion-v' | ||
* | ||
* export default defineComponent({ | ||
* setup() { | ||
* const controls = useAnimationControls() | ||
* | ||
* controls.start({ | ||
* x: 100, | ||
* transition: { duration: 0.5 }, | ||
* }) | ||
* | ||
* return () => ( | ||
* <motion.div animate={controls} /> | ||
* ) | ||
* } | ||
* }) | ||
* ``` | ||
* | ||
* @returns Animation controller with `start`, `stop`, `set` and `mount` methods | ||
* | ||
* @public | ||
*/ | ||
export function useAnimationControls() { | ||
const controls = animationControls() | ||
let unmount: () => void | ||
onMounted(() => { | ||
unmount = controls.mount() | ||
}) | ||
onUnmounted(() => { | ||
unmount() | ||
}) | ||
return controls | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './use-animate' | ||
export * from './hooks/use-animate' | ||
export * from './hooks/use-animation-controls' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,74 @@ | ||
// import type { AnimationControls as FramerAnimationControls, VisualElement } from 'framer-motion' | ||
import type { MotionState } from '@/state' | ||
import type { $Transition, Options } from '@/types' | ||
/** | ||
* @public | ||
*/ | ||
export interface AnimationControls { | ||
/** | ||
* Subscribes a component's animation controls to this. | ||
* | ||
* @param controls - The controls to subscribe | ||
* @returns An unsubscribe function. | ||
* | ||
* @internal | ||
*/ | ||
subscribe: (state: MotionState) => () => void | ||
|
||
// export interface AnimationControls extends FramerAnimationControls { | ||
// /** | ||
// * Subscribes a component's animation controls to this. | ||
// * | ||
// * @param controls - The controls to subscribe | ||
// * @returns An unsubscribe function. | ||
// * | ||
// * @internal | ||
// */ | ||
// subscribe: (visualElement: VisualElement) => () => void | ||
// } | ||
/** | ||
* Starts an animation on all linked components. | ||
* | ||
* @remarks | ||
* | ||
* ```jsx | ||
* controls.start("variantLabel") | ||
* controls.start({ | ||
* x: 0, | ||
* transition: { duration: 1 } | ||
* }) | ||
* ``` | ||
* | ||
* @param definition - Properties or variant label to animate to | ||
* @param transition - Optional `transtion` to apply to a variant | ||
* @returns - A `Promise` that resolves when all animations have completed. | ||
* | ||
* @public | ||
*/ | ||
start: ( | ||
definition: Options['animate'], | ||
transitionOverride?: $Transition | ||
) => Promise<any> | ||
|
||
/** | ||
* Instantly set to a set of properties or a variant. | ||
* | ||
* ```jsx | ||
* // With properties | ||
* controls.set({ opacity: 0 }) | ||
* | ||
* // With variants | ||
* controls.set("hidden") | ||
* ``` | ||
* | ||
* @privateRemarks | ||
* We could perform a similar trick to `.start` where this can be called before mount | ||
* and we maintain a list of of pending actions that get applied on mount. But the | ||
* expectation of `set` is that it happens synchronously and this would be difficult | ||
* to do before any children have even attached themselves. It's also poor practise | ||
* and we should discourage render-synchronous `.start` calls rather than lean into this. | ||
* | ||
* @public | ||
*/ | ||
set: (definition: Options['animate']) => void | ||
|
||
/** | ||
* Stops animations on all linked components. | ||
* | ||
* ```jsx | ||
* controls.stop() | ||
* ``` | ||
* | ||
* @public | ||
*/ | ||
stop: () => void | ||
mount: () => () => void | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import type { AnimationControls } from './types' | ||
|
||
export function isAnimationControls(v?: unknown): v is AnimationControls { | ||
return ( | ||
v !== null | ||
&& typeof v === 'object' | ||
&& typeof (v as AnimationControls).start === 'function' | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { isAnimationControls } from '@/animation/utils' | ||
import { Feature } from '@/features/feature' | ||
import type { MotionState } from '@/state' | ||
|
||
export class AnimationFeature extends Feature { | ||
unmountControls?: () => void | ||
constructor(state: MotionState) { | ||
super(state) | ||
} | ||
|
||
updateAnimationControlsSubscription() { | ||
const { animate } = this.state.options | ||
if (isAnimationControls(animate)) { | ||
this.unmountControls = animate.subscribe(this.state) | ||
} | ||
} | ||
|
||
/** | ||
* Subscribe any provided AnimationControls to the component's VisualElement | ||
*/ | ||
mount() { | ||
this.updateAnimationControlsSubscription() | ||
} | ||
|
||
update() { | ||
const { animate } = this.state.options | ||
const { animate: prevAnimate } = this.state.visualElement.prevProps || {} | ||
if (animate !== prevAnimate) { | ||
this.updateAnimationControlsSubscription() | ||
} | ||
} | ||
|
||
unmount() { | ||
this.unmountControls?.() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.