aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Discord Bots

Discord Profile Card: Level Cards with node-canvas

A discord profile card is that sleek image bundling a user's avatar, level and server rank into a single graphic. The cards you see in popular bots can be drawn in your own bot with node-canvas: a background, an avatar clipped to a circle, an XP progress bar and custom fonts. In this guide you'll build the card from scratch, send the result to Discord as a PNG, and learn the gotchas that keep your bot fast and reliable.

Why draw images server-side?

Discord embeds are limited to text, fields and a single image; you can't render a dynamic progress bar or a per-user layout with an embed alone. The solution is to generate the image on the server with node-canvas and attach it to the message as a file. node-canvas is the Node.js counterpart of the browser's Canvas 2D API: you use the same fillRect, arc and drawImage calls, but you get the output as a Buffer.

There are two common packages. The classic option is the canvas package (Cairo-based, may require compilation). The modern alternative, shipping precompiled binaries and generally faster, is @napi-rs/canvas. Their APIs are largely identical; this guide uses classic node-canvas.

npm install canvas discord.js

Preparing the canvas

The first step is a fixed-size canvas. 934×282 pixels is a common, well-proportioned size for these cards. We grab the drawing context with getContext('2d') and fill the background.

const { createCanvas, loadImage } = require('canvas');

function roundRect(ctx, x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r);
  ctx.arcTo(x, y, x + w, y, r);
  ctx.closePath();
}

const canvas = createCanvas(934, 282);
const ctx = canvas.getContext('2d');

// Background
ctx.fillStyle = '#23272a';
ctx.fillRect(0, 0, canvas.width, canvas.height);

// Semi-transparent inner panel
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
roundRect(ctx, 22, 22, 890, 238, 16);
ctx.fill();

The roundRect helper uses arcTo for rounded corners. Recent Node versions also expose a built-in ctx.roundRect(), but writing your own gives consistent results across every environment.

Clipping the avatar into a circle

The avatar is the star of the card. To make it circular, we define a clip path and then draw the image inside it. In discord.js you fetch the avatar URL with user.displayAvatarURL(); passing extension: 'png' avoids the animated WebP/GIF that node-canvas can't read.

const avatarURL = interaction.user.displayAvatarURL({
  extension: 'png',
  size: 256,
});
const avatar = await loadImage(avatarURL);

const cx = 141; // circle center X
const cy = 141; // circle center Y
const radius = 80;

ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
ctx.drawImage(avatar, cx - radius, cy - radius, radius * 2, radius * 2);
ctx.restore();

// Border around the avatar
ctx.strokeStyle = '#5865f2';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.stroke();

The save() / restore() pair is critical: clip() confines every following draw to the circle, so you must restore the context right after drawing the avatar; otherwise the text and bar get trapped inside the circle too.

Drawing text, level and rank

For a polished card the default system font isn't enough. Register a custom font file (.ttf / .otf) with registerFont, then reference the same family name in ctx.font. This call must run before you create the canvas.

const { registerFont } = require('canvas');
registerFont('./assets/Inter-Bold.ttf', { family: 'Inter', weight: 'bold' });
registerFont('./assets/Inter-Regular.ttf', { family: 'Inter' });

// ... inside the card drawing:
ctx.fillStyle = '#ffffff';
ctx.font = '40px "Inter"';
ctx.fillText(interaction.user.username, 250, 110);

ctx.fillStyle = '#b9bbbe';
ctx.font = '28px "Inter"';
ctx.fillText(`Level ${level}`, 250, 150);

// Right-align the rank
ctx.textAlign = 'right';
ctx.fillStyle = '#5865f2';
ctx.font = 'bold 34px "Inter"';
ctx.fillText(`#${rank}`, 890, 110);
ctx.textAlign = 'left'; // reset the alignment

textAlign changes the context's global state; if you don't switch back to 'left' after right-aligning, every later piece of text will be right-aligned too.

The XP progress bar

The progress bar is two rectangles: a grey track and a coloured fill drawn proportionally on top. You compute the ratio by dividing the user's current XP by the XP required for the next level.

const barX = 250, barY = 185, barW = 600, barH = 34;
const ratio = Math.min(currentXP / requiredXP, 1);

// Track
ctx.fillStyle = '#484b52';
roundRect(ctx, barX, barY, barW, barH, barH / 2);
ctx.fill();

// Filled part (at least one half-circle wide)
const fillW = Math.max(ratio * barW, barH);
ctx.fillStyle = '#5865f2';
roundRect(ctx, barX, barY, fillW, barH, barH / 2);
ctx.fill();

// XP text on top
ctx.fillStyle = '#ffffff';
ctx.font = '20px "Inter"';
ctx.textAlign = 'center';
ctx.fillText(`${currentXP} / ${requiredXP} XP`, barX + barW / 2, barY + 24);
ctx.textAlign = 'left';

Math.max(ratio * barW, barH) stops the rounded bar from looking odd when XP is low; the fill stays at least as wide as its own height.

Sending the card to Discord

In the final step you convert the canvas to a PNG buffer, wrap it in an AttachmentBuilder and reply to the interaction. Because rendering can take a few hundred milliseconds, calling deferReply() first keeps you safe from the three-second interaction timeout.

const { AttachmentBuilder } = require('discord.js');

await interaction.deferReply();

// ... all the drawing steps above ...

const buffer = canvas.toBuffer('image/png');
const file = new AttachmentBuilder(buffer, { name: 'profile.png' });

await interaction.editReply({ files: [file] });

You can wire the same layout into a /rank command, a welcome message or a level-up notification. The data (level, XP, rank) usually comes from SQLite or MySQL; the card is simply the layer that visualises it.

Frequently Asked Questions

Should I use node-canvas or @napi-rs/canvas?

Both do the same job. canvas (node-canvas) is mature and widespread but may require a Cairo build on some systems. @napi-rs/canvas ships precompiled binaries, installs cleanly and is usually faster. For a new project, trying the latter is reasonable; the API is nearly identical.

Why does the avatar sometimes fail to load?

The most common cause is animated avatars: the default URL can return a .gif that node-canvas can't draw. Always request a static PNG with displayAvatarURL({ extension: 'png' }). Also make sure you await loadImage(...) the avatar URL.

Will rendering a card on every command slow the bot down?

Individual calls are light, but on busy servers they can saturate the CPU. Cache a user's card briefly, load shared assets like the background once with loadImage and reuse them, and move rendering into a queue if needed.

Want a professional level system in your bot? I can build end-to-end custom profile cards with avatars, XP bars and rankings alongside economy and moderation systems. Get in touch and let's talk about your project.

Bu kategorideki tüm yazılar →

Devamı için