Please bookmark this page to avoid losing your image tool!

Image Elevation Map Filter Effect

(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, angleStr = "225", depth = 1, amount = 100) {
    // originalImg is expected to be a loaded JavaScript Image object.
    // angleStr: Light source direction in degrees. "0" is East (right), "90" is South (down),
    //           "180" is West (left), "270" is North (up).
    //           Default "225" corresponds to a light source from the Top-Left.
    // depth: The distance (in pixels) to the neighbor pixel for calculating the difference.
    //        Higher values create a more pronounced 3D effect but can lose detail.
    // amount: Strength of the effect (percentage). 100 means normal. Higher values exaggerate
    //         the light/shadow differences.

    const canvas = document.createElement('canvas');
    // Using { willReadFrequently: true } can optimize repeated getImageData/putImageData calls,
    // though for a single pass filter, its impact might be minimal. It's good practice.
    const ctx = canvas.getContext('2d', { willReadFrequently: true });

    const imgWidth = originalImg.naturalWidth || originalImg.width;
    const imgHeight = originalImg.naturalHeight || originalImg.height;

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

    // Draw the original image onto the canvas
    ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);

    let imageData;
    try {
        // Get the pixel data from the canvas
        imageData = ctx.getImageData(0, 0, imgWidth, imgHeight);
    } catch (e) {
        // This can happen due to cross-origin restrictions if the image source is from a different domain
        // and the proper CORS headers aren't set.
        console.error("Error getting ImageData:", e);
        // Draw an error message on the canvas as a fallback
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = "rgba(128, 128, 128, 0.5)"; // Semi-transparent gray
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = "black";
        ctx.font = "16px Arial";
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.fillText("Error: Could not process image.", canvas.width / 2, canvas.height / 2 - 10);
        ctx.fillText("(Possibly due to cross-origin restrictions)", canvas.width/2, canvas.height/2 + 10);
        return canvas; // Return the canvas with the error message
    }
    
    const data = imageData.data; // The pixel data array (RGBA)
    const width = imgWidth;
    const height = imgHeight;

    // Create a new ImageData object to store the processed pixels
    const outputImageData = ctx.createImageData(width, height);
    const outputData = outputImageData.data;

    // Parse and validate parameters
    let parsedAngle = parseFloat(angleStr);
    if (isNaN(parsedAngle)) {
        parsedAngle = 225; // Default to Top-Left light source
    }

    let parsedDepth = Number(depth);
    if (isNaN(parsedDepth) || parsedDepth <= 0) {
        parsedDepth = 1; // Default depth
    }

    let parsedAmount = Number(amount);
    if (isNaN(parsedAmount)) {
        parsedAmount = 100; // Default amount percentage
    }
    const scaleFactor = parsedAmount / 100.0;

    // Convert light angle from degrees to radians for trigonometric functions
    const angleRad = parsedAngle * (Math.PI / 180.0);
    
    // Calculate the offsets (lightDx, lightDy) for the neighbor pixel based on angle and depth.
    // This vector points IN THE DIRECTION OF THE LIGHT.
    // The neighbor pixel we compare against will be (current_pos - light_vector).
    const lightDx = Math.round(parsedDepth * Math.cos(angleRad));
    const lightDy = Math.round(parsedDepth * Math.sin(angleRad)); // Positive sin(angle) points downwards in canvas coords

    // Iterate over each pixel of the image
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const idx = (y * width + x) * 4; // Index for the current pixel in the data array

            // Get RGB values of the current pixel
            const r = data[idx];
            const g = data[idx+1];
            const b = data[idx+2];
            // Calculate luminance (brightness) of the current pixel
            const lumCurrent = 0.299 * r + 0.587 * g + 0.114 * b;

            // Determine coordinates of the "neighbor" pixel.
            // This neighbor is "behind" the current pixel relative to the light source.
            let nx = x - lightDx;
            let ny = y - lightDy;

            // Clamp neighbor coordinates to be within image bounds (edge handling)
            nx = Math.max(0, Math.min(width - 1, nx));
            ny = Math.max(0, Math.min(height - 1, ny));
            
            const nidx = (ny * width + nx) * 4; // Index for the neighbor pixel
            // Get RGB values of the neighbor pixel
            const nr = data[nidx];
            const ng = data[nidx+1];
            const nb = data[nidx+2];
            // Calculate luminance of the neighbor pixel
            const lumNeighbor = 0.299 * nr + 0.587 * ng + 0.114 * nb;

            // Calculate the difference in luminance. This simulates the "slope" of the surface.
            const diff = lumCurrent - lumNeighbor;
            
            // Scale the difference by the 'amount' factor and add to a base gray value (128).
            // This creates the emboss effect: slopes facing the light become brighter,
            // slopes facing away become darker.
            const scaledDiff = diff * scaleFactor;
            let pixelVal = 128 + scaledDiff;

            // Clamp the final pixel value to the valid 0-255 range
            pixelVal = Math.max(0, Math.min(255, pixelVal));
            
            // Set the RGB channels of the output pixel to the calculated grayscale value
            outputData[idx]     = pixelVal; // Red
            outputData[idx+1]   = pixelVal; // Green
            outputData[idx+2]   = pixelVal; // Blue
            // Preserve the original alpha channel
            outputData[idx+3]   = data[idx+3]; // Alpha
        }
    }

    // Put the processed pixel data back onto the canvas
    ctx.putImageData(outputImageData, 0, 0);
    
    return canvas; // Return the canvas with the applied effect
}

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 Elevation Map Filter Effect tool allows users to apply a 3D effect to their images, simulating an elevation map by adjusting the light and shadow based on the brightness of the pixels. Users can customize the direction of the light source, the depth of the effect, and the strength of the shadow and highlight differences. This tool can be utilized in graphic design to create stylized images, enhance textures for visualization, or create artistic effects for presentations and social media. It’s especially useful for artists, designers, and anyone looking to add depth and dimension to their images.

Leave a Reply

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