I'm trying to slow down an infinite animation using framer-motion by changing the duration value in the transition prop. I'm using the useMotionValue
hook and the animate function:
const [count, setCount] = useState(1);
const scale = useMotionValue(1);
React.useEffect(() => {
// the [1, 2] is for [from, to]
const controls = animate(scale, [1, 2], {
repeat: Infinity,
ease: "linear",
duration: count
});
return controls.stop;
}, [scale, count]);
The problem here is that my animation reset from the beginning.
If I don't set any from
value ( animate(scale, 2, ...)
) it doesn't reset, but the beginning on the next repeat will be the last value just before the duration changes. (eg. I reset when scale is value is 1.5 and my animation will loop between 1.5 and 2 instead of 1 and 2)
I've made a repro here: https://codesandbox.io/s/framer-motion-simple-animation-forked-n8pbb?file=/src/index.tsx
I don't think that there's an easy solution.
But it is possible to stop the infinite loop, perform the rest of the animation with the new speed and then start the infinite loop again.
I've implemented this functionality here:
const App = () => {
const [count, setCount] = useState(1); // speed of animation
const scale = useMotionValue(1); // the animating motion value
// when we increase count, this will be set to true
// and it will finish the remaining part of the animation
const hasToFinish = useRef(false);
const [triggerRerender, setTriggerRerender] = useState(false); // this is for triggering a rerender - see implementation below
const performResetAnimation = useRef(false); // this is for performing an animation from scale 2 to scale 1
React.useEffect(() => {
let controls;
// check if the animation has to finish, after count has been increased
if (hasToFinish.current) {
if (!performResetAnimation.current) {
// check the target of the running animation - bigger or smaller
if (scale.getPrevious() >= scale.get()) {
// it's getting smaller
// finish the animation from current scale to 1
controls = animate(scale, [scale.get(), 1], {
ease: "linear",
duration: count * (scale.get() - 1), // calculate remaining duration with new speed
onComplete: () => {
hasToFinish.current = false;
setTriggerRerender(!triggerRerender); // then trigger a rerender to go back to the infinite animation
}
});
} else {
// it's getting bigger
// finish the animation from current scale to 2
controls = animate(scale, [scale.get(), 2], {
ease: "linear",
duration: count * (2 - scale.get()), // calculate remaining duration with new speed
onComplete: () => {
// it has to animate back once to scale 1 because the infinite animation starts there
performResetAnimation.current = true;
setTriggerRerender(!triggerRerender); // trigger rerender to go to the reset animation
}
});
}
} else {
// perform reset animation
// if the count is increased while the reset animation plays, it should just proceed as usual
// and not go into the reset animation again, so we set performResetAnimation to false
performResetAnimation.current = false;
controls = animate(scale, [2, 1], {
ease: "linear",
duration: count,
onComplete: () => {
hasToFinish.current = false;
setTriggerRerender(!triggerRerender); // trigger rerender to go back to infinite animation
}
});
}
} else {
// if it doesn't have to finish, perform the infinite animation
controls = animate(scale, [1, 2], {
repeat: Infinity,
repeatType: "reverse",
ease: "linear",
duration: count
});
}
return controls.stop;
}, [triggerRerender, scale, count]);
return (
<>
<Add
onClick={() => {
hasToFinish.current = true; // set hasToFinish to true, so that it finishes the animation with the new duration
setCount(count + 1); // triggers a rerender with new duration
}}
/>
<div className="count">{count}</div>
<div className="example-container">
<motion.div style={{ scale }} key={count} />
</div>
</>
);
};
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.