Please bookmark this page to avoid losing your image tool!

Image Ripple Filter Application

(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, amplitudeStr = "10", frequencyStr = "0.1", phaseStr = "0", centerXRatioStr = "0.5", centerYRatioStr = "0.5") {
    // Helper function to create an error canvas with a message
    const createErrorCanvas = (width, height, message, baseImage = null) => {
        const errCanvas = document.createElement('canvas');
        // Ensure width and height are at least 1, default to 100 if not sensible
        errCanvas.width = (width && width > 0) ? width : 100;
        errCanvas.height = (height && height > 0) ? height : 100;
        const errCtx = errCanvas.getContext('2d');
        
        // Background: try to draw original image, else plain lightgray
        if (baseImage && baseImage.width > 0 && baseImage.height > 0) {
            try {
                // Preserve aspect ratio while fitting image onto error canvas
                let drawW = errCanvas.width;
                let drawH = errCanvas.height;
                const imgAspect = baseImage.width / baseImage.height;
                const canvasAspect = errCanvas.width / errCanvas.height;
                
                if (imgAspect > canvasAspect) { // Image wider than canvas aspect ratio
                    drawH = errCanvas.width / imgAspect;
                } else { // Image taller or same aspect ratio
                    drawW = errCanvas.height * imgAspect;
                }
                // Center the drawn image
                const drawX = (errCanvas.width - drawW) / 2;
                const drawY = (errCanvas.height - drawH) / 2;
                errCtx.drawImage(baseImage, drawX, drawY, drawW, drawH);
            } catch (e) { // Fallback if drawing baseImage fails
                errCtx.fillStyle = 'lightgray';
                errCtx.fillRect(0, 0, errCanvas.width, errCanvas.height);
            }
        } else {
            errCtx.fillStyle = 'lightgray';
            errCtx.fillRect(0, 0, errCanvas.width, errCanvas.height);
        }
        
        // Text styling for the error message
        const rectHeight = Math.max(40, errCanvas.height * 0.25); // Height of the text band
        const textY = errCanvas.height / 2; // Y position for the text

        errCtx.fillStyle = 'rgba(200, 0, 0, 0.75)'; // Semi-transparent red band
        errCtx.fillRect(0, textY - rectHeight / 2, errCanvas.width, rectHeight);

        errCtx.fillStyle = 'white';
        errCtx.textAlign = 'center';
        errCtx.textBaseline = 'middle';
        // Dynamic font size, trying to fit message
        const maxFontSize = Math.max(12, rectHeight * 0.4); 
        let fontSize = maxFontSize;
        if (message && message.length > 0) {
            fontSize = Math.min(maxFontSize, (errCanvas.width * 0.9) / (message.length * 0.6)); // Simple fit logic
        }
        errCtx.font = `bold ${fontSize}px Arial`;
        errCtx.fillText(message, errCanvas.width / 2, textY);
        
        return errCanvas;
    };

    // Parameter parsing
    const amplitude = Number(amplitudeStr);
    const frequency = Number(frequencyStr);
    const phase = Number(phaseStr);
    let centerXRatioNum = Number(centerXRatioStr);
    let centerYRatioNum = Number(centerYRatioStr);

    // Validate parameters
    if (isNaN(amplitude) || isNaN(frequency) || isNaN(phase) || isNaN(centerXRatioNum) || isNaN(centerYRatioNum)) {
        console.error("Invalid parameters for ripple filter. Parameters must be numbers.");
        let w = (originalImg && (originalImg.naturalWidth || originalImg.width)) || 0;
        let h = (originalImg && (originalImg.naturalHeight || originalImg.height)) || 0;
        return createErrorCanvas(w, h, "Error: Invalid Parameters", originalImg);
    }

    // Image loading check (specific to HTMLImageElement)
    if (originalImg instanceof HTMLImageElement && !originalImg.complete) {
        try {
            await new Promise((resolve, reject) => {
                // Ensure onload/onerror are attached before checking complete status again
                originalImg.onload = resolve;
                originalImg.onerror = () => reject(new Error("Image failed to load."));
                // If image is already complete by the time handlers are set up, resolve manually.
                if (originalImg.complete) resolve();
            });
        } catch (error) {
            console.error(error.message);
            return createErrorCanvas(originalImg.width, originalImg.height, "Error: Image Load Failed");
        }
    }
    
    // Determine image dimensions, using naturalWidth/Height for HTMLImageElement if available and valid
    const imgWidth = ('naturalWidth' in originalImg && originalImg.naturalWidth > 0) ? originalImg.naturalWidth : originalImg.width;
    const imgHeight = ('naturalHeight' in originalImg && originalImg.naturalHeight > 0) ? originalImg.naturalHeight : originalImg.height;

    if (!imgWidth || imgWidth === 0 || !imgHeight || imgHeight === 0) {
        console.error("Image has zero or invalid dimensions.");
        return createErrorCanvas(100, 100, "Error: Invalid Image Dimensions");
    }
    
    // Clamp center ratios to the [0, 1] range to ensure center is within image
    centerXRatioNum = Math.max(0, Math.min(1, centerXRatioNum));
    centerYRatioNum = Math.max(0, Math.min(1, centerYRatioNum));

    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    canvas.width = imgWidth;
    canvas.height = imgHeight;

    try {
        ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
    } catch (e) {
        // This can happen if originalImg is, e.g., a detached element or invalid source
        console.error("Error drawing original image to canvas:", e);
        return createErrorCanvas(imgWidth, imgHeight, "Error: Draw Image Failed");
    }

    let originalImageData;
    try {
        originalImageData = ctx.getImageData(0, 0, imgWidth, imgHeight);
    } catch (e) {
        // This typically happens due to CORS policy for cross-origin images
        console.error("Could not get ImageData (CORS issue or tainted canvas?):", e);
        // Pass originalImg to be drawn behind the error message if possible
        return createErrorCanvas(imgWidth, imgHeight, "Error: Canvas Access (CORS)", originalImg);
    }
    
    const originalData = originalImageData.data;
    const outputImageData = ctx.createImageData(imgWidth, imgHeight); // Use createImageData from context
    const outputData = outputImageData.data;

    const actualCenterX = imgWidth * centerXRatioNum;
    const actualCenterY = imgHeight * centerYRatioNum;

    for (let y = 0; y < imgHeight; y++) {
        for (let x = 0; x < imgWidth; x++) {
            const dx = x - actualCenterX; // Distance from center X
            const dy = y - actualCenterY; // Distance from center Y
            const distance = Math.sqrt(dx * dx + dy * dy);

            let srcX, srcY;

            if (distance === 0) { // Avoid division by zero for the exact center pixel
                srcX = x;
                srcY = y;
            } else {
                // Calculate ripple effect: a sinusoidal wave based on distance from center
                const waveValue = Math.sin(distance * frequency + phase);
                // Displacement amount based on amplitude and wave
                const displacement = amplitude * waveValue;
                
                // Normalized direction vector from center to current pixel (x,y)
                const normalizedDx = dx / distance;
                const normalizedDy = dy / distance;

                // Calculate source coordinates for this destination pixel (x,y)
                // The pixel at (x,y) gets its color from (srcX, srcY)
                // This displaces sampling radially.
                srcX = x + normalizedDx * displacement;
                srcY = y + normalizedDy * displacement;
            }
            
            // Use nearest neighbor interpolation by rounding source coordinates
            const roundSrcX = Math.round(srcX);
            const roundSrcY = Math.round(srcY);

            // Clamp coordinates to be within the image boundaries (edge clamping)
            const clampedSrcX = Math.max(0, Math.min(imgWidth - 1, roundSrcX));
            const clampedSrcY = Math.max(0, Math.min(imgHeight - 1, roundSrcY));
            
            // Calculate array indices for pixel data
            const srcPixelIndex = (clampedSrcY * imgWidth + clampedSrcX) * 4;
            const destPixelIndex = (y * imgWidth + x) * 4;

            // Copy RGBA values from source to destination
            outputData[destPixelIndex]     = originalData[srcPixelIndex];     // R
            outputData[destPixelIndex + 1] = originalData[srcPixelIndex + 1]; // G
            outputData[destPixelIndex + 2] = originalData[srcPixelIndex + 2]; // B
            outputData[destPixelIndex + 3] = originalData[srcPixelIndex + 3]; // A
        }
    }

    // Put the modified pixel data back onto the canvas
    ctx.putImageData(outputImageData, 0, 0);
    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 Ripple Filter Application allows users to apply a ripple effect to images, creating visually appealing distortions based on specified parameters such as amplitude, frequency, phase, and the center of the ripple effect. This tool is suitable for artists, graphic designers, and hobbyists looking to enhance their images with unique wave-like patterns. Use cases include creating engaging digital artwork, enhancing social media graphics, or adding artistic flair to personal photo collections.

Leave a Reply

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