Skip to content

Commit

Permalink
Merge pull request #1834 from framer/feature/more-easing
Browse files Browse the repository at this point in the history
Pregenerating keyframes for unsupported easing functions
  • Loading branch information
mergetron[bot] committed Dec 16, 2022
2 parents b38c621 + 7d74a7f commit f0a15c2
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 46 deletions.
18 changes: 0 additions & 18 deletions packages/framer-motion/src/animation/index.ts
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Expand Up @@ -166,7 +166,7 @@ export function animate<V = number>({
driverControls.stop()
},
sample: (t: number) => {
return animation.next(Math.max(0, t)).value
return animation.next(Math.max(0, t))
},
}
}
Expand Up @@ -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
Expand All @@ -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"
}
Expand Down Expand Up @@ -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
)
}
Expand Down
10 changes: 9 additions & 1 deletion 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})`
Expand Down
89 changes: 77 additions & 12 deletions packages/framer-motion/src/motion/__tests__/waapi.test.tsx
Expand Up @@ -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<HTMLDivElement>()
const Component = () => (
<motion.div
ref={ref}
initial={{ opacity: 0 }}
animate={{ opacity: 0.9 }}
transition={{ ease: (v) => v }}
transition={{ ease: () => 0.5, duration: 0.05 }}
/>
)
const { rerender } = render(<Component />)
rerender(<Component />)

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<HTMLDivElement>()
const Component = () => (
<motion.div
ref={ref}
initial={{ opacity: 0 }}
animate={{ opacity: 0.9 }}
transition={{ ease: "anticipate" }}
transition={{ ease: "anticipate", duration: 0.05 }}
/>
)
const { rerender } = render(<Component />)
rerender(<Component />)

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<HTMLDivElement>()
const Component = () => (
<motion.div
ref={ref}
initial={{ opacity: 0 }}
animate={{ opacity: 0.9 }}
transition={{ ease: "backInOut" }}
transition={{ ease: "backInOut", duration: 0.05 }}
/>
)
const { rerender } = render(<Component />)
rerender(<Component />)

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<HTMLDivElement>()
const Component = () => (
<motion.div
ref={ref}
initial={{ opacity: 0 }}
animate={{ opacity: 0.9 }}
transition={{ ease: "circInOut" }}
transition={{ ease: "circInOut", duration: 0.05 }}
/>
)
const { rerender } = render(<Component />)
rerender(<Component />)

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", () => {
Expand Down

0 comments on commit f0a15c2

Please sign in to comment.