Skip to content

Commit

Permalink
Add queue for animation when visualEl is deferred
Browse files Browse the repository at this point in the history
What's changed?
This pull request introduces changes to the animationControls function to handle scenarios where the visualElement might not immediately subscribe, especially in the context of the LazyMotion component.

Introduced Start Queue: An array that temporarily holds animation start calls. This ensures animations are deferred until their respective visual elements have subscribed to the animation controller.

Flush Mechanism: Once a visualElement subscribes, any queued animations are immediately processed and played.

Error Handling: Added robust error handling to cater to potential issues during the animation process. This ensures promises are either resolved upon successful completion or rejected in case of errors.

Benefits:
This change ensures animations are consistently played in scenarios involving delayed visualElement subscriptions, enhancing user experience and ensuring predictable behavior.

How to test:

Integrate the LazyMotion component, described in framer#2292

- Integrate the LazyMotion component into a project.
- Import features dynamically to keep bundle size small.
- Use m-Components.
- Call controls.start() before the visualElement has subscribed - in the effect.

Ensure that animations play consistently, irrespective of when the controls.start() is called in relation to the visualElement subscription.
  • Loading branch information
Gritsch Markus committed Aug 11, 2023
1 parent c938fd9 commit c867add
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 3 deletions.
76 changes: 74 additions & 2 deletions packages/framer-motion/src/animation/hooks/animation-controls.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
import { invariant } from "../../utils/errors"
import { setValues } from "../../render/utils/setters"
import type { VisualElement } from "../../render/VisualElement"
import { AnimationControls } from "../types"
import type {
AnimationControls,
AnimationDefinition,
Transition,
} from "../types"
import { animateVisualElement } from "../interfaces/visual-element"

/**
* Represents an individual animation start call.
* Each item encapsulates:
* - `definition`: The details of the desired animation.
* - `transitionOverride` (optional): Overrides for default transition settings.
* - `resolve`: Promise resolution function, invoked when the animation completes successfully.
* - `reject`: Promise rejection function, invoked upon animation error or failure.
*/
type StartQueueItem = {
definition: AnimationDefinition
transitionOverride?: Transition
resolve: (value?: any) => void
reject: (reason?: any) => void
}

function stopAnimation(visualElement: VisualElement) {
visualElement.values.forEach((value) => value.stop())
}
Expand All @@ -22,9 +41,19 @@ export function animationControls(): AnimationControls {
*/
const subscribers = new Set<VisualElement>()

/**
* `startQueue` is an array that temporarily holds animation start calls, ensuring they are
* deferred until their respective visual elements have subscribed to the animation controller.
*/
const startQueue: Array<StartQueueItem> = []

const controls: AnimationControls = {
subscribe(visualElement) {
subscribers.add(visualElement)

// Upon a new subscription, handle any queued animations.
flushStartQueue()

return () => void subscribers.delete(visualElement)
},

Expand All @@ -34,7 +63,27 @@ export function animationControls(): AnimationControls {
"controls.start() should only be called after a component has mounted. Consider calling within a useEffect hook."
)

const animations: Array<Promise<any>> = []
if (subscribers.size === 0) {
/*
* Return a new promise to keep track of the animation state.
* The promise will be resolved or rejected when the visual element eventually subscribes
* and the queued animations are processed.
*/
return new Promise((resolve, reject) => {
/*
* If there are no subscribers at the moment, add the animation details to the startQueue.
* This ensures that when a visual element eventually subscribes, the queued animations can be processed and played.
*/
startQueue.push({
definition,
transitionOverride,
resolve,
reject,
})
})
}

const animations: Array<Promise<void>> = []
subscribers.forEach((visualElement) => {
animations.push(
animateVisualElement(visualElement, definition, {
Expand Down Expand Up @@ -73,5 +122,28 @@ export function animationControls(): AnimationControls {
},
}

// Helper function to process the startQueue
async function flushStartQueue() {
// Copy startQueue to prevent mutation during iteration, ensuring consistent processing.
const currentQueue = [...startQueue]

// Clear the startQueue by setting its length to 0, efficiently removing all its items.
startQueue.length = 0

for (const item of currentQueue) {
const { definition, transitionOverride, resolve, reject } = item

try {
const result = controls.start(definition, transitionOverride)
if (result instanceof Promise) {
await result // This ensures that any rejection from the promise leads to the catch block.
}
resolve() // Signal successful completion
} catch (error) {
reject(error) // Signal that an error occurred
}
}
}

return controls
}
2 changes: 1 addition & 1 deletion packages/framer-motion/src/animation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export interface AnimationControls {
start(
definition: AnimationDefinition,
transitionOverride?: Transition
): Promise<any>
): Promise<void | Array<void>>

/**
* Instantly set to a set of properties or a variant.
Expand Down

0 comments on commit c867add

Please sign in to comment.