Skip to content

Commit

Permalink
Ensuring animations are set to ease: "linear" when passed to `scrol…
Browse files Browse the repository at this point in the history
…l` (#2869)

* Capturing broken test

* Adding test

* Fixing default scroll

* Updating

* LAtest
  • Loading branch information
mattgperry authored Nov 14, 2024
1 parent ffda2ed commit 2981179
Show file tree
Hide file tree
Showing 15 changed files with 140 additions and 43 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ Motion adheres to [Semantic Versioning](http://semver.org/).

Undocumented APIs should be considered internal and may change without warning.

## [11.11.16] 2024-11-13

### Fixed

- Ensuring animations passed to `scroll` are scrubbed linearly.
- Fixing `mini` types entrypoint.

## [11.11.15] 2024-11-13

### Fixed
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ test-nextjs: build test-mkdir

test-e2e: test-nextjs test-html test-react

test-single: build test-mkdir
yarn start-server-and-test "yarn dev-server" http://localhost:9990 "cd packages/framer-motion && cypress run --headless --spec cypress/integration/scroll.ts"

lint: bootstrap
yarn lint

Expand Down
2 changes: 1 addition & 1 deletion dev/next/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
28 changes: 16 additions & 12 deletions dev/react/src/tests/scroll-animate-style.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import { scroll, useAnimateMini } from "framer-motion"
import { scroll, useAnimateMini, animate } from "framer-motion"
import * as React from "react"
import { useEffect } from "react"

export const App = () => {
const [scope, animate] = useAnimateMini()
const [scope, miniAnimate] = useAnimateMini()

useEffect(() => {
if (!scope.current) return

return scroll(
animate(
scope.current,
{
backgroundColor: ["#fff", "#000"],
color: ["#000", "#fff"],
transform: ["none", "translateX(100px)"],
},
{ ease: "linear" }
)
const stopMiniScrollAnimation = scroll(
miniAnimate(scope.current, {
backgroundColor: ["#fff", "#000"],
color: ["#000", "#fff"],
})
)

const stopScrollAnimation = scroll(
animate(scope.current, { x: [0, 100] })
)

return () => {
stopMiniScrollAnimation()
stopScrollAnimation()
}
}, [])

return (
Expand Down
52 changes: 35 additions & 17 deletions dev/react/src/tests/scroll-animate-window.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,52 @@
import { scroll, animate } from "framer-motion"
import {
scroll,
animate,
animateMini,
useMotionValue,
useTransform,
motion,
} from "framer-motion"
import * as React from "react"
import { useEffect } from "react"

export const App = () => {
const progress = useMotionValue(0)

useEffect(() => {
/**
* Animate both transform (WAAPI) and colors (JS)
*/
return scroll(
animate(
"#color",
{
backgroundColor: ["#fff", "#000"],
color: ["#000", "#fff"],
transform: ["none", "translateX(100px)"],
},
{ ease: "linear" }
)
const stopScrollAnimation = scroll(
animate("#color", {
x: [0, 100],
opacity: [0, 1],
backgroundColor: ["#fff", "#000"],
})
)

const stopMiniScrollAnimation = scroll(
animateMini("#color", {
color: ["#000", "#fff"],
})
)

const stopMotionValueAnimation = scroll(animate(progress, 100))

return () => {
stopScrollAnimation()
stopMiniScrollAnimation()
stopMotionValueAnimation()
}
}, [])

const progressDisplay = useTransform(() => Math.round(progress.get()))

return (
<>
<div style={{ ...spacer, backgroundColor: "red" }} />
<div style={{ ...spacer, backgroundColor: "green" }} />
<div style={{ ...spacer, backgroundColor: "blue" }} />
<div style={{ ...spacer, backgroundColor: "yellow" }} />
<div id="color" style={progressStyle}>
A
</div>
<motion.div id="color" style={progressStyle}>
{progressDisplay}
</motion.div>
</>
)
}
Expand Down
12 changes: 10 additions & 2 deletions packages/framer-motion/cypress/integration/scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,20 @@ describe("scroll() animation", () => {
.wait(200)
.get("#color")
.should(([$element]: any) => {
// This is animated by animate and thus uses linear RGB mixing
expect(getComputedStyle($element).backgroundColor).to.equal(
"rgb(180, 180, 180)"
)

// This is animated by animate mini and thus doesn't use linear RGB mixing
expect(getComputedStyle($element).color).to.equal(
"rgb(180, 180, 180)"
"rgb(128, 128, 128)"
)

expect(getComputedStyle($element).opacity).to.equal("0.5")
expect($element.style.transform).to.equal("translateX(50px)")

expect($element.innerText).to.equal("50")
})
cy.viewport(100, 800)
.wait(200)
Expand All @@ -120,7 +128,7 @@ describe("scroll() animation", () => {
"rgb(221, 221, 221)"
)
expect(getComputedStyle($element).color).to.equal(
"rgb(128, 128, 128)"
"rgb(64, 64, 64)"
)
})
})
Expand Down
4 changes: 4 additions & 0 deletions packages/framer-motion/src/animation/GroupPlaybackControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ export class GroupPlaybackControls implements AnimationPlaybackControls {
this.animations.forEach((controls) => controls[methodName]())
}

flatten() {
this.runAll("flatten")
}

play() {
this.runAll("play")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function createTestAnimationControls(
then: (resolve: VoidFunction) => {
return Promise.resolve().then(resolve)
},
flatten: () => {},
complete: () => {},
cancel: () => {},
...partialControls,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ export abstract class BaseAnimation<T extends string | number, Resolved>
return this.currentFinishedPromise.then(resolve, reject)
}

flatten() {
this.options.type = "keyframes"
this.options.ease = "linear"
}

protected updateFinishedPromise() {
this.currentFinishedPromise = new Promise((resolve) => {
this.resolveFinishedPromise = resolve
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ export class MainThreadAnimation<
this.resolver.scheduleResolve()
}

flatten() {
super.flatten()

// If we've already resolved the animation, re-initialise it
if (this._resolved) {
Object.assign(
this._resolved,
this.initPlayback(this._resolved.keyframes)
)
}
}

protected initPlayback(keyframes: ResolvedKeyframes<T>) {
const {
type = "keyframes",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ export class NativeAnimation implements AnimationPlaybackControls {
return this.animation ? (this.animation.startTime as number) : null
}

flatten() {
if (!this.animation) return

this.animation.effect?.updateTiming({ easing: "linear" })
}

play() {
if (this.state === "finished") {
this.updateFinishedPromise()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { GroupPlaybackControls } from "../../GroupPlaybackControls"
import {
AnimationPlaybackControls,
AnimationScope,
DOMKeyframesDefinition,
DynamicAnimationOptions,
Expand All @@ -12,7 +13,7 @@ export const createScopedWaapiAnimate = (scope?: AnimationScope) => {
elementOrSelector: ElementOrSelector,
keyframes: DOMKeyframesDefinition,
options?: DynamicAnimationOptions
) {
): AnimationPlaybackControls {
return new GroupPlaybackControls(
animateElements(
elementOrSelector,
Expand Down
1 change: 1 addition & 0 deletions packages/framer-motion/src/animation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export interface AnimationPlaybackControls {
timeline: ProgressTimeline,
fallback?: (animation: AnimationPlaybackControls) => VoidFunction
) => VoidFunction
flatten: () => void
}

export type DynamicOption<T> = (i: number, total: number) => T
Expand Down
27 changes: 17 additions & 10 deletions packages/framer-motion/src/render/dom/scroll/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ScrollOptions, OnScroll, OnScrollWithInfo } from "./types"
import { scrollInfo } from "./track"
import { GroupPlaybackControls } from "../../../animation/GroupPlaybackControls"
import { ProgressTimeline, observeTimeline } from "./observe"
import { supportsScrollTimeline } from "./supports"
import { AnimationPlaybackControls } from "../../../animation/types"
import { noop } from "../../../utils/noop"

declare class ScrollTimeline implements ProgressTimeline {
constructor(options: ScrollOptions)
Expand Down Expand Up @@ -94,9 +95,11 @@ function scrollFunction(onScroll: OnScroll, options: ScrollOptions) {
}

function scrollAnimation(
animation: GroupPlaybackControls,
animation: AnimationPlaybackControls,
options: ScrollOptions
) {
animation.flatten()

if (needsElementTracking(options)) {
animation.pause()
return scrollInfo((info) => {
Expand All @@ -105,18 +108,22 @@ function scrollAnimation(
} else {
const timeline = getTimeline(options)

return animation.attachTimeline(timeline, (valueAnimation) => {
valueAnimation.pause()

return observeTimeline((progress) => {
valueAnimation.time = valueAnimation.duration * progress
}, timeline)
})
if (animation.attachTimeline) {
return animation.attachTimeline(timeline, (valueAnimation) => {
valueAnimation.pause()

return observeTimeline((progress) => {
valueAnimation.time = valueAnimation.duration * progress
}, timeline)
})
} else {
return noop as VoidFunction
}
}
}

export function scroll(
onScroll: OnScroll | GroupPlaybackControls,
onScroll: OnScroll | AnimationPlaybackControls,
{ axis = "y", ...options }: ScrollOptions = {}
): VoidFunction {
const optionsWithDefaults = { axis, ...options }
Expand Down
20 changes: 20 additions & 0 deletions packages/motion/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@ const reactTypes = {
plugins: typePlugins,
}

const miniTypes = {
input: "types/mini.d.ts",
output: {
format: "es",
file: "dist/mini.d.ts",
},
plugins: typePlugins,
}

const mTypes = {
input: "types/react-m.d.ts",
output: {
Expand All @@ -163,6 +172,15 @@ const mTypes = {
plugins: typePlugins,
}

const reactMiniTypes = {
input: "types/react-mini.d.ts",
output: {
format: "es",
file: "dist/react-mini.d.ts",
},
plugins: typePlugins,
}

const clientTypes = {
input: "types/react-client.d.ts",
output: {
Expand All @@ -185,6 +203,8 @@ export default [
es,
types,
reactTypes,
reactMiniTypes,
mTypes,
miniTypes,
clientTypes,
]

0 comments on commit 2981179

Please sign in to comment.