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