You can edit the below JavaScript code to customize the image tool.
async function processImage(originalImg, dentSizeStr = "25", dentDepthStr = "0.6", lightAngleDegStr = "45", specularStrengthStr = "0.7", metalBrightnessStr = "0.7") {
const dentSize = Math.max(5, parseInt(dentSizeStr)); // Visual scale of noise features (larger means bigger "dents")
const dentDepth = Math.max(0.0, Math.min(1.0, parseFloat(dentDepthStr))); // Intensity of the depth effect (shadow/highlight contrast)
const lightAngleDeg = parseFloat(lightAngleDegStr);
const specularStrength = Math.max(0.0, Math.min(1.0, parseFloat(specularStrengthStr))); // Intensity of bright highlights
const metalBrightness = Math.max(0.0, Math.min(1.0, parseFloat(metalBrightnessStr))); // Overall brightness multiplier for the final effect
const canvas = document.createElement('canvas');
const width = originalImg.width;
const height = originalImg.height;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// Draw original image to a temporary canvas to get its pixel data
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(originalImg, 0, 0);
const imgData = tempCtx.getImageData(0, 0, width, height);
const pixels = imgData.data;
const outputImageData = ctx.createImageData(width, height);
const outputPixels = outputImageData.data;
// --- Noise generation for bump map ---
// Basic pseudo-random number generator (Park-Miller LCG)
function srandom(seed) {
let t = seed % 2147483647;
if (t <= 0) t += 2147483646;
t = (16807 * t) % 2147483647;
return (t - 1) / 2147483646; // Output [0, 1)
}
// Interpolated noise (bilinear interpolation of srandom values)
function interpolatedNoise(x, y, seed, noiseGridCellSize) {
const gridX = x / noiseGridCellSize;
const gridY = y / noiseGridCellSize;
const intX = Math.floor(gridX);
const fracX = gridX - intX;
const intY = Math.floor(gridY);
const fracY = gridY - intY;
// Get random values at the four corners of the grid cell
const v1 = srandom(intX + intY * 5711 + seed); // Using a large prime for Y multiplier
const v2 = srandom((intX + 1) + intY * 5711 + seed);
const v3 = srandom(intX + (intY + 1) * 5711 + seed);
const v4 = srandom((intX + 1) + (intY + 1) * 5711 + seed);
// Bilinear interpolation
const i1 = v1 * (1 - fracX) + v2 * fracX;
const i2 = v3 * (1 - fracX) + v4 * fracX;
return i1 * (1 - fracY) + i2 * fracY; // Value in [0,1]
}
const bumpMap = new Float32Array(width * height);
const mainSeed = Math.floor(Math.random() * 100000); // Random seed for this application of the filter
const octaves = 4; // Number of noise layers for fractal noise (more octaves = more detail)
if (octaves > 0) {
let totalMaxAmplitude = 0;
let currentAmplitude = 1.0;
for(let i=0; i<octaves; i++) {
totalMaxAmplitude += currentAmplitude;
currentAmplitude *= 0.5;
}
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let totalValue = 0;
let amplitude = 1.0;
let frequency = 1.0; // This will be adjusted by dentSize conceptually
for (let i = 0; i < octaves; i++) {
// dentSize conceptually acts as the "base wavelength" for the first octave.
// Higher frequencies sample the noise grid more finely.
totalValue += interpolatedNoise(x * frequency, y * frequency, mainSeed + i * 71, dentSize / frequency) * amplitude;
amplitude *= 0.5; // Persistence: amplitude of higher frequencies decreases
frequency *= 2.0; // Lacunarity: frequency increases for higher octaves
}
bumpMap[y * width + x] = totalValue / totalMaxAmplitude; // Normalize to roughly [0,1]
}
}
// Explicitly normalize bumpMap to [0,1] for safety, as the above might not be perfect.
let minBump = Infinity, maxBump = -Infinity;
for (let i = 0; i < bumpMap.length; i++) {
if (bumpMap[i] < minBump) minBump = bumpMap[i];
if (bumpMap[i] > maxBump) maxBump = bumpMap[i];
}
if (maxBump > minBump) {
for (let i = 0; i < bumpMap.length; i++) {
bumpMap[i] = (bumpMap[i] - minBump) / (maxBump - minBump);
}
} else { // All values are the same (e.g., if octaves made it flat somehow)
for (let i = 0; i < bumpMap.length; i++) bumpMap[i] = 0.5; // Set to mid-gray
}
} else { // No octaves, fill with flat 0.5
for (let i = 0; i < bumpMap.length; i++) bumpMap[i] = 0.5;
}
const lightAngleRad = lightAngleDeg * Math.PI / 180;
const lightDx = Math.cos(lightAngleRad); // X component of light direction vector
const lightDy = Math.sin(lightAngleRad); // Y component of light direction vector
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
const originalR = pixels[idx];
const originalG = pixels[idx + 1];
const originalB = pixels[idx + 2];
const originalGray = (originalR + originalG + originalB) / 3.0; // Base brightness from original image
// Calculate "slope" by comparing bump map value at current point
// with a point slightly offset in the direction *opposite* to the light.
const offsetScale = 2; // How far to look for height difference; might need tuning or relating to dentSize
const prevX = Math.max(0, Math.min(width - 1, x - Math.round(lightDx * offsetScale)));
const prevY = Math.max(0, Math.min(height - 1, y - Math.round(lightDy * offsetScale)));
const bumpCurrent = bumpMap[y * width + x];
const bumpPrev = bumpMap[prevY * width + prevX];
// `diff` is positive if the current point is "higher" (in bump map terms)
// than the point on its "shadow side". This implies the surface is tilted towards the light.
const diff = bumpCurrent - bumpPrev; // Range is roughly [-1, 1] after normalization
// Shading effect: `diff` determines how much darker or lighter the pixel becomes.
// `dentDepth` scales the magnitude of this shading.
const shading = diff * dentDepth * 100; // Max shading effect: +/- (dentDepth * 100)
// Specular highlight effect
let highlight = 0;
const specularThreshold = 0.05; // `diff` must be greater than this to trigger specular highlight
if (diff > specularThreshold) {
// Normalize diff for specular calculation to [0,1] range for values above threshold
const normalizedDiffForSpecular = (diff - specularThreshold) / (1.0 - specularThreshold);
// `Math.pow` creates a sharper, more focused highlight. Exponent controls sharpness.
highlight = Math.pow(normalizedDiffForSpecular, 4) * specularStrength * 150; // Max highlight intensity
}
// Combine base lighting (mid-gray) with shading and highlight
let lightedEffect = 128 + shading + highlight; // Base of 128 is mid-gray
// Modulate the original image's grayscale value by this lighting effect.
// (lightedEffect / 128.0) acts as a multiplier: 1.0 for no change, >1 for brighter, <1 for darker.
let finalVal = originalGray * (lightedEffect / 128.0);
// Apply overall metal brightness adjustment
finalVal *= metalBrightness;
finalVal = Math.max(0, Math.min(255, finalVal)); // Clamp to [0, 255]
outputPixels[idx] = finalVal;
outputPixels[idx + 1] = finalVal;
outputPixels[idx + 2] = finalVal;
outputPixels[idx + 3] = pixels[idx + 3]; // Preserve original alpha
}
}
ctx.putImageData(outputImageData, 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!
The Image Hammered Metal Filter Effect tool allows users to apply a realistic hammered metal effect to their images. By adjusting parameters such as dent size, dent depth, light angle, specular strength, and metal brightness, users can create custom textures that mimic the appearance of metal surfaces. This filter is ideal for enhancing artwork, creating unique backgrounds, or adding metallic textures to digital designs. It can be used in various creative fields, including graphic design, web design, and digital art.