Please bookmark this page to avoid losing your image tool!

Image Puddle 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, puddleCount = "12", strength = "5", wavelength = "20", perspective = "0.7", brightness = "40", wetDarkness = "0.15", seed = "42") {
    if (!originalImg || !originalImg.width || !originalImg.height) return originalImg;
    
    let w = originalImg.width;
    let h = originalImg.height;
    
    let canvas = document.createElement('canvas');
    canvas.width = w;
    canvas.height = h;
    let ctx = canvas.getContext('2d');
    ctx.drawImage(originalImg, 0, 0);
    
    let imgData = ctx.getImageData(0, 0, w, h);
    let data = imgData.data;
    
    let outData = ctx.createImageData(w, h);
    let out = outData.data;
    
    // Parse parameters
    let p_count = isNaN(parseInt(puddleCount, 10)) ? 12 : Math.max(1, parseInt(puddleCount, 10));
    let p_strength = isNaN(parseFloat(strength)) ? 5 : parseFloat(strength);
    let p_wavelength = isNaN(parseFloat(wavelength)) ? 20 : Math.max(1, parseFloat(wavelength));
    let p_perspective = isNaN(parseFloat(perspective)) ? 0.7 : Math.max(0.1, parseFloat(perspective));
    let p_brightness = isNaN(parseFloat(brightness)) ? 40 : parseFloat(brightness);
    let p_wetDarkness = isNaN(parseFloat(wetDarkness)) ? 0.15 : parseFloat(wetDarkness);
    let s = isNaN(parseInt(seed, 10)) ? 42 : parseInt(seed, 10);
    
    // Simple PRNG to ensure distinct but deterministic puddles
    function PRNG(seed) {
        this.seed = seed % 2147483647;
        if (this.seed <= 0) this.seed += 2147483646;
    }
    PRNG.prototype.next = function() {
        this.seed = this.seed * 16807 % 2147483647;
        return this.seed / 2147483647;
    }
    
    let rng = new PRNG(s);
    let puddles = [];
    let dim = Math.max(w, h);
    
    // Generate random puddle centers and properties
    for (let i = 0; i < p_count; i++) {
        puddles.push({
            x: rng.next() * w,
            y: rng.next() * h,
            radius: (rng.next() * 0.15 + 0.1) * dim, 
            phase: rng.next() * Math.PI * 2
        });
    }
    
    // Auxiliary arrays to accumulate displacement and lighting
    let dx_arr = new Float32Array(w * h);
    let dy_arr = new Float32Array(w * h);
    let light_arr = new Float32Array(w * h);
    let env_arr = new Float32Array(w * h);
    
    // Accumulate ripple effects
    for (let i = 0; i < p_count; i++) {
        let p = puddles[i];
        
        let maxY_offset = p.radius * p_perspective;
        let minX = Math.max(0, Math.floor(p.x - p.radius));
        let maxX = Math.min(w, Math.ceil(p.x + p.radius));
        let minY = Math.max(0, Math.floor(p.y - maxY_offset));
        let maxY = Math.min(h, Math.ceil(p.y + maxY_offset));
        
        let radSq = p.radius * p.radius;
        
        for (let y = minY; y < maxY; y++) {
            let offset = y * w;
            for (let x = minX; x < maxX; x++) {
                let dx = x - p.x;
                let dy = y - p.y;
                let dy_eff = dy / p_perspective;
                let distSq = dx * dx + dy_eff * dy_eff;
                
                if (distSq < radSq && distSq > 0.0001) {
                    let dist = Math.sqrt(distSq);
                    let normalizedDist = dist / p.radius;
                    // Envelope function for smooth ripple fade-out (Hann window)
                    let envelope = (Math.cos(normalizedDist * Math.PI) + 1) / 2;
                    
                    let phase = dist / p_wavelength + p.phase;
                    let sinPhase = Math.sin(phase);
                    let cosPhase = Math.cos(phase);
                    
                    let amplitude = p_strength * envelope * sinPhase;
                    
                    let idx = offset + x;
                    dx_arr[idx] += (dx / dist) * amplitude;
                    dy_arr[idx] += (dy_eff / dist) * amplitude * p_perspective;
                    env_arr[idx] += envelope;
                    
                    // Ripple slope for dynamic shading
                    let d_envelope = (-Math.PI / (2 * p.radius)) * Math.sin(normalizedDist * Math.PI);
                    let slope = p_strength * (d_envelope * sinPhase + envelope * cosPhase / p_wavelength);
                    
                    // Surface Normal
                    let nx = -slope * (dx / dist);
                    let ny = -slope * (dy_eff / dist) * p_perspective; 
                    
                    // Lighting vector calculation (light from top-left)
                    light_arr[idx] += (nx * -0.707 + ny * -0.707);
                }
            }
        }
    }
    
    // Apply displacement and lighting to output pixels using bilinear interpolation
    for (let y = 0; y < h; y++) {
        let offset = y * w;
        for (let x = 0; x < w; x++) {
            let idx = offset + x;
            
            let final_dx = dx_arr[idx];
            let final_dy = dy_arr[idx];
            let shading = light_arr[idx] * p_brightness;
            let wetness = 1.0 - p_wetDarkness * Math.min(1.0, env_arr[idx]);
            
            let sx = x - final_dx;
            let sy = y - final_dy;
            
            let x0 = Math.floor(sx);
            let y0 = Math.floor(sy);
            let x1 = x0 + 1;
            let y1 = y0 + 1;
            
            let fx = sx - x0;
            let fy = sy - y0;
            
            // Constrain source coordinates to image boundaries
            x0 = Math.max(0, Math.min(w - 1, x0));
            x1 = Math.max(0, Math.min(w - 1, x1));
            y0 = Math.max(0, Math.min(h - 1, y0));
            y1 = Math.max(0, Math.min(h - 1, y1));
            
            let idx00 = (y0 * w + x0) * 4;
            let idx10 = (y0 * w + x1) * 4;
            let idx01 = (y1 * w + x0) * 4;
            let idx11 = (y1 * w + x1) * 4;
            
            let outIdx = idx * 4;
            
            for (let c = 0; c < 3; c++) {
                let v00 = data[idx00 + c];
                let v10 = data[idx10 + c];
                let v01 = data[idx01 + c];
                let v11 = data[idx11 + c];
                
                let val0 = v00 * (1 - fx) + v10 * fx;
                let val1 = v01 * (1 - fx) + v11 * fx;
                let val = val0 * (1 - fy) + val1 * fy;
                
                // Darken area for wetness impression, then apply highlight shading reflection
                val *= wetness; 
                val += shading; 
                
                out[outIdx + c] = Math.max(0, Math.min(255, val));
            }
            
            // Alpha Channel
            let a00 = data[idx00 + 3];
            let a10 = data[idx10 + 3];
            let a01 = data[idx01 + 3];
            let a11 = data[idx11 + 3];
            
            let aval0 = a00 * (1 - fx) + a10 * fx;
            let aval1 = a01 * (1 - fx) + a11 * fx;
            let aval = aval0 * (1 - fy) + aval1 * fy;
            
            out[outIdx + 3] = Math.max(0, Math.min(255, aval));
        }
    }
    
    ctx.putImageData(outData, 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 Puddle Filter is a digital effect tool that simulates the appearance of water puddles and ripples on an existing image. By applying mathematical displacement and lighting calculations, the tool creates realistic water surface distortions, ripple patterns, and wetness effects, including surface reflections and darkened ‘wet’ areas. This tool is ideal for photographers, digital artists, and graphic designers looking to add atmospheric weather effects, such as rain-soaked streets or pond-like disturbances, to their visual content.

Leave a Reply

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