import { GlobalFonts, createCanvas, loadImage } from '@napi-rs/canvas'; import { levelingService } from '@shared/modules/leveling/leveling.service'; import path from 'path'; // Register Fonts const fontDir = path.join(process.cwd(), 'bot', 'assets', 'fonts'); GlobalFonts.registerFromPath(path.join(fontDir, 'IBMPlexSansCondensed-SemiBold.ttf'), 'IBMPlexSansCondensed-SemiBold'); GlobalFonts.registerFromPath(path.join(fontDir, 'IBMPlexMono-Bold.ttf'), 'IBMPlexMono-Bold'); interface StudentCardData { username: string; avatarUrl: string; id: string; level: number; au: bigint; // Astral Units xp: bigint; className?: string | null; } export async function generateStudentIdCard(data: StudentCardData): Promise { const templatePath = path.join(process.cwd(), 'bot', 'assets', 'graphics', 'studentID', 'template.png'); const classTemplatePath = path.join(process.cwd(), 'bot', 'assets', 'graphics', 'studentID', `Constellation-${data.className}.png`); const template = await loadImage(templatePath); const classTemplate = await loadImage(classTemplatePath); const canvas = createCanvas(template.width, template.height); const ctx = canvas.getContext('2d'); // Draw Background Gradient with random hue const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height); const saturation = 40 + Math.random() * 20; // 40-60% const lightness = 20 + Math.random() * 20; // 20-40% const color2 = `hsl(${Math.random() * 360}, ${saturation}%, ${lightness}%)`; const color = `hsl(${Math.random() * 360}, ${saturation}%, ${lightness - 20}%)`; gradient.addColorStop(0, color); gradient.addColorStop(1, color2); ctx.fillStyle = gradient; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw Template ctx.drawImage(template, 0, 0); // Draw Class Template ctx.drawImage(classTemplate, 0, 0); // Draw Avatar const avatarSize = 140; const avatarX = 19; const avatarY = 76; 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 = '#DAC7A1'; ctx.textAlign = 'left'; ctx.fillText(`ID: ${data.id}`, 314, 30); ctx.restore(); // Draw Username ctx.save(); ctx.font = '24px IBMPlexSansCondensed-SemiBold'; ctx.fillStyle = '#FFFFFF'; ctx.textAlign = 'left'; ctx.fillText(data.username, 181, 122); ctx.restore(); // Draw AU ctx.save(); ctx.font = '24px IBMPlexMono-Bold'; ctx.fillStyle = '#FFFFFF'; ctx.textAlign = 'right'; ctx.fillText(`${data.au}`, 270, 183); ctx.restore(); // Draw Level ctx.save(); ctx.font = '24px IBMPlexMono-Bold'; ctx.fillStyle = '#FFFFFF'; ctx.textAlign = 'center'; ctx.fillText(`${data.level}`, 445, 255); ctx.restore(); // Draw XP Bar const xpForThisLevel = levelingService.getXpForNextLevel(data.level); // The total size of the current level bucket const xpAtStartOfLevel = levelingService.getXpToReachLevel(data.level); // The accumulated XP when this level started const currentLevelProgress = Number(data.xp) - xpAtStartOfLevel; // How much XP into this level const xpBarMaxWidth = 382; const xpBarWidth = Math.max(0, Math.min(xpBarMaxWidth, xpBarMaxWidth * currentLevelProgress / xpForThisLevel)); const xpBarHeight = 3; ctx.save(); ctx.fillStyle = '#B3AD93'; ctx.fillRect(32, 244, xpBarWidth, xpBarHeight); ctx.restore(); return canvas.toBuffer('image/png'); }