Please bookmark this page to avoid losing your image tool!

Photo Speech Bubble Adder

(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,
    text = "Hello World!",
    bubbleX = 50,
    bubbleY = 25,
    tailX = 50,
    tailY = 60,
    fontSize = 24,
    fontFamily = "Comic Sans MS",
    textColor = "black",
    fillColor = "white",
    strokeColor = "black",
    lineWidth = 2,
    padding = 15
) {
    /**
     * Helper to dynamically load a font if it's not a common web-safe one.
     * It will try to load from Google Fonts.
     * @param {string} font - The font-family string.
     * @returns {Promise<string>} The loaded font-family string or a fallback.
     */
    const loadFont = async (font) => {
        const webSafeFonts = [
            "arial", "verdana", "helvetica", "tahoma", "trebuchet ms", "times new roman",
            "georgia", "garamond", "courier new", "brush script mt", "impact", "comic sans ms"
        ];
        const fontName = font.split(',')[0].trim().replace(/['"]/g, '');
        if (webSafeFonts.includes(fontName.toLowerCase())) {
            return font; // It's a web-safe font, no need to load
        }

        try {
            // Check if the font is already available to the browser.
            if (document.fonts.check(`${fontSize}px ${fontName}`)) {
                return font;
            }

            // Assume it's a Google Font. Create the URL and a <link> element.
            const googleFontName = fontName.replace(/ /g, '+');
            const fontUrl = `https://fonts.googleapis.com/css2?family=${googleFontName}&display=swap`;
            const fontId = `font-stylesheet-${googleFontName}`;
            
            if (!document.getElementById(fontId)) {
                const link = document.createElement('link');
                link.id = fontId;
                link.rel = 'stylesheet';
                link.href = fontUrl;
                document.head.appendChild(link);
                // Wait for the stylesheet to be loaded and parsed
                await new Promise(resolve => {
                    link.onload = resolve;
                    link.onerror = resolve; // Continue even if it fails
                });
            }
            
            // Wait for the font to be ready for use in canvas
            await document.fonts.load(`${fontSize}px "${fontName}"`);
            return `"${fontName}"`;

        } catch (e) {
            console.warn(`Failed to load font "${font}". Falling back to Arial.`, e);
            return "Arial"; // Fallback to a safe font
        }
    };

    // 1. Setup Canvas and asynchronously load the font
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    canvas.width = originalImg.naturalWidth || originalImg.width;
    canvas.height = originalImg.naturalHeight || originalImg.height;

    const loadedFontFamily = await loadFont(fontFamily);

    // 2. Draw the original image onto the canvas
    ctx.drawImage(originalImg, 0, 0);

    // 3. Prepare text and measure its dimensions
    const lines = text.split('\n');
    const lineHeight = fontSize * 1.2;
    ctx.font = `${fontSize}px ${loadedFontFamily}`;
    
    let maxTextWidth = 0;
    lines.forEach(line => {
        maxTextWidth = Math.max(maxTextWidth, ctx.measureText(line).width);
    });

    const textBlockHeight = lineHeight * lines.length;
    const bubbleWidth = maxTextWidth + padding * 2;
    const bubbleHeight = textBlockHeight - (lineHeight - fontSize*1.05) + padding * 2; // Tighter vertical fit

    // 4. Calculate coordinates of the bubble and tail in pixels
    const bubbleCenterX = canvas.width * (bubbleX / 100);
    const bubbleCenterY = canvas.height * (bubbleY / 100);
    const targetTailX = canvas.width * (tailX / 100);
    const targetTailY = canvas.height * (tailY / 100);

    const x = bubbleCenterX - bubbleWidth / 2;
    const y = bubbleCenterY - bubbleHeight / 2;
    const w = bubbleWidth;
    const h = bubbleHeight;
    
    // 5. Draw the speech bubble shape
    ctx.fillStyle = fillColor;
    ctx.strokeStyle = strokeColor;
    ctx.lineWidth = lineWidth;
    ctx.lineJoin = 'round'; // For sharp corners on the tail

    const r = Math.max(0, Math.min(20, w / 4, h / 4)); // Corner radius
    const tailWidth = 20;

    const isTailOutside = targetTailX < x || targetTailX > x + w || targetTailY < y || targetTailY > y + h;
    
    ctx.beginPath();
    
    if (!isTailOutside) {
        // If the tail target is inside the bubble, just draw a rounded rectangle
        ctx.moveTo(x + r, y);
        ctx.arcTo(x + w, y, x + w, y + h, r);
        ctx.arcTo(x + w, y + h, x, y + h, r);
        ctx.arcTo(x, y + h, x, y, r);
        ctx.arcTo(x, y, x + w, y, r);
    } else {
        // Calculate the point on the box's edge where the tail should connect
        const dx = targetTailX - bubbleCenterX;
        const dy = targetTailY - bubbleCenterY;
        let edgePointX, edgePointY;

        if (Math.abs(dy * (w / 2)) < Math.abs(dx * (h / 2))) {
            const sign = dx > 0 ? 1 : -1;
            edgePointX = bubbleCenterX + sign * (w / 2);
            edgePointY = bubbleCenterY + dy * (sign * w / 2) / dx;
        } else {
            const sign = dy > 0 ? 1 : -1;
            edgePointY = bubbleCenterY + sign * (h / 2);
            edgePointX = bubbleCenterX + dx * (sign * h / 2) / dy;
        }
        
        // Build the path by drawing the rounded rectangle and injecting the tail
        ctx.moveTo(x + r, y); // Top Left
        if (Math.abs(edgePointY - y) < 1) { 
            const base1 = Math.max(x + r, edgePointX - tailWidth / 2);
            const base2 = Math.min(x + w - r, edgePointX + tailWidth / 2);
            ctx.lineTo(base1, y); ctx.lineTo(targetTailX, targetTailY); ctx.lineTo(base2, y);
        }
        ctx.lineTo(x + w - r, y); // Top Right
        ctx.arcTo(x + w, y, x + w, y + r, r);
        if (Math.abs(edgePointX - (x + w)) < 1) {
            const base1 = Math.max(y + r, edgePointY - tailWidth / 2);
            const base2 = Math.min(y + h - r, edgePointY + tailWidth / 2);
            ctx.lineTo(x + w, base1); ctx.lineTo(targetTailX, targetTailY); ctx.lineTo(x + w, base2);
        }
        ctx.lineTo(x + w, y + h - r); // Bottom Right
        ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
        if (Math.abs(edgePointY - (y + h)) < 1) {
            const base1 = Math.max(x + r, edgePointX - tailWidth / 2);
            const base2 = Math.min(x + w - r, edgePointX + tailWidth / 2);
            ctx.lineTo(base2, y + h); ctx.lineTo(targetTailX, targetTailY); ctx.lineTo(base1, y + h);
        }
        ctx.lineTo(x + r, y + h); // Bottom Left
        ctx.arcTo(x, y + h, x, y + h - r, r);
        if (Math.abs(edgePointX - x) < 1) {
            const base1 = Math.max(y + r, edgePointY - tailWidth / 2);
            const base2 = Math.min(y + h - r, edgePointY + tailWidth / 2);
            ctx.lineTo(x, base2); ctx.lineTo(targetTailX, targetTailY); ctx.lineTo(x, base1);
        }
        ctx.lineTo(x, y + r);
        ctx.arcTo(x, y, x + r, y, r);
    }
    
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    // 6. Draw the text inside the bubble
    ctx.fillStyle = textColor;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    
    const firstLineY = bubbleCenterY - (textBlockHeight / 2) + (lineHeight / 2);
    lines.forEach((line, index) => {
        ctx.fillText(line, bubbleCenterX, firstLineY + index * lineHeight);
    });

    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 Photo Speech Bubble Adder is a versatile online tool that allows users to enhance images by adding customizable speech bubbles containing text. You can adjust the position, font size, font family, text color, bubble fill color, and outline attributes to create engaging visuals. This tool is useful for creating comic-style graphics, engaging social media posts, or fun vignettes that convey messages in a playful manner. Perfect for educators, marketers, and social media influencers looking to make their visuals more communicative and expressive.

Leave a Reply

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