From 4341943e051666ee9d927ace9e453339622e0b71 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 10 Jul 2024 14:51:35 +0200 Subject: [PATCH] Auto `will-change` (#2700) * Removing translateZ(0) * Fixing tests * Debug * Defining expected behaviour * Adding tests * Fixing tests * Latest * Latest * Latest * Allowing willChange to be manullly set by string * Fixing tests * Latest * Updating changes * Removing logs: * Latest * Latest * Latest * Fixing tests * Update * Increasing test timeout * Fixing failing tesst * Removing * Fixing tests * Improving comments * Updating types * Fixing types * Adding will-change events * Removing event listener approach * Adding comment * Updating * Upping bundlesize * Speeding up code * Adding tests for static mode --- dev/react/src/examples/Animation-animate.tsx | 63 +---- .../src/examples/Animation-stagger-custom.tsx | 2 +- packages/framer-motion/package.json | 10 +- .../src/animation/__tests__/animate.test.tsx | 4 +- .../__tests__/css-variables.test.tsx | 12 +- .../src/animation/__tests__/index.test.tsx | 2 +- .../animators/AcceleratedAnimation.ts | 17 +- .../animators/MainThreadAnimation.ts | 1 - .../animators/utils/accelerated-values.ts | 12 + .../src/animation/interfaces/motion-value.ts | 11 +- .../interfaces/visual-element-target.ts | 12 +- .../animation/utils/create-visual-element.ts | 8 +- .../__tests__/AnimatePresence.test.tsx | 10 +- .../Reorder/__tests__/server.ssr.test.tsx | 4 +- .../drag/VisualElementDragControls.ts | 16 +- .../gestures/drag/__tests__/index.test.tsx | 79 +++++- .../motion/__tests__/animate-prop.test.tsx | 8 +- .../motion/__tests__/animated-values.test.tsx | 12 +- .../motion/__tests__/component-svg.test.tsx | 10 +- .../src/motion/__tests__/component.test.tsx | 14 +- .../src/motion/__tests__/ssr.test.tsx | 14 +- .../src/motion/__tests__/static-prop.test.tsx | 4 +- .../src/motion/__tests__/style-prop.test.tsx | 6 +- .../__tests__/transformTemplate.test.tsx | 16 +- .../__tests__/transition-keyframes.test.tsx | 6 +- .../src/motion/__tests__/variant.test.tsx | 43 +++- .../src/motion/utils/use-visual-state.ts | 73 +++++- .../framer-motion/src/render/VisualElement.ts | 19 +- .../src/render/dom/create-visual-element.ts | 3 +- .../framer-motion/src/render/dom/types.ts | 18 -- .../src/render/html/HTMLVisualElement.ts | 10 +- .../render/html/__tests__/use-props.test.ts | 16 +- .../src/render/html/config-motion.ts | 1 + .../src/render/html/use-props.ts | 20 +- .../html/utils/__tests__/build-styles.test.ts | 35 +-- .../utils/__tests__/build-transform.test.ts | 53 +--- .../src/render/html/utils/build-styles.ts | 4 - .../src/render/html/utils/build-transform.ts | 11 +- .../render/html/utils/create-render-state.ts | 4 +- .../render/html/utils/scrape-motion-values.ts | 8 + .../src/render/svg/SVGVisualElement.ts | 2 - .../src/render/svg/config-motion.ts | 1 - .../framer-motion/src/render/svg/use-props.ts | 1 - .../src/render/svg/utils/build-attrs.ts | 4 +- .../src/render/utils/motion-values.ts | 11 - .../value/__tests__/use-motion-value.test.tsx | 16 +- .../value/__tests__/use-transform.test.tsx | 12 +- .../use-will-change/__tests__/index.test.tsx | 112 --------- .../__tests__/will-change.ssr.test.tsx | 109 ++++++++ .../__tests__/will-change.test.tsx | 233 ++++++++++++++++++ .../value/use-will-change/add-will-change.ts | 29 +++ .../use-will-change/get-will-change-name.ts | 17 ++ .../src/value/use-will-change/index.ts | 69 +++--- .../src/value/use-will-change/types.ts | 4 +- packages/framer-motion/webpack.size.config.js | 75 ------ 55 files changed, 772 insertions(+), 594 deletions(-) create mode 100644 packages/framer-motion/src/animation/animators/utils/accelerated-values.ts delete mode 100644 packages/framer-motion/src/value/use-will-change/__tests__/index.test.tsx create mode 100644 packages/framer-motion/src/value/use-will-change/__tests__/will-change.ssr.test.tsx create mode 100644 packages/framer-motion/src/value/use-will-change/__tests__/will-change.test.tsx create mode 100644 packages/framer-motion/src/value/use-will-change/add-will-change.ts create mode 100644 packages/framer-motion/src/value/use-will-change/get-will-change-name.ts delete mode 100644 packages/framer-motion/webpack.size.config.js 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], - }, - ], - }, -}