Please bookmark this page to avoid losing your image tool!

Image Liquify 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, centerXParam, centerYParam, radiusParam, strengthParam, directionAngleParam) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    // Use naturalWidth/Height for intrinsic image dimensions
    const w = originalImg.naturalWidth || originalImg.width;
    const h = originalImg.naturalHeight || originalImg.height;

    if (w === 0 || h === 0) {
        // Handle cases like image not loaded or 0x0 image
        console.warn("Image has zero width or height. Cannot apply liquify filter.");
        canvas.width = w || 1; // Avoid 0-size canvas if problematic
        canvas.height = h || 1;
        // Optionally draw the (empty or tiny) original image if it makes sense
        if (w > 0 && h > 0) {
            try {
                ctx.drawImage(originalImg, 0, 0, w, h);
            } catch (e) {
                // Ignore if it cannot be drawn
            }
        }
        return canvas;
    }

    canvas.width = w;
    canvas.height = h;

    // Helper function for parsing numeric parameters with defaults
    const parseNumericParam = (param, defaultValue) => {
        if (param === undefined || param === null) {
            return defaultValue;
        }
        const strParam = String(param).trim();
        if (strParam === "") {
            return defaultValue;
        }
        const num = Number(strParam);
        return isNaN(num) ? defaultValue : num;
    };

    const centerX = parseNumericParam(centerXParam, w / 2);
    const centerY = parseNumericParam(centerYParam, h / 2);
    
    let radius = parseNumericParam(radiusParam, Math.min(w, h) / 4);
    if (radius <= 0) { // Ensure radius is positive
        radius = Math.min(w, h) / 4; // Try default again
        if (radius <= 0) radius = Math.max(w,h) > 0 ? 1 : 0; // Absolute fallback if min(w,h) is 0 but not w and h
    }

    // Strength can be 0 (no effect) or negative (opposite push).
    const strength = parseNumericParam(strengthParam, radius / 3); 

    const directionAngle = parseNumericParam(directionAngleParam, 0); // Default angle 0 (to the right)

    // Draw original image to a temporary canvas to get its pixel data
    // Using willReadFrequently hint for potential performance improvement
    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = w;
    tempCanvas.height = h;
    const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); 
    
    try {
        tempCtx.drawImage(originalImg, 0, 0, w, h);
    } catch (e) {
        console.error("Error drawing original image to temporary canvas: ", e);
        // Fallback: return original image drawn on the main canvas
        ctx.drawImage(originalImg, 0, 0, w, h);
        return canvas;
    }
    
    let originalImageData;
    try {
        originalImageData = tempCtx.getImageData(0, 0, w, h);
    } catch (e) {
        console.error("Error getting image data for liquify filter (possibly CORS issue): ", e);
        // Fallback: return original image drawn on the main canvas
        ctx.drawImage(originalImg, 0, 0, w, h);
        return canvas;
    }
    
    const originalPixels = originalImageData.data;
    const outputImageData = ctx.createImageData(w, h); // Use main canvas context to create ImageData
    const outputPixels = outputImageData.data;

    const angleRad = directionAngle * (Math.PI / 180);
    const pushDirX = Math.cos(angleRad);
    const pushDirY = Math.sin(angleRad);

    for (let y = 0; y < h; y++) {
        for (let x = 0; x < w; x++) {
            const currentPixelIndex = (y * w + x) * 4;

            const dXFromCenter = x - centerX;
            const dYFromCenter = y - centerY;
            const distanceFromCenter = Math.sqrt(dXFromCenter * dXFromCenter + dYFromCenter * dYFromCenter);

            let sourceX = x;
            let sourceY = y;

            if (distanceFromCenter < radius && radius > 0) { // Apply effect only within radius
                // Quadratic falloff: (1 - dist/radius)^2 for smoother effect
                let falloff = (radius - distanceFromCenter) / radius;
                falloff = falloff * falloff; 

                const displacement = strength * falloff;

                // Calculate source pixel by displacing against the push direction
                sourceX = x - displacement * pushDirX;
                sourceY = y - displacement * pushDirY;
            }

            // Clamp source coordinates to be within the image bounds
            const sX_clamped = Math.max(0, Math.min(w - 1, sourceX));
            const sY_clamped = Math.max(0, Math.min(h - 1, sourceY));

            // Bilinear interpolation for smoother results
            const x0 = Math.floor(sX_clamped);
            const y0 = Math.floor(sY_clamped);
            
            // Determine coordinates of the pixel to the right/bottom of (x0,y0), ensuring they stay within bounds.
            const x1 = Math.min(x0 + 1, w - 1);
            const y1 = Math.min(y0 + 1, h - 1);

            // Fractional parts for interpolation weights
            const fX = sX_clamped - x0; // Weight for x1 contribution
            const fY = sY_clamped - y0; // Weight for y1 contribution

            // Indices for the four neighboring pixels in the originalPixels array
            const p00_idx = (y0 * w + x0) * 4; // Top-left pixel (x0,y0)
            const p10_idx = (y0 * w + x1) * 4; // Top-right pixel (x1,y0)
            const p01_idx = (y1 * w + x0) * 4; // Bottom-left pixel (x0,y1)
            const p11_idx = (y1 * w + x1) * 4; // Bottom-right pixel (x1,y1)

            // Interpolate RGBA channels
            for (let channel = 0; channel < 4; channel++) {
                const C00 = originalPixels[p00_idx + channel];
                const C10 = originalPixels[p10_idx + channel];
                const C01 = originalPixels[p01_idx + channel];
                const C11 = originalPixels[p11_idx + channel];

                // Interpolate horizontally for top row: (x0,y0) and (x1,y0)
                const topInterpolation = C00 * (1 - fX) + C10 * fX;
                // Interpolate horizontally for bottom row: (x0,y1) and (x1,y1)
                const bottomInterpolation = C01 * (1 - fX) + C11 * fX;

                // Interpolate vertically between the results of horizontal interpolations
                const interpolatedValue = topInterpolation * (1 - fY) + bottomInterpolation * fY;
                
                outputPixels[currentPixelIndex + channel] = interpolatedValue;
            }
        }
    }

    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 Liquify Filter Application allows users to apply a liquify effect to images, enabling manipulation of specific areas by pushing or pulling pixels based on a defined radius and direction. Users can select a center point in the image, specify the strength of the effect, and control the direction in which the displacement occurs. This tool can be utilized for artistic image editing, enhancing creative expression in photography, or retouching images in graphic design. It is suitable for various applications, such as creating unique visual effects, correcting image distortions, or transforming portraits.

Leave a Reply

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