Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for linear() easing function #2812

Merged
merged 6 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,12 @@ interface ResolvedAcceleratedAnimation {
keyframes: string[] | number[]
}

const unsupportedEasingFunctions = {
anticipate,
backInOut,
circInOut,
}

export class AcceleratedAnimation<
T extends string | number
> extends BaseAnimation<T, ResolvedAcceleratedAnimation> {
Expand Down Expand Up @@ -159,6 +168,22 @@ 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() &&
ease in unsupportedEasingFunctions
) {
ease =
unsupportedEasingFunctions[
ease as keyof typeof unsupportedEasingFunctions
]
Copy link
Collaborator

@adamseckel adamseckel Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you made a type guard like:

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

You could avoid the casts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ty will implement

}

/**
* 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 10ms btw?

Copy link
Collaborator Author

@mattgperry mattgperry Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just somewhere between 8 (120fps) and 16 (60fps) that looks quite good without being too accurate (as there's a resolution/performance tradeoff)

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