Next.js Discord

Discord Forum

Animate SVG frames

Answered
Evanion posted this in #help-forum
Open in Discord
Not strictly a Next.js issue.
I have a series of 16 SVGs that make up one frame each of an animation (A bird flapping it's wings like it's flying).
I tried using the <animate /> [native SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate) element to update the d attribute with each frame path, but I get a weird frame that only renders a part of the path, and moves it .. it kind of looks like it's interpolating between two frame, and morphing the path, but it's only rendering the tip of a birds wing.

When I render each path in their separate SVGs and spread them out on the page, there is no frame with this issue.

So, how can I animate between these frames in a better way than using css @keyframe?
I'm considering using lottie, but I would like to avoid including a large library unless I absolutely have to. Not to mention spending the money for Adobe AE.

<svg height="541" viewBox="0 0 441 541" width="441" xmlns="http://www.w3.org/2000/svg">
  <path fill-rule="evenodd" transform="translate(13 171)" d="m220.05 7.35c-2.4-.83333333-...">
    <animate  dur="16s" repeatCount="indefinite" attributeName="d" values="
 // frame 1 path
 // frame 2 path
 // etc" />
  </path>
</svg>
Answered by Evanion
I finally solved it using requestAnimationFrame api.
import { FC, useEffect, useRef, useState } from 'react';
import styles from './animatedSvg.module.css';
type Frame = {
  d: string;
  transform: string
}

type AnimatedSvgProps = {
  duration?: number
  width?: number;
  height?: number;
  frozenFrame?: number;
  frames?:Frame[]
}

export const AnimatedSvg: FC<AnimatedSvgProps> = ({ duration = 1600, width = 100, height, frozenFrame = 13, frames = defaultFrames  }) => {
  const [currentFrame, setCurrentFrame] = useState(frozenFrame);
  const frameRef = useRef(currentFrame);
  const requestRef = useRef<number>(0);
  const timeoutRef = useRef<NodeJS.Timeout>();

  useEffect(() => {
    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (prefersReducedMotion) {
      setCurrentFrame(frozenFrame);
      return;
    }

    const totalFrames = frames.length;
    const frameDuration = duration / totalFrames;

    const animate = () => {
      frameRef.current = (frameRef.current + 1) % totalFrames;
      setCurrentFrame(frameRef.current);

      timeoutRef.current = setTimeout(() => {
        requestRef.current = requestAnimationFrame(animate);
      }, frameDuration);
    };

    requestRef.current = requestAnimationFrame(animate);

    return () => {
      cancelAnimationFrame(requestRef.current);
      clearTimeout(timeoutRef.current);
    };
  }, [duration, frozenFrame]);

  const { d, transform } = frames[currentFrame];

  return (
    <svg width={width} height={height} viewBox="0 0 421 541" xmlns="http://www.w3.org/2000/svg" className={styles['svg']}>
      <path d={d} transform={transform} fill="currentColor" />
    </svg>
  );
};
View full answer

1 Reply

I finally solved it using requestAnimationFrame api.
import { FC, useEffect, useRef, useState } from 'react';
import styles from './animatedSvg.module.css';
type Frame = {
  d: string;
  transform: string
}

type AnimatedSvgProps = {
  duration?: number
  width?: number;
  height?: number;
  frozenFrame?: number;
  frames?:Frame[]
}

export const AnimatedSvg: FC<AnimatedSvgProps> = ({ duration = 1600, width = 100, height, frozenFrame = 13, frames = defaultFrames  }) => {
  const [currentFrame, setCurrentFrame] = useState(frozenFrame);
  const frameRef = useRef(currentFrame);
  const requestRef = useRef<number>(0);
  const timeoutRef = useRef<NodeJS.Timeout>();

  useEffect(() => {
    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (prefersReducedMotion) {
      setCurrentFrame(frozenFrame);
      return;
    }

    const totalFrames = frames.length;
    const frameDuration = duration / totalFrames;

    const animate = () => {
      frameRef.current = (frameRef.current + 1) % totalFrames;
      setCurrentFrame(frameRef.current);

      timeoutRef.current = setTimeout(() => {
        requestRef.current = requestAnimationFrame(animate);
      }, frameDuration);
    };

    requestRef.current = requestAnimationFrame(animate);

    return () => {
      cancelAnimationFrame(requestRef.current);
      clearTimeout(timeoutRef.current);
    };
  }, [duration, frozenFrame]);

  const { d, transform } = frames[currentFrame];

  return (
    <svg width={width} height={height} viewBox="0 0 421 541" xmlns="http://www.w3.org/2000/svg" className={styles['svg']}>
      <path d={d} transform={transform} fill="currentColor" />
    </svg>
  );
};
Answer