Please bookmark this page to avoid losing your image tool!

Image Water Reflections 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.
function processImage(originalImg, reflectionHeightFactor = 0.5, reflectionOpacity = 0.6, rippleAmplitude = 4, rippleFrequency = 3) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    // Ensure originalImg is usable (e.g., loaded).
    // The problem statement implies originalImg is a ready-to-use Image object.
    const imgWidth = originalImg.width;
    const imgHeight = originalImg.height;

    // Sanitize and validate parameters
    reflectionHeightFactor = Number(reflectionHeightFactor);
    reflectionOpacity = Number(reflectionOpacity);
    rippleAmplitude = Number(rippleAmplitude);
    rippleFrequency = Number(rippleFrequency);

    // Clamp reflectionHeightFactor to [0, 1] and reflectionOpacity to [0, 1]
    reflectionHeightFactor = Math.max(0, Math.min(1, reflectionHeightFactor));
    reflectionOpacity = Math.max(0, Math.min(1, reflectionOpacity));
    // Ensure amplitude and frequency are non-negative
    rippleAmplitude = Math.max(0, rippleAmplitude);
    rippleFrequency = Math.max(0, rippleFrequency);

    const reflectionActualHeight = Math.floor(imgHeight * reflectionHeightFactor);

    canvas.width = imgWidth;
    canvas.height = imgHeight + reflectionActualHeight;

    // 1. Draw original image
    ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);

    // Proceed only if there's a reflection to draw
    if (reflectionActualHeight > 0) {
        // 2. Draw the reflected part of the image
        // The reflection should mirror the top part of the original image.
        ctx.save();
        // Translate to the bottom-left of where the reflection will be drawn.
        // The Y-axis will be flipped, so (0,0) in transformed space becomes the bottom-left of reflection.
        ctx.translate(0, imgHeight + reflectionActualHeight);
        ctx.scale(1, -1); // Flip the Y-axis. Positive Y now goes upwards from the new origin.
        
        // Source rectangle from originalImg: (sx, sy, sWidth, sHeight)
        // We take the top 'reflectionActualHeight' pixels of the original image.
        // sx=0, sy=0 (top-left of original image)
        // sWidth=imgWidth
        // sHeight=reflectionActualHeight (depth of original image to reflect)
        
        // Destination rectangle in transformed canvas space: (dx, dy, dWidth, dHeight)
        // dx=0, dy=0 (relative to the transformed origin)
        // dWidth=imgWidth
        // dHeight=reflectionActualHeight
        ctx.drawImage(originalImg, 
            0, 0, imgWidth, reflectionActualHeight, // Source rectangle
            0, 0, imgWidth, reflectionActualHeight  // Destination rectangle
        );
        ctx.restore(); // Restore context to original state (before translate and scale)

        // 3. Apply a fading gradient to the reflection
        // The gradient will make the reflection appear to fade out with distance.
        const grad = ctx.createLinearGradient(
            0, imgHeight, // Gradient start (top of reflection area)
            0, imgHeight + reflectionActualHeight // Gradient end (bottom of reflection area)
        );
        
        // Define color stops for the gradient.
        // Using rgba(0,0,0,alpha) for destination-in: only alpha matters.
        grad.addColorStop(0, `rgba(0,0,0, ${reflectionOpacity})`);         // Reflection is most opaque near the original image
        grad.addColorStop(0.7, `rgba(0,0,0, ${reflectionOpacity * 0.3})`); // Mid-point for a smoother falloff
        grad.addColorStop(1, `rgba(0,0,0, 0)`);                            // Reflection is fully transparent at the farthest point
        
        // Use 'destination-in' composite operation. This means the existing reflection pixels
        // (destination) will be kept where they overlap with the new shape (gradient-filled rectangle),
        // and their alpha will be multiplied by the alpha of the new shape.
        ctx.globalCompositeOperation = 'destination-in';
        ctx.fillStyle = grad;
        ctx.fillRect(0, imgHeight, imgWidth, reflectionActualHeight); // Apply gradient mask
        ctx.globalCompositeOperation = 'source-over'; // Reset composite operation to default

        // 4. Apply ripple effect to the reflection (if amplitude and frequency are positive)
        if (rippleAmplitude > 0 && rippleFrequency > 0) {
            const reflectionImageData = ctx.getImageData(0, imgHeight, imgWidth, reflectionActualHeight);
            const pixels = reflectionImageData.data;
            // Work on a copy of pixel data to avoid read-modify-write issues on the same array during iteration.
            const outputPixels = new Uint8ClampedArray(pixels.length);

            // 'rippleFrequency' is the number of full wave cycles across the reflection's height.
            // 'wavelength' is the height in pixels of one full sine wave cycle.
            const wavelength = reflectionActualHeight / rippleFrequency;

            for (let y = 0; y < reflectionActualHeight; y++) {
                // Ripple amplitude can decrease with distance (y) from the original image.
                // This makes ripples appear stronger/wider closer to the "shoreline".
                const amplitudeFactor = (1 - y / reflectionActualHeight); // Linearly decreases from 1 to 0
                const currentRippleAmplitude = rippleAmplitude * amplitudeFactor;
                
                // Calculate horizontal displacement (dx) for pixels in this row (y) using a sine wave.
                const displacementX = currentRippleAmplitude * Math.sin(2 * Math.PI * y / wavelength);

                for (let x = 0; x < imgWidth; x++) {
                    let srcX = Math.round(x + displacementX);
                    
                    // Clamp srcX to be within the image bounds [0, imgWidth - 1]
                    // to prevent reading pixels from outside the image data.
                    srcX = Math.max(0, Math.min(imgWidth - 1, srcX));
                    
                    const destIdx = (y * imgWidth + x) * 4; // Index for the destination pixel (current x,y)
                    const srcIdx = (y * imgWidth + srcX) * 4;  // Index for the source pixel (displaced x, same y)

                    // Copy RGBA values from source pixel to destination pixel
                    outputPixels[destIdx]     = pixels[srcIdx];
                    outputPixels[destIdx + 1] = pixels[srcIdx + 1];
                    outputPixels[destIdx + 2] = pixels[srcIdx + 2];
                    outputPixels[destIdx + 3] = pixels[srcIdx + 3];
                }
            }
            // Copy the modified pixel data from outputPixels back to the ImageData object.
            reflectionImageData.data.set(outputPixels);
            // Put the modified ImageData (with ripples) back onto the canvas.
            ctx.putImageData(reflectionImageData, 0, imgHeight);
        }
    }
    
    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 Water Reflections Filter is an online tool that allows users to create realistic water reflections of images. By applying adjustable parameters such as reflection height, opacity, and ripple effects, users can enhance their images to give them a reflective water surface appearance. This tool is particularly useful for graphic designers, photographers, and content creators who wish to add a creative touch to their visuals, making them more engaging and visually striking.

Leave a Reply

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