Skip to content

Commit

Permalink
Merge pull request #2194 from framer/fix/reverse-animation
Browse files Browse the repository at this point in the history
Fix for reverse animation with negative speed
  • Loading branch information
mergetron[bot] committed Jun 19, 2023
2 parents 15b658b + 6937a08 commit 6114276
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 7 deletions.
29 changes: 29 additions & 0 deletions dev/examples/Animation-reverse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useAnimate } from "framer-motion"
import * as React from "react"

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

return (
<div className="App" ref={scope}>
<div
className="four"
style={{ width: 50, height: 50, backgroundColor: "blue" }}
></div>
<p>reverse</p>
<button
onClick={() => {
const animation = animate(
".four",
{ x: 90 },
{ duration: 2 }
)
animation.time = animation.duration
animation.speed = -1
}}
>
play
</button>
</div>
)
}
53 changes: 53 additions & 0 deletions dev/tests/animate-reverse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { motion, animate } from "framer-motion"
import * as React from "react"
import { useEffect, useState } from "react"
import styled from "styled-components"

const Container = styled.section`
position: relative;
display: flex;
flex-direction: column;
padding: 100px;
div {
width: 100px;
height: 100px;
background-color: red;
}
`

export const App = () => {
const [count, setCount] = useState(0)
const [result, setResult] = useState("")

useEffect(() => {
if (count % 2 === 0) return

const output: number[] = []
const controls = animate(0, 100, {
duration: 0.5,
ease: "linear",
onUpdate: (v: number) => output.push(v),
onComplete: () =>
setResult(
output[1] === 100 && output.length !== 2
? "Success"
: "Fail"
),
})
controls.time = controls.duration
controls.speed = -1

return controls.stop
}, [count])

return (
<Container>
<button id="action" onClick={() => setCount((c) => c + 1)}>
Animate
</button>
<input id="result" readOnly value={result} />
<motion.div className="box" layout />
</Container>
)
}
13 changes: 13 additions & 0 deletions packages/framer-motion/cypress/integration/animate-reverse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
describe("animate() x layout prop in reverse speed", () => {
it("animate() plays as expected when layout prop is present", () => {
cy.visit("?test=animate-reverse")
.wait(1000)
.get("#action")
.trigger("click", 1, 1, { force: true })
.wait(600)
.get("#result")
.should(([$element]: any) => {
expect($element.value).to.equal("Success")
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,45 @@ describe("animate", () => {
expect(output).toEqual([0, 20, 40, 20, 0])
})

test("Reverse animation from the end", async () => {
const output: number[] = []

const animation = animateValue({
keyframes: [0, 100],
driver: syncDriver(20),
duration: 100,
ease: linear,
onUpdate: (v) => {
output.push(v)
},
})
animation.time = 100
animation.speed = -1

await animation

expect(output).toEqual([100, 80, 60, 40, 20, 0])
})

test("Reverse animation from the end with half speed", async () => {
const output: number[] = []

const animation = animateValue({
keyframes: [0, 100],
driver: syncDriver(20),
duration: 100,
ease: linear,
onUpdate: (v) => {
output.push(v)
},
})
animation.time = 100
animation.speed = -0.5

await animation
expect(output).toEqual([100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 0])
})

test("Correctly ends animations with duration: 0", async () => {
const animation = animateValue({
keyframes: [0, 100],
Expand Down
18 changes: 11 additions & 7 deletions packages/framer-motion/src/animation/animators/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,16 +143,22 @@ export function animateValue<V = number>({
* a pending operation that gets resolved here.
*/
if (speed > 0) startTime = Math.min(startTime, timestamp)
if (speed < 0)
startTime = Math.min(timestamp - totalDuration / speed, startTime)

if (holdTime !== null) {
currentTime = holdTime
} else {
currentTime = (timestamp - startTime) * speed
// Rounding the time because floating point arithmetic is not always accurate, e.g. 3000.367 - 1000.367 =
// 2000.0000000000002. This is a problem when we are comparing the currentTime with the duration, for
// example.
currentTime = Math.round(timestamp - startTime) * speed
}

// Rebase on delay
const timeWithoutDelay = currentTime - delay
const isInDelayPhase = timeWithoutDelay < 0
const timeWithoutDelay = currentTime - delay * (speed >= 0 ? 1 : -1)
const isInDelayPhase =
speed >= 0 ? timeWithoutDelay < 0 : timeWithoutDelay > totalDuration
currentTime = Math.max(timeWithoutDelay, 0)

/**
Expand Down Expand Up @@ -240,14 +246,12 @@ export function animateValue<V = number>({
let { done } = state

if (!isInDelayPhase && calculatedDuration !== null) {
done = currentTime >= totalDuration
done = speed >= 0 ? currentTime >= totalDuration : currentTime <= 0
}

const isAnimationFinished =
holdTime === null &&
(playState === "finished" ||
(playState === "running" && done) ||
(speed < 0 && currentTime <= 0))
(playState === "finished" || (playState === "running" && done))

if (onUpdate) {
onUpdate(state.value)
Expand Down

0 comments on commit 6114276

Please sign in to comment.