Please bookmark this page to avoid losing your image tool!

Image Pokemon Card Frame Creator Tool

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
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;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Pokemon Card Frame Creator Tool allows users to create customized Pokemon card frames using images of their choice. Users can specify important card details such as the Pokemon’s name, card stage, hit points (HP), energy type, attack details, and flavor text. This tool is ideal for Pokemon enthusiasts looking to design personalized cards for collection, gifts, or fan art. With the ability to adjust various design elements, users can create unique and visually appealing cards with their own images and text.

Leave a Reply

Your email address will not be published. Required fields are marked *