From 27b1a3c2101f77b016d174eedea4767f0b39e28b Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 16 Dec 2022 11:55:59 +0100 Subject: [PATCH 1/4] Pregenerating keyframes for unsupported easing functions --- packages/framer-motion/src/animation/index.ts | 18 ---- .../src/animation/legacy-popmotion/index.ts | 2 +- .../waapi/create-accelerated-animation.ts | 24 +++-- .../src/animation/waapi/easing.ts | 10 ++- .../src/motion/__tests__/waapi.test.tsx | 89 ++++++++++++++++--- 5 files changed, 97 insertions(+), 46 deletions(-) diff --git a/packages/framer-motion/src/animation/index.ts b/packages/framer-motion/src/animation/index.ts index 332eadb7f..54515e323 100644 --- a/packages/framer-motion/src/animation/index.ts +++ b/packages/framer-motion/src/animation/index.ts @@ -13,7 +13,6 @@ import { isAnimatable } from "./utils/is-animatable" import { getKeyframes } from "./utils/keyframes" import { getValueTransition, isTransitionDefined } from "./utils/transitions" import { supports } from "./waapi/supports" -import { supportedWaapiEasing } from "./waapi/easing" /** * A list of values that can be hardware-accelerated. @@ -128,29 +127,12 @@ export const createMotionValueAnimation = ( const visualElement = value.owner const element = visualElement && visualElement.current - /** - * WAAPI doesn't support all the same easings as Framer Motion. It doesn't - * support JavaScript functions. Otherwise, if defined as a string, we can - * only approximate some easings with cubic-bezier(). - * - * In the future it will be possible to support these by approximating with linear(). - */ - let supportsEasing = true - if ( - typeof options.ease === "function" || - (typeof options.ease === "string" && - !supportedWaapiEasing[options.ease]) - ) { - supportsEasing = false - } - const canAccelerateAnimation = supports.waapi() && acceleratedValues.has(valueName) && !options.repeatDelay && options.repeatType !== "mirror" && options.damping !== 0 && - supportsEasing && visualElement && element instanceof HTMLElement && !visualElement.getProps().onUpdate diff --git a/packages/framer-motion/src/animation/legacy-popmotion/index.ts b/packages/framer-motion/src/animation/legacy-popmotion/index.ts index 08ad7ce5e..948c82bf6 100644 --- a/packages/framer-motion/src/animation/legacy-popmotion/index.ts +++ b/packages/framer-motion/src/animation/legacy-popmotion/index.ts @@ -166,7 +166,7 @@ export function animate({ driverControls.stop() }, sample: (t: number) => { - return animation.next(Math.max(0, t)).value + return animation.next(Math.max(0, t)) }, } } diff --git a/packages/framer-motion/src/animation/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/waapi/create-accelerated-animation.ts index 3dbbaf9d3..5b6f100d0 100644 --- a/packages/framer-motion/src/animation/waapi/create-accelerated-animation.ts +++ b/packages/framer-motion/src/animation/waapi/create-accelerated-animation.ts @@ -3,9 +3,9 @@ import { sync } from "../../frameloop" import type { VisualElement } from "../../render/VisualElement" import type { MotionValue } from "../../value" import { animate } from "../legacy-popmotion" -import { spring } from "../legacy-popmotion/spring" import { AnimationOptions } from "../types" import { animateStyle } from "./" +import { isEasingSupported } from "./easing" /** * 10ms is chosen here as it strikes a balance between smooth @@ -22,25 +22,21 @@ export function createAcceleratedAnimation( let { keyframes, duration = 0.3, elapsed = 0, ease } = options /** - * If this is a spring animation, pre-generate keyframes and - * record duration. - * - * TODO: When introducing support for values beyond opacity it - * might be better to use `animate.sample()` + * If this animation needs pre-generated keyframes then generate. */ - if (options.type === "spring") { - const springAnimation = spring(options) + if (options.type === "spring" || !isEasingSupported(options.ease)) { + const sampleAnimation = animate(options) let state = { done: false, value: keyframes[0] } - const springKeyframes: number[] = [] + const pregeneratedKeyframes: number[] = [] let t = 0 while (!state.done) { - state = springAnimation.next(t) - springKeyframes.push(state.value) + state = sampleAnimation.sample(t) + pregeneratedKeyframes.push(state.value) t += sampleDelta } - keyframes = springKeyframes + keyframes = pregeneratedKeyframes duration = t - sampleDelta ease = "linear" } @@ -94,8 +90,8 @@ export function createAcceleratedAnimation( if (currentTime) { const sampleAnimation = animate(options) value.setWithVelocity( - sampleAnimation.sample(currentTime - sampleDelta), - sampleAnimation.sample(currentTime), + sampleAnimation.sample(currentTime - sampleDelta).value, + sampleAnimation.sample(currentTime).value, sampleDelta ) } diff --git a/packages/framer-motion/src/animation/waapi/easing.ts b/packages/framer-motion/src/animation/waapi/easing.ts index fd8063a9a..23e559df4 100644 --- a/packages/framer-motion/src/animation/waapi/easing.ts +++ b/packages/framer-motion/src/animation/waapi/easing.ts @@ -1,4 +1,12 @@ -import { BezierDefinition, EasingDefinition } from "../../easing/types" +import { BezierDefinition, Easing, EasingDefinition } from "../../easing/types" + +export function isEasingSupported(easing?: Easing | Easing[]) { + return ( + !easing || + Array.isArray(easing) || + (typeof easing === "string" && supportedWaapiEasing[easing]) + ) +} export const cubicBezierAsString = ([a, b, c, d]: BezierDefinition) => `cubic-bezier(${a}, ${b}, ${c}, ${d})` diff --git a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx index 98b92fca4..7b2e9ec40 100644 --- a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx @@ -335,68 +335,133 @@ describe("WAAPI animations", () => { expect(ref.current!.animate).not.toBeCalled() }) - test("Doesn't animate with WAAPI if ease is function", () => { + test("Pregenerates keyframes if ease is function", () => { const ref = createRef() const Component = () => ( v }} + transition={{ ease: () => 0.5, duration: 0.05 }} /> ) const { rerender } = render() rerender() - expect(ref.current!.animate).not.toBeCalled() + expect(ref.current!.animate).toBeCalled() + expect(ref.current!.animate).toBeCalledWith( + { + opacity: [0.45, 0.45, 0.45, 0.45, 0.45, 0.45], + offset: undefined, + }, + { + delay: -0, + direction: "normal", + duration: 50, + easing: "linear", + fill: "both", + iterations: 1, + } + ) }) - test("Doesn't animate with WAAPI if ease is anticipate", () => { + test("Pregenerates keyframes if ease is anticipate", () => { const ref = createRef() const Component = () => ( ) const { rerender } = render() rerender() - expect(ref.current!.animate).not.toBeCalled() + expect(ref.current!.animate).toBeCalled() + expect(ref.current!.animate).toBeCalledWith( + { + opacity: [ + -0.038019759996313955, 0.14036703066311026, 0.7875, + 0.89296875, 0.899560546875, + ], + offset: undefined, + }, + { + delay: -0, + direction: "normal", + duration: 50, + easing: "linear", + fill: "both", + iterations: 1, + } + ) }) - test("Doesn't animate with WAAPI if ease is backInOut", () => { + test("Pregenerates keyframes if ease is backInOut", () => { const ref = createRef() const Component = () => ( ) const { rerender } = render() rerender() - expect(ref.current!.animate).not.toBeCalled() + expect(ref.current!.animate).toBeCalled() + expect(ref.current!.animate).toBeCalledWith( + { + opacity: [ + 0, -0.038019759996313955, 0.14036703066311026, + 0.7596329693368897, 0.9380197599963139, 0.9, + ], + offset: undefined, + }, + { + delay: -0, + direction: "normal", + duration: 50, + easing: "linear", + fill: "both", + iterations: 1, + } + ) }) - test("Doesn't animate with WAAPI if ease is circInOut", () => { + test("Pregenerates keyframes if ease is circInOut", () => { const ref = createRef() const Component = () => ( ) const { rerender } = render() rerender() - expect(ref.current!.animate).not.toBeCalled() + expect(ref.current!.animate).toBeCalled() + expect(ref.current!.animate).toBeCalledWith( + { + opacity: [ + 0, 0.36000000000000004, 0.440908153700972, + 0.459091846299028, 0.5400000000000001, 0.9, + ], + offset: undefined, + }, + { + delay: -0, + direction: "normal", + duration: 50, + easing: "linear", + fill: "both", + iterations: 1, + } + ) }) test("Doesn't animate with WAAPI if repeatType is defined as mirror", () => { From df66a5d55e9461202c48895a69d38bdf71c0f5d5 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 16 Dec 2022 12:01:08 +0100 Subject: [PATCH 2/4] Fixing test --- packages/framer-motion/src/motion/__tests__/waapi.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx index 7b2e9ec40..8a3f1634b 100644 --- a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx @@ -382,7 +382,7 @@ describe("WAAPI animations", () => { expect(ref.current!.animate).toBeCalledWith( { opacity: [ - -0.038019759996313955, 0.14036703066311026, 0.7875, + 0, -0.038019759996313955, 0.14036703066311026, 0.7875, 0.89296875, 0.899560546875, ], offset: undefined, From a3580c19b8c5e224d5d01fb34fa799d7cbf5c0be Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 16 Dec 2022 15:40:57 +0100 Subject: [PATCH 3/4] Renaming supported easing --- .../src/animation/waapi/create-accelerated-animation.ts | 4 ++-- packages/framer-motion/src/animation/waapi/easing.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/framer-motion/src/animation/waapi/create-accelerated-animation.ts b/packages/framer-motion/src/animation/waapi/create-accelerated-animation.ts index 5b6f100d0..f456b47b2 100644 --- a/packages/framer-motion/src/animation/waapi/create-accelerated-animation.ts +++ b/packages/framer-motion/src/animation/waapi/create-accelerated-animation.ts @@ -5,7 +5,7 @@ import type { MotionValue } from "../../value" import { animate } from "../legacy-popmotion" import { AnimationOptions } from "../types" import { animateStyle } from "./" -import { isEasingSupported } from "./easing" +import { isWaapiSupportedEasing } from "./easing" /** * 10ms is chosen here as it strikes a balance between smooth @@ -24,7 +24,7 @@ export function createAcceleratedAnimation( /** * If this animation needs pre-generated keyframes then generate. */ - if (options.type === "spring" || !isEasingSupported(options.ease)) { + if (options.type === "spring" || !isWaapiSupportedEasing(options.ease)) { const sampleAnimation = animate(options) let state = { done: false, value: keyframes[0] } const pregeneratedKeyframes: number[] = [] diff --git a/packages/framer-motion/src/animation/waapi/easing.ts b/packages/framer-motion/src/animation/waapi/easing.ts index 23e559df4..a2567f53e 100644 --- a/packages/framer-motion/src/animation/waapi/easing.ts +++ b/packages/framer-motion/src/animation/waapi/easing.ts @@ -1,6 +1,6 @@ import { BezierDefinition, Easing, EasingDefinition } from "../../easing/types" -export function isEasingSupported(easing?: Easing | Easing[]) { +export function isWaapiSupportedEasing(easing?: Easing | Easing[]) { return ( !easing || Array.isArray(easing) || From 7d74a7f386ade883ce674c8efbfe954a887a2a74 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 16 Dec 2022 15:54:45 +0100 Subject: [PATCH 4/4] Adding comment --- packages/framer-motion/src/animation/waapi/easing.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/framer-motion/src/animation/waapi/easing.ts b/packages/framer-motion/src/animation/waapi/easing.ts index a2567f53e..47741bff6 100644 --- a/packages/framer-motion/src/animation/waapi/easing.ts +++ b/packages/framer-motion/src/animation/waapi/easing.ts @@ -2,8 +2,8 @@ import { BezierDefinition, Easing, EasingDefinition } from "../../easing/types" export function isWaapiSupportedEasing(easing?: Easing | Easing[]) { return ( - !easing || - Array.isArray(easing) || + !easing || // Default easing + Array.isArray(easing) || // Bezier curve (typeof easing === "string" && supportedWaapiEasing[easing]) ) }