Please bookmark this page to avoid losing your image tool!

Image Fantasy Tavern Wanted Poster Creator

(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, name = "Mysterious Stranger", crime = "Causing a Tavern Brawl", rewardAmount = "100 Gold Pieces", status = "Dead or Alive", fontNameParam = "IM Fell DW Pica", posterColor = "#F5DEB3", textColor = "#5C4033", imageEffect = "sepia") {

    // FONT LOADING HELPER
    async function ensureFontIsAvailable(fontFamilyToLoad, googleFontCssUrl = null) {
        let isFontLoaded = false;
        try {
            if (document.fonts && typeof document.fonts.values === 'function') {
                for (const fontFace of document.fonts.values()) {
                    if (fontFace.family === fontFamilyToLoad) {
                        if (fontFace.status === 'loaded') {
                            isFontLoaded = true;
                            break;
                        } else if (fontFace.status === 'unloaded' || fontFace.status === 'loading') {
                            await fontFace.load();
                            if (fontFace.status === 'loaded') {
                               isFontLoaded = true;
                               break;
                            }
                        }
                    }
                }
            }
        } catch(e) {
            // console.warn("Error accessing document.fonts: ", e);
        }

        if (!isFontLoaded && document.fonts && typeof document.fonts.check === 'function') {
            if (document.fonts.check(`12px "${fontFamilyToLoad}"`)) { // Check with a size and quotes
                 isFontLoaded = true;
            }
        }

        if (isFontLoaded) {
            return fontFamilyToLoad;
        }

        if (googleFontCssUrl) {
            try {
                const linkId = `dynamic-font-stylesheet-${fontFamilyToLoad.replace(/\s+/g, '-')}`;
                if (!document.getElementById(linkId)) {
                    const link = document.createElement('link');
                    link.id = linkId;
                    link.href = googleFontCssUrl;
                    link.rel = 'stylesheet';
                    
                    const p = new Promise((resolve, reject) => {
                        link.onload = resolve;
                        link.onerror = () => reject(new Error(`Failed to load stylesheet ${googleFontCssUrl}`));
                        document.head.appendChild(link);
                    });
                    await p;
                }

                if (document.fonts && typeof document.fonts.load === 'function') {
                    await document.fonts.load(`12px "${fontFamilyToLoad}"`); // Load with a style string
                    if (document.fonts.check(`12px "${fontFamilyToLoad}"`)) {
                        return fontFamilyToLoad;
                    }
                } else if (document.fonts && document.fonts.check && document.fonts.check(`12px "${fontFamilyToLoad}"`)) {
                    return fontFamilyToLoad; // Fallback check if load() not supported but check() is
                }
            } catch (e) {
                console.warn(`Failed to load font "${fontFamilyToLoad}" from URL: ${googleFontCssUrl}. Error: ${e}`);
            }
        }
        
        // console.warn(`Using "serif" as fallback for "${fontFamilyToLoad}".`);
        return "serif"; // Fallback font
    }

    const effectiveFontFamily = await ensureFontIsAvailable(
        fontNameParam,
        fontNameParam === "IM Fell DW Pica" ? "https://fonts.googleapis.com/css2?family=IM+Fell+DW+Pica:ital@0;1&display=swap" : null
    );

    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    const posterWidth = 600;
    const posterHeight = 900;
    canvas.width = posterWidth;
    canvas.height = posterHeight;
    const padding = 40;

    // Draw Background
    ctx.fillStyle = posterColor;
    ctx.fillRect(0, 0, posterWidth, posterHeight);
    
    // Optional: Draw a subtle, darker "aged" border inset from the edge
    ctx.strokeStyle = "rgba(0,0,0,0.1)"; // Darker, semi-transparent color for aged effect
    ctx.lineWidth = padding * 0.75; // Make it fairly thick
    // Draw rectangle slightly inset, so lineWidth creates border towards center
    ctx.strokeRect(ctx.lineWidth / 2, ctx.lineWidth / 2, posterWidth - ctx.lineWidth, posterHeight - ctx.lineWidth);
    ctx.lineWidth = 1; // Reset line width for other drawings


    // Text Drawing Helper Functions
    function drawText(text, x, y, fontStyleWeight, fontSize, color, align = 'center', maxWidth = posterWidth - 2 * padding) {
        ctx.font = `${fontStyleWeight} ${fontSize}px "${effectiveFontFamily}", serif`;
        ctx.fillStyle = color;
        ctx.textAlign = align;
        ctx.textBaseline = 'top'; // All y-coordinates for text will be their top edge
        ctx.fillText(text, x, y, maxWidth);
    }

    function drawWrappedText(text, x, y, fontStyleWeight, fontSize, color, align, maxWidth, lineHeight) {
        ctx.font = `${fontStyleWeight} ${fontSize}px "${effectiveFontFamily}", serif`;
        ctx.fillStyle = color;
        ctx.textAlign = align;
        ctx.textBaseline = 'top';

        const words = text.split(' ');
        let line = '';
        let currentLineY = y; // Y for the top of the current line being built

        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, currentLineY);
                line = words[n] + ' ';
                currentLineY += lineHeight;
            } else {
                line = testLine;
            }
        }
        ctx.fillText(line.trim(), x, currentLineY); // Draw the last line
        return currentLineY + lineHeight; // Return the Y-coordinate for the top of the *next* element
    }

    // --- Poster Layout and Drawing ---
    let currentY = padding; // Start current Y position from top padding

    // "WANTED" Text
    const wantedFontSize = 70;
    drawText("WANTED", posterWidth / 2, currentY, "bold", wantedFontSize, textColor);
    currentY += wantedFontSize + 10; // Advance Y by font size + margin

    // "Status" Text (e.g., Dead or Alive)
    const statusFontSize = 20;
    drawText(status.toUpperCase(), posterWidth / 2, currentY, "normal", statusFontSize, textColor);
    currentY += statusFontSize + 20; // Advance Y by font size + margin before image

    // --- Image Section ---
    const imgTop = currentY; // Top boundary for the image's container
    const imgContainerX = padding;
    
    // Estimate space needed for text elements below the image
    const spaceForBottomText = name.length > 25 ? 280 : 240; // Name, "For", Crime, "Reward", Amount + margins
                                                     // (dynamic adjustments could be more complex)
    let imgContainerHeight = posterHeight - imgTop - spaceForBottomText - padding;

    // Constrain image container height
    const minImgHeight = posterHeight * 0.20; // Minimum 20% of poster height
    const maxImgHeight = posterHeight * 0.45; // Maximum 45% of poster height
    if (imgContainerHeight < minImgHeight) imgContainerHeight = minImgHeight;
    if (imgContainerHeight > maxImgHeight) imgContainerHeight = maxImgHeight;
    
    const imgContainerWidth = posterWidth - 2 * padding;

    let drawWidth, drawHeight, dx, dy;
    const imgNaturalWidth = originalImg.naturalWidth || originalImg.width; // Ensure we have a width
    const imgNaturalHeight = originalImg.naturalHeight || originalImg.height; // Ensure we have a height

    if (imgNaturalWidth > 0 && imgNaturalHeight > 0) { // Only process if image has valid dimensions
        const imgAspect = imgNaturalWidth / imgNaturalHeight;
        if (imgNaturalWidth > imgContainerWidth || imgNaturalHeight > imgContainerHeight) { // Image needs scaling
            const containerAspect = imgContainerWidth / imgContainerHeight;
            if (imgAspect > containerAspect) { // Image is wider relative to container
                drawWidth = imgContainerWidth;
                drawHeight = drawWidth / imgAspect;
            } else { // Image is taller or same aspect relative to container
                drawHeight = imgContainerHeight;
                drawWidth = drawHeight * imgAspect;
            }
        } else { // Image is smaller than container, draw it at its original size
            drawWidth = imgNaturalWidth;
            drawHeight = imgNaturalHeight;
        }
    } else { // Fallback if image has no dimensions (e.g., not loaded, though prompt implies it is)
        console.warn("Original image has zero dimensions. Drawing placeholder.");
        drawWidth = imgContainerWidth * 0.8; // Placeholder size
        drawHeight = imgContainerHeight * 0.8;
    }


    // Center the scaled image within its allocated container space
    dx = imgContainerX + (imgContainerWidth - drawWidth) / 2;
    dy = imgTop + (imgContainerHeight - drawHeight) / 2;

    // Draw Image with effect
    ctx.save();
    if (imageEffect === "sepia") ctx.filter = "sepia(1)";
    else if (imageEffect === "grayscale") ctx.filter = "grayscale(1)";
    // else: "none" or other, no filter applied
    ctx.drawImage(originalImg, dx, dy, drawWidth, drawHeight);
    ctx.filter = "none"; // Reset filter for subsequent drawings
    ctx.restore();

    // Draw Border around image
    ctx.strokeStyle = textColor;
    ctx.lineWidth = 3;
    ctx.strokeRect(dx - 5, dy - 5, drawWidth + 10, drawHeight + 10); // Slightly padded border
    ctx.lineWidth = 1; // Reset line width

    // Update currentY to be below the image frame (dy is top of image, drawHeight is its height)
    currentY = dy + drawHeight + 10 + 20; // 10 for border, 20 for margin

    // --- Text Below Image ---
    // Person's Name
    const nameFontSize = Math.min(40, Math.floor( (posterWidth - 2 * padding) / (name.length * 0.6) ) +4 ); // Auto-adjust size a bit
    drawText(name, posterWidth / 2, currentY, "bold", nameFontSize, textColor, 'center', posterWidth - padding); // Wider max width for name
    currentY += nameFontSize + 10;

    // "FOR THE CRIME OF:" Text
    const forCrimeFontSize = 18;
    drawText("FOR THE CRIME OF:", posterWidth / 2, currentY, "italic", forCrimeFontSize, textColor);
    currentY += forCrimeFontSize + 10;

    // Crime Description (Wrapped)
    const crimeFontSize = 26;
    const crimeLineHeight = crimeFontSize * 1.2;
    currentY = drawWrappedText(crime, posterWidth / 2, currentY, "normal", crimeFontSize, textColor, 'center', posterWidth - 2 * padding - 20, crimeLineHeight);
    // currentY from drawWrappedText is already top of next element. Add a margin.
    currentY += 20; 

    // --- Reward Section (attempt to position towards bottom) ---
    const rewardSectionMinY = currentY; // Reward section cannot start above this Y
    
    const rewardTitleFontSize = 50;
    const rewardAmountFontSize = 30;
    // Calculate total height needed for the reward section texts plus spacing
    const rewardSectionVisualHeight = rewardTitleFontSize + 10 + rewardAmountFontSize; 
    
    // Attempt to position the reward section so its bottom aligns with poster bottom minus padding
    currentY = posterHeight - padding - rewardSectionVisualHeight; 
    
    // Ensure reward section doesn't overlap with content above it
    if (currentY < rewardSectionMinY) { 
        currentY = rewardSectionMinY; // If overlap, place it right after previous content
    }
    
    // "REWARD" Text
    drawText("REWARD", posterWidth / 2, currentY, "bold", rewardTitleFontSize, textColor);
    currentY += rewardTitleFontSize + 10; // Advance Y

    // Reward Amount Text
    drawText(rewardAmount, posterWidth / 2, currentY, "normal", rewardAmountFontSize, textColor);
    // currentY += rewardAmountFontSize; // No further text below this

    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 Fantasy Tavern Wanted Poster Creator allows users to create custom ‘Wanted’ posters featuring a specified individual’s image, name, alleged crime, and reward amount. Users can personalize the poster with various attributes such as poster color, text color, and font style. Ideal for role-playing games, themed events, or creative projects, this tool enables the production of visually engaging, sepia-toned or grayscale posters that embody a whimsical fantasy aesthetic.

Leave a Reply

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