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