You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, bubblePosition = "top-right", bubbleScale = 0.6, reflectionEffect = "grayscale", bubbleColor = "white", bubbleStrokeColor = "black") {
// 1. Set up the main canvas
const w = originalImg.width;
const h = originalImg.height;
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
// Draw the original image as the background
ctx.drawImage(originalImg, 0, 0);
// 2. Define thought bubble geometry based on parameters
const bubbleW = w * Math.max(0.1, Math.min(1.0, bubbleScale));
const bubbleH = bubbleW * 0.7; // Bubbles are often wider than they are tall
const padding = w * 0.05;
let bubbleX, bubbleY, tailTargetX, tailTargetY;
// The tail of the bubble will point towards the center of the image
tailTargetX = w / 2;
tailTargetY = h / 2;
switch (bubblePosition) {
case "top-left":
bubbleX = padding;
bubbleY = padding;
break;
case "bottom-left":
bubbleX = padding;
bubbleY = h - bubbleH - padding;
break;
case "bottom-right":
bubbleX = w - bubbleW - padding;
bubbleY = h - bubbleH - padding;
break;
case "top-right":
default:
bubbleX = w - bubbleW - padding;
bubbleY = padding;
break;
}
// 3. Create the thought bubble path
const bubblePath = new Path2D();
const centerX = bubbleX + bubbleW / 2;
const centerY = bubbleY + bubbleH / 2;
// Main cloud/bubble shape (an ellipse)
bubblePath.ellipse(centerX, centerY, bubbleW / 2, bubbleH / 2, 0, 0, Math.PI * 2);
// Smaller circles for the "tail"
const dx = tailTargetX - centerX;
const dy = tailTargetY - centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
const unitX = dx / dist;
const unitY = dy / dist;
// First tail bubble (larger, closer to the main bubble)
const tailRadius1 = bubbleW * 0.1;
const tailDist1 = (bubbleW / 2) + (tailRadius1 * 0.5);
const tailX1 = centerX + unitX * tailDist1;
const tailY1 = centerY + unitY * tailDist1;
bubblePath.moveTo(tailX1 + tailRadius1, tailY1);
bubblePath.arc(tailX1, tailY1, tailRadius1, 0, Math.PI * 2);
// Second tail bubble (smaller, further away)
const tailRadius2 = bubbleW * 0.07;
const tailDist2 = tailDist1 + tailRadius1 + (tailRadius2 * 0.5);
const tailX2 = centerX + unitX * tailDist2;
const tailY2 = centerY + unitY * tailDist2;
bubblePath.moveTo(tailX2 + tailRadius2, tailY2);
bubblePath.arc(tailX2, tailY2, tailRadius2, 0, Math.PI * 2);
// 4. Draw the bubble shape onto the canvas
ctx.fillStyle = bubbleColor;
ctx.strokeStyle = bubbleStrokeColor;
ctx.lineWidth = Math.max(2, w * 0.005); // Make line width responsive
ctx.fill(bubblePath);
ctx.stroke(bubblePath);
// 5. Prepare the "reflection" image on a temporary canvas
const tempCanvas = document.createElement('canvas');
tempCanvas.width = w;
tempCanvas.height = h;
const tempCtx = tempCanvas.getContext('2d');
// Apply the chosen effect
switch (reflectionEffect.toLowerCase()) {
case "grayscale":
tempCtx.filter = 'grayscale(100%)';
break;
case "blur":
tempCtx.filter = 'blur(4px)';
break;
case "flip":
tempCtx.translate(w, 0);
tempCtx.scale(-1, 1);
break;
case "none":
default:
// No filter applied
break;
}
// Draw the image onto the temp canvas to apply the effect
tempCtx.drawImage(originalImg, 0, 0);
// 6. Draw the reflection inside the thought bubble using a clipping mask
ctx.save();
ctx.clip(bubblePath); // Future drawing operations will only be visible inside this path
// Draw the temporary canvas (with the effect) onto the main canvas,
// scaled and positioned to fill the bubble's bounding box.
ctx.drawImage(tempCanvas, bubbleX, bubbleY, bubbleW, bubbleH);
ctx.restore(); // Remove the clipping mask
// Return the final canvas element
return canvas;
}
Apply Changes