import type { PicletInstance } from '$lib/db/schema'; import { embedPicletMetadata } from './picletMetadata'; /** * Generates a shareable image of a piclet with embedded metadata */ export async function generateShareableImage(piclet: PicletInstance): Promise { // Create canvas const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('Could not create canvas context'); // Polyfill for roundRect if not available if (!ctx.roundRect) { ctx.roundRect = function(x: number, y: number, width: number, height: number, radius: number) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); }; } // Set canvas size - full piclet size plus padding for name and stats const canvasWidth = 1024; const canvasHeight = 1400; // Extra height for name and stats canvas.width = canvasWidth; canvas.height = canvasHeight; // Fill with striped background like battle view ctx.fillStyle = '#f8f9fa'; ctx.fillRect(0, 0, canvasWidth, canvasHeight); // Create striped pattern const stripeHeight = 10; ctx.fillStyle = 'rgba(76, 175, 80, 0.2)'; // Light green for (let y = 0; y < canvasHeight; y += stripeHeight * 2) { ctx.fillRect(0, y, canvasWidth, stripeHeight); } // Add alternating darker stripes ctx.fillStyle = 'rgba(76, 175, 80, 0.1)'; // Lighter green for (let y = stripeHeight; y < canvasHeight; y += stripeHeight * 2) { ctx.fillRect(0, y, canvasWidth, stripeHeight); } // Load piclet image first const picletImg = await loadImage(piclet.imageData || piclet.imageUrl); const picletSize = 1024; // Full size! const picletX = 0; const picletY = 150; // Leave room for name at top // Load and draw grass platform positioned under the piclet const grassImg = await loadImage('/assets/grass.PNG'); const platformSize = 1200; // Larger than piclet for proper positioning const platformX = (canvasWidth - platformSize) / 2; const platformY = picletY + picletSize - 300 - 384; // Platform moved up by 384px ctx.drawImage(grassImg, platformX, platformY, platformSize, platformSize); // Draw piclet on top of platform ctx.drawImage(picletImg, picletX, picletY, picletSize, picletSize); // Add sleek modern text styling const nameText = piclet.nickname || piclet.typeId; // Create gradient for text const gradient = ctx.createLinearGradient(0, 50, 0, 120); gradient.addColorStop(0, '#ffffff'); gradient.addColorStop(1, '#e0e0e0'); // Modern sleek font with gradient and subtle shadow ctx.font = 'bold 72px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; // Subtle shadow for depth ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; ctx.shadowBlur = 15; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 3; // Dark outline for contrast ctx.strokeStyle = '#2c3e50'; ctx.lineWidth = 4; ctx.strokeText(nameText.toUpperCase(), canvasWidth / 2, 80); // Fill with gradient ctx.fillStyle = gradient; ctx.fillText(nameText.toUpperCase(), canvasWidth / 2, 80); // Reset shadow ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; // Draw base stats in a sleek format const statsY = picletY + picletSize + 50; // Position below piclet const stats = [ { label: 'HP', value: piclet.baseHp, color: '#4caf50' }, { label: 'ATK', value: piclet.baseAttack, color: '#f44336' }, { label: 'DEF', value: piclet.baseDefense, color: '#2196f3' }, { label: 'S.ATK', value: piclet.baseFieldAttack, color: '#ff9800' }, { label: 'S.DEF', value: piclet.baseFieldDefense, color: '#9c27b0' }, { label: 'SPD', value: piclet.baseSpeed, color: '#00bcd4' } ]; // Stats container background const containerX = 100; const containerY = statsY - 20; const containerWidth = canvasWidth - 200; const containerHeight = 150; // Draw semi-transparent background using the polyfilled roundRect ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; ctx.beginPath(); (ctx as any).roundRect(containerX, containerY, containerWidth, containerHeight, 20); ctx.fill(); // Draw stats bars const barHeight = 16; const barSpacing = 20; const maxStatValue = 255; const barMaxWidth = containerWidth - 240; // Leave room for labels and values stats.forEach((stat, index) => { const y = statsY + (index * barSpacing); // Draw stat label ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif'; ctx.fillStyle = '#ffffff'; ctx.textAlign = 'left'; ctx.fillText(stat.label, containerX + 20, y + 3); // Draw stat value ctx.textAlign = 'right'; ctx.fillText(stat.value.toString(), containerX + 100, y + 3); // Draw background bar const barX = containerX + 120; ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; ctx.beginPath(); (ctx as any).roundRect(barX, y - 8, barMaxWidth, barHeight, 8); ctx.fill(); // Draw filled bar const fillWidth = (stat.value / maxStatValue) * barMaxWidth; ctx.fillStyle = stat.color; ctx.beginPath(); (ctx as any).roundRect(barX, y - 8, fillWidth, barHeight, 8); ctx.fill(); // Add shine effect const shineGradient = ctx.createLinearGradient(barX, y - 8, barX, y - 8 + barHeight); shineGradient.addColorStop(0, 'rgba(255, 255, 255, 0.3)'); shineGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)'); shineGradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); ctx.fillStyle = shineGradient; ctx.beginPath(); (ctx as any).roundRect(barX, y - 8, fillWidth, barHeight / 2, 8); ctx.fill(); }); // Draw BST (Base Stat Total) const bst = piclet.bst || (piclet.baseHp + piclet.baseAttack + piclet.baseDefense + piclet.baseFieldAttack + piclet.baseFieldDefense + piclet.baseSpeed); ctx.font = 'bold 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif'; ctx.fillStyle = '#ffd700'; // Gold color ctx.textAlign = 'center'; ctx.fillText(`BST: ${bst}`, canvasWidth / 2, containerY + containerHeight - 10); // Load and draw translucent watermark in top-right const logoImg = await loadImage('/assets/snap_logo.png'); const logoSize = 120; // Slightly smaller for top placement ctx.globalAlpha = 0.3; // More translucent ctx.drawImage(logoImg, canvasWidth - logoSize - 30, 30, logoSize, logoSize); ctx.globalAlpha = 1.0; // Get the image as blob const blob = await canvasToBlob(canvas); // Embed metadata in the blob return embedPicletMetadata(blob, piclet); } /** * Downloads a piclet card image */ export async function downloadPicletCard(piclet: PicletInstance, filename?: string): Promise { const blob = await generateShareableImage(piclet); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename || `Piclet_${piclet.nickname || piclet.typeId}_Lv${piclet.level}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /** * Helper to load an image */ function loadImage(src: string): Promise { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => resolve(img); img.onerror = reject; img.src = src; }); } /** * Convert canvas to blob */ function canvasToBlob(canvas: HTMLCanvasElement): Promise { return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) resolve(blob); else reject(new Error('Failed to create blob')); }, 'image/png'); }); }