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

求助:Canvas仅绘制单张图片,多次调用drawImage无效的问题排查

解决Canvas仅绘制单张图片的问题

嘿,我帮你定位到问题根源了!你的Canvas只显示一张图片,核心问题出在图片加载的异步逻辑处理错误,尤其是数字图片的绘制部分,具体原因和修复方案如下:

问题分析

  1. 重复覆盖onload回调:在你的test函数里,每当需要绘制一个数字时,你都会给同一个images[totalBombs]对象重复设置onload回调和src。比如多个单元格需要数字1时,后面的回调会直接覆盖前面的,等图片加载完成后,只有最后一次绑定的回调会执行,所以只会绘制最后一个需要该数字的单元格。
  2. 图片加载时机混乱:炸弹图片的加载是异步的,但你没有确保它加载完成后再绘制;数字图片则是边计算边触发加载,导致逻辑混乱。
  3. 炸弹位置生成逻辑有隐患:原代码里递归调用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 18:47:37