From 4fc07904c561c5dd2fefebd1ee1368f1e6f1c97a Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 16 Dec 2022 16:29:01 +0100 Subject: [PATCH] Adding support for all easing types with WAAPI (#1830) * Adding support for back/circ easings to WAAPI * Updating * Updating changelog * Fixing logic * Updating test * Pregenerating keyframes for unsupported easing functions * Fixing test * Renaming supported easing * Adding comment --- CHANGELOG.md | 6 + packages/framer-motion/src/animation/index.ts | 1 - .../src/animation/legacy-popmotion/index.ts | 2 +- .../waapi/create-accelerated-animation.ts | 24 +- .../src/animation/waapi/easing.ts | 33 ++- .../framer-motion/src/easing/anticipate.ts | 14 +- packages/framer-motion/src/easing/back.ts | 11 +- .../src/motion/__tests__/waapi.test.tsx | 226 +++++++++++++++++- 8 files changed, 262 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3361fd92ec..e878f5e462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Framer Motion adheres to [Semantic Versioning](http://semver.org/). Undocumented APIs should be considered internal and may change without warning. +## [7.10.2] 2022-12-16 + +### Fixed + +- Adding support for all easing functions with WAAPI. + ## [7.10.1] 2022-12-16 ### Fixed diff --git a/packages/framer-motion/src/animation/index.ts b/packages/framer-motion/src/animation/index.ts index a5ca00a369..54515e3233 100644 --- a/packages/framer-motion/src/animation/index.ts +++ b/packages/framer-motion/src/animation/index.ts @@ -133,7 +133,6 @@ export const createMotionValueAnimation = ( !options.repeatDelay && options.repeatType !== "mirror" && options.damping !== 0 && - typeof options.ease !== "function" && 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 08ad7ce5ed..948c82bf65 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 3dbbaf9d36..f456b47b25 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 616dc7c8f0..47741bff6b 100644 --- a/packages/framer-motion/src/animation/waapi/easing.ts +++ b/packages/framer-motion/src/animation/waapi/easing.ts @@ -1,19 +1,26 @@ -import { BezierDefinition, EasingDefinition } from "../../easing/types" -import { camelToDash } from "../../render/dom/utils/camel-to-dash" +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})` -const validWaapiEasing = new Set([ - "linear", - "ease-in", - "ease-out", - "ease-in-out", -]) - -export function mapEasingName(easingName: string): string { - const name = camelToDash(easingName) - return validWaapiEasing.has(name) ? name : "ease" +export const supportedWaapiEasing = { + linear: "linear", + ease: "ease", + easeIn: "ease-in", + easeOut: "ease-out", + easeInOut: "ease-in-out", + circIn: cubicBezierAsString([0, 0.65, 0.55, 1]), + circOut: cubicBezierAsString([0.55, 0, 1, 0.45]), + backIn: cubicBezierAsString([0.31, 0.01, 0.66, -0.59]), + backOut: cubicBezierAsString([0.33, 1.53, 0.69, 0.99]), } export function mapEasingToNativeEasing( @@ -22,5 +29,5 @@ export function mapEasingToNativeEasing( if (!easing) return undefined return Array.isArray(easing) ? cubicBezierAsString(easing) - : mapEasingName(easing) + : supportedWaapiEasing[easing] } diff --git a/packages/framer-motion/src/easing/anticipate.ts b/packages/framer-motion/src/easing/anticipate.ts index 763001c64f..b41ba1eb02 100644 --- a/packages/framer-motion/src/easing/anticipate.ts +++ b/packages/framer-motion/src/easing/anticipate.ts @@ -1,12 +1,4 @@ -import { createBackIn } from "./back" -import { EasingFunction } from "./types" +import { backIn } from "./back" -const createAnticipate = (power?: number): EasingFunction => { - const backEasing = createBackIn(power) - return (p) => - (p *= 2) < 1 - ? 0.5 * backEasing(p) - : 0.5 * (2 - Math.pow(2, -10 * (p - 1))) -} - -export const anticipate = createAnticipate() +export const anticipate = (p: number) => + (p *= 2) < 1 ? 0.5 * backIn(p) : 0.5 * (2 - Math.pow(2, -10 * (p - 1))) diff --git a/packages/framer-motion/src/easing/back.ts b/packages/framer-motion/src/easing/back.ts index 1fb1967ff9..e345708d10 100644 --- a/packages/framer-motion/src/easing/back.ts +++ b/packages/framer-motion/src/easing/back.ts @@ -1,12 +1,7 @@ +import { cubicBezier } from "./cubic-bezier" import { mirrorEasing } from "./modifiers/mirror" import { reverseEasing } from "./modifiers/reverse" -import { EasingFunction } from "./types" -export const createBackIn = - (power: number = 1.525): EasingFunction => - (p) => - p * p * ((power + 1) * p - power) - -export const backIn = createBackIn() -export const backOut = reverseEasing(backIn) +export const backOut = cubicBezier(0.33, 1.53, 0.69, 0.99) +export const backIn = reverseEasing(backOut) export const backInOut = mirrorEasing(backIn) diff --git a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx index d7a565878a..8a3f1634b4 100644 --- a/packages/framer-motion/src/motion/__tests__/waapi.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/waapi.test.tsx @@ -161,11 +161,36 @@ describe("WAAPI animations", () => { ) }) - /** - * TODO: Wait for comments and either bump these back to the main thread, - * generate keyframes, generate linear() easing, or create similar cubic beziers. - */ - test("Maps remaining easings to 'ease'", () => { + test("Maps 'circIn' to 'cubic-bezier(0, 0.65, 0.55, 1)'", () => { + const ref = createRef() + const Component = () => ( + + ) + const { rerender } = render() + rerender() + + expect(ref.current!.animate).toBeCalled() + expect(ref.current!.animate).toBeCalledWith( + { opacity: [0, 1], offset: undefined }, + { + easing: "cubic-bezier(0, 0.65, 0.55, 1)", + delay: -0, + duration: 0.3, + direction: "normal", + fill: "both", + iterations: 1, + } + ) + }) + + test("Maps 'circOut' to 'cubic-bezier(0.55, 0, 1, 0.45)'", () => { const ref = createRef() const Component = () => ( { initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ - ease: "anticipate", + ease: "circOut", }} /> ) @@ -184,7 +209,65 @@ describe("WAAPI animations", () => { expect(ref.current!.animate).toBeCalledWith( { opacity: [0, 1], offset: undefined }, { - easing: "ease", + easing: "cubic-bezier(0.55, 0, 1, 0.45)", + delay: -0, + duration: 0.3, + direction: "normal", + fill: "both", + iterations: 1, + } + ) + }) + + test("Maps 'backIn' to 'cubic-bezier(0.31, 0.01, 0.66, -0.59)'", () => { + const ref = createRef() + const Component = () => ( + + ) + const { rerender } = render() + rerender() + + expect(ref.current!.animate).toBeCalled() + expect(ref.current!.animate).toBeCalledWith( + { opacity: [0, 1], offset: undefined }, + { + easing: "cubic-bezier(0.31, 0.01, 0.66, -0.59)", + delay: -0, + duration: 0.3, + direction: "normal", + fill: "both", + iterations: 1, + } + ) + }) + + test("Maps 'backOut' to 'cubic-bezier(0.33, 1.53, 0.69, 0.99)'", () => { + const ref = createRef() + const Component = () => ( + + ) + const { rerender } = render() + rerender() + + expect(ref.current!.animate).toBeCalled() + expect(ref.current!.animate).toBeCalledWith( + { opacity: [0, 1], offset: undefined }, + { + easing: "cubic-bezier(0.33, 1.53, 0.69, 0.99)", delay: -0, duration: 0.3, direction: "normal", @@ -252,6 +335,135 @@ describe("WAAPI animations", () => { expect(ref.current!.animate).not.toBeCalled() }) + test("Pregenerates keyframes if ease is function", () => { + const ref = createRef() + const Component = () => ( + 0.5, duration: 0.05 }} + /> + ) + const { rerender } = render() + rerender() + + 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("Pregenerates keyframes if ease is anticipate", () => { + const ref = createRef() + const Component = () => ( + + ) + const { rerender } = render() + rerender() + + 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("Pregenerates keyframes if ease is backInOut", () => { + const ref = createRef() + const Component = () => ( + + ) + const { rerender } = render() + rerender() + + 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("Pregenerates keyframes if ease is circInOut", () => { + const ref = createRef() + const Component = () => ( + + ) + const { rerender } = render() + rerender() + + 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", () => { const ref = createRef() const Component = () => (