You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Canvas精灵动画瞬间播放后消失问题求助

问题分析与解决方案

核心问题

你的动画快速播放后消失,根源在两个关键问题:

  1. 精灵图重复加载+异步绘制:每次调用SpriteAni都新建Image实例并设置src,图片加载是异步的。前几次drawImage执行时图片还没加载完成,根本没画出内容;等图片加载好时,currentFrame已经被requestAnimationFrame的60fps循环快速推进到超出精灵图的帧范围,导致后续绘制空白。
  2. 帧更新无节流:即使删除手动更新currentFrame的代码,requestAnimationFrame每秒跑60次,加上图片加载的延迟,等图片能绘制时,状态或绘制逻辑的累积变化依然会导致帧快速跳过。

修复步骤

1. 缓存精灵图,只加载一次

把精灵图的加载逻辑从SpriteAni中抽离,放在组件内缓存,避免重复加载:

// 在Canvas组件内添加图片缓存逻辑
const spriteCache = useRef({});

const loadSprite = (animName) => {
  if (!spriteCache.current[animName]) {
    const img = new Image();
    img.src = `./images/char_${animName}.png`;
    spriteCache.current[animName] = img;
  }
  return spriteCache.current[animName];
};

修改SpriteAni,接收已加载的图片,不再内部创建,同时添加加载判断:

export default function SpriteAni(ctx, charSprite, currentFrame) {
    const posx = 320;
    const posy = 200;
    const sheet_width = 32;
    const sheet_height = 40;
    const sprite_width = sheet_width * 1.5;
    const sprite_height = sheet_height * 1.5;

    // 图片加载完成后再绘制
    if (charSprite.complete) {
      ctx.drawImage(charSprite, currentFrame * sheet_width, 0, sheet_width, sheet_height, posx, posy, sprite_width * 1.5, sprite_height * 1.5);
    }
}

2. 控制帧更新速度

requestAnimationFrame是60fps,直接每帧更新currentFrame会导致动画过快。添加节流逻辑,比如每秒播放10帧:

// 在Canvas组件内添加节流相关的ref
const lastFrameUpdateTime = useRef(Date.now());
const frameRate = 10; // 每秒10帧
const frameInterval = 1000 / frameRate;

const drawFrame = () => {
    const ctx = canvasRef.current.getContext('2d');  
    ctx.clearRect(0,0,constants.CANVAS_WIDTH,constants.CANVAS_HEIGHT);

    handleBackground(ctx);
    const charSprite = loadSprite(charAnim);
    SpriteAni(ctx, charSprite, currentFrame);

    // 达到间隔时间再更新帧
    const now = Date.now();
    if (now - lastFrameUpdateTime.current >= frameInterval) {
      setCurrentFrame(prev => {
        if (prev >= walk_frames) return 0;
        return prev + 1;
      });
      lastFrameUpdateTime.current = now;
    }
}

3. 修正组件清理与依赖逻辑

确保useEffect的依赖数组正确,同时保证动画循环的清理逻辑可靠:

useEffect(() => {
    const tick = () => {
        if (!canvasRef.current) return;
        drawFrame();
        requestIdRef.current = requestAnimationFrame(tick);
    };
    requestIdRef.current = requestAnimationFrame(tick);

    return () => {
        cancelAnimationFrame(requestIdRef.current);
    };
}, [charAnim])

最终修改后的Canvas组件片段

import { useEffect, useRef, useState } from "react";
import SpriteAni from '../SpriteAni.jsx';

const Canvas = ({ width, height }) => {
    const canvasRef = useRef(null);
    const requestIdRef = useRef(null);
    const spriteCache = useRef({});
    const lastFrameUpdateTime = useRef(Date.now());
    const frameRate = 10;
    const frameInterval = 1000 / frameRate;

    const [currentFrame, setCurrentFrame] = useState(0);
    const [charAnim, setCharAnim] = useState('walk')
    const walk_frames = 9;

    const loadSprite = (animName) => {
        if (!spriteCache.current[animName]) {
            const img = new Image();
            img.src = `./images/char_${animName}.png`;
            spriteCache.current[animName] = img;
        }
        return spriteCache.current[animName];
    };

    const drawFrame = () => {
        const ctx = canvasRef.current.getContext('2d');  
        ctx.clearRect(0,0,constants.CANVAS_WIDTH,constants.CANVAS_HEIGHT);

        handleBackground(ctx);
        const charSprite = loadSprite(charAnim);
        SpriteAni(ctx, charSprite, currentFrame);

        const now = Date.now();
        if (now - lastFrameUpdateTime.current >= frameInterval) {
            setCurrentFrame(prev => prev >= walk_frames ? 0 : prev + 1);
            lastFrameUpdateTime.current = now;
        }
    }

    useEffect(() => {
        const tick = () => {
            if (!canvasRef.current) return;
            drawFrame();
            requestIdRef.current = requestAnimationFrame(tick);
        };
        requestIdRef.current = requestAnimationFrame(tick);

        return () => {
            cancelAnimationFrame(requestIdRef.current);
        };
    }, [charAnim])

    return (
        <canvas 
            width={width} 
            height={height}
            style={canvasStyle}
            ref={canvasRef}
             />
    )
}

export default Canvas

const canvasStyle = {
    border: "1px solid black",
    position: "absolute",
}

内容的提问来源于stack exchange,提问作者CakeoftheCup

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 05:36:00