Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimised appear animations V2 #2440

Merged
merged 17 commits into from
Dec 8, 2023
135 changes: 135 additions & 0 deletions dev/optimized-appear/defer-handoff.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 = 2
const x = motionValue(0)

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: 100, 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(() => {
/**
* 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.
*/
animate(
box,
{ scale: 2, opacity: 0.1 },
{ duration: 0.3, ease: "linear" }
).then(() => {
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 (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(100px)"],
{
duration: duration * 1000,
ease: "linear",
},
(animation) => {
setTimeout(() => {
ReactDOM.hydrateRoot(root, Component)
}, (duration * 1000) / 2)
}
)
</script>
</body>
</html>
109 changes: 109 additions & 0 deletions dev/optimized-appear/persist-optimised-animation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<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 = 2
const x = motionValue(0)

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: 100, 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(() => {
box.style.transform = "scale(2)"

const { width } = box.getBoundingClientRect()
if (width !== 100) {
showError(
document.getElementById("box"),
`Setting transform became visible, which means optimised animation has been prematurely cancelled. Width was ${width}px instead of 200px.`
)
}
}, 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(100px)"],
{
duration: duration * 1000,
ease: "linear",
},
(animation) => {
setTimeout(() => {
ReactDOM.hydrateRoot(root, Component)
}, (duration * 1000) / 2)
}
)
</script>
</body>
</html>
6 changes: 3 additions & 3 deletions dev/package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"name": "framer-motion--dev",
"version": "10.16.15",
"version": "10.16.16-alpha.2",
"private": true,
"scripts": {
"dev": "webpack serve --config ./webpack/config.js --hot"
},
"dependencies": {
"@react-three/drei": "^7.27.3",
"@react-three/fiber": "^8.2.2",
"framer-motion": "^10.16.15",
"framer-motion-3d": "^10.16.15",
"framer-motion": "^10.16.16-alpha.2",
"framer-motion-3d": "^10.16.16-alpha.2",
"path-browserify": "^1.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "10.16.15",
"version": "10.16.16-alpha.2",
"packages": [
"packages/*"
],
Expand Down
6 changes: 3 additions & 3 deletions packages/framer-motion-3d/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "framer-motion-3d",
"version": "10.16.15",
"version": "10.16.16-alpha.2",
"description": "A simple and powerful React animation library for @react-three/fiber",
"main": "dist/cjs/index.js",
"module": "dist/es/index.mjs",
Expand Down Expand Up @@ -46,7 +46,7 @@
"postpublish": "git push --tags"
},
"dependencies": {
"framer-motion": "^10.16.15",
"framer-motion": "^10.16.16-alpha.2",
"react-merge-refs": "^2.0.1"
},
"peerDependencies": {
Expand All @@ -60,5 +60,5 @@
"@react-three/test-renderer": "^9.0.0",
"@rollup/plugin-commonjs": "^22.0.1"
},
"gitHead": "2ccbc6d1dbf9589758548135b09c15e7845ce3af"
"gitHead": "d19ddfe7a56ccc372d6a6795eff3ce539eb39c41"
}
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 @@
["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.html","portal.html","resync-delay.html","resync.html","start-after-hydration.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"]
10 changes: 5 additions & 5 deletions packages/framer-motion/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "framer-motion",
"version": "10.16.15",
"version": "10.16.16-alpha.2",
"description": "A simple and powerful JavaScript animation library",
"main": "dist/cjs/index.js",
"module": "dist/es/index.mjs",
Expand Down Expand Up @@ -93,28 +93,28 @@
},
{
"path": "./dist/size-rollup-dom-animation.js",
"maxSize": "15.1 kB"
"maxSize": "15.2 kB"
},
{
"path": "./dist/size-rollup-dom-max.js",
"maxSize": "26.5 kB"
},
{
"path": "./dist/size-rollup-animate.js",
"maxSize": "16.5 kB"
"maxSize": "16.52 kB"
},
{
"path": "./dist/size-webpack-m.js",
"maxSize": "5.45 kB"
},
{
"path": "./dist/size-webpack-dom-animation.js",
"maxSize": "19.7 kB"
"maxSize": "19.75 kB"
},
{
"path": "./dist/size-webpack-dom-max.js",
"maxSize": "31.8 kB"
}
],
"gitHead": "2ccbc6d1dbf9589758548135b09c15e7845ce3af"
"gitHead": "d19ddfe7a56ccc372d6a6795eff3ce539eb39c41"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EasingDefinition } from "../../../easing/types"
import { frame, cancelFrame, frameData } from "../../../frameloop"
import { frame, cancelFrame } from "../../../frameloop"
import type { VisualElement } from "../../../render/VisualElement"
import type { MotionValue } from "../../../value"
import { AnimationPlaybackControls, ValueAnimationOptions } from "../../types"
Expand Down Expand Up @@ -136,20 +136,6 @@ export function createAcceleratedAnimation(
}
)

/**
* WAAPI animations don't resolve startTime synchronously. But a blocked
* thread could delay the startTime resolution by a noticeable amount.
* For synching handoff animations with the new Motion animation we want
* to ensure startTime is synchronously set.
*/
if (options.syncStart) {
Copy link
Contributor

Choose a reason for hiding this comment

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

great that we can get rid of this! 🎉

animation.startTime = frameData.isProcessing
? frameData.timestamp
: document.timeline
? document.timeline.currentTime
: performance.now()
}

const cancelAnimation = () => animation.cancel()

const safeCancel = () => {
Expand Down
12 changes: 11 additions & 1 deletion packages/framer-motion/src/animation/interfaces/motion-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const animateMotionValue = (
valueName: string,
value: MotionValue,
target: ResolvedValueTarget,
transition: Transition & { elapsed?: number } = {}
transition: Transition & { elapsed?: number; isHandoff?: boolean } = {}
): StartAnimation => {
return (onComplete: VoidFunction): AnimationPlaybackControls => {
const valueTransition = getValueTransition(transition, valueName) || {}
Expand Down Expand Up @@ -118,8 +118,18 @@ export const animateMotionValue = (
* Animate via WAAPI if possible.
*/
if (
/**
* If this is a handoff animation, the optimised animation will be running via
* WAAPI. Therefore, this animation must be JS to ensure it runs "under" the
* optimised animation.
*/
!transition.isHandoff &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add a comment here to explain why we want to animate individual transforms when it's a handoff one.

value.owner &&
value.owner.current instanceof HTMLElement &&
/**
* If we're outputting values to onUpdate then we can't use WAAPI as there's
* no way to read the value from WAAPI every frame.
*/
!value.owner.getProps().onUpdate
) {
const acceleratedAnimation = createAcceleratedAnimation(
Expand Down
Loading