Skip to content

Commit

Permalink
Support for linear() easing function (#2812)
Browse files Browse the repository at this point in the history
* Adding support for linear easing

* Fixing tests

* Updating linear()

* Tweaking linear easing

* Make minimum number of linear points

* Adding guard
  • Loading branch information
mattgperry authored Sep 26, 2024
1 parent bc91591 commit c14c9da
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 31 deletions.
8 changes: 4 additions & 4 deletions packages/framer-motion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,23 +98,23 @@
"bundlesize": [
{
"path": "./dist/size-rollup-motion.js",
"maxSize": "33.85 kB"
"maxSize": "34.02 kB"
},
{
"path": "./dist/size-rollup-m.js",
"maxSize": "5.9 kB"
},
{
"path": "./dist/size-rollup-dom-animation.js",
"maxSize": "16.9 kB"
"maxSize": "17 kB"
},
{
"path": "./dist/size-rollup-dom-max.js",
"maxSize": "29 kB"
"maxSize": "29.1 kB"
},
{
"path": "./dist/size-rollup-animate.js",
"maxSize": "17.7 kB"
"maxSize": "17.9 kB"
},
{
"path": "./dist/size-rollup-scroll.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { anticipate } from "../../easing/anticipate"
import { backInOut } from "../../easing/back"
import { circInOut } from "../../easing/circ"
import { EasingDefinition } from "../../easing/types"
import { DOMKeyframesResolver } from "../../render/dom/DOMKeyframesResolver"
import { ResolvedKeyframes } from "../../render/utils/KeyframesResolver"
Expand All @@ -20,7 +23,7 @@ import {
import { MainThreadAnimation } from "./MainThreadAnimation"
import { acceleratedValues } from "./utils/accelerated-values"
import { animateStyle } from "./waapi"
import { isWaapiSupportedEasing } from "./waapi/easing"
import { isWaapiSupportedEasing, supportsLinearEasing } from "./waapi/easing"
import { getFinalKeyframe } from "./waapi/utils/get-final-keyframe"

const supportsWaapi = /*@__PURE__*/ memo(() =>
Expand Down Expand Up @@ -110,6 +113,18 @@ interface ResolvedAcceleratedAnimation {
keyframes: string[] | number[]
}

const unsupportedEasingFunctions = {
anticipate,
backInOut,
circInOut,
}

function isUnsupportedEase(
key: string
): key is keyof typeof unsupportedEasingFunctions {
return key in unsupportedEasingFunctions
}

export class AcceleratedAnimation<
T extends string | number
> extends BaseAnimation<T, ResolvedAcceleratedAnimation> {
Expand Down Expand Up @@ -159,6 +174,19 @@ export class AcceleratedAnimation<
return false
}

/**
* If the user has provided an easing function name that isn't supported
* by WAAPI (like "anticipate"), we need to provide the corressponding
* function. This will later get converted to a linear() easing function.
*/
if (
typeof ease === "string" &&
supportsLinearEasing() &&
isUnsupportedEase(ease)
) {
ease = unsupportedEasingFunctions[ease]
}

/**
* If this animation needs pre-generated keyframes then generate.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isWaapiSupportedEasing } from "../easing"
import { supportsFlags } from "../utils/supports-flags"

test("isWaapiSupportedEasing", () => {
expect(isWaapiSupportedEasing()).toEqual(true)
Expand All @@ -7,6 +8,9 @@ test("isWaapiSupportedEasing", () => {
expect(isWaapiSupportedEasing("anticipate")).toEqual(false)
expect(isWaapiSupportedEasing("backInOut")).toEqual(false)
expect(isWaapiSupportedEasing([0, 1, 2, 3])).toEqual(true)
supportsFlags.linearEasing = true
expect(isWaapiSupportedEasing((v) => v)).toEqual(true)
supportsFlags.linearEasing = false
expect(isWaapiSupportedEasing((v) => v)).toEqual(false)
expect(isWaapiSupportedEasing(["linear", "easeIn"])).toEqual(true)
expect(isWaapiSupportedEasing(["linear", "easeIn", [0, 1, 2, 3]])).toEqual(
Expand All @@ -15,6 +19,9 @@ test("isWaapiSupportedEasing", () => {
expect(isWaapiSupportedEasing(["linear", "easeIn", "anticipate"])).toEqual(
false
)
supportsFlags.linearEasing = true
expect(isWaapiSupportedEasing(["linear", "easeIn", (v) => v])).toEqual(true)
supportsFlags.linearEasing = false
expect(isWaapiSupportedEasing(["linear", "easeIn", (v) => v])).toEqual(
false
)
Expand Down
37 changes: 26 additions & 11 deletions packages/framer-motion/src/animation/animators/waapi/easing.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import { BezierDefinition, Easing } from "../../../easing/types"
import { isBezierDefinition } from "../../../easing/utils/is-bezier-definition"
import { generateLinearEasing } from "./utils/linear"
import { memoSupports } from "./utils/memo-supports"

export const supportsLinearEasing = /*@__PURE__*/ memoSupports(() => {
try {
document
.createElement("div")
.animate({ opacity: 0 }, { easing: "linear(0, 1)" })
} catch (e) {
return false
}
return true
}, "linearEasing")

export function isWaapiSupportedEasing(easing?: Easing | Easing[]): boolean {
return Boolean(
!easing ||
(typeof easing === "string" && easing in supportedWaapiEasing) ||
(typeof easing === "function" && supportsLinearEasing()) ||
!easing ||
(typeof easing === "string" &&
(easing in supportedWaapiEasing || supportsLinearEasing())) ||
isBezierDefinition(easing) ||
(Array.isArray(easing) && easing.every(isWaapiSupportedEasing))
)
Expand All @@ -25,22 +40,22 @@ export const supportedWaapiEasing = {
backOut: /*@__PURE__*/ cubicBezierAsString([0.33, 1.53, 0.69, 0.99]),
}

function mapEasingToNativeEasingWithDefault(easing: Easing): string {
return (
(mapEasingToNativeEasing(easing) as string) ||
supportedWaapiEasing.easeOut
)
}

export function mapEasingToNativeEasing(
easing?: Easing | Easing[]
easing: Easing | Easing[] | undefined,
duration: number
): undefined | string | string[] {
if (!easing) {
return undefined
} else if (typeof easing === "function" && supportsLinearEasing()) {
return generateLinearEasing(easing, duration)
} else if (isBezierDefinition(easing)) {
return cubicBezierAsString(easing)
} else if (Array.isArray(easing)) {
return easing.map(mapEasingToNativeEasingWithDefault)
return easing.map(
(segmentEasing) =>
(mapEasingToNativeEasing(segmentEasing, duration) as string) ||
supportedWaapiEasing.easeOut
)
} else {
return supportedWaapiEasing[easing as keyof typeof supportedWaapiEasing]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function animateStyle(
const keyframeOptions: PropertyIndexedKeyframes = { [valueName]: keyframes }
if (times) keyframeOptions.offset = times

const easing = mapEasingToNativeEasing(ease)
const easing = mapEasingToNativeEasing(ease, duration)

/**
* If this is an easing array, apply to keyframes, not animation as a whole
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { noop } from "../../../../../utils/noop"
import { generateLinearEasing } from "../linear"

describe("generateLinearEasing", () => {
test("Converts easing function into string of points", () => {
expect(generateLinearEasing(noop, 110)).toEqual(
"linear(0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1)"
)
expect(generateLinearEasing(() => 0.5, 200)).toEqual(
"linear(0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5)"
)
expect(generateLinearEasing(() => 0.5, 0)).toEqual("linear(0.5, 0.5)")
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { EasingFunction } from "../../../../easing/types"
import { progress } from "../../../../utils/progress"

// Create a linear easing point for every 10 ms
const resolution = 10

export const generateLinearEasing = (
easing: EasingFunction,
duration: number // as milliseconds
): string => {
let points = ""
const numPoints = Math.max(Math.round(duration / resolution), 2)

for (let i = 0; i < numPoints; i++) {
points += easing(progress(0, numPoints - 1, i)) + ", "
}

return `linear(${points.substring(0, points.length - 2)})`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { memo } from "../../../../utils/memo"
import { supportsFlags } from "./supports-flags"

export function memoSupports<T extends any>(
callback: () => T,
supportsFlag: keyof typeof supportsFlags
) {
const memoized = memo(callback)
return () => supportsFlags[supportsFlag] ?? memoized()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Add the ability for test suites to manually set support flags
* to better test more environments.
*/
export const supportsFlags: Record<string, boolean | undefined> = {
linearEasing: undefined,
}
Loading

0 comments on commit c14c9da

Please sign in to comment.