Please bookmark this page to avoid losing your image tool!

Image Caustics 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, scale = 60.0, strength = 0.5, brightness = 1.0, lightColorRgb = "255,255,220", numLayers = 4, distortion = 20.0, sharpness = 15.0, phaseOffset = 0.0) {
    // Ensure numeric parameters are indeed numbers and have sensible fallbacks/ranges
    scale = Number(scale) || 60.0;
    strength = Math.max(0, Math.min(1, Number(strength) || 0.5));
    brightness = Math.max(0, Number(brightness) || 1.0);
    numLayers = Math.max(0, Math.floor(Number(numLayers) || 4));
    distortion = Number(distortion) || 20.0;
    sharpness = Math.max(0.1, Number(sharpness) || 15.0); // Sharpness must be > 0 for Math.pow
    phaseOffset = Number(phaseOffset) || 0.0;

    const canvas = document.createElement('canvas');
    // Use willReadFrequently hint for potential performance optimization by the browser
    const ctx = canvas.getContext('2d', { willReadFrequently: true });

    // Use naturalWidth/Height for HTMLImageElement to ensure image is loaded, fallback to width/height
    canvas.width = originalImg.naturalWidth || originalImg.width;
    canvas.height = originalImg.naturalHeight || originalImg.height;

    if (canvas.width === 0 || canvas.height === 0) {
        console.error("Image has zero dimensions. Ensure the image is loaded and has valid dimensions.");
        // Return an empty (or minimal) canvas if image dimensions are invalid
        canvas.width = canvas.width || 1;
        canvas.height = canvas.height || 1;
        return canvas;
    }

    ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
    
    let imageData;
    try {
        imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    } catch (e) {
        console.error("Could not process image due to getImageData error (possibly CORS issue): ", e);
        // If getImageData fails (e.g., CORS), return the canvas with the original image drawn.
        return canvas;
    }
    
    const data = imageData.data;

    const colorParts = String(lightColorRgb).split(',').map(s => s.trim());
    const lightR = parseInt(colorParts[0], 10) || 255;
    const lightG = parseInt(colorParts[1], 10) || 255;
    const lightB = parseInt(colorParts[2], 10) || 255;

    const layerParams = [];
    let totalAmplitude = 0;

    for (let i = 0; i < numLayers; i++) {
        const amp = Math.pow(0.65, i); // Amplitude decreases for each layer
        layerParams.push({
            // Angle offset varies per layer to introduce directional variety
            angleOffset: (Math.random() - 0.5) * 0.5 * Math.PI + (i * Math.PI * 0.3 / numLayers), 
            phaseX: (Math.random() * Math.PI * 2) + phaseOffset,
            phaseY: (Math.random() * Math.PI * 2) + phaseOffset,
            amplitude: amp,
            frequencyFactor: Math.pow(1.75, i) // Frequency increases for each layer (smaller details)
        });
        totalAmplitude += amp;
    }
    
    // This function calculates the caustic intensity (0-1) for a given pixel
    function getCausticValueAtPixel(x, y) {
        let Px = x; // Use copies for coordinate perturbation
        let Py = y;
        let value = 0; // Accumulated wave value

        if (numLayers === 0) return 0; // No effect if no layers

        for (let i = 0; i < numLayers; i++) {
            const lp = layerParams[i];
            const currentScale = Math.max(0.1, scale / lp.frequencyFactor); // Prevent division by zero or tiny scale

            // Calculate two orthogonal wave components for this layer
            const dirX = Math.cos(lp.angleOffset);
            const dirY = Math.sin(lp.angleOffset);
            
            const waveVal1 = Math.sin((Px * dirX + Py * dirY) / currentScale + lp.phaseX);
            const waveVal2 = Math.cos((Px * -dirY + Py * dirX) / currentScale + lp.phaseY); // Orthogonal wave

            // Combine waves; multiplication creates more complex interference
            const layerValue = waveVal1 * waveVal2; // Result is in [-1, 1]
            value += layerValue * lp.amplitude;

            // Perturb coordinates for the next layer, scaled by distortion and current layer's amplitude
            if (i < numLayers - 1) {
                // Use components of the current layer's waves to drive distortion
                const angleForDistortX = (Py / currentScale + lp.phaseY) * (lp.frequencyFactor / 2 + 0.5);
                const angleForDistortY = (Px / currentScale + lp.phaseX) * (lp.frequencyFactor / 2 + 0.5);
                
                Px += distortion * Math.cos(angleForDistortX) * lp.amplitude;
                Py += distortion * Math.sin(angleForDistortY) * lp.amplitude;
            }
        }

        // Normalize accumulated value (expected range approx -totalAmplitude to +totalAmplitude)
        if (totalAmplitude > 0.0001) {
            value = value / totalAmplitude; // Normalize to approx [-1, 1]
        } else {
             value = 0; // Handle cases where totalAmplitude is ~0
        }
        
        // Shape the normalized value to create caustic lines
        // `1.0 - Math.abs(value)` maps values near 0 (wave cancellations) to high intensity (1.0)
        // and values near -1 or 1 (wave peaks) to low intensity (0.0).
        // This tends to create networks of bright lines.
        value = 1.0 - Math.abs(value); 
        value = Math.pow(value, sharpness); // Higher sharpness makes lines thinner and brighter
        
        return Math.max(0, Math.min(1, value)); // Clamp final positive_value to [0, 1]
    }

    // Apply the caustics effect pixel by pixel
    for (let y = 0; y < canvas.height; y++) {
        for (let x = 0; x < canvas.width; x++) {
            const idx = (y * canvas.width + x) * 4;
            const origR = data[idx];
            const origG = data[idx + 1];
            const origB = data[idx + 2];

            const causticPatternValue = getCausticValueAtPixel(x, y); 

            // Calculate the light intensity to add from the caustics
            const effectiveLightR = lightR * brightness * causticPatternValue;
            const effectiveLightG = lightG * brightness * causticPatternValue;
            const effectiveLightB = lightB * brightness * causticPatternValue;

            // Additive blending: original_color + caustic_light * strength
            // Strength controls how much of the caustic effect is applied.
            data[idx]     = Math.min(255, Math.max(0, origR + effectiveLightR * strength));
            data[idx + 1] = Math.min(255, Math.max(0, origG + effectiveLightG * strength));
            data[idx + 2] = Math.min(255, Math.max(0, origB + effectiveLightB * strength));
            // Alpha (data[idx + 3]) remains unchanged
        }
    }

    ctx.putImageData(imageData, 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 Caustics Filter is a tool designed to enhance images by applying a caustics effect, which simulates the way light refracts and creates complex patterns on surfaces, similar to those seen in water or glass. Users can customize parameters such as scale, strength, brightness, and color of the light to create unique visual effects. This tool can be useful for artists looking to add artistic flair to their digital images, for photographers wanting to embellish photos with interesting lighting effects, or for graphic designers aiming to create striking visuals for marketing materials or social media posts.

Leave a Reply

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