Please bookmark this page to avoid losing your image tool!

Image Terrain Height 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.
function processImage(originalImg, gradientColorsStr = "#0000FF,#4169E1,#228B22,#8FBC8F,#A0522D,#FFFFFF", thresholdsStr = "50,80,120,170,220,255", grayscaleMethod = "luminosity") {

    /**
     * Converts a HEX color string to an RGB object.
     * Supports "RRGGBB", "#RRGGBB", "RGB", "#RGB".
     * Returns null if the hex string is invalid.
     */
    function hexToRgb(hex) {
        // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
        const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
        hex = hex.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(hex);
        return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
        } : null;
    }

    // --- 1. Parse and Validate Parameters ---
    const parsedColorsHex = gradientColorsStr.split(',').map(c => c.trim());
    const parsedThresholdsNum = thresholdsStr.split(',').map(t => {
        const num = parseInt(t.trim(), 10);
        return isNaN(num) ? -1 : Math.max(0, Math.min(255, num)); // Clamp 0-255, mark invalid as -1
    });

    let pairedGradient = [];
    const len = Math.min(parsedColorsHex.length, parsedThresholdsNum.length);

    for (let i = 0; i < len; i++) {
        const rgbColor = hexToRgb(parsedColorsHex[i]);
        const thresholdVal = parsedThresholdsNum[i];
        // Ensure color is valid and threshold was parsed correctly (not -1 from above)
        if (rgbColor && thresholdVal !== -1) { 
            pairedGradient.push({ color: rgbColor, threshold: thresholdVal });
        }
    }

    // Sort by threshold to ensure correct application order
    pairedGradient.sort((a, b) => a.threshold - b.threshold);

    // Separate back into final arrays for processing
    let finalColorsRgb = pairedGradient.map(p => p.color);
    let finalThresholds = pairedGradient.map(p => p.threshold);

    // Fallback if parsing resulted in an empty or invalid gradient
    if (finalColorsRgb.length === 0) {
        console.warn("Provided gradient colors/thresholds were invalid or insufficient. Using default gradient.");
        
        // Use the default values specified in function signature
        const defaultColorsArray = ("#0000FF,#4169E1,#228B22,#8FBC8F,#A0522D,#FFFFFF").split(',').map(c => c.trim());
        const defaultThresholdsArray = ("50,80,120,170,220,255").split(',').map(t => parseInt(t.trim(), 10));
        
        let defaultPairedGradient = [];
        for (let i = 0; i < Math.min(defaultColorsArray.length, defaultThresholdsArray.length); i++) {
            const rgb = hexToRgb(defaultColorsArray[i]);
            if (rgb) { // Default colors should always be valid
                 defaultPairedGradient.push({ color: rgb, threshold: defaultThresholdsArray[i] });
            }
        }
        defaultPairedGradient.sort((a,b) => a.threshold - b.threshold);
        
        finalColorsRgb = defaultPairedGradient.map(p => p.color);
        finalThresholds = defaultPairedGradient.map(p => p.threshold);

        // If even defaults fail (e.g. hexToRgb has a bug or default strings are malformed), use a failsafe black/white.
        if (finalColorsRgb.length === 0 || finalColorsRgb.some(c => !c)) {
            console.error("Critical error: Default gradient processing failed. Using black/white failsafe.");
            finalColorsRgb = [ { r: 0, g: 0, b: 0 }, { r: 255, g: 255, b: 255 } ];
            finalThresholds = [ 127, 255 ];
        }
    }
    
    // --- 2. Canvas Setup ---
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    // Use naturalWidth/Height if available, otherwise fallback to width/height
    // This typically gives the actual image dimensions if it's loaded.
    canvas.width = originalImg.naturalWidth || originalImg.width;
    canvas.height = originalImg.naturalHeight || originalImg.height;

    if (canvas.width === 0 || canvas.height === 0) {
        // Handle cases where image might not be fully loaded or is an empty image
        console.warn("Image has zero width or height.");
        return canvas; // Return empty canvas
    }

    ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
    
    let imageData;
    try {
        imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    } catch (e) {
        // Potential CORS issue if image is from another domain and canvas is tainted
        console.error("Could not getImageData: Canvas might be tainted by cross-origin data.", e);
        ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear previous drawImage
        ctx.fillStyle = 'rgba(200, 200, 200, 0.8)'; // Semi-transparent background
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = 'red';
        ctx.font = `bold ${Math.min(24, canvas.width / 15)}px Arial`;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText('Error: Cannot process cross-origin image.', canvas.width / 2, canvas.height / 2);
        return canvas;
    }
    
    const data = imageData.data;
    
    // --- 3. Pixel Processing ---
    const lumR = 0.299, lumG = 0.587, lumB = 0.114; // Standard luminosity coefficients

    for (let i = 0; i < data.length; i += 4) {
        const r = data[i];
        const g = data[i+1];
        const b = data[i+2];
        // Alpha (data[i+3]) is preserved

        // Calculate "height" based on grayscale value
        let gray;
        if (grayscaleMethod === "average") {
            gray = (r + g + b) / 3;
        } else { // Default to "luminosity" or if method is misspelled
            gray = lumR * r + lumG * g + lumB * b;
        }
        gray = Math.round(Math.max(0, Math.min(255, gray))); // Clamp to 0-255 and round

        // Determine output color based on height (gray value)
        let R_out, G_out, B_out;
        let colorApplied = false;

        // finalColorsRgb and finalThresholds are guaranteed non-empty by fallback logic
        for (let k = 0; k < finalThresholds.length; k++) {
            if (gray <= finalThresholds[k]) {
                R_out = finalColorsRgb[k].r;
                G_out = finalColorsRgb[k].g;
                B_out = finalColorsRgb[k].b;
                colorApplied = true;
                break;
            }
        }

        if (!colorApplied) {
            // If gray is greater than all defined thresholds (e.g., last threshold in user input isn't 255)
            // Use the color of the highest defined band.
            const lastColor = finalColorsRgb[finalColorsRgb.length - 1];
            R_out = lastColor.r;
            G_out = lastColor.g;
            B_out = lastColor.b;
        }
        
        data[i] = R_out;
        data[i+1] = G_out;
        data[i+2] = B_out;
    }

    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 Terrain Height Filter Effect Tool allows users to apply a gradient-based color filter to images based on their grayscale values, simulating a height map effect. Users can customize the gradient colors and threshold values to create unique visual representations of terrains, making this tool useful for artists, designers, and cartographers who wish to visualize elevation data or create stylized artwork. The tool automatically adjusts the image’s colors according to specified thresholds, helping to bring depth and dimension to flat images.

Leave a Reply

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