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

Canvas调用clearRect后元素未重新渲染问题排查

问题原因

核心问题不在clearRect本身,出在渲染逻辑的执行时序和资源加载方式的设计缺陷:

  1. 你在每帧执行LoadSprite时都会新建Image对象,并且把绘制逻辑绑定在异步的onload回调里。你预期的执行顺序是「清空画布→调用LoadSprite→立刻绘制内容」,但实际顺序是「同步执行clearRect清空画布→同步调用LoadSprite创建Image对象、设置src、绑定onload→当前帧同步代码执行结束→浏览器在后续时机异步触发onload才把内容画到画布上」
  2. 没加clearRect时看起来正常,是因为画到画布上的内容不会被清除,哪怕onload晚几帧触发,内容也会留在画布上。加了clearRect之后,每一帧的第一步就会把上一帧异步画上去的内容全部擦掉,而新一帧的绘制要等onload异步触发才能执行,永远赶不上当前帧的清空操作,最终表现为画布一直空白。
  3. 额外的隐性问题:创建Canvas后没有手动设置width/height属性,会使用默认的300*150尺寸,如果你通过CSS修改了画布显示大小,会出现绘制拉伸、清空范围不匹配的问题;另外drawImageAPI不存在最后一个布尔类型的参数,你传的true是无效参数。
修复方案
  1. 新增资源预加载逻辑:游戏启动前先把所有需要用到的图片全部加载完成,存入缓存,不要在每帧渲染的时候临时创建Image对象等加载。
  2. 渲染阶段同步绘制:渲染时直接从缓存取已经加载完成的图片,同步调用drawImage绘制,完全去掉渲染流程里的异步onload逻辑。
  3. 初始化Canvas时显式设置宽高属性,不要依赖默认值。

修正后的核心代码参考:

// Game类修正
export default class Game {
    constructor() {
        // 新增图片缓存Map
        this.imageCache = new Map();
    }

    // 新增预加载方法,启动游戏前调用
    PreloadImages(assetSrcList) {
        const loadTasks = assetSrcList.map(src => {
            return new Promise((resolve, reject) => {
                const img = new Image();
                img.src = src;
                img.onload = () => {
                    this.imageCache.set(src, img);
                    resolve();
                };
                img.onerror = reject;
            })
        })
        return Promise.all(loadTasks);
    }

    // 创建Canvas时显式传入宽高
    CreateCanvas (width = 512, height = 256) {
        this.canvas = document.createElement("canvas");
        this.canvas.width = width;
        this.canvas.height = height;
        this.ctx = this.canvas.getContext("2d");
        document.body.appendChild(this.canvas);
    }

    ClearCanvas() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }

    SetBackgroundColor (color) {
        this.ctx.fillStyle = color;
        this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
    }

    // 修正LoadSprite逻辑,直接读缓存同步绘制
    LoadSprite (src, position) {
        const targetImg = this.imageCache.get(src);
        if (!targetImg) return;
        // 去掉无效的true参数
        this.ctx.drawImage(targetImg, ...position);
    }
}
// App类修正
class App {
    constructor() {
        this.player = new Player("Player 1", 0, 102);
        this.game = new Game();
        // 初始化时传入你需要的画布尺寸
        this.game.CreateCanvas(512, 256);

        // 所有需要用到的图片资源先统一预加载
        const allAssets = [assets.mario, assets.block];
        this.game.PreloadImages(allAssets).then(() => {
            // 资源加载完成后再绑定事件、启动动画循环
            window.addEventListener('keyup', () => this.player.UpKey());
            window.addEventListener('keydown', (event)=> this.player.HandlerKeyPress(event));
            // 注意类名和你定义的LoopAnimation保持一致
            new LoopAnimation(this.Display.bind(this));
        })
    }

    Display() {
        this.game.ClearCanvas();
        this.SetLevel();
    }

    SetLevel() {
        this.game.LoadSprite(assets.mario, [...player_assets.mario.small.standing_right,  ...this.player.GetPosition(), 16, 16]);
        this.StaticObject();
    }

    StaticObject () {
        for(let i = 0; i < 32; i++){
            this.game.LoadSprite(assets.block, [...lvl_1.block.ground.size,  i* 16, 134, 16,16]);
            this.game.LoadSprite(assets.block, [...lvl_1.block.ground.size,  i* 16, 118, 16,16]);
        }
    }
}

new App();
// LoopAnimation类不需要改动,保持原有逻辑即可
export default class LoopAnimation {
    constructor(display) {
        this.display = display;
        this.Animation = this.Animation.bind(this);
        this.Animation();
    }

    Animation() {
        requestAnimationFrame(this.Animation);
        this.display();
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 04:39:17