forked from syntaxbullet/AuroraBot-discord
160 lines
4.9 KiB
TypeScript
160 lines
4.9 KiB
TypeScript
import { GlobalFonts, createCanvas, loadImage } from '@napi-rs/canvas';
|
|
import { levelingService } from '@/modules/leveling/leveling.service';
|
|
import path from 'path';
|
|
|
|
// Register Fonts
|
|
const fontDir = path.join(process.cwd(), 'src', 'assets', 'fonts');
|
|
GlobalFonts.registerFromPath(path.join(fontDir, 'IBMPlexSansCondensed-SemiBold.ttf'), 'IBMPlexSansCondensed-SemiBold');
|
|
GlobalFonts.registerFromPath(path.join(fontDir, 'IBMPlexMono-Bold.ttf'), 'IBMPlexMono-Bold');
|
|
|
|
const textColor = '#FFE7C5';
|
|
const secondaryTextColor = '#9C7D53';
|
|
const accentColor = '#3F2923';
|
|
|
|
interface StudentCardData {
|
|
username: string;
|
|
avatarUrl: string;
|
|
id: string;
|
|
level: number;
|
|
au: bigint; // Astral Units
|
|
cu: bigint; // Constellation Units
|
|
xp: bigint; // Experience
|
|
className?: string | null;
|
|
}
|
|
|
|
export async function generateStudentIdCard(data: StudentCardData): Promise<Buffer> {
|
|
const templatePath = path.join(process.cwd(), 'src', 'assets', 'student-id-template.png');
|
|
const template = await loadImage(templatePath);
|
|
|
|
const canvas = createCanvas(template.width, template.height);
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Draw Template
|
|
ctx.drawImage(template, 0, 0);
|
|
|
|
// Draw Avatar
|
|
const avatarSize = 111;
|
|
const avatarX = 18;
|
|
const avatarY = 64;
|
|
|
|
try {
|
|
const avatar = await loadImage(data.avatarUrl);
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
// Square avatar
|
|
ctx.rect(avatarX, avatarY, avatarSize, avatarSize);
|
|
ctx.clip();
|
|
ctx.drawImage(avatar, avatarX, avatarY, avatarSize, avatarSize);
|
|
ctx.restore();
|
|
} catch (e) {
|
|
console.error("Failed to load avatar", e);
|
|
}
|
|
|
|
// Draw ID
|
|
ctx.save();
|
|
ctx.font = '12px IBMPlexMono-Bold';
|
|
ctx.fillStyle = secondaryTextColor;
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(`ID: ${data.id}`, 302, 30);
|
|
ctx.restore();
|
|
|
|
// Draw Username
|
|
ctx.save();
|
|
ctx.font = '32px IBMPlexSansCondensed-SemiBold';
|
|
ctx.fillStyle = textColor;
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(data.username, 140, 107);
|
|
ctx.restore();
|
|
|
|
// Draw Class
|
|
try {
|
|
const classBadge = await loadImage(path.join(process.cwd(), 'src', 'assets', 'student-id-class-badge.png'));
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.drawImage(classBadge, 100, 126, 58, 58);
|
|
ctx.font = '32px IBMPlexMono-Bold';
|
|
ctx.fillStyle = textColor;
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(data.className || 'Unknown', 120, 162);
|
|
ctx.restore();
|
|
} catch (e) {
|
|
console.error("Failed to load class badge", e);
|
|
}
|
|
|
|
// Draw Astral Units
|
|
ctx.save();
|
|
ctx.font = '32px IBMPlexMono-Bold';
|
|
ctx.fillStyle = textColor;
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(data.au.toString(), 163, 218);
|
|
ctx.restore();
|
|
|
|
// Draw Constellation Units
|
|
ctx.save();
|
|
ctx.font = '32px IBMPlexMono-Bold';
|
|
ctx.fillStyle = textColor;
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(data.cu.toString(), 163, 153);
|
|
ctx.restore();
|
|
|
|
// Draw Level
|
|
ctx.save();
|
|
// check how many digits the level has and adjust the position accordingly
|
|
const levelDigits = data.level.toString().length;
|
|
ctx.font = '12px IBMPlexMono-Bold';
|
|
ctx.fillStyle = secondaryTextColor;
|
|
ctx.textAlign = 'left';
|
|
if (levelDigits === 1) {
|
|
ctx.fillText(data.level.toString(), 406, 265);
|
|
} else {
|
|
ctx.fillText(data.level.toString(), 400, 265);
|
|
}
|
|
ctx.restore();
|
|
|
|
// Draw Next Level
|
|
ctx.save();
|
|
// check how many digits the next level has and adjust the position accordingly
|
|
const nextLevelDigits = (data.level + 1).toString().length;
|
|
ctx.font = '24px IBMPlexMono-Bold';
|
|
ctx.fillStyle = textColor;
|
|
ctx.textAlign = 'left';
|
|
if (nextLevelDigits === 1) {
|
|
ctx.fillText((data.level + 1).toString(), 438, 255);
|
|
} else {
|
|
ctx.fillText((data.level + 1).toString(), 431, 255);
|
|
}
|
|
ctx.restore();
|
|
|
|
// Draw xp bar
|
|
const xpbarX = 2;
|
|
const xpbarY = 268;
|
|
const xpbarMaxWidth = 400; // in pixels
|
|
const xpbarHeight = 4;
|
|
|
|
const xp = data.xp;
|
|
const requiredXp = BigInt(levelingService.getXpForLevel(data.level));
|
|
|
|
// Check to avoid division by zero, though requiredXp should be >= 100
|
|
let percentage = 0;
|
|
if (requiredXp > 0n) {
|
|
percentage = Number(xp) / Number(requiredXp);
|
|
}
|
|
|
|
const xpFilledWidth = Math.min(Math.max(percentage * xpbarMaxWidth, 0), xpbarMaxWidth);
|
|
|
|
const gradient = ctx.createLinearGradient(xpbarX, xpbarY, xpbarX + xpFilledWidth, xpbarY);
|
|
gradient.addColorStop(0, '#FFFFFF');
|
|
gradient.addColorStop(1, '#D9B178');
|
|
ctx.save();
|
|
ctx.fillStyle = accentColor;
|
|
ctx.fillRect(xpbarX, xpbarY, xpbarMaxWidth, xpbarHeight);
|
|
|
|
if (xpFilledWidth > 0) {
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(xpbarX, xpbarY, xpFilledWidth, xpbarHeight);
|
|
}
|
|
ctx.restore();
|
|
|
|
return canvas.toBuffer('image/png');
|
|
}
|