Skip to content

Commit

Permalink
Merge pull request #2440 from framer/feature/optimised-appear-v2
Browse files Browse the repository at this point in the history
Optimised appear animations V2
  • Loading branch information
mergetron[bot] authored Dec 8, 2023
2 parents a0be835 + e29abca commit d6565a3
Show file tree
Hide file tree
Showing 14 changed files with 331 additions and 92 deletions.
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) {
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 &&
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

0 comments on commit d6565a3

Please sign in to comment.