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..f456b47b2 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 { isWaapiSupportedEasing } 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" || !isWaapiSupportedEasing(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..47741bff6 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 isWaapiSupportedEasing(easing?: Easing | Easing[]) { + return ( + !easing || // Default easing + Array.isArray(easing) || // Bezier curve + (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..8a3f1634b 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, -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", () => {