Please bookmark this page to avoid losing your image tool!

Image Bubble Wrap Filter

(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, 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;
}

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 Bubble Wrap Filter is a creative online tool that allows users to transform their images by applying a bubble wrap effect. This effect creates a visually engaging appearance by distorting the image within circular bubble shapes, simulating a bubble wrap texture. Users can customize various parameters including bubble radius, grid spacing, distortion, and highlight/shadow colors to achieve the desired look. This tool is perfect for enhancing images for use in graphic design, social media posts, or simply for fun personal projects.

Leave a Reply

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