a hook wrapping a simple requestAnimationFrame based animation function (with an option to dynamically update the duration)

Published on 4 March 2024

import { useEffect, useRef } from 'react';

type draw = (fraction: number) => void;
type duration = number;

const useAnimation = (duration: duration, draw: draw) => {
  const updateDurationRef = useRef<null | ((newDuration: number) => void)>(null);
  const currentAnimationIdRef = useRef(0);
  const elapsedTimeRef = useRef(0);

  useEffect(() => {
    const animate = (duration: duration, draw: draw) => {
      let start: number | null = null;

      const animation = (timestamp: number) => {
        if (!start) start = timestamp;
        elapsedTimeRef.current = timestamp - start;
        const percentage = Math.min(elapsedTimeRef.current / duration, 1);

        draw(percentage);

        if (elapsedTimeRef.current < duration) {
          currentAnimationIdRef.current = requestAnimationFrame(animation);
        } else {
          start = null;
          currentAnimationIdRef.current = requestAnimationFrame(animation);
        }
      };

      const updateDuration = (newDuration: number) => {
        const currentProgress = elapsedTimeRef.current / duration;
        start = performance.now() - newDuration * currentProgress;
        duration = newDuration;
        cancelAnimationFrame(currentAnimationIdRef.current);
        currentAnimationIdRef.current = requestAnimationFrame(animation);
      };

      currentAnimationIdRef.current = requestAnimationFrame(animation);

      return updateDuration;
    };

    updateDurationRef.current = animate(duration, draw);

    return () => {
      cancelAnimationFrame(currentAnimationIdRef.current);
    };
  }, [duration, draw]);

  return updateDurationRef.current;
};

export { useAnimation };