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

Discord-profielkaart: levelkaarten met node-canvas

Een Discord-profielkaart is die strakke afbeelding die de avatar, het level en de serverrang van een gebruiker in één visual samenbrengt. De kaarten die je in populaire bots ziet, kun je in je eigen bot tekenen met node-canvas: een achtergrond, een tot een cirkel bijgesneden avatar, een XP-voortgangsbalk en eigen lettertypen. In deze gids bouw je de kaart vanaf nul, stuur je het resultaat als PNG naar Discord en leer je de valkuilen die je bot snel en betrouwbaar houden.

Waarom afbeeldingen aan de serverkant tekenen?

Discord-embeds zijn beperkt tot tekst, velden en één enkele afbeelding; een dynamische voortgangsbalk of een per-gebruiker indeling kun je niet met een embed alleen renderen. De oplossing is de afbeelding op de server genereren met node-canvas en als bestand aan het bericht koppelen. node-canvas is de Node.js-tegenhanger van de Canvas 2D API van de browser: je gebruikt dezelfde fillRect-, arc- en drawImage-aanroepen, maar krijgt het resultaat als een Buffer.

Er zijn twee gangbare pakketten. De klassieke optie is het canvas-pakket (op Cairo gebaseerd, kan compilatie vereisen). Het moderne alternatief, met voorgecompileerde binaries en doorgaans sneller, is @napi-rs/canvas. Hun API's zijn grotendeels identiek; deze gids gebruikt het klassieke node-canvas.

npm install canvas discord.js

Het canvas voorbereiden

De eerste stap is een canvas met vaste afmetingen. 934×282 pixels is een gangbaar, goed geproportioneerd formaat voor dit soort kaarten. We halen de tekencontext op met getContext('2d') en vullen de achtergrond.

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');

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

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

De roundRect-helper gebruikt arcTo voor afgeronde hoeken. Recente Node-versies bieden ook een ingebouwde ctx.roundRect(), maar je eigen functie schrijven geeft consistente resultaten in elke omgeving.

De avatar tot een cirkel bijsnijden

De avatar is de ster van de kaart. Om hem rond te maken, definiëren we een clip-pad en tekenen daarna de afbeelding erbinnen. In discord.js haal je de avatar-URL op met user.displayAvatarURL(); door extension: 'png' mee te geven voorkom je de geanimeerde WebP/GIF die node-canvas niet kan lezen.

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

const cx = 141; // middelpunt X van de cirkel
const cy = 141; // middelpunt Y van de cirkel
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();

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

Het paar save() / restore() is cruciaal: clip() beperkt elke volgende tekening tot de cirkel, dus moet je de context direct na het tekenen van de avatar herstellen; anders raken ook de tekst en de balk binnen de cirkel opgesloten.

Tekst, level en rang tekenen

Voor een verzorgde kaart volstaat het standaard systeemlettertype niet. Registreer een eigen lettertypebestand (.ttf / .otf) met registerFont en verwijs daarna naar dezelfde family-naam in ctx.font. Deze aanroep moet vóór het maken van het canvas gebeuren.

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

// ... binnen het tekenen van de kaart:
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);

// De rang rechts uitlijnen
ctx.textAlign = 'right';
ctx.fillStyle = '#5865f2';
ctx.font = 'bold 34px "Inter"';
ctx.fillText(`#${rank}`, 890, 110);
ctx.textAlign = 'left'; // de uitlijning resetten

textAlign verandert de globale staat van de context; als je na rechts uitlijnen niet terugschakelt naar 'left', wordt alle latere tekst ook rechts uitgelijnd.

De XP-voortgangsbalk

De voortgangsbalk bestaat uit twee rechthoeken: een grijze baan en een gekleurde vulling die er proportioneel overheen wordt getekend. Je berekent de verhouding door de huidige XP van de gebruiker te delen door de XP die voor het volgende level nodig is.

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

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

// Gevuld deel (minstens één halve cirkel breed)
const fillW = Math.max(ratio * barW, barH);
ctx.fillStyle = '#5865f2';
roundRect(ctx, barX, barY, fillW, barH, barH / 2);
ctx.fill();

// XP-tekst erbovenop
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) voorkomt dat de afgeronde balk er vreemd uitziet bij lage XP; de vulling blijft minstens zo breed als zijn eigen hoogte.

De kaart naar Discord sturen

In de laatste stap converteer je het canvas naar een PNG-buffer, verpak je het in een AttachmentBuilder en beantwoord je de interactie. Omdat renderen enkele honderden milliseconden kan duren, houd je je veilig voor de interactie-time-out van drie seconden door eerst deferReply() aan te roepen.

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

await interaction.deferReply();

// ... alle tekenstappen hierboven ...

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

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

Je kunt dezelfde indeling koppelen aan een /rank-commando, een welkomstbericht of een level-up-melding. De data (level, XP, rang) komt meestal uit SQLite of MySQL; de kaart is enkel de laag die het visualiseert.

Veelgestelde vragen

Moet ik node-canvas of @napi-rs/canvas gebruiken?

Beide doen hetzelfde werk. canvas (node-canvas) is volwassen en wijdverbreid maar kan op sommige systemen een Cairo-build vereisen. @napi-rs/canvas wordt geleverd met voorgecompileerde binaries, installeert vlot en is meestal sneller. Voor een nieuw project is de tweede proberen verstandig; de API is vrijwel identiek.

Waarom laadt de avatar soms niet?

De meest voorkomende oorzaak zijn geanimeerde avatars: de standaard-URL kan een .gif teruggeven die node-canvas niet kan tekenen. Vraag altijd een statische PNG aan met displayAvatarURL({ extension: 'png' }). Zorg ook dat je await loadImage(...) op de avatar-URL gebruikt.

Vertraagt het renderen van een kaart bij elk commando de bot?

Losse aanroepen zijn licht, maar op drukke servers kunnen ze de CPU verzadigen. Cache de kaart van een gebruiker kort, laad gedeelde assets zoals de achtergrond één keer met loadImage en hergebruik ze, en verplaats het renderen indien nodig naar een wachtrij.

Wil je een professioneel levelsysteem in je bot? Ik bouw end-to-end aangepaste profielkaarten met avatars, XP-balken en ranglijsten, naast economie- en moderatiesystemen. Neem contact op en laten we het over je project hebben.

Bu kategorideki tüm yazılar →

Devamı için