Please bookmark this page to avoid losing your image tool!

Image Space Bounty Hunter 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,
    bountyName = "MYSTERIOUS OUTLAW",
    rewardAmount = "GALACTIC CREDITS: 1,000,000",
    wantedText = "WANTED",
    subText = "DEAD OR ALIVE",
    crimeDescription = "For crimes against the Galactic Concord, including unauthorized warp jumps, smuggling rare space-emeralds, and telling bad jokes at the Interstellar Comedy Club.",
    posterWidth = 600,
    posterHeight = 900,
    backgroundColor = "#EADCB8", // Parchment paper-like color
    textColor = "#3A2D22", // Dark brown, aged text color
    headlineFont = "Ultra", // A bold Google Font for "WANTED"
    bodyFont = "Special Elite", // A typewriter-style Google Font for other text
    imageBorderColor = "#2F251B", // Darker border for the image
    imageBorderWidth = 4,
    applySepiaToImage = 1, // 1 for true (apply sepia), 0 for false
    applyNoiseToBackground = 1, // 1 for true (add noise), 0 for false
    noiseIntensity = 0.1 // Noise intensity (0.0 to 1.0)
) {

    // --- Helper Function: Font Loader ---
    // Loads a font from Google Fonts, with fallbacks.
    function _loadFont(fontFamily) {
        const fontId = `dynamic-font-${fontFamily.replace(/\s+/g, '-')}`;

        if (document.fonts.check(`12px "${fontFamily}"`)) {
            return Promise.resolve(); // Font already available (system or previously loaded)
        }

        if (document.getElementById(fontId)) { // Link tag already added
            return document.fonts.load(`12px "${fontFamily}"`).catch(() => {
                 // Font loading might still fail, but promise resolves for fallback.
            });
        }
        
        return new Promise((resolve) => {
            const link = document.createElement('link');
            link.id = fontId;
            link.href = `https://fonts.googleapis.com/css2?family=${fontFamily.replace(/\s+/g, '+')}:wght@400&display=swap`;
            link.rel = 'stylesheet';
            
            link.onload = () => {
                document.fonts.load(`12px "${fontFamily}"`)
                    .then(resolve) // Font face loaded
                    .catch(() => {
                        console.warn(`Font face for ${fontFamily} failed to load after CSS was fetched.`);
                        resolve(); // Resolve anyway for fallback logic
                    });
            };
            link.onerror = (err) => {
                console.warn(`Failed to load Google Font CSS for "${fontFamily}":`, err);
                resolve(); // Resolve for fallback logic
            };
            document.head.appendChild(link);
        });
    }

    // --- Helper Function: Wrap Text ---
    // Draws multi-line text, centered, within a max width.
    function _wrapText(context, text, x, y, maxWidth, lineHeight, fontName, fallbackFont) {
        const effectiveFont = document.fonts.check(`1em "${fontName}"`) ? fontName : fallbackFont;
        context.font = `${lineHeight}px "${effectiveFont}"`;
        context.textAlign = "center";
        
        const words = text.split(' ');
        let line = '';
        let currentY = y;
        const lineSpacing = lineHeight * 1.2; // 1.2 provides decent spacing

        for (let n = 0; n < words.length; n++) {
            const testLine = line + words[n] + ' ';
            const metrics = context.measureText(testLine);
            const testWidth = metrics.width;
            if (testWidth > maxWidth && n > 0) {
                context.fillText(line.trim(), x, currentY);
                line = words[n] + ' ';
                currentY += lineSpacing;
            } else {
                line = testLine;
            }
        }
        context.fillText(line.trim(), x, currentY);
    }

    // 1. Load Fonts asynchronously
    // Catch individual font loading errors to prevent Promise.all from rejecting prematurely.
    await Promise.all([
        _loadFont(headlineFont).catch(e => console.warn(`Error loading headline font ${headlineFont}:`, e)),
        _loadFont(bodyFont).catch(e => console.warn(`Error loading body font ${bodyFont}:`, e))
    ]);

    // Determine effective fonts to use (with fallbacks if custom fonts failed to load)
    const FONT_HEADLINE_FALLBACK = "Georgia, serif"; // A common serif font
    const FONT_BODY_FALLBACK = "Courier New, monospace"; // A common monospace font

    const effectiveHeadlineFont = document.fonts.check(`1em "${headlineFont}"`) ? headlineFont : FONT_HEADLINE_FALLBACK;
    const effectiveBodyFont = document.fonts.check(`1em "${bodyFont}"`) ? bodyFont : FONT_BODY_FALLBACK;
    
    // 2. Canvas Setup
    const canvas = document.createElement('canvas');
    canvas.width = posterWidth;
    canvas.height = posterHeight;
    const ctx = canvas.getContext('2d');

    // 3. Draw Background & Optional Noise
    ctx.fillStyle = backgroundColor;
    ctx.fillRect(0, 0, posterWidth, posterHeight);

    if (applyNoiseToBackground === 1 && noiseIntensity > 0) {
        const imageData = ctx.getImageData(0, 0, posterWidth, posterHeight);
        const pixels = imageData.data;
        const len = pixels.length;
        const noiseFactor = 255 * Math.max(0, Math.min(1, noiseIntensity)); // Clamp intensity

        for (let i = 0; i < len; i += 4) {
            // Add monochrome noise to RGB channels
            const noiseVal = (Math.random() - 0.5) * noiseFactor;
            pixels[i] = Math.max(0, Math.min(255, pixels[i] + noiseVal));
            pixels[i + 1] = Math.max(0, Math.min(255, pixels[i + 1] + noiseVal));
            pixels[i + 2] = Math.max(0, Math.min(255, pixels[i + 2] + noiseVal));
            // Alpha channel (pixels[i+3]) remains unchanged
        }
        ctx.putImageData(imageData, 0, 0);
    }

    // 4. Layout Constants
    const M = posterWidth * 0.05; // Margin
    const CW = posterWidth - 2 * M; // Content width for text elements

    ctx.fillStyle = textColor; // Set default text color
    ctx.textBaseline = "top"; // Consistent baseline for text Y positioning

    // 5. "WANTED" Text
    ctx.textAlign = "center"; // Center all text horizontally
    const wantedTextSize = posterHeight * 0.12;
    ctx.font = `${wantedTextSize}px "${effectiveHeadlineFont}"`;
    const yWanted = M;
    ctx.fillText(wantedText.toUpperCase(), posterWidth / 2, yWanted);

    // 6. "SUBTEXT" (e.g., DEAD OR ALIVE)
    const subTextSize = posterHeight * 0.04;
    ctx.font = `${subTextSize}px "${effectiveBodyFont}"`;
    const ySubtext = yWanted + wantedTextSize * 0.9 + (M * 0.1); // Positioned below "WANTED"
    ctx.fillText(subText.toUpperCase(), posterWidth / 2, ySubtext);
    
    // 7. Image Area Setup
    const imgAreaTop = ySubtext + subTextSize + M;
    const imgAreaHeight = posterHeight * 0.40; // Image gets 40% of poster height
    const maxImgDisplayWidth = CW * 0.9; // Max width for image display (90% of content width)
    const maxImgDisplayHeight = imgAreaHeight;

    let imgDrawWidth = 0;
    let imgDrawHeight = 0;
    let imageToDraw = originalImg; // Default to original image

    if (originalImg && originalImg.naturalWidth > 0 && originalImg.naturalHeight > 0) {
        imgDrawWidth = originalImg.naturalWidth;
        imgDrawHeight = originalImg.naturalHeight;
        const imgAspectRatio = imgDrawWidth / imgDrawHeight;

        // Scale image to fit within maxImgDisplayWidth and maxImgDisplayHeight
        if (imgDrawWidth > maxImgDisplayWidth) {
            imgDrawWidth = maxImgDisplayWidth;
            imgDrawHeight = imgDrawWidth / imgAspectRatio;
        }
        if (imgDrawHeight > maxImgDisplayHeight) {
            imgDrawHeight = maxImgDisplayHeight;
            imgDrawWidth = imgDrawHeight * imgAspectRatio;
        }
        // Final check for width constraint after height adjustment
        if (imgDrawWidth > maxImgDisplayWidth) {
            imgDrawWidth = maxImgDisplayWidth;
            imgDrawHeight = imgDrawWidth / imgAspectRatio;
        }

        // Apply Sepia filter to Image if requested
        if (applySepiaToImage === 1) {
            const tempCanvas = document.createElement('canvas');
            tempCanvas.width = imgDrawWidth;
            tempCanvas.height = imgDrawHeight;
            const tempCtx = tempCanvas.getContext('2d');
            
            tempCtx.drawImage(originalImg, 0, 0, imgDrawWidth, imgDrawHeight); // Draw scaled image to temp
            
            const imageData = tempCtx.getImageData(0, 0, imgDrawWidth, imgDrawHeight);
            const pixels = imageData.data;
            for (let i = 0; i < pixels.length; i += 4) {
                const r = pixels[i], g = pixels[i+1], b = pixels[i+2];
                pixels[i]   = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189); // Red
                pixels[i+1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168); // Green
                pixels[i+2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131); // Blue
            }
            tempCtx.putImageData(imageData, 0, 0);
            imageToDraw = tempCanvas; // Update imageToDraw to the sepia version
        }
    }
    
    // Calculate image position (centered in its allocated space)
    const imgX = (posterWidth - imgDrawWidth) / 2;
    const imgY = imgAreaTop + (maxImgDisplayHeight - imgDrawHeight) / 2;

    // Draw the image (original or sepia-toned, or placeholder if invalid)
    if (imgDrawWidth > 0 && imgDrawHeight > 0) {
        ctx.drawImage(imageToDraw, imgX, imgY, imgDrawWidth, imgDrawHeight);
        // Draw Image Border
        if (imageBorderWidth > 0) {
            ctx.strokeStyle = imageBorderColor;
            ctx.lineWidth = imageBorderWidth;
            // Border is drawn centered on the image edge, so half inside, half outside
            ctx.strokeRect(imgX, imgY, imgDrawWidth, imgDrawHeight);
        }
    } else { // Placeholder if image is not available/valid
        const phX = (posterWidth - maxImgDisplayWidth * 0.8) / 2;
        const phY = imgAreaTop + (maxImgDisplayHeight - maxImgDisplayHeight * 0.8) / 2;
        const phW = maxImgDisplayWidth * 0.8;
        const phH = maxImgDisplayHeight * 0.8;
        
        ctx.strokeStyle = textColor;
        ctx.lineWidth = 1;
        ctx.strokeRect(phX, phY, phW, phH);
        ctx.font = `${posterHeight * 0.02}px "${effectiveBodyFont}"`;
        ctx.textAlign = "center";
        // Vertical center for placeholder text (simple approximation)
        ctx.fillText("Image Not Available", phX + phW / 2, phY + phH / 2 - (posterHeight * 0.02) / 2);
    }
    
    // 8. Bounty Name
    // Y position calculations are based on the allocated image area, not the actual image dimensions
    // This ensures text placement is consistent even if image is missing/small.
    let yCurrent = imgAreaTop + maxImgDisplayHeight + M;
    if (imgDrawWidth > 0 && imageBorderWidth > 0) { // Add half border width if border exists
         yCurrent += imageBorderWidth / 2;
    }

    const nameTextSize = Math.min(posterHeight * 0.07, CW / (bountyName.length * 0.45 + 1)); // Auto-size font
    ctx.font = `${nameTextSize}px "${effectiveBodyFont}"`;
    ctx.fillText(bountyName.toUpperCase(), posterWidth / 2, yCurrent);
    yCurrent += nameTextSize + (M * 0.4);

    // 9. Reward Amount
    const rewardTextSize = Math.min(posterHeight * 0.05, CW / (rewardAmount.length * 0.4 + 1)); // Auto-size font
    ctx.font = `${rewardTextSize}px "${effectiveBodyFont}"`;
    ctx.fillText(rewardAmount.toUpperCase(), posterWidth / 2, yCurrent);
    yCurrent += rewardTextSize + M;

    // 10. Crime Description
    const descTextSize = posterHeight * 0.025;
    const crimeDescMaxWidth = CW * 0.9; // Description can take up 90% of content width
    ctx.fillStyle = textColor; // Ensure text color set for wrapText
    _wrapText(ctx, crimeDescription, posterWidth / 2, yCurrent, crimeDescMaxWidth, descTextSize, bodyFont, FONT_BODY_FALLBACK);
    
    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 Space Bounty Hunter Poster Creator is an online tool that allows users to create personalized bounty posters. Users can upload an image and customize various elements of the poster, including the bounty name, reward amount, and details of the alleged crimes. The tool can set text styles, colors, and even apply effects like sepia tones and background noise to enhance the vintage look of the poster. This tool is ideal for creating fun and themed graphics for events, parties, or personal projects, especially for fans of sci-fi or retro aesthetics.

Leave a Reply

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