diff --git a/CHANGELOG.md b/CHANGELOG.md index 2015c4630e..103b4d0fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Undocumented APIs should be considered internal and may change without warning. ### Fixed - `.stop()` stops animations permanently. +- `useSpring` timing. +- `animate()` with `repeat: 1` and `repeatType` `"reverse"` or `"mirror"` correctly applies final keyframe. ## [10.5.0] 2023-03-16 diff --git a/packages/framer-motion/src/animation/__tests__/animate.test.tsx b/packages/framer-motion/src/animation/__tests__/animate.test.tsx index 16e85c03c2..c889b024f1 100644 --- a/packages/framer-motion/src/animation/__tests__/animate.test.tsx +++ b/packages/framer-motion/src/animation/__tests__/animate.test.tsx @@ -145,6 +145,58 @@ describe("animate", () => { }) }) + test("Applies final target keyframe when animation has finished, repeat: reverse", async () => { + const div = document.createElement("div") + const animation = animate( + div, + { opacity: [0.2, 0.5] }, + { + duration, + repeat: 1, + repeatType: "reverse", + } + ) + await animation.then(() => { + expect(div).toHaveStyle("opacity: 0.2") + }) + }) + + test("Applies final target keyframe when animation has finished, repeat: reverse even", async () => { + const div = document.createElement("div") + const animation = animate( + div, + { opacity: [0.2, 0.5] }, + { duration, repeat: 2, repeatType: "reverse" } + ) + await animation.then(() => { + expect(div).toHaveStyle("opacity: 0.5") + }) + }) + + test("Applies final target keyframe when animation has finished, repeat: mirror", async () => { + const div = document.createElement("div") + const animation = animate( + div, + { opacity: [0.2, 0.5] }, + { duration, repeat: 1, repeatType: "mirror" } + ) + await animation.then(() => { + expect(div).toHaveStyle("opacity: 0.2") + }) + }) + + test("Applies final target keyframe when animation has finished, repeat: mirror even", async () => { + const div = document.createElement("div") + const animation = animate( + div, + { opacity: [0.2, 0.5] }, + { duration, repeat: 1, repeatType: "mirror" } + ) + await animation.then(() => { + expect(div).toHaveStyle("opacity: 0.2") + }) + }) + test("time sets and gets time", async () => { const div = document.createElement("div") const animation = animate(div, { x: 100 }, { duration: 10 }) diff --git a/packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts b/packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts index ddc5ef8290..c9fa124c1d 100644 --- a/packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts +++ b/packages/framer-motion/src/animation/animators/js/__tests__/animate.test.ts @@ -237,7 +237,7 @@ describe("animate", () => { }) test("Correctly applies repeat type 'reverse'", async () => { - return new Promise((resolve) => { + await new Promise((resolve) => { testAnimate( { keyframes: [0, 100], @@ -249,10 +249,26 @@ describe("animate", () => { resolve ) }) + + return new Promise((resolve) => { + testAnimate( + { + keyframes: [0, 100], + ease: "linear", + repeat: 2, + repeatType: "reverse", + }, + [ + 0, 20, 40, 60, 80, 100, 80, 60, 40, 20, 0, 20, 40, 60, 80, + 100, + ], + resolve + ) + }) }) test("Correctly applies repeat type 'mirror'", async () => { - return new Promise((resolve) => { + await new Promise((resolve) => { testAnimate( { keyframes: [0, 100], @@ -264,6 +280,19 @@ describe("animate", () => { resolve ) }) + + return new Promise((resolve) => { + testAnimate( + { + keyframes: [0, 100], + repeat: 2, + ease: reverseEasing((v) => v * v), + repeatType: "mirror", + }, + [0, 36, 64, 84, 96, 100, 64, 36, 16, 4, 0, 36, 64, 84, 96, 100], + resolve + ) + }) }) test("Correctly applies repeatDelay", async () => { diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index 3489cbffb4..ebbde5bf33 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -199,12 +199,16 @@ export function animateValue({ if (!iterationProgress && progress >= 1) { iterationProgress = 1 } + iterationProgress === 1 && currentIteration-- + currentIteration = Math.min(currentIteration, repeat + 1) + /** * Reverse progress if we're not running in "normal" direction */ - const iterationIsOdd = currentIteration % 2 + const iterationIsOdd = Boolean(currentIteration % 2) + if (iterationIsOdd) { if (repeatType === "reverse") { iterationProgress = 1 - iterationProgress @@ -216,12 +220,11 @@ export function animateValue({ } } - const p = - time >= totalDuration - ? repeatType === "reverse" && iterationIsOdd - ? 0 - : 1 - : clamp(0, 1, iterationProgress) + let p = clamp(0, 1, iterationProgress) + + if (time > totalDuration) { + p = repeatType === "reverse" && iterationIsOdd ? 1 : 0 + } elapsed = p * resolvedDuration }