简体   繁体   中英

Change animation duration without reset on framer-motion

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>
    </>
  );
};

Check this forked codesandbox out

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM