Skip to content

Commit

Permalink
Fixing optimised appear
Browse files Browse the repository at this point in the history
  • Loading branch information
mattgperry authored and mergatron[bot] committed May 15, 2024
1 parent c53765c commit e42759b
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 11 deletions.
197 changes: 197 additions & 0 deletions dev/optimized-appear/defer-handoff-block.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
<html>
<head>
<style>
body {
padding: 100px;
margin: 0;
}

#box {
width: 100px;
height: 100px;
background-color: #0077ff;
}

[data-layout-correct="false"] {
background: #dd1144 !important;
opacity: 1 !important;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="../../node_modules/react/umd/react.development.js"></script>
<script src="../../node_modules/react-dom/umd/react-dom.development.js"></script>
<script src="../../node_modules/react-dom/umd/react-dom-server-legacy.browser.development.js"></script>
<script src="../../packages/framer-motion/dist/framer-motion.dev.js"></script>
<script src="../projection/script-assert.js"></script>

<script>
const {
motion,
animateStyle,
animate,
startOptimizedAppearAnimation,
optimizedAppearDataAttribute,
motionValue,
frame,
} = window.Motion
const { matchViewportBox } = window.Assert
const root = document.getElementById("root")

const duration = 4
const x = motionValue(0)
const xTarget = 500

let isFirstFrame = true

// This is the tree to be rendered "server" and client-side.
const Component = React.createElement(motion.div, {
id: "box",
initial: { x: 0, opacity: 0 },
animate: { x: xTarget, opacity: 1 },
transition: { duration, ease: "linear" },
style: { x },
/**
* On animation start, check the values we expect to see here
*/
onAnimationStart: () => {
const box = document.getElementById("box")

box.style.backgroundColor = "green"

setTimeout(() => {
/**
* By blocking the main thread here, we ensure that
* the keyframes
*/
frame.postRender(() => {
console.log(
"Blocking main thread before keyframe resolution"
)
const startTime = performance.now()
while (performance.now() - startTime < 1000) {}
})

let prevLeft = 0
frame.postRender(() => {
const left = box.getBoundingClientRect().left

if (left < prevLeft) {
showError(
document.getElementById("box"),
`Stutter detected`
)
}

prevLeft = left
}, true)

/**
* This animation interrupts the optimised animation. Notably, we are animating
* x in the optimised transform animation and only scale here. This ensures
* that any transform can force the cancellation of the optimised animation on transform,
* not just those involved in the original animation.
*/
const options = { duration: 0.5, ease: "linear" }
let frameCounter = 0

box.style.backgroundColor = "red"

animate(
box,
{ scale: [1, 1] },
{
...options,
onUpdate: () => {
frameCounter++
console.log(
getComputedStyle(box).transform,
box.style.transform,
box.getBoundingClientRect().left
)
},
}
)
// animate(box, { opacity: 0.1 }, options).then(() => {
// return
// frame.postRender(() => {
// if (frameCounter < 5) {
// showError(
// document.getElementById("box"),
// `Fewer animation frames detected than expected`
// )
// }

// if (getComputedStyle(box).opacity !== "0.1") {
// showError(
// document.getElementById("box"),
// `opacity animation didn't interrupt optimised animation. Opacity was ${
// getComputedStyle(box).opacity
// } instead of 0.1.`
// )
// }
// if (getComputedStyle(box).opacity !== "0.1") {
// showError(
// document.getElementById("box"),
// `opacity animation didn't interrupt optimised animation. Opacity was ${
// getComputedStyle(box).opacity
// } instead of 0.1.`
// )
// }

// const { width, left } =
// box.getBoundingClientRect()
// if (Math.round(width) !== 200) {
// showError(
// document.getElementById("box"),
// `scale animation didn't interrupt optimised animation. Width was ${width}px instead of 200px.`
// )
// }

// if (left <= 100) {
// showError(
// document.getElementById("box"),
// `scale animation incorrectly interrupted optimised animation. Left was ${left}px instead of 100px.`
// )
// }
// })
// })
}, 100)
},
[optimizedAppearDataAttribute]: "a",
children: "Content",
})

// Emulate server rendering of element
root.innerHTML = ReactDOMServer.renderToString(Component)

// Start optimised opacity animation
startOptimizedAppearAnimation(
document.getElementById("box"),
"opacity",
[0, 1],
{
duration: duration * 1000,
ease: "linear",
}
)

// Start WAAPI animation
const animation = startOptimizedAppearAnimation(
document.getElementById("box"),
"transform",
["translateX(0px)", `translateX(${xTarget}px)`],
{
duration: duration * 1000,
ease: "linear",
},
(animation) => {
setTimeout(() => {
ReactDOM.hydrateRoot(root, Component)
}, (duration * 1000) / 4)
}
)
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion packages/framer-motion/cypress/fixtures/appear-tests.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
["defer-handoff-layout.html","defer-handoff.html","interrupt-delay-after.html","interrupt-delay-before-accelerated.html","interrupt-delay-before.html","interrupt-spring.html","interrupt-tween-opacity-waapi.html","interrupt-tween-opacity.html","interrupt-tween-transforms.html","interrupt-tween-x.html","persist-optimised-animation.html","persist.html","portal.html","resync-delay.html","resync.html","start-after-hydration.html"]
["defer-handoff-block.html","defer-handoff-layout.html","defer-handoff.html","interrupt-delay-after.html","interrupt-delay-before-accelerated.html","interrupt-delay-before.html","interrupt-spring.html","interrupt-tween-opacity-waapi.html","interrupt-tween-opacity.html","interrupt-tween-transforms.html","interrupt-tween-x.html","persist-optimised-animation.html","persist.html","portal.html","resync-delay.html","resync.html","start-after-hydration.html"]
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,15 @@ export function animateTarget(
let isHandoff = false
if (window.HandoffAppearAnimations) {
const props = visualElement.getProps()
const appearId =
props[optimizedAppearDataAttribute]
const appearId = props[optimizedAppearDataAttribute]

if (appearId) {
const elapsed = window.HandoffAppearAnimations(appearId, key)
const elapsed = window.HandoffAppearAnimations(
appearId,
key,
value,
frame
)

if (elapsed !== null) {
valueTransition.elapsed = elapsed
Expand Down
25 changes: 20 additions & 5 deletions packages/framer-motion/src/animation/optimized-appear/handoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export function handoffOptimizedAppearAnimation(
*
* Remove in early 2024.
*/
_value: MotionValue,
_frame: Batcher
_value?: MotionValue,
frame?: Batcher
): number | null {
const optimisedValueName = transformProps.has(valueName)
? "transform"
Expand All @@ -34,9 +34,24 @@ export function handoffOptimizedAppearAnimation(
const cancelAnimation = () => {
appearAnimationStore.delete(storeId)

try {
animation.cancel()
} catch (error) {}
if (frame) {
/**
* If we've been provided the frameloop as an argument, use it to defer
* cancellation until keyframes of the subsequent animation have been resolved.
* Otherwise cancel immediately.
*/
frame.render(() =>
frame.render(() => {
try {
animation.cancel()
} catch (error) {}
})
)
} else {
try {
animation.cancel()
} catch (error) {}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type { MotionValue } from "../../value"
export type HandoffFunction = (
storeId: string,
valueName: string,
_value?: MotionValue,
_frame?: Batcher
value?: MotionValue,
frame?: Batcher
) => null | number

/**
Expand Down

0 comments on commit e42759b

Please sign in to comment.