Skip to content

Commit

Permalink
Merge pull request #2672 from framer/fix/appear
Browse files Browse the repository at this point in the history
Fixing optimised appear jump with `useLayoutEffect` and layout animations
  • Loading branch information
mergetron[bot] committed May 22, 2024
2 parents 71bcc54 + c58cc95 commit 24ae541
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 5 deletions.
135 changes: 135 additions & 0 deletions dev/optimized-appear/defer-handoff-layout-useeffect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<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 = 0.5
const x = motionValue(0)

let isFirstFrame = true

function Component() {
const [top, setTop] = React.useState(0)
React.useEffect(() => {
setTop(100)
}, [])

return React.createElement(motion.div, {
id: "box",
initial: { x: 0, opacity: 0 },
animate: { x: 100, opacity: 1 },
transition: {
duration,
ease: "linear",
layout: { ease: () => 0, duration: 10 },
},
style: {
x,
top,
position: "relative",
background: top ? "red" : "blue",
},
layout: true,
onLayoutAnimationStart: () => {
requestAnimationFrame(() => {
const box = document.getElementById("box")
const { top } = box.getBoundingClientRect()

if (top !== 100) {
showError(
box,
`layout animation overridden by optimised animation`
)
}
})
},
onAnimationComplete: () => {
const box = document.getElementById("box")
const { left } = box.getBoundingClientRect()

console.log(left)
if (left !== 200) {
showError(
box,
`optimised animation conflict with layout measurements`
)
}
},
[optimizedAppearDataAttribute]: "a",
children: "Content",
})
}

// Emulate server rendering of element
root.innerHTML = ReactDOMServer.renderToString(
React.createElement(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(100px)"],
{
duration: duration * 1000,
ease: "linear",
},
(animation) => {
setTimeout(() => {
ReactDOM.hydrateRoot(
root,
React.createElement(Component)
)
}, (duration * 1000) / 2)
}
)
</script>
</body>
</html>
134 changes: 134 additions & 0 deletions dev/optimized-appear/defer-handoff-layout-uselayouteffect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<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 = 0.5
const x = motionValue(0)

let isFirstFrame = true

function Component() {
const [top, setTop] = React.useState(0)
React.useLayoutEffect(() => {
setTop(100)
}, [])

return React.createElement(motion.div, {
id: "box",
initial: { x: 0, opacity: 0 },
animate: { x: 100, opacity: 1 },
transition: {
duration,
ease: "linear",
layout: { ease: () => 0, duration: 10 },
},
style: {
x,
top,
position: "relative",
background: top ? "red" : "blue",
},
layout: true,
onLayoutAnimationStart: () => {
requestAnimationFrame(() => {
const box = document.getElementById("box")
const { top } = box.getBoundingClientRect()

if (top !== 100) {
showError(
box,
`layout animation overridden by optimised animation`
)
}
})
},
onAnimationComplete: () => {
const box = document.getElementById("box")
const { left } = box.getBoundingClientRect()

if (left !== 200) {
showError(
box,
`optimised animation conflict with layout measurements`
)
}
},
[optimizedAppearDataAttribute]: "a",
children: "Content",
})
}

// Emulate server rendering of element
root.innerHTML = ReactDOMServer.renderToString(
React.createElement(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(100px)"],
{
duration: duration * 1000,
ease: "linear",
},
(animation) => {
setTimeout(() => {
ReactDOM.hydrateRoot(
root,
React.createElement(Component)
)
}, (duration * 1000) / 2)
}
)
</script>
</body>
</html>
7 changes: 6 additions & 1 deletion dev/optimized-appear/defer-handoff-layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@
ease: "linear",
layout: { ease: () => 0, duration: 10 },
},
style: { x, top, position: "relative" },
style: {
x,
top,
position: "relative",
background: top ? "red" : "blue",
},
layout: true,
onLayoutAnimationStart: () => {
requestAnimationFrame(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function useVisualElement<Instance, RenderState>(
useIsomorphicLayoutEffect(() => {
if (!visualElement) return

microtask.postRender(visualElement.render)
microtask.render(visualElement.render)

/**
* Ideally this function would always run in a useEffect.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,17 @@ export function createProjectionNode<I>({
if (this.isUpdateBlocked()) return

this.isUpdating = true

/**
* If we're running optimised appear animations then these must be
* cancelled before measuring the DOM. This is so we can measure
* the true layout of the element rather than the WAAPI animation
* which will be unaffected by the resetSkewAndRotate step.
*/
if (window.HandoffCancelAllAnimations) {
window.HandoffCancelAllAnimations()
}

this.nodes && this.nodes.forEach(resetSkewAndRotation)
this.animationId++
}
Expand Down Expand Up @@ -647,9 +658,6 @@ export function createProjectionNode<I>({
/**
* Write
*/
if (window.HandoffCancelAllAnimations) {
window.HandoffCancelAllAnimations()
}
this.nodes!.forEach(resetTransformStyle)

/**
Expand Down

0 comments on commit 24ae541

Please sign in to comment.