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

NodeJS+Canvas实现21点桌时最后一张卡牌无法渲染的问题

问题分析与解决

核心原因

你调用drawCard异步函数时未使用await等待执行完成,导致生成Canvas的PNG流(canvas.createPNGStream())时,卡牌的加载与绘制操作还未完成,最终图片自然无法显示卡牌。

当你把drawCard内的代码直接移到循环里时,循环中的await loadImage会阻塞流程,确保每张卡牌加载并绘制完成后才继续执行,因此能正常渲染。

修复方案

在循环中调用drawCard时添加await,确保所有卡牌绘制完成后再生成图片流:

// 修改循环部分代码
for (let i = 0; i < joined.length; i++) {
  let plPic = await loadImage(joined[i].pic);
  let [x, y] = [
    PORTIONS * (i + 1) - OFFSET_X,
    i % 2 === 0 && joined.length > 2 ? PLAYER_Y_ODD : PLAYER_Y
  ];
  joined[i].pos = { x, y };
  ctx.drawImage(plPic, x, y);
  // 添加await等待drawCard执行完成
  await drawCard(
    joined[i],
    deck,
    Math.floor(x - OFFSET_X - 32 * joined[i].hand.length),
    y - 96
  );
}

另外可检查client.draw方法是否正确实现了抽牌逻辑(从卡组中移除已抽卡牌),这虽不影响当前渲染问题,但会影响游戏逻辑的正确性。

完整优化代码(关键修改处已标注)

await int.deferReply();

interface Player {
  pic: string;
  hand: string[];
  pos?: { x: number; y: number };
}

const joinTable = (user: User): Player => ({
  pic: user.displayAvatarURL({ extension: "png" }),
  hand: []
});

const joined = [
  joinTable(int.user),
  joinTable(int.user),
  joinTable(int.user),
  joinTable(int.user),
  joinTable(int.user)
];
const [W, H] = [1920, 1080];
const [DEALER_Y, PLAYER_Y] = [H * 0.1, H * 0.75];
const PLAYER_Y_ODD = PLAYER_Y - 300;
const [HALF_W, OFFSET_X] = [W * 0.5, 64];
const [CARD_DIMS, CARD_OFFSET_X] = [[52, 82] as [number, number], 32];
const PORTIONS = W / (joined.length + 1);
const canvas = createCanvas(W, H);
const ctx = canvas.getContext("2d");
const { img, cards } = PATHS;

const drawCard = async (
  p: Player,
  deck: string[],
  x: number,
  y: number,
  n: number = 1
) => {
  for (let i = 0; i < n; i++) {
    let c = client.draw(deck, p.hand);
    p.hand.push(c);
    let cSprite = await loadImage(cards + c + ".png");
    ctx.drawImage(cSprite, x, y, ...CARD_DIMS);
    // 可保留日志用于调试
    //console.log(`${c}: (${x}, ${y})`);
  }
};

let deck = client.deck();
let bg = await loadImage(img + "green.jpg");
let dProPic = await loadImage(
  client.user.displayAvatarURL({ extension: "png" })
);
ctx.drawImage(bg, 0, 0);
ctx.drawImage(dProPic, HALF_W - OFFSET_X, DEALER_Y);

for (let i = 0; i < joined.length; i++) {
  let plPic = await loadImage(joined[i].pic);
  let [x, y] = [
    PORTIONS * (i + 1) - OFFSET_X,
    i % 2 === 0 && joined.length > 2 ? PLAYER_Y_ODD : PLAYER_Y
  ];
  joined[i].pos = { x, y };
  ctx.drawImage(plPic, x, y);
  // 关键修改:添加await等待drawCard完成
  await drawCard(
    joined[i],
    deck,
    Math.floor(x - OFFSET_X - 32 * joined[i].hand.length),
    y - 96
  );
}

let atc = new AttachmentBuilder(canvas.createPNGStream());
await int.editReply({ files: [atc] });

额外说明

Canvas绘制是同步API,但图片加载是异步操作,必须确保所有异步的图片加载与绘制操作完成后,再将Canvas转换为流或图片。如果不等待异步函数完成,就会出现"代码执行到了但没生效"的情况——实际绘制逻辑还在等待图片加载,而此时已经生成了最终的图片流。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 21:33:19