如何在discord.js自定义机器人中实现MEE6风格的动态自定义图形
Discord.js 自定义动态图形卡片实现方案
核心可行性说明
这类动态生成的图形内容完全可以落地,实现逻辑本质是在服务端通过画布能力逐像素绘制完目标图片后,以消息附件的形式发送到频道,刚好可以补足Discord原生embed的短板:embed本身不支持自定义字体、自定义用户图标裁切、精细化渐变/阴影特效,完全适配类MEE6等级条的功能需求。
必需引入的依赖包
@napi-rs/canvas:Node端跨平台兼容的画布实现,替代老旧的canvas包,安装踩坑率低,支持本地字体加载、图片渲染、形状绘制、像素操作全流程能力,是整个图形生成的核心依赖discord.js:利用其内置的AttachmentBuilder能力,把绘制完成的图片Buffer转成可发送的消息附件,现有成熟的命令处理逻辑可以直接对接这个能力,不需要额外调整消息发送的基础逻辑- 可选依赖:如果需要做动态GIF效果的图形,额外引入
gifencoder即可把多帧绘制的画布内容编码成可发送的动图,不需要引入Playwright这类重型无头浏览器依赖
对接现有命令流的实现框架(module.exports 结构)
const { AttachmentBuilder } = require('discord.js'); const { createCanvas, GlobalFonts } = require('@napi-rs/canvas'); // 启动时预先注册本地自定义字体,解决embed不支持自定义字体的限制 GlobalFonts.registerFromPath('./assets/fonts/自定义粗体字体.ttf', 'CardCustomFont'); module.exports = { /** * 核心卡片绘制方法 * @param {import('discord.js').User} targetUser 动态获取的目标用户实例 * @param {number} userLevel 用户当前等级 * @param {number} currentExp 用户当前累计经验 * @param {number} expRequired 升级到下一级所需经验 * @returns {Promise<AttachmentBuilder>} 可直接发送的图片附件 */ async generateCard(targetUser, userLevel, currentExp, expRequired) { // 初始化常用尺寸的画布(800*240是目前等级卡的通用适配尺寸) const canvas = createCanvas(800, 240); const ctx = canvas.getContext('2d'); // 1. 绘制渐变背景层 const bgGradient = ctx.createLinearGradient(0, 0, 800, 240); bgGradient.addColorStop(0, '#2f3136'); bgGradient.addColorStop(1, '#202225'); ctx.fillStyle = bgGradient; ctx.fillRect(0, 0, 800, 240); // 2. 绘制圆形裁切的用户动态头像(embed无法实现的自定义图标能力) const avatarUrl = targetUser.displayAvatarURL({ extension: 'png', size: 128 }); const avatarBuffer = await fetch(avatarUrl).then(res => res.arrayBuffer()); const avatarImg = await ctx.loadImage(avatarBuffer); ctx.beginPath(); ctx.arc(120, 120, 80, 0, Math.PI * 2); ctx.fillStyle = '#5865f2'; ctx.fill(); ctx.clip(); // 按圆形裁切范围绘制头像 ctx.drawImage(avatarImg, 40, 40, 160, 160); // 3. 绘制动态文本内容(使用自定义字体) ctx.font = '32px CardCustomFont'; ctx.fillStyle = '#ffffff'; ctx.fillText(targetUser.globalName ?? targetUser.username, 230, 100); ctx.font = '20px CardCustomFont'; ctx.fillStyle = '#b9bbbe'; ctx.fillText(`等级 ${userLevel}`, 230, 125); // 4. 绘制动态经验进度条 const progressRatio = Math.min(currentExp / expRequired, 1); ctx.fillStyle = '#36393f'; ctx.fillRect(230, 145, 520, 18); ctx.fillStyle = '#5865f2'; ctx.fillRect(230, 145, 520 * progressRatio, 18); ctx.fillStyle = '#b9bbbe'; ctx.fillText(`${currentExp} / ${expRequired}`, 230, 190); // 把画布内容转成可发送的PNG Buffer const outputBuffer = canvas.toBuffer('image/png'); return new AttachmentBuilder(outputBuffer, { name: 'level-card.png' }); }, // 命令触发后的执行逻辑,直接对接现有命令处理流即可 async execute(interaction) { const targetUser = interaction.options.getUser('目标用户') ?? interaction.user; // 替换为自有存储的用户等级、经验值查询逻辑 const userStats = { level: 12, currentExp: 450, expToNext: 800 }; const cardAttachment = await this.generateCard( targetUser, userStats.level, userStats.currentExp, userStats.expToNext ); // 直接回复带附件的消息,不需要依赖embed承载图片 await interaction.reply({ files: [cardAttachment] }); } }
实现注意事项
- 性能优化:可以把固定使用的装饰素材、字体、背景图在服务启动时预加载到内存,避免每次绘制都重新读取磁盘文件;对高频请求的卡片可以做5-10秒的短时间缓存,降低重复绘制的CPU消耗
- 兼容性处理:拉取用户头像时要做无头像的兜底逻辑,加载本地字体时要做路径校验,避免生产环境出现绘制失败的问题
- 特效扩展:如果要实现辉光、投影、渐变描边这类酷炫效果,直接在canvas的上下文设置对应参数即可,不需要引入重型的前端渲染框架
内容的提问来源于stack exchange,提问作者Tyrcnex
相关产品推荐
相关产品推荐

