求助:Canvas仅绘制单张图片,多次调用drawImage无效的问题排查
解决Canvas仅绘制单张图片的问题
嘿,我帮你定位到问题根源了!你的Canvas只显示一张图片,核心问题出在图片加载的异步逻辑处理错误,尤其是数字图片的绘制部分,具体原因和修复方案如下:
问题分析
- 重复覆盖
onload回调:在你的test函数里,每当需要绘制一个数字时,你都会给同一个images[totalBombs]对象重复设置onload回调和src。比如多个单元格需要数字1时,后面的回调会直接覆盖前面的,等图片加载完成后,只有最后一次绑定的回调会执行,所以只会绘制最后一个需要该数字的单元格。 - 图片加载时机混乱:炸弹图片的加载是异步的,但你没有确保它加载完成后再绘制;数字图片则是边计算边触发加载,导致逻辑混乱。
- 炸弹位置生成逻辑有隐患:原代码里递归调用
numOfBombs可能导致栈溢出,遇到重复位置时处理不够稳妥。
修复后的完整代码
我重构了你的代码,优化了图片加载和绘制逻辑,确保所有图片都能正确显示:
const canvas = document.getElementById("myCanvas") const ctx = canvas.getContext("2d") class Cell { constructor(x, y, w) { this.x = x this.y = y this.w = w this.bomb = false this.revealed = false } show() { const cell = new Path2D(); cell.rect(this.x, this.y, this.w, this.w); ctx.stroke(cell); this.cell = cell; } } const w = canvas.width const h = canvas.height const ColumnRow = w / 15 const cells = [] const bombs = 5 const bombPosition = [] let checked = true const arr = [ [-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0] ] // 存储每个数字需要绘制的位置 const numberPositions = { 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 7: [], 8: [] } // 统一管理所有图片对象 const images = { bomb: new Image(), 1: new Image(), 2: new Image(), 3: new Image(), 4: new Image(), 5: new Image(), 6: new Image(), 7: new Image(), 8: new Image() } // 标记所有图片是否加载完成 let allImagesLoaded = false // 预加载所有图片,确保加载完成后再绘制 function loadImages() { // 设置所有图片的资源地址 images.bomb.src = "https://raw.githubusercontent.com/americosp/Minesweeper/master/Minesweeper/images/mine.png"; for (let i = 1; i <= 8; i++) { images[i].src = `https://raw.githubusercontent.com/americosp/Minesweeper/master/Minesweeper/images/number_${i}.png` } let loadedCount = 0; const totalImages = Object.keys(images).length; // 检查所有图片是否加载完成 function checkAllLoaded() { loadedCount++; if (loadedCount === totalImages) { allImagesLoaded = true; } } // 给每个图片绑定加载完成事件 images.bomb.onload = checkAllLoaded; for (let i = 1; i <= 8; i++) { images[i].onload = checkAllLoaded; } } function setup() { for (let x = 0; x < w - 1; x += ColumnRow) { for (let y = 0; y < h - 1; y += ColumnRow) { cells.push(new Cell(x, y, ColumnRow)) } } } function drawCells() { for (let c of cells) { c.show() } } function numOfBombs() { for (let i = 0; i < bombs; i++) { const randomX = Math.floor(Math.random() * w / ColumnRow) * ColumnRow const randomY = Math.floor(Math.random() * h / ColumnRow) * ColumnRow // 检查当前位置是否已有炸弹,避免重复 const hasDuplicate = bombPosition.some(pos => pos.x === randomX && pos.y === randomY); if (!hasDuplicate) { bombPosition.push({ x: randomX, y: randomY }); } else { // 有重复则重新生成当前位置 i--; } } } function drawBomb() { if (images.bomb.complete) { bombPosition.forEach(pos => { ctx.drawImage(images.bomb, pos.x, pos.y, ColumnRow, ColumnRow) }) } } // 先收集所有数字需要绘制的位置,统一绘制 function collectNumberPositions() { // 重置位置存储 Object.keys(numberPositions).forEach(key => numberPositions[key] = []); for (let cell of cells) { if (cell.bomb) continue; let totalBombs = 0; // 检查周围8个格子的炸弹数量 for (let offset of arr) { const tmpx = cell.x + offset[0] * ColumnRow; const tmpy = cell.y + offset[1] * ColumnRow; const foundCell = cells.find(c => c.x === tmpx && c.y === tmpy); if (foundCell && foundCell.bomb) { totalBombs++; } } if (totalBombs > 0) { numberPositions[totalBombs].push({ x: cell.x, y: cell.y }); } } } function drawNumbers() { if (!allImagesLoaded) return; // 遍历每个数字,绘制所有对应位置 for (let num in numberPositions) { const positions = numberPositions[num]; const img = images[num]; positions.forEach(pos => { ctx.drawImage(img, pos.x, pos.y, ColumnRow, ColumnRow); }) } } function draw() { // 每次绘制前清空画布,避免重复叠加 ctx.clearRect(0, 0, canvas.width, canvas.height); drawCells() if (checked) { // 初始化时收集所有数字位置 collectNumberPositions(); // 标记初始化完成,避免重复执行 checked = false } // 所有图片加载完成后再绘制炸弹和数字 if (allImagesLoaded) { drawBomb() drawNumbers() } requestAnimationFrame(draw) } function update() { setup() numOfBombs() // 标记单元格是否为炸弹 cells.forEach(cell => { cell.bomb = bombPosition.some(pos => pos.x === cell.x && pos.y === cell.y); }) // 开始预加载所有图片 loadImages() } update() draw()
<canvas id="myCanvas" width="600" height="600"></canvas>
关键改进点
- 预加载所有图片:提前创建并加载所有需要的图片,统一监听加载完成状态,确保所有资源就绪后再绘制。
- 统一收集绘制位置:先遍历所有单元格,把每个数字需要绘制的位置收集起来,等图片加载完成后一次性绘制,避免重复覆盖
onload回调。 - 修复炸弹位置生成逻辑:用
i--替代递归,避免栈溢出问题,更安全高效。 - 清空画布:每次绘制前清空画布,解决重复绘制导致的画面混乱问题。
内容的提问来源于stack exchange,提问作者NoNameYet
相关产品推荐
相关产品推荐

