Please bookmark this page to avoid losing your image tool!

Image Geological Layer Filter Effect Tool

(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, layerHeightParam = 20, colorPaletteStrParam = "auto", noiseAmountParam = 0.1, distortionAmountParam = 5) {

    // Helper function: clamp a value between a minimum and maximum
    function clamp(value, min, max) {
        return Math.max(min, Math.min(max, value));
    }

    // Helper function: convert hex color string to an RGB object
    function hexToRgb(hex) {
        if (hex === null || typeof hex === 'undefined') return { r: 128, g: 128, b: 128 }; // Default to gray for null/undefined

        let normalizedHex = String(hex).trim();
        if (!normalizedHex) return { r: 128, g: 128, b: 128 }; // Default to gray for empty string

        if (!normalizedHex.startsWith('#')) {
            normalizedHex = '#' + normalizedHex;
        }

        // Expand shorthand form (e.g. "#03F") to full form (e.g. "#0033FF")
        const shorthandRegex = /^#([a-f\d])([a-f\d])([a-f\d])$/i;
        normalizedHex = normalizedHex.replace(shorthandRegex, (m, r, g, b) => '#' + r + r + g + g + b + b);
        
        const result = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(normalizedHex);
        return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
        } : { r: 128, g: 128, b: 128 }; // Default to gray if parsing fails
    }

    // Helper function: calculate the average color for a specific region (layer) of an image
    function calculateAverageColorForLayerRegion(srcPixelData, imgWidth, imgHeight, layerNominalStartY, currentLayerHeight) {
        let rSum = 0, gSum = 0, bSum = 0;
        let pixelCount = 0;
        
        // Determine the actual start and end Y coordinates for averaging, clamped to image bounds
        const startY = clamp(layerNominalStartY, 0, imgHeight - 1);
        const endY = clamp(layerNominalStartY + currentLayerHeight, 0, imgHeight);

        // If the calculated layer region is outside image bounds or has no height
        if (startY >= endY) {
             return { r: 128, g: 128, b: 128}; // Default to gray
        }

        for (let y = startY; y < endY; y++) {
            for (let x = 0; x < imgWidth; x++) {
                const idx = (y * imgWidth + x) * 4;
                rSum += srcPixelData[idx];
                gSum += srcPixelData[idx + 1];
                bSum += srcPixelData[idx + 2];
                pixelCount++;
            }
        }

        if (pixelCount === 0) return { r: 128, g: 128, b: 128 }; // Default gray if no pixels were processed

        return {
            r: rSum / pixelCount,
            g: gSum / pixelCount,
            b: bSum / pixelCount
        };
    }

    // --- Parameter parsing and sanitization ---
    const layerHeight = Math.max(1, Number(layerHeightParam) || 20);
    // Normalize colorPaletteStrParam for consistent checking
    const normalizedColorPaletteStr = String(colorPaletteStrParam).trim().toLowerCase();
    const noiseAmount = clamp(Number(noiseAmountParam) || 0.1, 0, 1);
    const distortionAmount = Math.max(0, Number(distortionAmountParam) || 5);

    const DEFAULT_PALETTE_HEX_ARRAY = ["#A37E58","#8C6D56","#755C54","#5E4B4D","#473A42","#B89A78","#D1B797"];

    // --- Canvas setup ---
    const canvas = document.createElement('canvas');
    // Using { willReadFrequently: true } can be an optimization hint for repeated getImageData/putImageData calls.
    const ctx = canvas.getContext('2d', { willReadFrequently: true });

    const width = originalImg.naturalWidth || originalImg.width || 0;
    const height = originalImg.naturalHeight || originalImg.height || 0;

    if (width === 0 || height === 0) {
        console.error("Image has zero dimensions. Returning empty canvas.");
        canvas.width = 1; // Minimal canvas dimensions
        canvas.height = 1;
        return canvas;
    }
    canvas.width = width;
    canvas.height = height;

    // --- Original image data extraction (for "auto" color mode) ---
    let originalPixelData = null;
    let useAutoColorMode = (normalizedColorPaletteStr === "auto");

    if (useAutoColorMode) {
        const originalCanvas = document.createElement('canvas');
        const originalCtx = originalCanvas.getContext('2d', { willReadFrequently: true });
        originalCanvas.width = width;
        originalCanvas.height = height;
        originalCtx.drawImage(originalImg, 0, 0);
        try {
            originalPixelData = originalCtx.getImageData(0, 0, width, height).data;
        } catch (e) {
            console.warn("Could not get image data for 'auto' mode, possibly due to cross-origin restrictions. Falling back from 'auto' mode.", e);
            useAutoColorMode = false; // Fallback: disable auto mode
        }
    }
    
    // --- Determine active palette (if not in successful "auto" mode) ---
    let activePaletteRgbArray = [];
    if (!useAutoColorMode) {
        let hexStringsForPalette = DEFAULT_PALETTE_HEX_ARRAY; // Default assumption

        if (normalizedColorPaletteStr !== "auto" && normalizedColorPaletteStr !== "") {
            // User provided a specific palette string (not "auto" and not empty)
            const customHexStrings = String(colorPaletteStrParam).trim().split(',') // Use original param for case-sensitive hex
                .map(s => s.trim())
                .filter(s => s !== ""); // Remove empty strings that might result from " ,, "

            if (customHexStrings.length > 0) {
                hexStringsForPalette = customHexStrings; // Use user's palette
            }
        }
        activePaletteRgbArray = hexStringsForPalette.map(hex => hexToRgb(hex));
        
        // Ensure palette is not empty (e.g., if default palette was somehow cleared or user provided invalid list)
        if (activePaletteRgbArray.length === 0) {
             activePaletteRgbArray = DEFAULT_PALETTE_HEX_ARRAY.map(hex => hexToRgb(hex));
        }
    }

    // --- Main image processing loop ---
    const outputImgData = ctx.createImageData(width, height);
    const outputData = outputImgData.data;
    const layerAvgColorsCache = new Map(); // Cache for "auto" mode layer colors

    for (let y = 0; y < height; y++) {
        const currentTrueLayerIndex = Math.floor(y / layerHeight); // Layer index based on non-distorted y
        for (let x = 0; x < width; x++) {
            let distortedY = y;
            if (distortionAmount > 0) {
                // Create a wavy distortion. Phase depends on x and the true layer index to vary patterns.
                // (width / (Math.PI * 6)) means roughly 3 sine waves across the image width.
                const distortionPhase = (x / (width / (Math.PI * 6))) + (currentTrueLayerIndex * Math.PI / 2.0);
                const distortionVal = Math.sin(distortionPhase) * distortionAmount;
                distortedY = clamp(y + distortionVal, 0, height - 1); // Clamp to image bounds
            }
            
            const effectiveLayerIndex = Math.floor(distortedY / layerHeight); // Layer index based on (potentially) distorted y
            let finalLayerColorRgb;

            if (useAutoColorMode && originalPixelData) { // "Auto" color mode
                if (!layerAvgColorsCache.has(effectiveLayerIndex)) {
                    const avgColor = calculateAverageColorForLayerRegion(originalPixelData, width, height, effectiveLayerIndex * layerHeight, layerHeight);
                    layerAvgColorsCache.set(effectiveLayerIndex, avgColor);
                }
                finalLayerColorRgb = layerAvgColorsCache.get(effectiveLayerIndex);
            } else { // Predefined or default palette mode
                finalLayerColorRgb = activePaletteRgbArray[effectiveLayerIndex % activePaletteRgbArray.length];
            }
            
            // Fallback if color somehow didn't resolve (should be rare)
            if (!finalLayerColorRgb) finalLayerColorRgb = {r:128, g:128, b:128}; 

            let r = finalLayerColorRgb.r;
            let g = finalLayerColorRgb.g;
            let b = finalLayerColorRgb.b;

            // Add noise
            if (noiseAmount > 0) {
                const noiseVal = (Math.random() - 0.5) * 2 * 255 * noiseAmount; // Monochromatic noise
                r = clamp(r + noiseVal, 0, 255);
                g = clamp(g + noiseVal, 0, 255);
                b = clamp(b + noiseVal, 0, 255);
            }

            const idx = (y * width + x) * 4; // Pixel index in the ImageData array
            outputData[idx]     = r;
            outputData[idx + 1] = g;
            outputData[idx + 2] = b;
            outputData[idx + 3] = 255; // Set alpha to fully opaque
        }
    }
    ctx.putImageData(outputImgData, 0, 0); // Draw the processed image data onto the canvas
    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 Geological Layer Filter Effect Tool allows users to apply a unique geological layer effect to images. By manipulating the layers of color based on user-defined parameters such as layer height, color palette, noise amount, and distortion, this tool can transform ordinary images into artistic representations that mimic geological formations. This can be useful for graphic designers, digital artists, and anyone looking to create visually striking imagery with layered effects, adding depth and texture to their artwork. Use cases include creating backgrounds for websites, enhancing illustrations, or generating unique pieces of digital art.

Leave a Reply

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