You can edit the below JavaScript code to customize the image tool.
Apply Changes
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;
}
Apply Changes