Skip to content

Defer keyframe resolution#2448

Merged
mergetron[bot] merged 44 commits intomainfrom
fix/async-animation
Mar 12, 2024
Merged

Defer keyframe resolution#2448
mergetron[bot] merged 44 commits intomainfrom
fix/async-animation

Conversation

@mattgperry
Copy link
Copy Markdown
Collaborator

@mattgperry mattgperry commented Dec 14, 2023

When keyframes aren't inherently mixable, we make them mixable by measuring the DOM or resolving values from it.

animate(element, {
  height: "auto", // What is auto?
  width: ["50%", "100px"], // How do these mix?
  x: [null, 100], // What is the initial keyframe?
  background: "var(--tint)" // What color is this?
})

This is currently done synchronously per animate() call / motion component animation which can lead to layout thrashing when many are performed and/or when mixed in amongst other layout-measuring effects.

Screenshot 2024-02-27 at 10 50 19

Solution

This PR defers keyframe resolution until the following animation frame. All reads and writes are batched so we have at max 1 style recalculation/ layout reflow.

It also refactors JS and WAAPI animations into a MainThreadAnimation and AcceleratedAnimation class inheriting from BaseAnimation to share as much of this new logic as possible.

Animations still return synchronously-available animation controls like .play() / .time etc. As much of this is computed or otherwise relies on resolved keyframes, they internally use this.resolved which is behind a getter. This getter will synchronously flush all pending animations as a de-opt when accessed. Some methods have already been changed to allow pending operations but this can be improved further. In the absolute worst case, where animations are created/accessed

const a = animate()
a.time

const b = animate()
b.time

Performance is the same as today. But in the best case, cold start performance is 2.5x faster and unit conversion performance is 6x faster, and much of that work is lifted out of the effect and into the next animation frame.

Screenshot 2024-02-27 at 10 56 39

Bonus:

  • Support unit conversion and CSS variables in keyframes

Fixes #2168
Fixes #2309
Fixes #2339

@mattgperry mattgperry force-pushed the fix/async-animation branch 2 times, most recently from 7d7fd64 to 442a1bf Compare December 15, 2023 14:01
@mattgperry mattgperry force-pushed the fix/async-animation branch 3 times, most recently from f22a711 to 76580f0 Compare January 26, 2024 11:14
@mattgperry mattgperry marked this pull request as ready for review February 27, 2024 09:42
@@ -28,15 +28,15 @@
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can ignore the /benchmarks

chain = chain
.trigger("pointermove", 360, baseY + y, { force: true })
.wait(50)
.wait(100)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flakey test

{ duration: 1, transform: { duration: 2 } }
)

await nextFrame()
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WAAPI nextFrame calls generally added as the WAAPI animation is only initialised after keyframes are resolved. We could add more custom logic here to say if something is WAAPI we can resolve fewer keyframes as it can handle CSS vars etc itself

visualElement.values.forEach((value) => value.stop())
}

function setVariants(visualElement: VisualElement, variantLabels: string[]) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved in from utils/setters as only used in this file.

let { elapsed = 0 } = transition
elapsed = elapsed - secondsToMilliseconds(delay)

const keyframes = getKeyframes(
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to can-animate

@adamseckel adamseckel self-requested a review February 28, 2024 09:26
Comment thread packages/framer-motion/src/render/utils/animation-state.ts Outdated
}

readValueFromInstance(instance: SVGElement, key: string) {
// console.log("reading", key, "from", instance)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// console.log("reading", key, "from", instance)

Comment on lines +4 to +36
export function makeNoneKeyframesAnimatable(
unresolvedKeyframes: UnresolvedKeyframes<string | number>,
noneKeyframeIndexes: number[],
name?: string
) {
/**
* If we detected "none"-equivalent keyframes, we need to find a template
*/
let i = 0
let animatableTemplate: string | undefined = undefined
while (i < unresolvedKeyframes.length && !animatableTemplate) {
if (
typeof unresolvedKeyframes[i] === "string" &&
unresolvedKeyframes[i] !== "none" &&
unresolvedKeyframes[i] !== "0"
) {
animatableTemplate = unresolvedKeyframes[i] as string
}
i++
}

if (animatableTemplate && name) {
for (const noneIndex of noneKeyframeIndexes) {
unresolvedKeyframes[noneIndex] = getAnimatableNone(
name,
animatableTemplate
)
}
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having a hard time understanding what this is doing. What is the template, just some keyframe value like a color string or numeric value?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me add a comment - but to explain given a bunch of keyframes

[0, null, "#fff 300px 300px"]

We're looking for one that can serve as a template. In this case we want the animation to output something like the last keyframe so we take that and turn each value into a zero, currently "#fff0 0px 0px" and use that in place of 0.

Comment thread packages/framer-motion/src/gestures/hover.ts
Copy link
Copy Markdown
Collaborator

@adamseckel adamseckel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if before merging this, it would be worthwhile to create a FramerStudio branch with this alpha, and see what tests pass/fail as an indicator of if we are missing anything critical?

) as any

if (name === "height" && this.suspendedScrollY !== undefined) {
window.scrollTo(0, this.suspendedScrollY)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to scroll the window to 0?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're scrolling it to the value it was before measurement - the measurement can fuck with page scroll

@mattgperry mattgperry force-pushed the fix/async-animation branch from 777eb3a to 1175f07 Compare March 12, 2024 09:51
@mergetron mergetron Bot merged commit 24b1c44 into main Mar 12, 2024
@mergetron mergetron Bot deleted the fix/async-animation branch March 12, 2024 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants