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!
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.