You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, bubbleRadius = 20, bubbleGridStep = 35, distortionFactor = 1.2, highlightColorStr = "rgba(255,255,255,0.5)", shadowColorStr = "rgba(0,0,0,0.3)") {
// Sanitize numeric inputs
bubbleRadius = Math.max(1, Number(bubbleRadius));
bubbleGridStep = Math.max(1, Number(bubbleGridStep));
distortionFactor = Math.max(0.01, Number(distortionFactor)); // Avoid zero/negative
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
canvas.width = imgWidth;
canvas.height = imgHeight;
if (imgWidth === 0 || imgHeight === 0) {
console.warn("Image has zero dimensions. Is it loaded?");
return canvas; // Return empty canvas
}
// Helper function to create a transparent version of a color string
// e.g., "rgba(255,0,0,0.5)" -> "rgba(255,0,0,0)"
// e.g., "rgb(255,0,0)" -> "rgba(255,0,0,0)"
// e.g., "#FF0000" -> "rgba(255,0,0,0)"
// e.g., "red" -> "rgba(255,0,0,0)"
function makeTransparent(colorStr, forHighlight = true) {
if (typeof colorStr !== 'string') { // Safety check for type
return forHighlight ? "rgba(255,255,255,0)" : "rgba(0,0,0,0)";
}
// Optimized common cases for "rgba(...)" and "rgb(...)"
if (colorStr.startsWith("rgba(")) {
return colorStr.replace(/,\s*[\d\.]+\s*\)$/, ", 0)");
}
if (colorStr.startsWith("rgb(")) {
return colorStr.replace("rgb(", "rgba(").replace(")", ", 0)");
}
// For other formats (hex, named colors), use a temporary canvas to parse the color
// This is a robust way to get the RGBA components regardless of input format.
// This temporary canvas is created only if needed.
const tempParserCanvas = document.createElement('canvas');
tempParserCanvas.width = 1;
tempParserCanvas.height = 1;
const tempCtx = tempParserCanvas.getContext('2d');
tempCtx.fillStyle = colorStr; // Assign the color string
const parsedColor = tempCtx.fillStyle; // Read it back; browser typically converts to "rgba(r,g,b,a)" or "rgb(r,g,b)"
if (parsedColor.startsWith("rgba(")) { // Browser converted to rgba
return parsedColor.replace(/,\s*[\d\.]+\s*\)$/, ", 0)");
} else if (parsedColor.startsWith("rgb(")) { // Browser converted to rgb
return parsedColor.replace("rgb(", "rgba(").replace(")", ", 0)");
}
// Fallback if parsing still fails (e.g. invalid color string)
return forHighlight ? "rgba(255,255,255,0)" : "rgba(0,0,0,0)";
}
const transparentHighlightEnd = makeTransparent(highlightColorStr, true);
const transparentShadowEnd = makeTransparent(shadowColorStr, false);
// Calculate number of columns and rows for the bubble grid
// Add +1 to ensure coverage even if centers are slightly off-canvas
const numCols = Math.ceil(imgWidth / bubbleGridStep) + 1;
const numRows = Math.ceil(imgHeight / bubbleGridStep) + 1;
for (let row = 0; row < numRows; row++) {
for (let col = 0; col < numCols; col++) {
let centerX = col * bubbleGridStep;
let centerY = row * bubbleGridStep;
// Stagger rows for a more hexagonal/offset grid appearance
if (row % 2 !== 0) {
centerX += bubbleGridStep / 2;
}
ctx.save(); // Save context state before clipping
// 1. Define and clip to the circular bubble shape
ctx.beginPath();
ctx.arc(centerX, centerY, bubbleRadius, 0, Math.PI * 2);
ctx.clip();
// 2. Draw the distorted (magnified/minified) part of the original image
const destDiameter = bubbleRadius * 2;
const srcDiameter = destDiameter / distortionFactor;
const srcRadius = srcDiameter / 2;
const sx = centerX - srcRadius; // Source X from original image
const sy = centerY - srcRadius; // Source Y from original image
const sWidth = srcDiameter; // Source Width
const sHeight = srcDiameter; // Source Height
const dx = centerX - bubbleRadius; // Destination X on canvas
const dy = centerY - bubbleRadius; // Destination Y on canvas
const dWidth = destDiameter; // Destination Width
const dHeight = destDiameter; // Destination Height
try {
ctx.drawImage(originalImg, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
} catch (e) {
// This might happen if originalImg is tainted (cross-origin) and operations are restricted,
// or if sWidth/sHeight are exceptionally problematic.
// console.error("Error drawing image segment for bubble:", e);
// Optionally, fill with a fallback color:
// ctx.fillStyle = 'rgba(128,128,128,0.1)'; // A neutral placeholder
// ctx.fillRect(dx, dy, dWidth, dHeight);
}
// 3. Add 3D effect using highlights and shadows
// Highlight (typically top-leftish)
const highlightOffsetFactor = 0.3; // Determines how offset the gradient center is
const gradHighlightX = centerX - bubbleRadius * highlightOffsetFactor;
const gradHighlightY = centerY - bubbleRadius * highlightOffsetFactor;
const highlightGradient = ctx.createRadialGradient(
gradHighlightX, gradHighlightY, 0, // Inner circle (center of highlight)
gradHighlightX, gradHighlightY, bubbleRadius // Outer circle (radius of bubble)
);
highlightGradient.addColorStop(0, highlightColorStr); // Highlight color at its center
highlightGradient.addColorStop(1, transparentHighlightEnd); // Fades to transparent
ctx.fillStyle = highlightGradient;
ctx.fillRect(dx, dy, dWidth, dHeight); // Apply gradient over the bubble area
// Shadow (typically bottom-rightish)
const gradShadowX = centerX + bubbleRadius * highlightOffsetFactor;
const gradShadowY = centerY + bubbleRadius * highlightOffsetFactor;
const shadowGradient = ctx.createRadialGradient(
gradShadowX, gradShadowY, 0, // Inner circle (center of shadow)
gradShadowX, gradShadowY, bubbleRadius * 1.2 // Outer circle (make shadow appear softer at edges)
);
shadowGradient.addColorStop(0, shadowColorStr); // Shadow color at its center
shadowGradient.addColorStop(1, transparentShadowEnd); // Fades to transparent
ctx.fillStyle = shadowGradient;
ctx.fillRect(dx, dy, dWidth, dHeight); // Apply gradient over the bubble area
ctx.restore(); // Restore context (removes clipping path)
}
}
return canvas;
}
Apply Changes