| import type { PicletInstance } from '$lib/db/schema'; |
| import { embedPicletMetadata } from './picletMetadata'; |
|
|
| |
| |
| |
| export async function generateShareableImage(piclet: PicletInstance): Promise<Blob> { |
| |
| const canvas = document.createElement('canvas'); |
| const ctx = canvas.getContext('2d'); |
| if (!ctx) throw new Error('Could not create canvas context'); |
| |
| |
| 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(); |
| }; |
| } |
| |
| |
| const canvasWidth = 1024; |
| const canvasHeight = 1400; |
| canvas.width = canvasWidth; |
| canvas.height = canvasHeight; |
| |
| |
| ctx.fillStyle = '#f8f9fa'; |
| ctx.fillRect(0, 0, canvasWidth, canvasHeight); |
| |
| |
| const stripeHeight = 10; |
| ctx.fillStyle = 'rgba(76, 175, 80, 0.2)'; |
| for (let y = 0; y < canvasHeight; y += stripeHeight * 2) { |
| ctx.fillRect(0, y, canvasWidth, stripeHeight); |
| } |
| |
| |
| ctx.fillStyle = 'rgba(76, 175, 80, 0.1)'; |
| for (let y = stripeHeight; y < canvasHeight; y += stripeHeight * 2) { |
| ctx.fillRect(0, y, canvasWidth, stripeHeight); |
| } |
| |
| |
| const picletImg = await loadImage(piclet.imageData || piclet.imageUrl); |
| const picletSize = 1024; |
| const picletX = 0; |
| const picletY = 150; |
| |
| |
| const grassImg = await loadImage('/assets/grass.PNG'); |
| const platformSize = 1200; |
| const platformX = (canvasWidth - platformSize) / 2; |
| const platformY = picletY + picletSize - 300 - 384; |
| ctx.drawImage(grassImg, platformX, platformY, platformSize, platformSize); |
| |
| |
| ctx.drawImage(picletImg, picletX, picletY, picletSize, picletSize); |
| |
| |
| const nameText = piclet.nickname || piclet.typeId; |
| |
| |
| const gradient = ctx.createLinearGradient(0, 50, 0, 120); |
| gradient.addColorStop(0, '#ffffff'); |
| gradient.addColorStop(1, '#e0e0e0'); |
| |
| |
| ctx.font = 'bold 72px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif'; |
| ctx.textAlign = 'center'; |
| ctx.textBaseline = 'middle'; |
| |
| |
| ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; |
| ctx.shadowBlur = 15; |
| ctx.shadowOffsetX = 0; |
| ctx.shadowOffsetY = 3; |
| |
| |
| ctx.strokeStyle = '#2c3e50'; |
| ctx.lineWidth = 4; |
| ctx.strokeText(nameText.toUpperCase(), canvasWidth / 2, 80); |
| |
| |
| ctx.fillStyle = gradient; |
| ctx.fillText(nameText.toUpperCase(), canvasWidth / 2, 80); |
| |
| |
| ctx.shadowColor = 'transparent'; |
| ctx.shadowBlur = 0; |
| ctx.shadowOffsetX = 0; |
| ctx.shadowOffsetY = 0; |
| |
| |
| const statsY = picletY + picletSize + 50; |
| 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' } |
| ]; |
| |
| |
| const containerX = 100; |
| const containerY = statsY - 20; |
| const containerWidth = canvasWidth - 200; |
| const containerHeight = 150; |
| |
| |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; |
| ctx.beginPath(); |
| (ctx as any).roundRect(containerX, containerY, containerWidth, containerHeight, 20); |
| ctx.fill(); |
| |
| |
| const barHeight = 16; |
| const barSpacing = 20; |
| const maxStatValue = 255; |
| const barMaxWidth = containerWidth - 240; |
| |
| stats.forEach((stat, index) => { |
| const y = statsY + (index * barSpacing); |
| |
| |
| 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); |
| |
| |
| ctx.textAlign = 'right'; |
| ctx.fillText(stat.value.toString(), containerX + 100, y + 3); |
| |
| |
| 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(); |
| |
| |
| const fillWidth = (stat.value / maxStatValue) * barMaxWidth; |
| ctx.fillStyle = stat.color; |
| ctx.beginPath(); |
| (ctx as any).roundRect(barX, y - 8, fillWidth, barHeight, 8); |
| ctx.fill(); |
| |
| |
| 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(); |
| }); |
| |
| |
| 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'; |
| ctx.textAlign = 'center'; |
| ctx.fillText(`BST: ${bst}`, canvasWidth / 2, containerY + containerHeight - 10); |
| |
| |
| const logoImg = await loadImage('/assets/snap_logo.png'); |
| const logoSize = 120; |
| ctx.globalAlpha = 0.3; |
| ctx.drawImage(logoImg, canvasWidth - logoSize - 30, 30, logoSize, logoSize); |
| ctx.globalAlpha = 1.0; |
| |
| |
| const blob = await canvasToBlob(canvas); |
| |
| |
| return embedPicletMetadata(blob, piclet); |
| } |
|
|
| |
| |
| |
| export async function downloadPicletCard(piclet: PicletInstance, filename?: string): Promise<void> { |
| 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); |
| } |
|
|
| |
| |
| |
| function loadImage(src: string): Promise<HTMLImageElement> { |
| return new Promise((resolve, reject) => { |
| const img = new Image(); |
| img.crossOrigin = 'anonymous'; |
| img.onload = () => resolve(img); |
| img.onerror = reject; |
| img.src = src; |
| }); |
| } |
|
|
| |
| |
| |
| function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> { |
| return new Promise((resolve, reject) => { |
| canvas.toBlob((blob) => { |
| if (blob) resolve(blob); |
| else reject(new Error('Failed to create blob')); |
| }, 'image/png'); |
| }); |
| } |
|
|
|
|