Une carte de profil Discord, c'est cette image élégante qui rassemble l'avatar d'un utilisateur, son niveau et son rang sur le serveur en un seul visuel. Les cartes que tu vois dans les bots populaires, tu peux les dessiner dans ton propre bot avec node-canvas : un fond, un avatar découpé en cercle, une barre de progression d'XP et des polices personnalisées. Dans ce guide, tu vas construire la carte de zéro, envoyer le résultat à Discord sous forme de PNG et découvrir les pièges qui gardent ton bot rapide et fiable.
Pourquoi dessiner les images côté serveur ?
Les embeds Discord se limitent au texte, aux champs et à une seule image ; tu ne peux pas rendre une barre de progression dynamique ni une mise en page propre à chaque utilisateur avec un embed seul. La solution est de générer l'image sur le serveur avec node-canvas et de l'attacher au message en tant que fichier. node-canvas est l'équivalent Node.js de l'API Canvas 2D du navigateur : tu utilises les mêmes appels fillRect, arc et drawImage, mais tu récupères le résultat sous forme de Buffer.
Il existe deux paquets courants. L'option classique est le paquet canvas (basé sur Cairo, parfois à compiler). L'alternative moderne, livrée avec des binaires précompilés et généralement plus rapide, est @napi-rs/canvas. Leurs API sont très proches ; ce guide utilise le node-canvas classique.
npm install canvas discord.js
Préparer le canevas
La première étape est un canevas de taille fixe. 934×282 pixels est une dimension courante et bien proportionnée pour ce type de carte. On récupère le contexte de dessin avec getContext('2d'), puis on remplit le fond.
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');
// Fond
ctx.fillStyle = '#23272a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Panneau interne semi-transparent
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
roundRect(ctx, 22, 22, 890, 238, 16);
ctx.fill();
La fonction roundRect utilise arcTo pour les coins arrondis. Les versions récentes de Node exposent aussi une méthode native ctx.roundRect(), mais écrire la tienne donne un résultat cohérent dans tous les environnements.
Découper l'avatar en cercle
L'avatar est la vedette de la carte. Pour le rendre circulaire, on définit un chemin de découpe (clip) puis on dessine l'image à l'intérieur. Avec discord.js, tu récupères l'URL de l'avatar via user.displayAvatarURL() ; passer extension: 'png' évite le WebP/GIF animé que node-canvas ne sait pas lire.
const avatarURL = interaction.user.displayAvatarURL({
extension: 'png',
size: 256,
});
const avatar = await loadImage(avatarURL);
const cx = 141; // centre X du cercle
const cy = 141; // centre Y du cercle
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();
// Bordure autour de l'avatar
ctx.strokeStyle = '#5865f2';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.stroke();
Le couple save() / restore() est essentiel : clip() confine tous les dessins suivants au cercle, il faut donc restaurer le contexte juste après avoir dessiné l'avatar ; sinon le texte et la barre se retrouvent eux aussi enfermés dans le cercle.
Écrire le texte, le niveau et le rang
Pour une carte soignée, la police système par défaut ne suffit pas. Enregistre un fichier de police personnalisé (.ttf / .otf) avec registerFont, puis référence le même nom de family dans ctx.font. Cet appel doit être exécuté avant de créer le canevas.
const { registerFont } = require('canvas');
registerFont('./assets/Inter-Bold.ttf', { family: 'Inter', weight: 'bold' });
registerFont('./assets/Inter-Regular.ttf', { family: 'Inter' });
// ... dans le dessin de la carte :
ctx.fillStyle = '#ffffff';
ctx.font = '40px "Inter"';
ctx.fillText(interaction.user.username, 250, 110);
ctx.fillStyle = '#b9bbbe';
ctx.font = '28px "Inter"';
ctx.fillText(`Niveau ${level}`, 250, 150);
// Aligner le rang à droite
ctx.textAlign = 'right';
ctx.fillStyle = '#5865f2';
ctx.font = 'bold 34px "Inter"';
ctx.fillText(`#${rank}`, 890, 110);
ctx.textAlign = 'left'; // réinitialiser l'alignement
textAlign modifie l'état global du contexte ; si tu ne reviens pas à 'left' après l'alignement à droite, tout texte ultérieur sera lui aussi aligné à droite.
La barre de progression d'XP
La barre de progression est faite de deux rectangles : une piste grise et un remplissage coloré dessiné proportionnellement par-dessus. Tu calcules le ratio en divisant l'XP actuel de l'utilisateur par l'XP requis pour le niveau suivant.
const barX = 250, barY = 185, barW = 600, barH = 34;
const ratio = Math.min(currentXP / requiredXP, 1);
// Piste
ctx.fillStyle = '#484b52';
roundRect(ctx, barX, barY, barW, barH, barH / 2);
ctx.fill();
// Partie remplie (au moins un demi-cercle de large)
const fillW = Math.max(ratio * barW, barH);
ctx.fillStyle = '#5865f2';
roundRect(ctx, barX, barY, fillW, barH, barH / 2);
ctx.fill();
// Texte XP par-dessus
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) évite que la barre arrondie ait l'air étrange quand l'XP est faible ; le remplissage reste au moins aussi large que sa propre hauteur.
Envoyer la carte à Discord
Dans la dernière étape, tu convertis le canevas en buffer PNG, tu l'enveloppes dans un AttachmentBuilder et tu réponds à l'interaction. Comme le rendu peut prendre quelques centaines de millisecondes, appeler deferReply() d'abord te met à l'abri du délai d'interaction de trois secondes.
const { AttachmentBuilder } = require('discord.js');
await interaction.deferReply();
// ... toutes les étapes de dessin ci-dessus ...
const buffer = canvas.toBuffer('image/png');
const file = new AttachmentBuilder(buffer, { name: 'profil.png' });
await interaction.editReply({ files: [file] });
Tu peux brancher la même mise en page sur une commande /rank, un message de bienvenue ou une notification de passage de niveau. Les données (niveau, XP, rang) viennent généralement de SQLite ou MySQL ; la carte n'est que la couche qui les met en image.
Questions fréquentes
Dois-je utiliser node-canvas ou @napi-rs/canvas ?
Les deux font le même travail. canvas (node-canvas) est mûr et répandu mais peut exiger une compilation de Cairo sur certains systèmes. @napi-rs/canvas est livré avec des binaires précompilés, s'installe sans accroc et est généralement plus rapide. Pour un nouveau projet, essayer le second est raisonnable ; l'API est quasi identique.
Pourquoi l'avatar ne se charge-t-il parfois pas ?
La cause la plus fréquente est l'avatar animé : l'URL par défaut peut renvoyer un .gif que node-canvas ne sait pas dessiner. Demande toujours un PNG statique avec displayAvatarURL({ extension: 'png' }). Assure-toi aussi de bien faire await loadImage(...) sur l'URL de l'avatar.
Générer une carte à chaque commande ralentit-il le bot ?
Les appels isolés sont légers, mais sur les serveurs chargés ils peuvent saturer le CPU. Mets en cache la carte d'un utilisateur pour un court instant, charge les ressources communes comme le fond une seule fois avec loadImage et réutilise-les, et déplace le rendu dans une file d'attente si nécessaire.
Tu veux un système de niveaux professionnel dans ton bot ? Je peux construire de bout en bout des cartes de profil personnalisées avec avatars, barres d'XP et classements, aux côtés de systèmes d'économie et de modération. Contacte-moi et parlons de ton projet.