diff --git a/dev/react/src/examples/Animation-animate.tsx b/dev/react/src/examples/Animation-animate.tsx
index 1abfa949fe..4b511a61be 100644
--- a/dev/react/src/examples/Animation-animate.tsx
+++ b/dev/react/src/examples/Animation-animate.tsx
@@ -1,6 +1,5 @@
+import { motion } from "framer-motion"
import { useEffect, useState } from "react"
-import { motion, motionValue, useAnimate } from "framer-motion"
-import { frame } from "framer-motion"
/**
* An example of the tween transition type
@@ -11,56 +10,18 @@ const style = {
height: 100,
background: "white",
}
-
-const Child = ({ setState }: any) => {
- const [width] = useState(100)
- const [target, setTarget] = useState(0)
- const transition = {
- duration: 10,
- }
-
- const [scope, animate] = useAnimate()
-
+export const App = () => {
+ const [state, setState] = useState(false)
useEffect(() => {
- const controls = animate([
- [
- "div",
- { x: 500, opacity: 0 },
- { type: "spring", duration: 1, bounce: 0 },
- ],
- ])
-
- controls.then(() => {
- controls.play()
- })
-
- return () => controls.stop()
- }, [target])
-
+ setTimeout(() => {
+ setState(true)
+ }, 300)
+ })
return (
-
-
{
- setTarget(target + 100)
- // setWidth(width + 100)
- }}
- initial={{ borderRadius: 10 }}
- />
- {/* setState(false)} /> */}
-
+
)
- return
-}
-
-export const App = () => {
- const [state, setState] = useState(true)
-
- return state &&
}
diff --git a/dev/react/src/examples/Animation-stagger-custom.tsx b/dev/react/src/examples/Animation-stagger-custom.tsx
index 523ec296b9..855d462730 100644
--- a/dev/react/src/examples/Animation-stagger-custom.tsx
+++ b/dev/react/src/examples/Animation-stagger-custom.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect } from "react"
import { useAnimation, distance2D, wrap } from "framer-motion"
import { motion } from "framer-motion"
diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json
index f8c9961b83..b4b5a37e64 100644
--- a/packages/framer-motion/package.json
+++ b/packages/framer-motion/package.json
@@ -85,23 +85,23 @@
"bundlesize": [
{
"path": "./dist/size-rollup-motion.js",
- "maxSize": "33.45 kB"
+ "maxSize": "33.72 kB"
},
{
"path": "./dist/size-rollup-m.js",
- "maxSize": "5.85 kB"
+ "maxSize": "6 kB"
},
{
"path": "./dist/size-rollup-dom-animation.js",
- "maxSize": "16.88 kB"
+ "maxSize": "17 kB"
},
{
"path": "./dist/size-rollup-dom-max.js",
- "maxSize": "28.6 kB"
+ "maxSize": "28.8 kB"
},
{
"path": "./dist/size-rollup-animate.js",
- "maxSize": "17.86 kB"
+ "maxSize": "18 kB"
}
],
"gitHead": "3a6a6e20deb697df3a20201607a48150e2d77255"
diff --git a/packages/framer-motion/src/animation/__tests__/animate.test.tsx b/packages/framer-motion/src/animation/__tests__/animate.test.tsx
index a4b6a0c61b..0b0db19b0d 100644
--- a/packages/framer-motion/src/animation/__tests__/animate.test.tsx
+++ b/packages/framer-motion/src/animation/__tests__/animate.test.tsx
@@ -31,9 +31,7 @@ describe("animate", () => {
const [value, element] = await promise
expect(value.get()).toBe(200)
- expect(element).toHaveStyle(
- "transform: translateX(200px) translateZ(0)"
- )
+ expect(element).toHaveStyle("transform: translateX(200px)")
})
test("correctly animates normal values", async () => {
diff --git a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx
index 8e97587cd3..dd2879e430 100644
--- a/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx
+++ b/packages/framer-motion/src/animation/__tests__/css-variables.test.tsx
@@ -102,8 +102,16 @@ describe("css variables", () => {
const results = await promise
expect(results).toEqual([
- { "--a": "20px", "--color": "rgba(0, 0, 0, 1)" },
- { "--a": "20px", "--color": "rgba(0, 0, 0, 1)" },
+ {
+ "--a": "20px",
+ "--color": "rgba(0, 0, 0, 1)",
+ willChange: "auto",
+ },
+ {
+ "--a": "20px",
+ "--color": "rgba(0, 0, 0, 1)",
+ willChange: "auto",
+ },
])
})
diff --git a/packages/framer-motion/src/animation/__tests__/index.test.tsx b/packages/framer-motion/src/animation/__tests__/index.test.tsx
index c77d481a87..ac1606ea92 100644
--- a/packages/framer-motion/src/animation/__tests__/index.test.tsx
+++ b/packages/framer-motion/src/animation/__tests__/index.test.tsx
@@ -233,7 +233,7 @@ describe("useAnimation", () => {
}
const { container } = render()
expect(container.firstChild as HTMLElement).toHaveStyle(
- "transform: translateX(10px) translateZ(0); background: rgb(255, 255, 255)"
+ "transform: translateX(10px); background: rgb(255, 255, 255)"
)
})
diff --git a/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts b/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts
index d92397c031..ccf850c124 100644
--- a/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts
+++ b/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts
@@ -15,6 +15,7 @@ import {
ValueAnimationOptionsWithDefaults,
} from "./BaseAnimation"
import { MainThreadAnimation } from "./MainThreadAnimation"
+import { acceleratedValues } from "./utils/accelerated-values"
import { animateStyle } from "./waapi"
import { isWaapiSupportedEasing } from "./waapi/easing"
import { getFinalKeyframe } from "./waapi/utils/get-final-keyframe"
@@ -23,19 +24,6 @@ const supportsWaapi = memo(() =>
Object.hasOwnProperty.call(Element.prototype, "animate")
)
-/**
- * A list of values that can be hardware-accelerated.
- */
-const acceleratedValues = new Set([
- "opacity",
- "clipPath",
- "filter",
- "transform",
- // TODO: Can be accelerated but currently disabled until https://issues.chromium.org/issues/41491098 is resolved
- // or until we implement support for linear() easing.
- // "background-color"
-])
-
/**
* 10ms is chosen here as it strikes a balance between smooth
* results (more than one keyframe per frame at 60fps) and
@@ -372,6 +360,9 @@ export class AcceleratedAnimation<
)
}
+ const { onStop } = this.options
+ onStop && onStop()
+
this.cancel()
}
diff --git a/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts b/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts
index 20cc6d53cb..7c0a2168ff 100644
--- a/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts
+++ b/packages/framer-motion/src/animation/animators/MainThreadAnimation.ts
@@ -489,7 +489,6 @@ export class MainThreadAnimation<
this.resolver.cancel()
this.isStopped = true
if (this.state === "idle") return
-
this.teardown()
const { onStop } = this.options
onStop && onStop()
diff --git a/packages/framer-motion/src/animation/animators/utils/accelerated-values.ts b/packages/framer-motion/src/animation/animators/utils/accelerated-values.ts
new file mode 100644
index 0000000000..14db545a50
--- /dev/null
+++ b/packages/framer-motion/src/animation/animators/utils/accelerated-values.ts
@@ -0,0 +1,12 @@
+/**
+ * A list of values that can be hardware-accelerated.
+ */
+export const acceleratedValues = new Set([
+ "opacity",
+ "clipPath",
+ "filter",
+ "transform",
+ // TODO: Can be accelerated but currently disabled until https://issues.chromium.org/issues/41491098 is resolved
+ // or until we implement support for linear() easing.
+ // "background-color"
+])
diff --git a/packages/framer-motion/src/animation/interfaces/motion-value.ts b/packages/framer-motion/src/animation/interfaces/motion-value.ts
index bc9c3baf10..a9f4235327 100644
--- a/packages/framer-motion/src/animation/interfaces/motion-value.ts
+++ b/packages/framer-motion/src/animation/interfaces/motion-value.ts
@@ -21,7 +21,14 @@ export const animateMotionValue =
target: V | UnresolvedKeyframes,
transition: Transition & { elapsed?: number } = {},
element?: VisualElement,
- isHandoff?: boolean
+ isHandoff?: boolean,
+ /**
+ * Currently used to remove values from will-change when an animation ends.
+ * Preferably this would be handled by event listeners on the MotionValue
+ * but these aren't consistent enough yet when considering the different ways
+ * an animation can be cancelled.
+ */
+ onEnd?: VoidFunction
): StartAnimation =>
(onComplete): AnimationPlaybackControls => {
const valueTransition = getValueTransition(transition, name) || {}
@@ -53,7 +60,9 @@ export const animateMotionValue =
onComplete: () => {
onComplete()
valueTransition.onComplete && valueTransition.onComplete()
+ onEnd && onEnd()
},
+ onStop: onEnd,
name,
motionValue: value,
element: isHandoff ? undefined : element,
diff --git a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts
index c28d9adac6..4dc27c9192 100644
--- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts
+++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts
@@ -4,12 +4,12 @@ import type { VisualElement } from "../../render/VisualElement"
import type { TargetAndTransition } from "../../types"
import type { VisualElementAnimationOptions } from "./types"
import { animateMotionValue } from "./motion-value"
-import { isWillChangeMotionValue } from "../../value/use-will-change/is"
import { setTarget } from "../../render/utils/setters"
import { AnimationPlaybackControls } from "../types"
import { getValueTransition } from "../utils/transitions"
import { frame } from "../../frameloop"
import { getOptimisedAppearId } from "../optimized-appear/get-appear-id"
+import { addValueToWillChange } from "../../value/use-will-change/add-will-change"
/**
* Decide whether we should block this animation. Previously, we achieved this
@@ -39,8 +39,6 @@ export function animateTarget(
...target
} = targetAndTransition
- const willChange = visualElement.getValue("willChange")
-
if (transitionOverride) transition = transitionOverride
const animations: AnimationPlaybackControls[] = []
@@ -103,18 +101,14 @@ export function animateTarget(
? { type: false }
: valueTransition,
visualElement,
- isHandoff
+ isHandoff,
+ addValueToWillChange(visualElement, key)
)
)
const animation = value.animation
if (animation) {
- if (isWillChangeMotionValue(willChange)) {
- willChange.add(key)
- animation.then(() => willChange.remove(key))
- }
-
animations.push(animation)
}
}
diff --git a/packages/framer-motion/src/animation/utils/create-visual-element.ts b/packages/framer-motion/src/animation/utils/create-visual-element.ts
index a98927487c..31552fd197 100644
--- a/packages/framer-motion/src/animation/utils/create-visual-element.ts
+++ b/packages/framer-motion/src/animation/utils/create-visual-element.ts
@@ -19,12 +19,8 @@ export function createVisualElement(element: HTMLElement | SVGElement) {
},
}
const node = isSVGElement(element)
- ? new SVGVisualElement(options, {
- enableHardwareAcceleration: false,
- })
- : new HTMLVisualElement(options, {
- enableHardwareAcceleration: true,
- })
+ ? new SVGVisualElement(options)
+ : new HTMLVisualElement(options)
node.mount(element as any)
diff --git a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx
index ac9a4e91de..a937f79bf8 100644
--- a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx
+++ b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx
@@ -1,5 +1,5 @@
import { render } from "../../../../jest.setup"
-import { createRef } from "react";
+import { createRef } from "react"
import { act } from "react-dom/test-utils"
import {
AnimatePresence,
@@ -68,9 +68,7 @@ describe("AnimatePresence", () => {
})
const element = await promise
- expect(element).toHaveStyle(
- "transform: translateX(100px) translateZ(0)"
- )
+ expect(element).toHaveStyle("transform: translateX(100px)")
})
test("Animates out a component when its removed", async () => {
@@ -646,9 +644,7 @@ describe("AnimatePresence with custom components", () => {
})
const element = await promise
- expect(element).toHaveStyle(
- "transform: translateX(100px) translateZ(0)"
- )
+ expect(element).toHaveStyle("transform: translateX(100px)")
})
test("Animation controls children of initial={false} don't throw`", async () => {
diff --git a/packages/framer-motion/src/components/Reorder/__tests__/server.ssr.test.tsx b/packages/framer-motion/src/components/Reorder/__tests__/server.ssr.test.tsx
index 5ab9603f8f..633ab039e4 100644
--- a/packages/framer-motion/src/components/Reorder/__tests__/server.ssr.test.tsx
+++ b/packages/framer-motion/src/components/Reorder/__tests__/server.ssr.test.tsx
@@ -13,7 +13,7 @@ describe("Reorder", () => {
const staticMarkup = renderToStaticMarkup()
const string = renderToString()
- const expectedMarkup = ``
+ const expectedMarkup = ``
expect(staticMarkup).toBe(expectedMarkup)
expect(string).toBe(expectedMarkup)
@@ -32,7 +32,7 @@ describe("Reorder", () => {
const staticMarkup = renderToStaticMarkup()
const string = renderToString()
- const expectedMarkup = ``
+ const expectedMarkup = ``
expect(staticMarkup).toBe(expectedMarkup)
expect(string).toBe(expectedMarkup)
diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts
index 4167a66360..a9fe135872 100644
--- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts
+++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts
@@ -33,6 +33,7 @@ import { percent } from "../../value/types/numbers/units"
import { animateMotionValue } from "../../animation/interfaces/motion-value"
import { getContextWindow } from "../../utils/get-context-window"
import { frame } from "../../frameloop"
+import { addValueToWillChange } from "../../value/use-will-change/add-will-change"
export const elementDragControls = new WeakMap<
VisualElement,
@@ -78,6 +79,8 @@ export class VisualElementDragControls {
*/
private elastic = createBox()
+ private removeWillChange: VoidFunction | undefined
+
constructor(visualElement: VisualElement) {
this.visualElement = visualElement
}
@@ -157,6 +160,12 @@ export class VisualElementDragControls {
frame.postRender(() => onDragStart(event, info))
}
+ this.removeWillChange?.()
+ this.removeWillChange = addValueToWillChange(
+ this.visualElement,
+ "transform"
+ )
+
const { animationState } = this.visualElement
animationState && animationState.setActive("whileDrag", true)
}
@@ -235,6 +244,8 @@ export class VisualElementDragControls {
}
private stop(event: PointerEvent, info: PanInfo) {
+ this.removeWillChange?.()
+
const isDragging = this.isDragging
this.cancel()
if (!isDragging) return
@@ -443,13 +454,16 @@ export class VisualElementDragControls {
transition: Transition
) {
const axisValue = this.getAxisMotionValue(axis)
+
return axisValue.start(
animateMotionValue(
axis,
axisValue,
0,
transition,
- this.visualElement
+ this.visualElement,
+ false,
+ addValueToWillChange(this.visualElement, axis)
)
)
}
diff --git a/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx b/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx
index 62da17a455..aa9bbf11b7 100644
--- a/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx
+++ b/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx
@@ -1,8 +1,9 @@
-import { useState } from "react";
+import { useState } from "react"
import { pointerDown, render } from "../../../../jest.setup"
import { BoundingBox, motion, motionValue, MotionValue } from "../../../"
import { MockDrag, drag, deferred, dragFrame, Point, sleep } from "./utils"
import { nextFrame } from "../../__tests__/utils"
+import { WillChangeMotionValue } from "../../../value/use-will-change"
describe("drag", () => {
test("onDragStart fires", async () => {
@@ -27,6 +28,67 @@ describe("drag", () => {
})
describe("dragging", () => {
+ test("willChange is applied correctly", async () => {
+ const willChange = new WillChangeMotionValue("auto")
+ const Component = () => (
+
+
+
+ )
+
+ const { container, rerender } = render()
+ rerender()
+
+ const pointer = await drag(container.firstChild).to(100, 100)
+
+ await nextFrame()
+
+ expect(expect(willChange.get()).toBe("transform"))
+
+ pointer.end()
+ })
+
+ test("willChange is applied correctly when other values are animating", async () => {
+ const Component = () => (
+
+
+
+ )
+
+ const { container, getByTestId, rerender } = render()
+ rerender()
+
+ const pointer = await drag(container.firstChild).to(100, 100)
+
+ await nextFrame()
+
+ expect(getByTestId("draggable")).toHaveStyle("will-change: transform;")
+
+ pointer.end()
+
+ await nextFrame()
+
+ expect(getByTestId("draggable")).toHaveStyle("will-change: transform;")
+ })
+
test("dragStart doesn't fire if dragListener === false", async () => {
const onDragStart = jest.fn()
const Component = () => (
@@ -258,11 +320,17 @@ describe("dragging", () => {
await pointer.to(50, 50)
pointer.end()
- const checkPointer = new Promise((resolve) => {
- setTimeout(() => resolve(x.get()), 40)
+ const endValue = await new Promise((resolve) => {
+ setTimeout(() => {
+ expect(container.firstChild).toHaveStyle(
+ "will-change: transform;"
+ )
+
+ resolve(x.get())
+ }, 40)
})
- return await expect(checkPointer).resolves.toBeGreaterThan(50)
+ return expect(endValue).toBeGreaterThan(50)
})
test.skip("outputs to external values if provided", async () => {
@@ -555,6 +623,7 @@ describe("dragging", () => {
rerender()
const pointer = await drag(getByTestId("child")).to(10, 10)
+
await pointer.to(20, 20)
pointer.end()
@@ -865,7 +934,7 @@ describe("dragging", () => {
pointer.end()
expect(container.firstChild).toHaveStyle(
- "transform: translateX(105px) translateY(0px) translateZ(0)"
+ "transform: translateX(105px) translateY(0px)"
)
})
})
diff --git a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx
index 5b510cc4b3..e99d35c5a4 100644
--- a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx
+++ b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx
@@ -161,7 +161,7 @@ describe("animate prop as object", () => {
)
})
return expect(promise).resolves.toHaveStyle(
- "transform: translateX(20px) scale(0) translateZ(0)"
+ "transform: translateX(20px) scale(0)"
)
})
test("style doesnt overwrite in subsequent renders", async () => {
@@ -224,7 +224,7 @@ describe("animate prop as object", () => {
rerender()
})
return expect(promise).resolves.toHaveStyle(
- "transform: translateY(30px) translateX(30px) translateZ(0)"
+ "transform: translateY(30px) translateX(30px)"
)
})
test("animating between none/block fires onAnimationComplete", async () => {
@@ -920,7 +920,7 @@ describe("animate prop as object", () => {
await nextFrame()
return expect(container.firstChild as Element).toHaveStyle(
- "transform: translateX(0px) translateY(100px) translateZ(0)"
+ "transform: translateX(0px) translateY(100px)"
)
})
@@ -940,7 +940,7 @@ describe("animate prop as object", () => {
await nextFrame()
return expect(container.firstChild as Element).toHaveStyle(
- "transform: translateX(0px) translateY(100px) translateZ(0)"
+ "transform: translateX(0px) translateY(100px)"
)
})
diff --git a/packages/framer-motion/src/motion/__tests__/animated-values.test.tsx b/packages/framer-motion/src/motion/__tests__/animated-values.test.tsx
index 76a6df2869..c8912df96c 100644
--- a/packages/framer-motion/src/motion/__tests__/animated-values.test.tsx
+++ b/packages/framer-motion/src/motion/__tests__/animated-values.test.tsx
@@ -6,7 +6,7 @@ import {
useMotionValue,
useTransform,
} from "../.."
-import { createRef, useRef } from "react";
+import { createRef, useRef } from "react"
const degreesToRadians = (degrees: number) => (degrees * Math.PI) / 180
@@ -30,9 +30,7 @@ describe("values prop", () => {
await promise.then(([x, element]) => {
expect(x).toBe(20)
- expect(element).not.toHaveStyle(
- "transform: translateX(20px) translateZ(0)"
- )
+ expect(element).not.toHaveStyle("transform: translateX(20px)")
})
})
@@ -63,9 +61,7 @@ describe("values prop", () => {
await promise.then(([x, element]) => {
expect(x).toBe(20)
- expect(element).toHaveStyle(
- "transform: translateX(40px) translateZ(0)"
- )
+ expect(element).toHaveStyle("transform: translateX(40px)")
})
})
@@ -110,7 +106,7 @@ describe("values prop", () => {
await promise.then(([x, element]) => {
expect(x).toBe(35)
expect(element).toHaveStyle(
- "transform: translateX(35px) translateY(35px) translateZ(0)"
+ "transform: translateX(35px) translateY(35px)"
)
expect(element).not.toHaveStyle("distance: 50")
})
diff --git a/packages/framer-motion/src/motion/__tests__/component-svg.test.tsx b/packages/framer-motion/src/motion/__tests__/component-svg.test.tsx
index b45b48c957..68839ed083 100644
--- a/packages/framer-motion/src/motion/__tests__/component-svg.test.tsx
+++ b/packages/framer-motion/src/motion/__tests__/component-svg.test.tsx
@@ -1,6 +1,6 @@
import { render } from "../../../jest.setup"
import { motion, motionValue, useMotionValue, useTransform } from "../../"
-import { useRef } from "react";
+import { useRef } from "react"
import { nextFrame } from "../../gestures/__tests__/utils"
describe("SVG", () => {
@@ -12,12 +12,8 @@ describe("SVG", () => {
)
- expect(getByTestId("g")).not.toHaveStyle(
- "transform: translateX(100px) translateZ(0)"
- )
- expect(getByTestId("h")).not.toHaveStyle(
- "transform: translateX(100px) translateZ(0)"
- )
+ expect(getByTestId("g")).not.toHaveStyle("transform: translateX(100px)")
+ expect(getByTestId("h")).not.toHaveStyle("transform: translateX(100px)")
})
test("accepts attrX/attrY/attrScale in types", () => {
diff --git a/packages/framer-motion/src/motion/__tests__/component.test.tsx b/packages/framer-motion/src/motion/__tests__/component.test.tsx
index b4ec7dec4c..417f52d28f 100644
--- a/packages/framer-motion/src/motion/__tests__/component.test.tsx
+++ b/packages/framer-motion/src/motion/__tests__/component.test.tsx
@@ -142,7 +142,7 @@ describe("motion component rendering and styles", () => {
)
expect(container.firstChild).toHaveStyle(
- "transform: translateX(10px) translateZ(0); background: #fff"
+ "transform: translateX(10px); background: #fff"
)
expect(container.firstChild).toHaveStyle("background: #fff")
})
@@ -152,7 +152,7 @@ describe("motion component rendering and styles", () => {
)
expect(container.firstChild).toHaveStyle(
- "transform: translateX(10px) translateZ(0); background: rgb(255, 255, 255)"
+ "transform: translateX(10px); background: rgb(255, 255, 255)"
)
})
@@ -162,7 +162,7 @@ describe("motion component rendering and styles", () => {
)
expect(container.firstChild).toHaveStyle(
- "transform: translateX(10px) translateZ(0); background: rgb(255, 255, 255)"
+ "transform: translateX(10px); background: rgb(255, 255, 255)"
)
})
@@ -171,7 +171,7 @@ describe("motion component rendering and styles", () => {
)
expect(container.firstChild).toHaveStyle(
- "transform: translateX(100px) translateZ(0);"
+ "transform: translateX(100px);"
)
})
@@ -195,9 +195,7 @@ describe("motion component rendering and styles", () => {
)
expect(getByTestId("a")).toHaveStyle("transform: none")
- expect(getByTestId("b")).toHaveStyle(
- "transform: translateX(10px) translateZ(0)"
- )
+ expect(getByTestId("b")).toHaveStyle("transform: translateX(10px)")
})
it("generates style attribute for children if passed initial as variant label", () => {
@@ -242,7 +240,7 @@ describe("motion component rendering and styles", () => {
)
expect(getByTestId("child")).not.toHaveStyle(
- "opacity: 0; transform: translateY(50px) translateZ(0)"
+ "opacity: 0; transform: translateY(50px)"
)
})
diff --git a/packages/framer-motion/src/motion/__tests__/ssr.test.tsx b/packages/framer-motion/src/motion/__tests__/ssr.test.tsx
index 7e2db66918..d404894ed0 100644
--- a/packages/framer-motion/src/motion/__tests__/ssr.test.tsx
+++ b/packages/framer-motion/src/motion/__tests__/ssr.test.tsx
@@ -60,7 +60,7 @@ function runTests(render: (components: any) => string) {
)
expect(div).toBe(
- '
'
+ '
'
)
})
@@ -79,7 +79,7 @@ function runTests(render: (components: any) => string) {
)
expect(customElement).toBe(
- '
'
+ '
'
)
})
@@ -128,7 +128,7 @@ function runTests(render: (components: any) => string) {
)
expect(div).toBe(
- `
`
+ `
`
)
})
@@ -166,7 +166,7 @@ function runTests(render: (components: any) => string) {
)
expect(div).toBe(
- `
`
+ `
`
)
})
@@ -182,7 +182,7 @@ function runTests(render: (components: any) => string) {
const div = render(
)
expect(div).toBe(
- `
`
+ `
`
)
})
@@ -198,7 +198,7 @@ function runTests(render: (components: any) => string) {
const div = render(
)
expect(div).toBe(
- `
`
+ `
`
)
})
@@ -214,7 +214,7 @@ function runTests(render: (components: any) => string) {
const div = render(
)
expect(div).toBe(
- `
`
+ `
`
)
})
diff --git a/packages/framer-motion/src/motion/__tests__/static-prop.test.tsx b/packages/framer-motion/src/motion/__tests__/static-prop.test.tsx
index 90bc32c960..f15ea8b791 100644
--- a/packages/framer-motion/src/motion/__tests__/static-prop.test.tsx
+++ b/packages/framer-motion/src/motion/__tests__/static-prop.test.tsx
@@ -1,6 +1,6 @@
import { render } from "../../../jest.setup"
import { motion, useMotionValue } from "../.."
-import { useEffect } from "react";
+import { useEffect } from "react"
import { motionValue } from "../../value"
import { MotionConfig } from "../../components/MotionConfig"
import { globalProjectionState } from "../../projection/node/state"
@@ -78,7 +78,7 @@ describe("isStatic prop", () => {
rerender(
)
expect(getByTestId("child") as Element).toHaveStyle(
- "transform: translateX(100px) translateZ(0)"
+ "transform: translateX(100px)"
)
})
diff --git a/packages/framer-motion/src/motion/__tests__/style-prop.test.tsx b/packages/framer-motion/src/motion/__tests__/style-prop.test.tsx
index 6444720c58..9d97801319 100644
--- a/packages/framer-motion/src/motion/__tests__/style-prop.test.tsx
+++ b/packages/framer-motion/src/motion/__tests__/style-prop.test.tsx
@@ -37,7 +37,7 @@ describe("style prop", () => {
await nextMicrotask()
expect(container.firstChild as Element).toHaveStyle(
- "transform: translateX(1px) translateZ(0)"
+ "transform: translateX(1px)"
)
rerender(
)
@@ -61,13 +61,13 @@ describe("style prop", () => {
const { container, rerender } = render(
)
expect(container.firstChild as Element).toHaveStyle(
- "transform: translateX(1px) translateZ(0)"
+ "transform: translateX(1px)"
)
rerender(
)
expect(container.firstChild as Element).not.toHaveStyle(
- "transform: translateX(2px) translateZ(0)"
+ "transform: translateX(2px)"
)
})
diff --git a/packages/framer-motion/src/motion/__tests__/transformTemplate.test.tsx b/packages/framer-motion/src/motion/__tests__/transformTemplate.test.tsx
index 6c80c4cced..bf763b91ef 100644
--- a/packages/framer-motion/src/motion/__tests__/transformTemplate.test.tsx
+++ b/packages/framer-motion/src/motion/__tests__/transformTemplate.test.tsx
@@ -14,7 +14,7 @@ describe("transformTemplate", () => {
/>
)
expect(container.firstChild).toHaveStyle(
- "transform: translateY(10px) translateX(10px) translateZ(0)"
+ "transform: translateY(10px) translateX(10px)"
)
})
@@ -28,7 +28,7 @@ describe("transformTemplate", () => {
/>
)
expect(container.firstChild).toHaveStyle(
- "transform: translateY(10px) translateX(10px) translateZ(0)"
+ "transform: translateY(10px) translateX(10px)"
)
rerender(
@@ -43,7 +43,7 @@ describe("transformTemplate", () => {
await nextMicrotask()
expect(container.firstChild).toHaveStyle(
- "transform: translateY(20px) translateX(10px) translateZ(0)"
+ "transform: translateY(20px) translateX(10px)"
)
})
@@ -57,7 +57,7 @@ describe("transformTemplate", () => {
/>
)
expect(container.firstChild).toHaveStyle(
- "transform: translateY(20px) translateX(10px) translateZ(0)"
+ "transform: translateY(20px) translateX(10px)"
)
})
@@ -81,9 +81,7 @@ describe("transformTemplate", () => {
await new Promise((resolve) => frame.postRender(resolve))
- expect(container.firstChild).toHaveStyle(
- "transform: translateX(20px) translateZ(0)"
- )
+ expect(container.firstChild).toHaveStyle("transform: translateX(20px)")
})
it("removes transformTemplate if prop is removed and transform is not changed", async () => {
@@ -98,9 +96,7 @@ describe("transformTemplate", () => {
await new Promise((resolve) => frame.postRender(resolve))
- expect(container.firstChild).toHaveStyle(
- "transform: translateX(10px) translateZ(0)"
- )
+ expect(container.firstChild).toHaveStyle("transform: translateX(10px)")
})
it("removes transformTemplate if prop is removed", async () => {
diff --git a/packages/framer-motion/src/motion/__tests__/transition-keyframes.test.tsx b/packages/framer-motion/src/motion/__tests__/transition-keyframes.test.tsx
index fa8dbd2577..b8022836ba 100644
--- a/packages/framer-motion/src/motion/__tests__/transition-keyframes.test.tsx
+++ b/packages/framer-motion/src/motion/__tests__/transition-keyframes.test.tsx
@@ -25,9 +25,7 @@ describe("keyframes transition", () => {
rerender(
)
})
- expect(promise).resolves.toHaveStyle(
- "transform: translateX(200px) translateZ(0)"
- )
+ expect(promise).resolves.toHaveStyle("transform: translateX(200px)")
})
test("hasUpdated detects only changed keyframe arrays", async () => {
@@ -126,6 +124,8 @@ describe("keyframes transition", () => {
output.push(Math.round(latest.x as number))
}
onAnimationComplete={() => resolve(output)}
+ // Manually setting willChange to auto to prevent changes to willChange triggering onUpdate
+ style={{ willChange: "auto" }}
/>
)
diff --git a/packages/framer-motion/src/motion/__tests__/variant.test.tsx b/packages/framer-motion/src/motion/__tests__/variant.test.tsx
index 5b7ad2ae97..a60eea05f2 100644
--- a/packages/framer-motion/src/motion/__tests__/variant.test.tsx
+++ b/packages/framer-motion/src/motion/__tests__/variant.test.tsx
@@ -4,8 +4,8 @@ import {
pointerUp,
render,
} from "../../../jest.setup"
-import { motion, MotionConfig, useMotionValue } from "../../"
-import { Fragment, useEffect, memo, useState } from "react";
+import { frame, motion, MotionConfig, useMotionValue } from "../../"
+import { Fragment, useEffect, memo, useState } from "react"
import { Variants } from "../../types"
import { motionValue } from "../../value"
import { nextFrame } from "../../gestures/__tests__/utils"
@@ -774,6 +774,7 @@ describe("animate prop as variant", () => {
updateDelayedBy(0)
order.push(1)
}}
+ style={{ willChange: "auto" }}
/>
{
updateDelayedBy(1)
order.push(2)
}}
+ style={{ willChange: "auto" }}
/>
@@ -790,6 +792,7 @@ describe("animate prop as variant", () => {
updateDelayedBy(2)
order.push(3)
}}
+ style={{ willChange: "auto" }}
/>
{
updateDelayedBy(3)
order.push(4)
}}
+ style={{ willChange: "auto" }}
/>
@@ -811,7 +815,11 @@ describe("animate prop as variant", () => {
const promise = new Promise((resolve) => {
let latest = {}
+ let frameCount = 0
+
const onUpdate = (l: { [key: string]: number | string }) => {
+ frameCount++
+ if (frameCount === 2) expect(l.willChange).toBe("transform")
latest = l
}
@@ -821,7 +829,9 @@ describe("animate prop as variant", () => {
initial={{ x: 0, y: 0 }}
animate={{ x: 100, y: 100 }}
transition={{ duration: 0.1 }}
- onAnimationComplete={() => resolve(latest)}
+ onAnimationComplete={() => {
+ frame.postRender(() => resolve(latest))
+ }}
/>
)
@@ -829,7 +839,11 @@ describe("animate prop as variant", () => {
rerender(
)
})
- return expect(promise).resolves.toEqual({ x: 100, y: 100 })
+ return expect(promise).resolves.toEqual({
+ willChange: "auto",
+ x: 100,
+ y: 100,
+ })
})
test("onUpdate doesnt fire if no values have changed", async () => {
@@ -837,12 +851,17 @@ describe("animate prop as variant", () => {
await new Promise
((resolve) => {
const x = motionValue(0)
+
const Component = ({ xTarget = 0 }) => (
{
+ expect(latest.willChange).not.toBe("auto")
+ onUpdate(latest)
+ }}
+ // Manually setting willChange to avoid triggering onUpdate
+ style={{ x, willChange: "transform" }}
/>
)
@@ -984,7 +1003,7 @@ describe("animate prop as variant", () => {
await nextFrame()
expect(element).toHaveStyle("opacity: 1")
- expect(element).toHaveStyle("transform: rotate(1deg) translateZ(0)")
+ expect(element).toHaveStyle("transform: rotate(1deg)")
rerender()
rerender()
@@ -998,7 +1017,7 @@ describe("animate prop as variant", () => {
await nextFrame()
expect(element).toHaveStyle("opacity: 0.5")
- expect(element).toHaveStyle("transform: rotate(0.5deg) translateZ(0)")
+ expect(element).toHaveStyle("transform: rotate(0.5deg)")
// Re-adding value to animated stack will animate value correctly
rerender()
@@ -1006,7 +1025,7 @@ describe("animate prop as variant", () => {
await nextFrame()
expect(element).toHaveStyle("opacity: 1")
- expect(element).toHaveStyle("transform: rotate(1deg) translateZ(0)")
+ expect(element).toHaveStyle("transform: rotate(1deg)")
// While animate is active, changing style doesn't change value
rerender()
@@ -1014,7 +1033,7 @@ describe("animate prop as variant", () => {
await nextFrame()
expect(element).toHaveStyle("opacity: 1")
- expect(element).toHaveStyle("transform: rotate(1deg) translateZ(0)")
+ expect(element).toHaveStyle("transform: rotate(1deg)")
})
test("variants work the same whether defined inline or not", async () => {
@@ -1127,9 +1146,7 @@ describe("animate prop as variant", () => {
await nextFrame()
- expect(element).toHaveStyle(
- "transform: translateX(100px) translateZ(0)"
- )
+ expect(element).toHaveStyle("transform: translateX(100px)")
rerender()
await nextFrame()
diff --git a/packages/framer-motion/src/motion/utils/use-visual-state.ts b/packages/framer-motion/src/motion/utils/use-visual-state.ts
index 880bedfad8..4865da69d8 100644
--- a/packages/framer-motion/src/motion/utils/use-visual-state.ts
+++ b/packages/framer-motion/src/motion/utils/use-visual-state.ts
@@ -14,6 +14,9 @@ import {
isControllingVariants as checkIsControllingVariants,
isVariantNode as checkIsVariantNode,
} from "../../render/utils/is-controlling-variants"
+import { getWillChangeName } from "../../value/use-will-change/get-will-change-name"
+import { addUniqueItem } from "../../utils/array"
+import { TargetAndTransition } from "../../types"
export interface VisualState {
renderState: RenderState
@@ -27,6 +30,7 @@ export type UseVisualState = (
) => VisualState
export interface UseVisualStateConfig {
+ applyWillChange?: boolean
scrapeMotionValuesFromProps: ScrapeMotionValuesFromProps
createRenderState: () => RenderState
onMount?: (
@@ -38,19 +42,22 @@ export interface UseVisualStateConfig {
function makeState(
{
+ applyWillChange = false,
scrapeMotionValuesFromProps,
createRenderState,
onMount,
}: UseVisualStateConfig,
props: MotionProps,
context: MotionContextProps,
- presenceContext: PresenceContextProps | null
+ presenceContext: PresenceContextProps | null,
+ isStatic: boolean
) {
const state: VisualState = {
latestValues: makeLatestValues(
props,
context,
presenceContext,
+ isStatic ? false : applyWillChange,
scrapeMotionValuesFromProps
),
renderState: createRenderState(),
@@ -68,22 +75,58 @@ export const makeUseVisualState =
(props: MotionProps, isStatic: boolean): VisualState => {
const context = useContext(MotionContext)
const presenceContext = useContext(PresenceContext)
- const make = () => makeState(config, props, context, presenceContext)
+ const make = () =>
+ makeState(config, props, context, presenceContext, isStatic)
return isStatic ? make() : useConstant(make)
}
+function addWillChange(willChange: string[], name: string) {
+ const memberName = getWillChangeName(name)
+
+ if (memberName) {
+ addUniqueItem(willChange, memberName)
+ }
+}
+
+function forEachDefinition(
+ props: MotionProps,
+ definition: MotionProps["animate"] | MotionProps["initial"],
+ callback: (
+ target: TargetAndTransition,
+ transitionEnd: ResolvedValues
+ ) => void
+) {
+ const list = Array.isArray(definition) ? definition : [definition]
+ for (let i = 0; i < list.length; i++) {
+ const resolved = resolveVariantFromProps(props, list[i] as any)
+ if (resolved) {
+ const { transitionEnd, transition, ...target } = resolved
+ callback(target, transitionEnd as ResolvedValues)
+ }
+ }
+}
+
function makeLatestValues(
props: MotionProps,
context: MotionContextProps,
presenceContext: PresenceContextProps | null,
+ shouldApplyWillChange: boolean,
scrapeMotionValues: ScrapeMotionValuesFromProps
) {
const values: ResolvedValues = {}
+ const willChange: string[] = []
+ const applyWillChange =
+ shouldApplyWillChange && props.style?.willChange === undefined
const motionValues = scrapeMotionValues(props, {})
for (const key in motionValues) {
values[key] = resolveMotionValue(motionValues[key])
+
+ // If a value is an externally-provided motion value, add it to will-change
+ if (applyWillChange) {
+ addWillChange(willChange, key)
+ }
}
let { initial, animate } = props
@@ -112,13 +155,7 @@ function makeLatestValues(
typeof variantToSet !== "boolean" &&
!isAnimationControls(variantToSet)
) {
- const list = Array.isArray(variantToSet) ? variantToSet : [variantToSet]
- list.forEach((definition) => {
- const resolved = resolveVariantFromProps(props, definition)
- if (!resolved) return
-
- const { transitionEnd, transition, ...target } = resolved
-
+ forEachDefinition(props, variantToSet, (target, transitionEnd) => {
for (const key in target) {
let valueTarget = target[key as keyof typeof target]
@@ -137,12 +174,28 @@ function makeLatestValues(
values[key] = valueTarget as string | number
}
}
- for (const key in transitionEnd)
+ for (const key in transitionEnd) {
values[key] = transitionEnd[
key as keyof typeof transitionEnd
] as string | number
+ }
})
}
+ // Add animating values to will-change
+ if (applyWillChange) {
+ if (animate && initial !== false && !isAnimationControls(animate)) {
+ forEachDefinition(props, animate, (target) => {
+ for (const key in target) {
+ addWillChange(willChange, key)
+ }
+ })
+ }
+
+ if (willChange.length) {
+ values.willChange = willChange.join(",")
+ }
+ }
+
return values
}
diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts
index d9ed3ad59b..571588dfb0 100644
--- a/packages/framer-motion/src/render/VisualElement.ts
+++ b/packages/framer-motion/src/render/VisualElement.ts
@@ -15,7 +15,6 @@ import {
} from "../utils/reduced-motion/state"
import { SubscriptionManager } from "../utils/subscription-manager"
import { motionValue, MotionValue } from "../value"
-import { isWillChangeMotionValue } from "../value/use-will-change/is"
import { isMotionValue } from "../value/utils/is-motion-value"
import { transformProps } from "./html/utils/transform"
import {
@@ -129,7 +128,6 @@ export abstract class VisualElement<
abstract build(
renderState: RenderState,
latestValues: ResolvedValues,
- options: Options,
props: MotionProps
): void
@@ -145,6 +143,12 @@ export abstract class VisualElement<
projection?: IProjectionNode
): void
+ /**
+ * If true, will-change will be applied to the element. Only HTMLVisualElements
+ * currently support this.
+ */
+ applyWillChange = false
+
resolveKeyframes = (
keyframes: UnresolvedKeyframes,
// We use an onComplete callback here rather than a Promise as a Promise
@@ -398,10 +402,6 @@ export abstract class VisualElement<
if (latestValues[key] !== undefined && isMotionValue(value)) {
value.set(latestValues[key], false)
-
- if (isWillChangeMotionValue(willChange)) {
- willChange.add(key)
- }
}
}
}
@@ -550,12 +550,7 @@ export abstract class VisualElement<
notifyUpdate = () => this.notify("Update", this.latestValues)
triggerBuild() {
- this.build(
- this.renderState,
- this.latestValues,
- this.options,
- this.props
- )
+ this.build(this.renderState, this.latestValues, this.props)
}
render = () => {
diff --git a/packages/framer-motion/src/render/dom/create-visual-element.ts b/packages/framer-motion/src/render/dom/create-visual-element.ts
index 214c991e65..c3791a3b98 100644
--- a/packages/framer-motion/src/render/dom/create-visual-element.ts
+++ b/packages/framer-motion/src/render/dom/create-visual-element.ts
@@ -11,9 +11,8 @@ export const createDomVisualElement: CreateVisualElement<
options: VisualElementOptions
) => {
return isSVGComponent(Component)
- ? new SVGVisualElement(options, { enableHardwareAcceleration: false })
+ ? new SVGVisualElement(options)
: new HTMLVisualElement(options, {
allowProjection: Component !== Fragment,
- enableHardwareAcceleration: true,
})
}
diff --git a/packages/framer-motion/src/render/dom/types.ts b/packages/framer-motion/src/render/dom/types.ts
index 1b1974eb0f..84056dfb34 100644
--- a/packages/framer-motion/src/render/dom/types.ts
+++ b/packages/framer-motion/src/render/dom/types.ts
@@ -1,25 +1,7 @@
-import { TransformPoint } from "../../projection/geometry/types"
import { HTMLMotionComponents } from "../html/types"
import { SVGMotionComponents } from "../svg/types"
export interface DOMVisualElementOptions {
- /**
- * A function that can map a page point between spaces. Used by Framer
- * to support dragging and layout animations within scaled space.
- *
- * @public
- */
- transformPagePoint?: TransformPoint
-
- /**
- * Allow `transform` to be set as `"none"` if all transforms are their default
- * values. Switching to this removes the element as a GPU layer which can lead to subtle
- * graphical shifts.
- *
- * @public
- */
- allowTransformNone?: boolean
-
/**
* If `true`, this element will be included in the projection tree.
*
diff --git a/packages/framer-motion/src/render/html/HTMLVisualElement.ts b/packages/framer-motion/src/render/html/HTMLVisualElement.ts
index 45cd66b2ef..6db39d2c28 100644
--- a/packages/framer-motion/src/render/html/HTMLVisualElement.ts
+++ b/packages/framer-motion/src/render/html/HTMLVisualElement.ts
@@ -27,6 +27,8 @@ export class HTMLVisualElement extends DOMVisualElement<
> {
type = "html"
+ applyWillChange = true
+
readValueFromInstance(
instance: HTMLElement,
key: string
@@ -55,15 +57,9 @@ export class HTMLVisualElement extends DOMVisualElement<
build(
renderState: HTMLRenderState,
latestValues: ResolvedValues,
- options: DOMVisualElementOptions,
props: MotionProps
) {
- buildHTMLStyles(
- renderState,
- latestValues,
- options,
- props.transformTemplate
- )
+ buildHTMLStyles(renderState, latestValues, props.transformTemplate)
}
scrapeMotionValuesFromProps(
diff --git a/packages/framer-motion/src/render/html/__tests__/use-props.test.ts b/packages/framer-motion/src/render/html/__tests__/use-props.test.ts
index 1408d3d88e..c69072ebdf 100644
--- a/packages/framer-motion/src/render/html/__tests__/use-props.test.ts
+++ b/packages/framer-motion/src/render/html/__tests__/use-props.test.ts
@@ -14,13 +14,12 @@ describe("HTML useProps", () => {
},
{
x: 3,
- },
- false
+ }
)
)
expect(result.current).toEqual({
- style: { transform: "translateX(3px) translateZ(0)" },
+ style: { transform: "translateX(3px)" },
})
})
@@ -35,8 +34,7 @@ describe("HTML useProps", () => {
},
{
x: 3,
- },
- true
+ }
)
)
@@ -49,7 +47,7 @@ describe("HTML useProps", () => {
const initialState = { x: 100 } as any
const { result, rerender } = renderHook(
- ({ state }: any) => useHTMLProps({}, state, true),
+ ({ state }: any) => useHTMLProps({}, state),
{ initialProps: { state: initialState } }
)
@@ -67,7 +65,7 @@ describe("HTML useProps", () => {
test("should generate the correct props when drag is enabled", () => {
const { result } = renderHook(() =>
- useHTMLProps({ drag: true }, { x: 3 }, true)
+ useHTMLProps({ drag: true }, { x: 3 })
)
expect(result.current).toEqual({
@@ -82,7 +80,7 @@ describe("HTML useProps", () => {
})
const { result: resultX } = renderHook(() =>
- useHTMLProps({ drag: "x" }, { x: 3 }, true)
+ useHTMLProps({ drag: "x" }, { x: 3 })
)
expect(resultX.current).toEqual({
@@ -97,7 +95,7 @@ describe("HTML useProps", () => {
})
const { result: resultY } = renderHook(() =>
- useHTMLProps({ drag: "y" }, { x: 3 }, true)
+ useHTMLProps({ drag: "y" }, { x: 3 })
)
expect(resultY.current).toEqual({
diff --git a/packages/framer-motion/src/render/html/config-motion.ts b/packages/framer-motion/src/render/html/config-motion.ts
index d13545c637..06c3ec2794 100644
--- a/packages/framer-motion/src/render/html/config-motion.ts
+++ b/packages/framer-motion/src/render/html/config-motion.ts
@@ -8,6 +8,7 @@ export const htmlMotionConfig: Partial<
MotionComponentConfig
> = {
useVisualState: makeUseVisualState({
+ applyWillChange: true,
scrapeMotionValuesFromProps,
createRenderState: createHtmlRenderState,
}),
diff --git a/packages/framer-motion/src/render/html/use-props.ts b/packages/framer-motion/src/render/html/use-props.ts
index 04b391be71..1597380000 100644
--- a/packages/framer-motion/src/render/html/use-props.ts
+++ b/packages/framer-motion/src/render/html/use-props.ts
@@ -21,18 +21,12 @@ export function copyRawValuesOnly(
function useInitialMotionValues(
{ transformTemplate }: MotionProps,
- visualState: ResolvedValues,
- isStatic: boolean
+ visualState: ResolvedValues
) {
return useMemo(() => {
const state = createHtmlRenderState()
- buildHTMLStyles(
- state,
- visualState,
- { enableHardwareAcceleration: !isStatic },
- transformTemplate
- )
+ buildHTMLStyles(state, visualState, transformTemplate)
return Object.assign({}, state.vars, state.style)
}, [visualState])
@@ -40,8 +34,7 @@ function useInitialMotionValues(
function useStyle(
props: MotionProps,
- visualState: ResolvedValues,
- isStatic: boolean
+ visualState: ResolvedValues
): ResolvedValues {
const styleProp = props.style || {}
const style = {}
@@ -51,19 +44,18 @@ function useStyle(
*/
copyRawValuesOnly(style, styleProp as any, props)
- Object.assign(style, useInitialMotionValues(props, visualState, isStatic))
+ Object.assign(style, useInitialMotionValues(props, visualState))
return style
}
export function useHTMLProps(
props: MotionProps & HTMLProps,
- visualState: ResolvedValues,
- isStatic: boolean
+ visualState: ResolvedValues
) {
// The `any` isn't ideal but it is the type of createElement props argument
const htmlProps: any = {}
- const style = useStyle(props, visualState, isStatic)
+ const style = useStyle(props, visualState)
if (props.drag && props.dragListener !== false) {
// Disable the ghost element when a user drags
diff --git a/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts b/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts
index a81e781863..046dff3c14 100644
--- a/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts
+++ b/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts
@@ -28,7 +28,7 @@ describe("buildHTMLStyles", () => {
expect(style).toEqual({
transform:
- "perspective(200px) translateX(1px) translateY(2px) rotateX(90deg) translateZ(0)",
+ "perspective(200px) translateX(1px) translateY(2px) rotateX(90deg)",
})
})
@@ -49,18 +49,7 @@ describe("buildHTMLStyles", () => {
build(latest, { style })
expect(style).toEqual({
- transform:
- "translateX(1vw) translateY(2%) rotateX(90turn) translateZ(0)",
- })
- })
-
- test("Builds transform without translateZ if enableHardwareAcceleration is false", () => {
- const latest = { x: 1, y: 2, rotateX: 90 }
- const style = {}
- build(latest, { style, config: { enableHardwareAcceleration: false } })
-
- expect(style).toEqual({
- transform: "translateX(1px) translateY(2px) rotateX(90deg)",
+ transform: "translateX(1vw) translateY(2%) rotateX(90turn)",
})
})
@@ -74,16 +63,6 @@ describe("buildHTMLStyles", () => {
})
})
- test("Doesn't build transform none if all transforms are default if allowTransformNone is false", () => {
- const latest = { x: 0, y: 0, scale: 1 }
- const style = {}
- build(latest, { style, config: { allowTransformNone: false } })
-
- expect(style).toEqual({
- transform: "translateX(0px) translateY(0px) scale(1) translateZ(0)",
- })
- })
-
test("Builds transformOrigin with correct default value types", () => {
const latest = { originX: 0.2, originY: "60%", originZ: 10 }
const style = {}
@@ -106,7 +85,7 @@ describe("buildHTMLStyles", () => {
})
expect(style).toEqual({
- transform: "translateY(2) translateX(1px) translateZ(0)",
+ transform: "translateY(2) translateX(1px)",
})
})
@@ -122,7 +101,7 @@ describe("buildHTMLStyles", () => {
})
expect(style).toEqual({
- transform: "translateY(2) translateX(1px) translateZ(0)",
+ transform: "translateY(2) translateX(1px)",
})
})
})
@@ -142,10 +121,7 @@ function build(
vars = {},
transform = {},
transformOrigin = {},
- config = {
- enableHardwareAcceleration: true,
- allowTransformNone: true,
- },
+ config = {},
}: Partial = {}
) {
buildHTMLStyles(
@@ -156,7 +132,6 @@ function build(
transformOrigin,
},
latest,
- config,
config.transformTemplate
)
}
diff --git a/packages/framer-motion/src/render/html/utils/__tests__/build-transform.test.ts b/packages/framer-motion/src/render/html/utils/__tests__/build-transform.test.ts
index 42631dc7dc..ae7b69489d 100644
--- a/packages/framer-motion/src/render/html/utils/__tests__/build-transform.test.ts
+++ b/packages/framer-motion/src/render/html/utils/__tests__/build-transform.test.ts
@@ -15,60 +15,25 @@ describe("transformProps.has", () => {
describe("buildTransform", () => {
it("Outputs 'none' when transformIsDefault is true", () => {
- expect(buildTransform({ x: 0 }, {}, true)).toBe("none")
- })
-
- it("Outputs the provided transform when transformIsDefault is false", () => {
- expect(
- buildTransform(
- { x: 0 },
- { enableHardwareAcceleration: true, allowTransformNone: false },
- true
- )
- ).toBe("translateX(0) translateZ(0)")
- })
-
- it("Only outputs translateZ(0) if enableHardwareAcceleration is enabled", () => {
- expect(
- buildTransform(
- { x: 0 },
- {
- enableHardwareAcceleration: false,
- allowTransformNone: false,
- },
- false
- )
- ).toBe("translateX(0)")
+ expect(buildTransform({ x: 0 }, true)).toBe("none")
})
it("Still outputs translateZ if z is explicitly assigned", () => {
- expect(
- buildTransform(
- { x: 0, z: "5px" },
- {
- enableHardwareAcceleration: false,
- allowTransformNone: false,
- },
- false
- )
- ).toBe("translateX(0) translateZ(5px)")
+ expect(buildTransform({ x: 0, z: "5px" }, false)).toBe(
+ "translateX(0) translateZ(5px)"
+ )
})
it("Correctly handles transformPerspective", () => {
expect(
- buildTransform(
- { x: "100px", transformPerspective: "200px" },
- {},
- false
- )
- ).toBe("perspective(200px) translateX(100px) translateZ(0)")
+ buildTransform({ x: "100px", transformPerspective: "200px" }, false)
+ ).toBe("perspective(200px) translateX(100px)")
})
it("Correctly handles transformTemplate if provided", () => {
expect(
buildTransform(
{ x: "5px" },
- { enableHardwareAcceleration: true, allowTransformNone: false },
true,
({ x }: { x: string }) => `translateX(${parseFloat(x) * 2}px)`
)
@@ -85,12 +50,10 @@ describe("buildTransform", () => {
y: "10px",
rotateZ: "190deg",
},
-
- { enableHardwareAcceleration: true, allowTransformNone: false },
- true
+ false
)
).toBe(
- "translateX(0) translateY(10px) scale(2) rotate(90deg) rotateZ(190deg) translateZ(0)"
+ "translateX(0) translateY(10px) scale(2) rotate(90deg) rotateZ(190deg)"
)
})
})
diff --git a/packages/framer-motion/src/render/html/utils/build-styles.ts b/packages/framer-motion/src/render/html/utils/build-styles.ts
index 58f2479011..73152154c5 100644
--- a/packages/framer-motion/src/render/html/utils/build-styles.ts
+++ b/packages/framer-motion/src/render/html/utils/build-styles.ts
@@ -1,7 +1,6 @@
import { MotionProps } from "../../../motion/types"
import { HTMLRenderState } from "../types"
import { ResolvedValues } from "../../types"
-import { DOMVisualElementOptions } from "../../dom/types"
import { buildTransform } from "./build-transform"
import { isCSSVariableName } from "../../dom/utils/is-css-variable"
import { transformProps } from "./transform"
@@ -11,7 +10,6 @@ import { numberValueTypes } from "../../dom/value-types/number"
export function buildHTMLStyles(
state: HTMLRenderState,
latestValues: ResolvedValues,
- options: DOMVisualElementOptions,
transformTemplate?: MotionProps["transformTemplate"]
) {
const { style, vars, transform, transformOrigin } = state
@@ -57,7 +55,6 @@ export function buildHTMLStyles(
} else if (key.startsWith("origin")) {
// If this is a transform origin, flag and enable further transform-origin processing
hasTransformOrigin = true
-
transformOrigin[key as keyof typeof transformOrigin] = valueAsType
} else {
style[key] = valueAsType
@@ -68,7 +65,6 @@ export function buildHTMLStyles(
if (hasTransform || transformTemplate) {
style.transform = buildTransform(
state.transform,
- options,
transformIsNone,
transformTemplate
)
diff --git a/packages/framer-motion/src/render/html/utils/build-transform.ts b/packages/framer-motion/src/render/html/utils/build-transform.ts
index d418f1e102..7c99319f05 100644
--- a/packages/framer-motion/src/render/html/utils/build-transform.ts
+++ b/packages/framer-motion/src/render/html/utils/build-transform.ts
@@ -1,5 +1,4 @@
import { transformPropOrder } from "./transform"
-import { DOMVisualElementOptions } from "../../dom/types"
import { MotionProps } from "../../../motion/types"
import { HTMLRenderState } from "../types"
@@ -20,10 +19,6 @@ const numTransforms = transformPropOrder.length
*/
export function buildTransform(
transform: HTMLRenderState["transform"],
- {
- enableHardwareAcceleration = true,
- allowTransformNone = true,
- }: DOMVisualElementOptions,
transformIsDefault: boolean,
transformTemplate?: MotionProps["transformTemplate"]
) {
@@ -42,10 +37,6 @@ export function buildTransform(
}
}
- if (enableHardwareAcceleration && !transform.z) {
- transformString += "translateZ(0)"
- }
-
transformString = transformString.trim()
// If we have a custom `transform` template, pass our transform values and
@@ -55,7 +46,7 @@ export function buildTransform(
transform,
transformIsDefault ? "" : transformString
)
- } else if (allowTransformNone && transformIsDefault) {
+ } else if (transformIsDefault) {
transformString = "none"
}
diff --git a/packages/framer-motion/src/render/html/utils/create-render-state.ts b/packages/framer-motion/src/render/html/utils/create-render-state.ts
index b989525e8b..80592c3199 100644
--- a/packages/framer-motion/src/render/html/utils/create-render-state.ts
+++ b/packages/framer-motion/src/render/html/utils/create-render-state.ts
@@ -1,4 +1,6 @@
-export const createHtmlRenderState = () => ({
+import { HTMLRenderState } from "../types"
+
+export const createHtmlRenderState = (): HTMLRenderState => ({
style: {},
transform: {},
transformOrigin: {},
diff --git a/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts b/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts
index d971f5886e..a39d58a157 100644
--- a/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts
+++ b/packages/framer-motion/src/render/html/utils/scrape-motion-values.ts
@@ -23,5 +23,13 @@ export function scrapeMotionValuesFromProps(
}
}
+ /**
+ * If the willChange style has been manually set as a string, set
+ * applyWillChange to false to prevent it from automatically being applied.
+ */
+ if (visualElement && style && typeof style.willChange === "string") {
+ visualElement.applyWillChange = false
+ }
+
return newValues
}
diff --git a/packages/framer-motion/src/render/svg/SVGVisualElement.ts b/packages/framer-motion/src/render/svg/SVGVisualElement.ts
index 552e6afdef..187591029d 100644
--- a/packages/framer-motion/src/render/svg/SVGVisualElement.ts
+++ b/packages/framer-motion/src/render/svg/SVGVisualElement.ts
@@ -57,13 +57,11 @@ export class SVGVisualElement extends DOMVisualElement<
build(
renderState: SVGRenderState,
latestValues: ResolvedValues,
- options: DOMVisualElementOptions,
props: MotionProps
) {
buildSVGAttrs(
renderState,
latestValues,
- options,
this.isSVGTag,
props.transformTemplate
)
diff --git a/packages/framer-motion/src/render/svg/config-motion.ts b/packages/framer-motion/src/render/svg/config-motion.ts
index f9c1078063..5e2b185d65 100644
--- a/packages/framer-motion/src/render/svg/config-motion.ts
+++ b/packages/framer-motion/src/render/svg/config-motion.ts
@@ -37,7 +37,6 @@ export const svgMotionConfig: Partial<
buildSVGAttrs(
renderState,
latestValues,
- { enableHardwareAcceleration: false },
isSVGTag(instance.tagName),
props.transformTemplate
)
diff --git a/packages/framer-motion/src/render/svg/use-props.ts b/packages/framer-motion/src/render/svg/use-props.ts
index b292c3497f..214627cc14 100644
--- a/packages/framer-motion/src/render/svg/use-props.ts
+++ b/packages/framer-motion/src/render/svg/use-props.ts
@@ -18,7 +18,6 @@ export function useSVGProps(
buildSVGAttrs(
state,
visualState,
- { enableHardwareAcceleration: false },
isSVGTag(Component),
props.transformTemplate
)
diff --git a/packages/framer-motion/src/render/svg/utils/build-attrs.ts b/packages/framer-motion/src/render/svg/utils/build-attrs.ts
index 1ffb25fa92..e407b13f81 100644
--- a/packages/framer-motion/src/render/svg/utils/build-attrs.ts
+++ b/packages/framer-motion/src/render/svg/utils/build-attrs.ts
@@ -1,4 +1,3 @@
-import { DOMVisualElementOptions } from "../../dom/types"
import { buildHTMLStyles } from "../../html/utils/build-styles"
import { ResolvedValues } from "../../types"
import { calcSVGTransformOrigin } from "./transform-origin"
@@ -23,11 +22,10 @@ export function buildSVGAttrs(
// This is object creation, which we try to avoid per-frame.
...latest
}: ResolvedValues,
- options: DOMVisualElementOptions,
isSVGTag: boolean,
transformTemplate?: MotionProps["transformTemplate"]
) {
- buildHTMLStyles(state, latest, options, transformTemplate)
+ buildHTMLStyles(state, latest, transformTemplate)
/**
* For svg tags we just want to make sure viewBox is animatable and treat all the styles
diff --git a/packages/framer-motion/src/render/utils/motion-values.ts b/packages/framer-motion/src/render/utils/motion-values.ts
index d7dcb39e53..df3116e104 100644
--- a/packages/framer-motion/src/render/utils/motion-values.ts
+++ b/packages/framer-motion/src/render/utils/motion-values.ts
@@ -1,4 +1,3 @@
-import { isWillChangeMotionValue } from "../../value/use-will-change/is"
import { MotionStyle } from "../../motion/types"
import { warnOnce } from "../../utils/warn-once"
import { motionValue } from "../../value"
@@ -10,8 +9,6 @@ export function updateMotionValuesFromProps(
next: MotionStyle,
prev: MotionStyle
) {
- const { willChange } = next
-
for (const key in next) {
const nextValue = next[key as keyof MotionStyle]
const prevValue = prev[key as keyof MotionStyle]
@@ -23,10 +20,6 @@ export function updateMotionValuesFromProps(
*/
element.addValue(key, nextValue)
- if (isWillChangeMotionValue(willChange)) {
- willChange.add(key)
- }
-
/**
* Check the version of the incoming motion value with this version
* and warn against mismatches.
@@ -43,10 +36,6 @@ export function updateMotionValuesFromProps(
* create a new motion value from that
*/
element.addValue(key, motionValue(nextValue, { owner: element }))
-
- if (isWillChangeMotionValue(willChange)) {
- willChange.remove(key)
- }
} else if (prevValue !== nextValue) {
/**
* If this is a flat value that has changed, update the motion value
diff --git a/packages/framer-motion/src/value/__tests__/use-motion-value.test.tsx b/packages/framer-motion/src/value/__tests__/use-motion-value.test.tsx
index 0724198bd5..8471d25553 100644
--- a/packages/framer-motion/src/value/__tests__/use-motion-value.test.tsx
+++ b/packages/framer-motion/src/value/__tests__/use-motion-value.test.tsx
@@ -12,9 +12,7 @@ describe("useMotionValue", () => {
}
const { container } = render()
- expect(container.firstChild).toHaveStyle(
- "transform: translateX(100px) translateZ(0)"
- )
+ expect(container.firstChild).toHaveStyle("transform: translateX(100px)")
})
test("can be set manually", async () => {
@@ -27,9 +25,7 @@ describe("useMotionValue", () => {
}
const { container } = render()
- expect(container.firstChild).toHaveStyle(
- "transform: translateX(500px) translateZ(0)"
- )
+ expect(container.firstChild).toHaveStyle("transform: translateX(500px)")
})
test("accepts new motion values", async () => {
@@ -46,9 +42,7 @@ describe("useMotionValue", () => {
await nextMicrotask()
- expect(container.firstChild).toHaveStyle(
- "transform: translateX(5px) translateZ(0)"
- )
+ expect(container.firstChild).toHaveStyle("transform: translateX(5px)")
})
test("fires callbacks", async () => {
@@ -76,8 +70,6 @@ describe("useMotionValue", () => {
}
const { container } = render()
- expect(container.firstChild).toHaveStyle(
- "transform: translateX(100px) translateZ(0)"
- )
+ expect(container.firstChild).toHaveStyle("transform: translateX(100px)")
})
})
diff --git a/packages/framer-motion/src/value/__tests__/use-transform.test.tsx b/packages/framer-motion/src/value/__tests__/use-transform.test.tsx
index 307d037bea..c840147231 100644
--- a/packages/framer-motion/src/value/__tests__/use-transform.test.tsx
+++ b/packages/framer-motion/src/value/__tests__/use-transform.test.tsx
@@ -1,5 +1,5 @@
import { render } from "../../../jest.setup"
-import { useEffect } from "react";
+import { useEffect } from "react"
import { cancelFrame, frame, motion } from "../../"
import { useMotionValue } from "../use-motion-value"
import { useTransform } from "../use-transform"
@@ -32,7 +32,7 @@ describe("as function", () => {
const { container } = render()
expect(container.firstChild).toHaveStyle(
- "transform: translateX(100px) translateY(-100px) translateZ(0)"
+ "transform: translateX(100px) translateY(-100px)"
)
})
})
@@ -169,7 +169,7 @@ describe("as input/output range", () => {
await nextFrame()
expect(container.firstChild).toHaveStyle(
- "transform: translateX(20px) translateY(120px) translateZ(0)"
+ "transform: translateX(20px) translateY(120px)"
)
})
})
@@ -221,7 +221,7 @@ test("frame scheduling", async () => {
const { container, rerender } = render()
rerender()
- });
+ })
})
test("can be re-pointed to another `MotionValue`", async () => {
@@ -239,12 +239,12 @@ test("can be re-pointed to another `MotionValue`", async () => {
await nextMicrotask()
expect(container.firstChild as Element).toHaveStyle(
- "transform: translateX(4px) translateZ(0)"
+ "transform: translateX(4px)"
)
rerender()
await nextMicrotask()
expect(container.firstChild as Element).toHaveStyle(
- "transform: translateX(2px) translateZ(0)"
+ "transform: translateX(2px)"
)
})
diff --git a/packages/framer-motion/src/value/use-will-change/__tests__/index.test.tsx b/packages/framer-motion/src/value/use-will-change/__tests__/index.test.tsx
deleted file mode 100644
index c51ccec090..0000000000
--- a/packages/framer-motion/src/value/use-will-change/__tests__/index.test.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import { render } from "../../../../jest.setup"
-import { useWillChange } from ".."
-import { motion, useMotionValue } from "../../.."
-
-describe("useWillChange", () => {
- test("Applies 'will-change: auto' by default", async () => {
- const Component = () => {
- const willChange = useWillChange()
- return
- }
-
- const { container } = render()
- expect(container.firstChild).toHaveStyle("will-change: auto;")
- })
-
- test("Adds externally-provided motion values", async () => {
- const Component = () => {
- const willChange = useWillChange()
- const height = useMotionValue("height")
- return
- }
-
- const { container } = render()
-
- return new Promise((resolve) => {
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
- expect(container.firstChild).toHaveStyle(
- "will-change: height;"
- )
- resolve()
- })
- })
- })
- })
-
- test("Adds 'transform' when transform is animating", async () => {
- const Component = () => {
- const willChange = useWillChange()
- return (
-
- )
- }
-
- const { container } = render()
-
- return new Promise((resolve) => {
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
- expect(container.firstChild).toHaveStyle(
- "will-change: transform, background-color;"
- )
- resolve()
- })
- })
- })
- })
-
- test("Removes `transform` when all transforms finish animating", async () => {
- return new Promise((resolve) => {
- const Component = () => {
- const willChange = useWillChange()
- return (
- {
- requestAnimationFrame(() => {
- expect(container.firstChild).toHaveStyle(
- "will-change: auto;"
- )
- resolve()
- })
- }}
- />
- )
- }
-
- const { container } = render()
- })
- })
-
- test("Reverts to `auto` when all values finish animating", async () => {
- return new Promise((resolve) => {
- const Component = () => {
- const willChange = useWillChange()
- return (
- {
- requestAnimationFrame(() => {
- expect(container.firstChild).toHaveStyle(
- "will-change: auto;"
- )
- resolve()
- })
- }}
- />
- )
- }
-
- const { container } = render()
- })
- })
-})
diff --git a/packages/framer-motion/src/value/use-will-change/__tests__/will-change.ssr.test.tsx b/packages/framer-motion/src/value/use-will-change/__tests__/will-change.ssr.test.tsx
new file mode 100644
index 0000000000..4a9ceb990b
--- /dev/null
+++ b/packages/framer-motion/src/value/use-will-change/__tests__/will-change.ssr.test.tsx
@@ -0,0 +1,109 @@
+import { renderToString, renderToStaticMarkup } from "react-dom/server"
+import { MotionConfig, motion } from "../../../"
+import { motionValue } from "../../../value"
+
+function runTests(render: (components: any) => string) {
+ test("will-change correctly applied", () => {
+ const div = render(
+
+ )
+
+ expect(div).toBe(
+ ``
+ )
+ })
+
+ test("will-change not set in static mode", () => {
+ const div = render(
+
+
+
+ )
+
+ expect(div).toBe(
+ ``
+ )
+ })
+
+ test("will-change manually set", () => {
+ const div = render(
+
+ )
+
+ expect(div).toBe(
+ ``
+ )
+ })
+
+ test("will-change manually set without animated values", () => {
+ const div = render()
+
+ expect(div).toBe(``)
+ })
+
+ test("will-change not set without animated values", () => {
+ const div = render()
+
+ expect(div).toBe(``)
+ })
+
+ test("will-change manually set by MotionValue", () => {
+ const willChange = motionValue("opacity")
+ const div = render(
+
+ )
+
+ expect(div).toBe(
+ ``
+ )
+ })
+
+ test("will-change correctly not applied when isStatic", () => {
+ const div = render(
+
+
+
+ )
+
+ expect(div).toBe(
+ ``
+ )
+ })
+}
+
+describe("render", () => {
+ runTests(renderToString)
+})
+
+describe("renderToStaticMarkup", () => {
+ runTests(renderToStaticMarkup)
+})
diff --git a/packages/framer-motion/src/value/use-will-change/__tests__/will-change.test.tsx b/packages/framer-motion/src/value/use-will-change/__tests__/will-change.test.tsx
new file mode 100644
index 0000000000..e06c756be1
--- /dev/null
+++ b/packages/framer-motion/src/value/use-will-change/__tests__/will-change.test.tsx
@@ -0,0 +1,233 @@
+import { render } from "../../../../jest.setup"
+import { MotionConfig, frame, motion, useMotionValue } from "../../.."
+import { nextFrame } from "../../../gestures/__tests__/utils"
+import { WillChangeMotionValue } from ".."
+
+describe("WillChangeMotionValue", () => {
+ test("Can manage transform alongside independent transforms", async () => {
+ const willChange = new WillChangeMotionValue("auto")
+ const removeTransform = willChange.add("transform")
+ expect(willChange.get()).toBe("transform")
+ removeTransform!()
+ expect(willChange.get()).toBe("auto")
+ const removeX = willChange.add("x")
+ const removeY = willChange.add("y")
+ expect(willChange.get()).toBe("transform")
+ removeX!()
+ expect(willChange.get()).toBe("transform")
+ removeY!()
+ expect(willChange.get()).toBe("auto")
+ })
+})
+
+describe("willChange", () => {
+ test("Don't apply will-change if nothing has been defined", async () => {
+ const Component = () =>
+ const { container } = render()
+ expect(container.firstChild).not.toHaveStyle("will-change: auto;")
+ })
+
+ test("If will-change is set via style, render that value", async () => {
+ const Component = () => {
+ return
+ }
+ const { container } = render()
+ expect(container.firstChild).toHaveStyle("will-change: transform;")
+ })
+
+ test("Renders values defined in animate on initial render", async () => {
+ const Component = () => {
+ return
+ }
+
+ const { container } = render()
+
+ expect(container.firstChild).toHaveStyle(
+ "will-change: transform,background-color;"
+ )
+ })
+
+ test("Static mode: Doesn't render values defined in animate on initial render", async () => {
+ const Component = () => {
+ return (
+
+
+
+ )
+ }
+
+ const { container } = render()
+
+ expect(container.firstChild).not.toHaveStyle(
+ "will-change: transform,background-color;"
+ )
+ })
+
+ test("Renders values defined in animate on initial render", async () => {
+ const Component = () => {
+ return
+ }
+
+ const { container } = render()
+
+ expect(container.firstChild).toHaveStyle("will-change: transform;")
+ })
+
+ test("Doesn't render CSS variables or non-hardware accelerated values", async () => {
+ const Component = () => {
+ return (
+
+ )
+ }
+
+ const { container } = render()
+
+ expect(container.firstChild).toHaveStyle("will-change: filter;")
+ })
+
+ test("Don't render values defined in animate on initial render if initial is false", async () => {
+ const Component = () => {
+ return (
+
+ )
+ }
+
+ const { container } = render()
+
+ expect(container.firstChild).not.toHaveStyle("will-change: auto;")
+ expect(container.firstChild).not.toHaveStyle("will-change: transform;")
+ })
+
+ test("Add externally-provided motion values", async () => {
+ const Component = () => {
+ const opacity = useMotionValue(0)
+ const height = useMotionValue(100)
+ return
+ }
+
+ const { container } = render()
+
+ expect(container.firstChild).toHaveStyle("will-change: opacity;")
+ })
+
+ test("Static mode: Doesn't add externally-provided motion values", async () => {
+ const Component = () => {
+ const opacity = useMotionValue(0)
+ const height = useMotionValue(100)
+ return (
+
+
+
+ )
+ }
+
+ const { container } = render()
+
+ expect(container.firstChild).not.toHaveStyle("will-change: opacity;")
+ })
+
+ test("Removes values when they finish animating", async () => {
+ return new Promise((resolve) => {
+ const Component = () => {
+ return (
+ {
+ frame.postRender(() => {
+ expect(container.firstChild).toHaveStyle(
+ "will-change: auto;"
+ )
+ resolve()
+ })
+ }}
+ />
+ )
+ }
+
+ const { container } = render()
+
+ expect(container.firstChild).toHaveStyle("will-change: transform;")
+ })
+ })
+
+ test("Doesn't remove transform when some transforms are still animating", async () => {
+ const Component = ({ animate }: any) => (
+
+ )
+ const { container, rerender } = render()
+ await nextFrame()
+
+ expect(container.firstChild).not.toHaveStyle("will-change: transform;")
+ rerender()
+
+ await nextFrame()
+ await nextFrame()
+ await nextFrame()
+ await nextFrame()
+ await nextFrame()
+ await nextFrame()
+
+ expect(container.firstChild).toHaveStyle("will-change: transform;")
+ })
+
+ test("Add values when they start animating", async () => {
+ const Component = ({ animate }: any) => (
+
+ )
+ const { container, rerender } = render()
+ await nextFrame()
+
+ expect(container.firstChild).not.toHaveStyle("will-change: transform;")
+ rerender()
+
+ await nextFrame()
+
+ expect(container.firstChild).toHaveStyle("will-change: transform;")
+ })
+
+ test("Doesn't remove values when animation interrupted", async () => {
+ const Component = ({ animate }: any) => (
+
+ )
+ const { container, rerender } = render()
+ await nextFrame()
+
+ expect(container.firstChild).not.toHaveStyle("will-change: transform;")
+ rerender()
+
+ await nextFrame()
+
+ expect(container.firstChild).toHaveStyle("will-change: transform;")
+ rerender()
+
+ await nextFrame()
+ expect(container.firstChild).toHaveStyle("will-change: transform;")
+ })
+})
diff --git a/packages/framer-motion/src/value/use-will-change/add-will-change.ts b/packages/framer-motion/src/value/use-will-change/add-will-change.ts
new file mode 100644
index 0000000000..51ff394c11
--- /dev/null
+++ b/packages/framer-motion/src/value/use-will-change/add-will-change.ts
@@ -0,0 +1,29 @@
+import { WillChangeMotionValue } from "."
+import type { VisualElement } from "../../render/VisualElement"
+import { isWillChangeMotionValue } from "./is"
+
+export function addValueToWillChange(
+ visualElement: VisualElement,
+ key: string
+) {
+ if (!visualElement.applyWillChange) return
+
+ let willChange = visualElement.getValue("willChange")
+
+ /**
+ * If we haven't created a willChange MotionValue, and the we haven't been
+ * manually provided one, create one.
+ */
+ if (!willChange && !visualElement.props.style?.willChange) {
+ willChange = new WillChangeMotionValue("auto")
+ visualElement.addValue("willChange", willChange)
+ }
+
+ /**
+ * It could be that a user has set willChange to a regular MotionValue,
+ * in which case we can't add the value to it.
+ */
+ if (isWillChangeMotionValue(willChange)) {
+ return willChange.add(key)
+ }
+}
diff --git a/packages/framer-motion/src/value/use-will-change/get-will-change-name.ts b/packages/framer-motion/src/value/use-will-change/get-will-change-name.ts
new file mode 100644
index 0000000000..591b6091fa
--- /dev/null
+++ b/packages/framer-motion/src/value/use-will-change/get-will-change-name.ts
@@ -0,0 +1,17 @@
+import { acceleratedValues } from "../../animation/animators/utils/accelerated-values"
+import { camelToDash } from "../../render/dom/utils/camel-to-dash"
+import { transformProps } from "../../render/html/utils/transform"
+
+export function getWillChangeName(name: string): string | undefined {
+ if (transformProps.has(name)) {
+ return "transform"
+ } else if (
+ acceleratedValues.has(name) ||
+ // Manually check for backgroundColor as accelerated animations
+ // are currently disabled for this value (see `acceleratedValues`)
+ // but can still be put on the compositor.
+ name === "backgroundColor"
+ ) {
+ return camelToDash(name)
+ }
+}
diff --git a/packages/framer-motion/src/value/use-will-change/index.ts b/packages/framer-motion/src/value/use-will-change/index.ts
index ec66072292..c9bc37b2ad 100644
--- a/packages/framer-motion/src/value/use-will-change/index.ts
+++ b/packages/framer-motion/src/value/use-will-change/index.ts
@@ -1,50 +1,53 @@
-import { isCSSVariableName } from "../../render/dom/utils/is-css-variable"
-import { transformProps } from "../../render/html/utils/transform"
-import { addUniqueItem, removeItem } from "../../utils/array"
import { useConstant } from "../../utils/use-constant"
import { MotionValue } from ".."
import { WillChange } from "./types"
-import { camelToDash } from "../../render/dom/utils/camel-to-dash"
+import { getWillChangeName } from "./get-will-change-name"
+import { removeItem } from "../../utils/array"
export class WillChangeMotionValue extends MotionValue implements WillChange {
- private members: string[] = []
- private transforms = new Set()
-
- add(name: string): void {
- let memberName: string | undefined
-
- if (transformProps.has(name)) {
- this.transforms.add(name)
- memberName = "transform"
- } else if (
- !name.startsWith("origin") &&
- !isCSSVariableName(name) &&
- name !== "willChange"
- ) {
- memberName = camelToDash(name)
- }
+ private output: string[] = []
+ private counts = new Map()
+
+ add(name: string) {
+ const styleName = getWillChangeName(name)
+
+ if (!styleName) return
- if (memberName) {
- addUniqueItem(this.members, memberName)
+ /**
+ * Update counter. Each value has an indepdent counter
+ * as multiple sources could be requesting the same value
+ * gets added to will-change.
+ */
+ const prevCount = this.counts.get(styleName) || 0
+ this.counts.set(styleName, prevCount + 1)
+
+ if (prevCount === 0) {
+ this.output.push(styleName)
this.update()
}
- }
- remove(name: string): void {
- if (transformProps.has(name)) {
- this.transforms.delete(name)
- if (!this.transforms.size) {
- removeItem(this.members, "transform")
+ /**
+ * Prevents the remove function from being called multiple times.
+ */
+ let hasRemoved = false
+
+ return () => {
+ if (hasRemoved) return
+
+ hasRemoved = true
+
+ const newCount = this.counts.get(styleName)! - 1
+ this.counts.set(styleName, newCount)
+
+ if (newCount === 0) {
+ removeItem(this.output, styleName)
+ this.update()
}
- } else {
- removeItem(this.members, camelToDash(name))
}
-
- this.update()
}
private update() {
- this.set(this.members.length ? this.members.join(", ") : "auto")
+ this.set(this.output.length ? this.output.join(", ") : "auto")
}
}
diff --git a/packages/framer-motion/src/value/use-will-change/types.ts b/packages/framer-motion/src/value/use-will-change/types.ts
index 167d25b595..463f2fdda5 100644
--- a/packages/framer-motion/src/value/use-will-change/types.ts
+++ b/packages/framer-motion/src/value/use-will-change/types.ts
@@ -1,7 +1,5 @@
import type { MotionValue } from ".."
export interface WillChange extends MotionValue {
- add(name: string): void
- remove(name: string): void
- get(): void
+ add(name: string): undefined | VoidFunction
}
diff --git a/packages/framer-motion/webpack.size.config.js b/packages/framer-motion/webpack.size.config.js
deleted file mode 100644
index 51ccc9add7..0000000000
--- a/packages/framer-motion/webpack.size.config.js
+++ /dev/null
@@ -1,75 +0,0 @@
-const path = require("path")
-const TerserPlugin = require("terser-webpack-plugin")
-
-const tsLoader = {
- loader: "ts-loader",
- options: { transpileOnly: true },
-}
-
-module.exports = {
- mode: "production",
- entry: {
- "size-webpack-m": path.join(
- __dirname,
- "./src/render/dom/motion-minimal.ts"
- ),
- "size-webpack-dom-animation": path.join(
- __dirname,
- "./src/render/dom/features-animation.ts"
- ),
- "size-webpack-dom-max": path.join(
- __dirname,
- "./src/render/dom/features-max.ts"
- ),
- },
- output: {
- path: path.join(__dirname, "dist"),
- filename: "[name].js",
- },
- devtool: false,
- optimization: {
- usedExports: true,
- minimize: true,
- minimizer: [
- new TerserPlugin({
- terserOptions: {
- output: { comments: false },
- },
- }),
- ],
- },
- externals: {
- react: {
- root: "React",
- commonjs2: "react",
- commonjs: "react",
- amd: "react",
- },
- "react-dom": {
- root: "ReactDOM",
- commonjs2: "ReactDOM",
- commonjs: "ReactDOM",
- amd: "ReactDOM",
- },
- "@emotion/is-prop-valid": {
- root: "@emotion/is-prop-valid",
- commonjs2: "@emotion/is-prop-valid",
- commonjs: "@emotion/is-prop-valid",
- amd: "@emotion/is-prop-valid",
- },
- },
- resolve: {
- modules: ["node_modules"],
- extensions: [".ts", ".tsx", ".js", ".json"],
- // alias: convertPathsToAliases(tsconfig),
- },
- module: {
- rules: [
- {
- test: /\.ts(x?)$/,
- exclude: [/__tests__/, /node_modules/],
- use: [tsLoader],
- },
- ],
- },
-}