Skip to content
 

png 序列 : 序列帧动画组件实现

更新: 9/5/2025字数: 0 字 时长: 0 分钟

序列帧动画是 Web 开发中常见的动画技术之一,通过快速切换静态图片帧来创建流畅的动画效果。

组件概述

实现原理是:组件使用 CSS 雪碧图技术,将所有动画帧垂直排列在一张图片中,通过改变 background-position-y 来切换帧:

这个SequenceAnimation组件是一个灵活的序列帧动画解决方案,主要特点包括:
1、支持播放控制(播放/暂停)
2、可配置帧率(FPS)
3、支持循环和非循环模式
4、响应式尺寸调整
5、高性能的动画渲染

实现效果

png序列帧动画

组件实现

1、tsx

tsx
"use client";
import React, { useEffect, useRef, useState } from "react";
import "./index.scss";

interface SequenceAnimationProps {
  index?: number;
  frameCount?: number;
  imageUrl: string;
  text?: string;
  isPlaying?: boolean;
  fps?: number;
  loop?: boolean;
  frameHeight?: number;
  className?: string;
  style?: React.CSSProperties;
}

/**
 * @description  序列帧动画
 * @param index  当前帧索引
 * @param frameCount  总帧数
 * @param imageUrl  图片地址
 * @param text  文本
 * @param isPlaying  是否播放
 * @param fps  帧率
 * @param loop  是否循环
 * @param frameHeight  帧高度
 * @param className  类名
 * @param style  行内样式
 * @returns
 */
const SequenceAnimation: React.FC<SequenceAnimationProps> = ({
  index = 0,
  frameCount = 32,
  imageUrl = "/imgs/network.png",
  isPlaying = false,
  fps = 24,
  loop = true,
  frameHeight = 24, // 默认高度改为20px
  className = "",
  style = {}
}) => {
  const containerRef = useRef < HTMLDivElement > null;
  const animationRef = useRef < number > 0;
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [currentFrame, setCurrentFrame] = useState < number > 0;
  const lastUpdateTimeRef = useRef < number > 0;
  const accumulatedTimeRef = useRef < number > 0;

  // 计算每帧间隔时间(毫秒)
  const frameInterval = 1000 / fps;

  // 动画帧函数
  const animate = (timestamp: number) => {
    if (!isPlaying) return;

    // 初始化或重置后第一次运行
    if (lastUpdateTimeRef.current === 0) {
      lastUpdateTimeRef.current = timestamp;
      animationRef.current = requestAnimationFrame(animate);
      return;
    }

    // 计算时间差
    const deltaTime = timestamp - lastUpdateTimeRef.current;
    lastUpdateTimeRef.current = timestamp;
    accumulatedTimeRef.current += deltaTime;

    // 当累积时间超过帧间隔时更新帧
    while (accumulatedTimeRef.current >= frameInterval) {
      setCurrentFrame((prev) => {
        let nextFrame = prev + 1;

        // 处理循环或停止逻辑
        if (nextFrame >= frameCount) {
          nextFrame = loop ? 0 : frameCount - 1;
          if (!loop) {
            // 不循环时停止在最后一帧
            cancelAnimationFrame(animationRef.current);
            return frameCount - 1;
          }
        }

        // 更新DOM显示
        if (containerRef.current) {
          containerRef.current.style.backgroundPositionY = `-${nextFrame * frameHeight}px`;
        }

        return nextFrame;
      });

      accumulatedTimeRef.current -= frameInterval;
    }

    animationRef.current = requestAnimationFrame(animate);
  };

  // 控制动画开始/停止
  useEffect(() => {
    if (isPlaying) {
      lastUpdateTimeRef.current = 0;
      accumulatedTimeRef.current = 0;
      animationRef.current = requestAnimationFrame(animate);
    } else {
      cancelAnimationFrame(animationRef.current);
    }

    return () => {
      cancelAnimationFrame(animationRef.current);
    };
  }, [isPlaying, fps, loop, frameCount, frameHeight]);

  // 重置当前帧
  useEffect(() => {
    if (!isPlaying) {
      setCurrentFrame(0);
      if (containerRef.current) {
        containerRef.current.style.backgroundPositionY = "0px";
      }
    }
  }, [isPlaying]);

  // 初始化背景样式
  useEffect(() => {
    if (containerRef.current) {
      containerRef.current.style.backgroundImage = `url(${imageUrl})`;
      containerRef.current.style.backgroundSize = `auto ${frameCount * frameHeight}px`;
      containerRef.current.style.height = `${frameHeight}px`;
      containerRef.current.style.width = `${frameHeight}px`;
    }
  }, [imageUrl, frameCount, frameHeight]);

  return (
    <div>
      <div className={`sequence-animation-container ${className}`} style={style}>
        <div ref={containerRef} className={`sequence-animation sequence-animation-${index}`} />
      </div>
    </div>
  );
};

export default SequenceAnimation;

2、scss

scss
.sequence-animation-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.sequence-animation {
  background-position: 0 0;
  background-repeat: no-repeat;
}

组件使用

isPlaying 控制动画播放,为 true 时播放,为 false 时暂停。

tsx
import { SequenceAnimation } from "@/components/SequenceAnimation";

<SequenceAnimation style={{ marginLeft: "-2px" }} imageUrl={`${config.ossIconBaseUrl}/messageItem/sequence-network-48.png`} isPlaying={message.loading}></SequenceAnimation>;

我见青山多妩媚,料青山见我应如是。