You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
pokemonName = "Pikachu",
cardStage = "BASIC",
hp = "60 HP",
energyType = "Lightning", // Full name: "Grass", "Fire", "Water", "Lightning", "Psychic", "Fighting", "Darkness", "Metal", "Fairy", "Dragon", "Colorless"
attack1Name = "Thunder Shock",
attack1CostString = "L,C", // Comma-separated single letter codes: G,R,W,L,P,F,D,M,Y,N,C (R for Fire)
attack1Damage = "10",
attack1Description = "Flip a coin. If heads, the Defending Pokémon is now Paralyzed.",
flavorText = "It occasionally uses an electric shock to recharge a fellow Pikachu that is in a weakened state.",
illustrator = "Atsuko Nishida",
cardSetInfo = "SWSH020",
borderColor = "#FFCC00" // Main outer border color
) {
const fontFamilies = [
'Barlow+Condensed:wght@700', // For Name, HP
'Roboto:wght@400;700;i400' // For body text, attack names, flavor text
];
const fontId = 'pokemon-card-fonts-dynamic-loader';
if (!document.getElementById(fontId)) {
const link = document.createElement('link');
link.id = fontId;
link.href = `https://fonts.googleapis.com/css2?family=${fontFamilies.join('&family=')}&display=swap`;
link.rel = 'stylesheet';
document.head.appendChild(link);
try {
// Wait for fonts to be loaded using document.fonts.ready
// This ensures that all fonts specified in CSS (including via <link>) are loaded.
await document.fonts.ready;
// Optionally, check for specific fonts if needed, though document.fonts.ready is generally sufficient
// await Promise.all([
// document.fonts.load('700 10px "Barlow Condensed"'),
// document.fonts.load('400 10px "Roboto"'),
// document.fonts.load('700 10px "Roboto"'),
// document.fonts.load('italic 400 10px "Roboto"')
// ]);
} catch (e) {
console.warn("Font loading failed or timed out, using fallback system fonts.", e);
}
}
const canvas = document.createElement('canvas');
const cardWidth = 375;
const cardHeight = Math.round(cardWidth * (8.8 / 6.3)); // Approx 524 ~ 525px
canvas.width = cardWidth;
canvas.height = cardHeight;
const ctx = canvas.getContext('2d');
// Color definitions
const typeColors = { // For header background and main type symbol
"Grass": "#78C850", "Fire": "#F08030", "Water": "#6890F0",
"Lightning": "#F8D030", "Psychic": "#F85888", "Fighting": "#C03028",
"Darkness": "#705848", "Metal": "#B8B8D0", "Fairy": "#EE99AC",
"Dragon": "#7038F8", "Colorless": "#A8A878",
// Add aliases or less common TCG types if needed
"Poison": "#A040A0", "Ground": "#E0C068", "Rock": "#B8A038",
"Ice": "#98D8D8", "Bug": "#A8B820", "Ghost": "#705898", "Steel": "#B8B8D0",
};
const costEnergyColors = { // For attack costs - single letter keys
"G": typeColors["Grass"], "R": typeColors["Fire"], "W": typeColors["Water"],
"L": typeColors["Lightning"], "P": typeColors["Psychic"], "F": typeColors["Fighting"],
"D": typeColors["Darkness"], "M": typeColors["Metal"], "Y": typeColors["Fairy"],
"N": typeColors["Dragon"], "C": typeColors["Colorless"]
};
function getTextColorForBackground(hexBgColor) {
if (!hexBgColor || hexBgColor.length < 7) return 'black'; // Default for invalid
const r = parseInt(hexBgColor.slice(1, 3), 16);
const g = parseInt(hexBgColor.slice(3, 5), 16);
const b = parseInt(hexBgColor.slice(5, 7), 16);
const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
return luminance > 0.4 ? 'black' : 'white'; // Adjusted threshold for better visibility
}
// Helper to draw energy symbol
function drawEnergySymbol(ctx, energyLetter, x, y, radius, forCost = true) {
const symbolColor = forCost ? costEnergyColors[energyLetter] : typeColors[energyLetter]; // Use full name for main type
if (!symbolColor) { // Fallback for unknown type
console.warn(`Unknown energy type: ${energyLetter}`);
ctx.fillStyle = typeColors["Colorless"];
} else {
ctx.fillStyle = symbolColor;
}
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
// Simple black border for symbols
ctx.strokeStyle = 'black';
ctx.lineWidth = Math.max(1, radius / 10); // Scale border with radius
ctx.stroke();
// Letter inside (optional, good for costs)
if (forCost) {
ctx.fillStyle = getTextColorForBackground(symbolColor);
ctx.font = `bold ${Math.round(radius * 1.2)}px Roboto`; // Adjusted for clarity
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(energyLetter, x, y + 1); // Small offset for better centering
}
}
// Helper for text wrapping
function wrapText(ctx, text, x, y, maxWidth, lineHeight, currentYOffset = 0) {
const words = text.split(' ');
let line = '';
let textY = y + currentYOffset;
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
ctx.fillText(line.trim(), x, textY);
line = words[n] + ' ';
textY += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line.trim(), x, textY);
return textY + lineHeight; // Return Y for next line start
}
// --- Main Drawing ---
// Background color for canvas (transparent if not set, good for PNG)
// ctx.fillStyle = 'white';
// ctx.fillRect(0, 0, cardWidth, cardHeight);
// 1. Outer Border
ctx.fillStyle = borderColor;
ctx.fillRect(0, 0, cardWidth, cardHeight);
const outerMargin = Math.round(cardWidth * 0.015); // ~5-6px for 375 width
const innerCardX = outerMargin;
const innerCardY = outerMargin;
const innerCardWidth = cardWidth - 2 * outerMargin;
const innerCardHeight = cardHeight - 2 * outerMargin;
// 2. Inner "card stock" background (a light neutral color)
ctx.fillStyle = "#EFEFEF"; // Light gray, similar to card paper
ctx.fillRect(innerCardX, innerCardY, innerCardWidth, innerCardHeight);
const padding = Math.round(innerCardWidth * 0.03); // ~10px
// --- Header Section ---
const headerHeight = Math.round(innerCardHeight * 0.09); // ~45px
const headerColor = typeColors[energyType] || typeColors["Colorless"];
ctx.fillStyle = headerColor;
ctx.fillRect(innerCardX, innerCardY, innerCardWidth, headerHeight);
const headerTextColor = getTextColorForBackground(headerColor);
// Stage Text (e.g., BASIC, STAGE 1)
ctx.font = `bold ${Math.round(headerHeight * 0.3)}px "Barlow Condensed"`; // ~13px
ctx.fillStyle = headerTextColor;
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const stageTextX = innerCardX + padding;
const stageTextY = innerCardY + headerHeight * 0.3; // Upper part of header
if (cardStage) {
ctx.fillText(cardStage.toUpperCase(), stageTextX, stageTextY);
}
// Pokemon Name
ctx.font = `bold ${Math.round(headerHeight * 0.55)}px "Barlow Condensed"`; // ~25px
ctx.fillStyle = headerTextColor;
const nameTextY = innerCardY + headerHeight * 0.70; // Lower part of header
ctx.fillText(pokemonName, stageTextX, nameTextY);
// HP and Main Energy Type Symbol
const hpFontSize = Math.round(headerHeight * 0.35); // ~15px
ctx.font = `bold ${hpFontSize}px "Barlow Condensed"`;
ctx.fillStyle = headerTextColor;
ctx.textAlign = 'right';
const hpText = hp; // Parameter is full string e.g. "60 HP"
const hpTextMetrics = ctx.measureText(hpText);
const hpTextX = innerCardX + innerCardWidth - padding - (headerHeight * 0.5); // space for symbol
ctx.fillText(hpText, hpTextX, stageTextY + 2); // Align with stage text line
drawEnergySymbol(ctx, energyType, hpTextX + (headerHeight * 0.25), nameTextY - (headerHeight * 0.05) , headerHeight * 0.25, false);
// --- Image Area ---
const imageAreaY = innerCardY + headerHeight;
const imageAreaHeight = Math.round(innerCardHeight * 0.38); // ~190px
// Border for the image itself (classic cards have this prominent inner border)
const imageBorderThickness = Math.round(innerCardWidth * 0.01); // ~3-4px
ctx.fillStyle = '#333333'; // Dark gray for image border
ctx.fillRect(innerCardX + padding - imageBorderThickness,
imageAreaY + padding - imageBorderThickness,
innerCardWidth - 2 * padding + 2 * imageBorderThickness,
imageAreaHeight + 2 * imageBorderThickness);
const imgBoxX = innerCardX + padding;
const imgBoxY = imageAreaY + padding;
const imgBoxWidth = innerCardWidth - 2 * padding;
const imgBoxHeight = imageAreaHeight;
if (originalImg && originalImg.width > 0 && originalImg.height > 0) {
const imgAspect = originalImg.width / originalImg.height;
const boxAspect = imgBoxWidth / imgBoxHeight;
let drawWidth, drawHeight, dX, dY;
if (imgAspect > boxAspect) { // Image wider than box
drawWidth = imgBoxWidth;
drawHeight = drawWidth / imgAspect;
dX = imgBoxX;
dY = imgBoxY + (imgBoxHeight - drawHeight) / 2;
} else { // Image taller or same aspect
drawHeight = imgBoxHeight;
drawWidth = drawHeight * imgAspect;
dY = imgBoxY;
dX = imgBoxX + (imgBoxWidth - drawWidth) / 2;
}
ctx.drawImage(originalImg, dX, dY, drawWidth, drawHeight);
} else { // Placeholder if no image
ctx.fillStyle = '#CCCCCC';
ctx.fillRect(imgBoxX, imgBoxY, imgBoxWidth, imgBoxHeight);
ctx.fillStyle = '#888888';
ctx.textAlign = 'center';
ctx.font = '16px Roboto';
ctx.fillText("Image Area", imgBoxX + imgBoxWidth / 2, imgBoxY + imgBoxHeight / 2);
}
// --- Description / Attacks Area ---
let currentY = imageAreaY + padding + imageAreaHeight + imageBorderThickness + padding * 0.5;
const descriptionAreaX = innerCardX + padding;
const descriptionAreaWidth = innerCardWidth - 2 * padding;
// Background for Attacks/Description section (often has a lighter texture or slight gradient)
// For simplicity using a color derived from type, or a fixed light color
// const descBgGradient = ctx.createLinearGradient(0, currentY, 0, currentY + (innerCardHeight - (currentY - innerCardY) - Math.round(innerCardHeight * 0.05) - padding));
// const lightTypeColor = typeColors[energyType] ? `${typeColors[energyType]}33` : '#FFFFFF33'; // transparent version
// descBgGradient.addColorStop(0, '#FFFFFF'); // Start white-ish
// descBgGradient.addColorStop(1, lightTypeColor); // End with slight type color tint
// ctx.fillStyle = descBgGradient;
ctx.fillStyle = '#FAFAFA'; // A very light off-white
ctx.fillRect(innerCardX, imageAreaY + imageAreaHeight + imageBorderThickness*2, innerCardWidth, innerCardHeight - headerHeight - (imageAreaHeight + imageBorderThickness*2) - (Math.round(innerCardHeight * 0.05) + padding) + outerMargin);
// Attack 1
if (attack1Name) {
const attackRowY = currentY + padding * 0.75;
const energySymbolRadius = Math.round(innerCardWidth * 0.025); // ~9px
let costSymbolX = descriptionAreaX + energySymbolRadius;
// Draw Attack Costs
const costs = attack1CostString.split(',');
costs.forEach(cost => {
if (cost.trim()) {
drawEnergySymbol(ctx, cost.trim().toUpperCase(), costSymbolX, attackRowY, energySymbolRadius, true);
costSymbolX += energySymbolRadius * 2 + Math.round(innerCardWidth * 0.01); // spacing
}
});
// Attack Name
ctx.font = `bold ${Math.round(innerCardWidth * 0.04)}px Roboto`; // ~14-15px
ctx.fillStyle = 'black';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const attackNameX = costSymbolX + (costs.length > 0 ? padding * 0.25 : 0); // Add space if costs were drawn
ctx.fillText(attack1Name, attackNameX , attackRowY);
// Attack Damage
if (attack1Damage) {
ctx.font = `bold ${Math.round(innerCardWidth * 0.045)}px "Barlow Condensed"`; // ~16-17px
ctx.textAlign = 'right';
ctx.fillText(attack1Damage, descriptionAreaX + descriptionAreaWidth - padding*0.5, attackRowY);
}
currentY = attackRowY + energySymbolRadius + padding * 0.25; // Move Y down past symbols/name row
// Attack Description
ctx.font = `${Math.round(innerCardWidth * 0.03)}px Roboto`; // ~11px
ctx.fillStyle = 'black';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
const lineHeight = Math.round(innerCardWidth * 0.035); // ~13px
currentY = wrapText(ctx, attack1Description, descriptionAreaX, currentY, descriptionAreaWidth, lineHeight);
}
// Flavor Text (if space allows, or if no attack)
if (flavorText) {
currentY += padding * 0.5; // A bit of space before flavor text
ctx.font = `italic ${Math.round(innerCardWidth * 0.028)}px Roboto`; // ~10-11px
ctx.fillStyle = '#333333'; // Dark gray for flavor text
const flavorLineHeight = Math.round(innerCardWidth * 0.033); // ~12px
currentY = wrapText(ctx, flavorText, descriptionAreaX, currentY, descriptionAreaWidth, flavorLineHeight);
}
// --- Bottom Strip (Illustrator, Set Info) ---
const bottomStripHeight = Math.round(innerCardHeight * 0.05); // ~25px
const bottomStripY = innerCardY + innerCardHeight - bottomStripHeight;
// No separate background for this, text directly on the description area background or main card stock
ctx.fillStyle = '#555555'; // Footer text color
ctx.font = `${Math.round(innerCardWidth * 0.025)}px Roboto`; // ~9px
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
if (illustrator) {
ctx.fillText(`Illus. ${illustrator}`, innerCardX + padding, bottomStripY + bottomStripHeight / 2);
}
if (cardSetInfo) {
ctx.textAlign = 'right';
ctx.fillText(cardSetInfo, innerCardX + innerCardWidth - padding, bottomStripY + bottomStripHeight / 2);
}
return canvas;
}
Apply Changes