114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
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(), 'src', '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<Buffer> {
|
|
const templatePath = path.join(process.cwd(), 'src', 'assets', 'graphics', 'studentID', 'template.png');
|
|
const classTemplatePath = path.join(process.cwd(), 'src', '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');
|
|
}
|