Eine Discord-Profilkarte ist jenes elegante Bild, das Avatar, Level und Serverrang eines Nutzers in einer einzigen Grafik bündelt. Die Karten, die du in beliebten Bots siehst, kannst du in deinem eigenen Bot mit node-canvas zeichnen: ein Hintergrund, ein auf einen Kreis zugeschnittener Avatar, eine XP-Fortschrittsleiste und eigene Schriftarten. In dieser Anleitung baust du die Karte von Grund auf, schickst das Ergebnis als PNG an Discord und lernst die Stolperfallen kennen, die deinen Bot schnell und zuverlässig halten.
Warum Bilder serverseitig zeichnen?
Discord-Embeds beschränken sich auf Text, Felder und ein einzelnes Bild; eine dynamische Fortschrittsleiste oder ein nutzerspezifisches Layout lässt sich mit einem Embed allein nicht rendern. Die Lösung ist, das Bild auf dem Server mit node-canvas zu erzeugen und es der Nachricht als Datei anzuhängen. node-canvas ist das Node.js-Gegenstück zur Canvas-2D-API des Browsers: Du verwendest dieselben Aufrufe fillRect, arc und drawImage, erhältst die Ausgabe aber als Buffer.
Es gibt zwei gängige Pakete. Die klassische Option ist das canvas-Paket (auf Cairo basierend, kann eine Kompilierung erfordern). Die moderne Alternative mit vorkompilierten Binaries und meist schneller ist @napi-rs/canvas. Ihre APIs sind weitgehend identisch; diese Anleitung nutzt das klassische node-canvas.
npm install canvas discord.js
Das Canvas vorbereiten
Der erste Schritt ist ein Canvas mit fester Größe. 934×282 Pixel ist eine gängige, gut proportionierte Größe für solche Karten. Wir holen den Zeichenkontext mit getContext('2d') und füllen den Hintergrund.
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');
// Hintergrund
ctx.fillStyle = '#23272a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Halbtransparentes Innenpanel
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
roundRect(ctx, 22, 22, 890, 238, 16);
ctx.fill();
Die Hilfsfunktion roundRect nutzt arcTo für abgerundete Ecken. Neuere Node-Versionen bieten auch eine eingebaute ctx.roundRect(), doch eine eigene Funktion liefert in jeder Umgebung konsistente Ergebnisse.
Den Avatar in einen Kreis zuschneiden
Der Avatar ist der Star der Karte. Um ihn rund zu machen, definieren wir einen Clip-Pfad und zeichnen das Bild anschließend darin. In discord.js holst du die Avatar-URL mit user.displayAvatarURL(); mit extension: 'png' vermeidest du das animierte WebP/GIF, das node-canvas nicht lesen kann.
const avatarURL = interaction.user.displayAvatarURL({
extension: 'png',
size: 256,
});
const avatar = await loadImage(avatarURL);
const cx = 141; // Kreismittelpunkt X
const cy = 141; // Kreismittelpunkt 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();
// Rahmen um den Avatar
ctx.strokeStyle = '#5865f2';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.stroke();
Das Paar save() / restore() ist entscheidend: clip() begrenzt jede folgende Zeichnung auf den Kreis, deshalb musst du den Kontext direkt nach dem Zeichnen des Avatars wiederherstellen; sonst werden auch Text und Leiste im Kreis eingeschlossen.
Text, Level und Rang zeichnen
Für eine ansprechende Karte reicht die Standard-Systemschrift nicht. Registriere eine eigene Schriftdatei (.ttf / .otf) mit registerFont und verweise dann in ctx.font auf denselben family-Namen. Dieser Aufruf muss vor dem Erstellen des Canvas erfolgen.
const { registerFont } = require('canvas');
registerFont('./assets/Inter-Bold.ttf', { family: 'Inter', weight: 'bold' });
registerFont('./assets/Inter-Regular.ttf', { family: 'Inter' });
// ... innerhalb des Karten-Zeichnens:
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);
// Den Rang rechtsbündig setzen
ctx.textAlign = 'right';
ctx.fillStyle = '#5865f2';
ctx.font = 'bold 34px "Inter"';
ctx.fillText(`#${rank}`, 890, 110);
ctx.textAlign = 'left'; // Ausrichtung zurücksetzen
textAlign ändert den globalen Zustand des Kontexts; wenn du nach der Rechtsausrichtung nicht auf 'left' zurückschaltest, wird jeder spätere Text ebenfalls rechtsbündig.
Die XP-Fortschrittsleiste
Die Fortschrittsleiste besteht aus zwei Rechtecken: einer grauen Spur und einer farbigen Füllung, die proportional darüber gezeichnet wird. Das Verhältnis berechnest du, indem du die aktuelle XP des Nutzers durch die für das nächste Level nötige XP teilst.
const barX = 250, barY = 185, barW = 600, barH = 34;
const ratio = Math.min(currentXP / requiredXP, 1);
// Spur
ctx.fillStyle = '#484b52';
roundRect(ctx, barX, barY, barW, barH, barH / 2);
ctx.fill();
// Gefüllter Teil (mindestens ein Halbkreis breit)
const fillW = Math.max(ratio * barW, barH);
ctx.fillStyle = '#5865f2';
roundRect(ctx, barX, barY, fillW, barH, barH / 2);
ctx.fill();
// XP-Text darüber
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) verhindert, dass die abgerundete Leiste bei niedriger XP seltsam aussieht; die Füllung bleibt mindestens so breit wie ihre eigene Höhe.
Die Karte an Discord senden
Im letzten Schritt wandelst du das Canvas in einen PNG-Buffer um, verpackst es in einen AttachmentBuilder und antwortest auf die Interaktion. Da das Rendern einige hundert Millisekunden dauern kann, schützt dich ein vorheriges deferReply() vor dem Drei-Sekunden-Interaktions-Timeout.
const { AttachmentBuilder } = require('discord.js');
await interaction.deferReply();
// ... alle Zeichenschritte von oben ...
const buffer = canvas.toBuffer('image/png');
const file = new AttachmentBuilder(buffer, { name: 'profil.png' });
await interaction.editReply({ files: [file] });
Du kannst dasselbe Layout an einen /rank-Befehl, eine Willkommensnachricht oder eine Level-up-Benachrichtigung anbinden. Die Daten (Level, XP, Rang) kommen meist aus SQLite oder MySQL; die Karte ist lediglich die Schicht, die sie visualisiert.
Häufige Fragen
Soll ich node-canvas oder @napi-rs/canvas verwenden?
Beide erledigen dieselbe Aufgabe. canvas (node-canvas) ist ausgereift und verbreitet, kann aber auf manchen Systemen einen Cairo-Build erfordern. @napi-rs/canvas liefert vorkompilierte Binaries, installiert sich reibungslos und ist meist schneller. Für ein neues Projekt ist es sinnvoll, Letzteres zu probieren; die API ist nahezu identisch.
Warum lädt der Avatar manchmal nicht?
Die häufigste Ursache sind animierte Avatare: Die Standard-URL kann ein .gif zurückgeben, das node-canvas nicht zeichnen kann. Fordere immer ein statisches PNG mit displayAvatarURL({ extension: 'png' }) an. Stelle außerdem sicher, dass du die Avatar-URL mit await loadImage(...) abwartest.
Verlangsamt das Rendern einer Karte bei jedem Befehl den Bot?
Einzelne Aufrufe sind leicht, doch auf stark frequentierten Servern können sie die CPU auslasten. Cache die Karte eines Nutzers kurz, lade gemeinsame Assets wie den Hintergrund einmal mit loadImage und verwende sie wieder, und verlagere das Rendern bei Bedarf in eine Warteschlange.
Möchtest du ein professionelles Level-System in deinem Bot? Ich baue durchgängige individuelle Profilkarten mit Avataren, XP-Leisten und Ranglisten, ergänzt um Wirtschafts- und Moderationssysteme. Nimm Kontakt auf und lass uns über dein Projekt sprechen.