You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, bumpDepth = 10, bumpDetail = 7, highlightIntensity = 0.75, lightAngleDeg = 45, lightColorStr = "255,255,255", ambientLight = 0.2) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Ensure the image is loaded and has dimensions
// The problem implies originalImg is a loaded JS Image object
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
if (imgWidth === 0 || imgHeight === 0) {
// Return an empty canvas or handle error appropriately
canvas.width = 0;
canvas.height = 0;
return canvas;
}
canvas.width = imgWidth;
canvas.height = imgHeight;
// Draw original image to a temporary canvas to get pixel data
const tempCanvas = document.createElement('canvas');
tempCanvas.width = imgWidth;
tempCanvas.height = imgHeight;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(originalImg, 0, 0);
const originalImageData = tempCtx.getImageData(0, 0, imgWidth, imgHeight);
const originalPixels = originalImageData.data;
const outputImageData = ctx.createImageData(imgWidth, imgHeight);
const outputPixels = outputImageData.data;
// --- Parameter Parsing and Setup ---
let lightColor = [255, 255, 255]; // Default to white
try {
const parts = lightColorStr.split(',').map(s => parseInt(s.trim(), 10));
if (parts.length === 3 && parts.every(p => !isNaN(p) && p >= 0 && p <= 255)) {
lightColor = parts;
}
} catch (e) {
// Parsing failed, use default white light
console.warn("Failed to parse lightColorStr, using default white.", e);
}
const lightColorNorm = [lightColor[0] / 255, lightColor[1] / 255, lightColor[2] / 255];
const lightAngleRad = lightAngleDeg * Math.PI / 180;
// --- Noise Generation Setup ---
// bumpDetail: 1 (coarse) to 15 (fine). Inverse relationship with cell size.
const noiseScaleFactor = Math.max(1, 16 - Math.max(1, Math.min(15, bumpDetail)));
const noiseGridCellSize = Math.max(4, Math.floor(Math.min(imgWidth, imgHeight) / (noiseScaleFactor * 1.5 + 5)));
const noiseGridW = Math.ceil(imgWidth / noiseGridCellSize) + 2; // Add buffer for edge sampling
const noiseGridH = Math.ceil(imgHeight / noiseGridCellSize) + 2;
const noiseMap = new Float32Array(noiseGridW * noiseGridH);
for (let i = 0; i < noiseMap.length; i++) {
noiseMap[i] = Math.random(); // Simple random noise values [0, 1)
}
// --- Helper Functions ---
const lerp = (a, b, t) => a * (1 - t) + b * t;
const s_curve = (t) => t * t * (3.0 - 2.0 * t); // Smoothstep function
const getHeight = (imgX, imgY) => {
const x = imgX / noiseGridCellSize; // Convert image coordinates to noise grid coordinates
const y = imgY / noiseGridCellSize;
const gxi0 = Math.floor(x); // Integer part of grid coordinate
const gyi0 = Math.floor(y);
const tx = s_curve(x - gxi0); // Fractional part for interpolation, smoothed
const ty = s_curve(y - gyi0);
// Access noiseMap, clamping indices to be within bounds
const C = (ix, iy) => {
const clampedIx = Math.max(0, Math.min(ix, noiseGridW - 1));
const clampedIy = Math.max(0, Math.min(iy, noiseGridH - 1));
return noiseMap[clampedIy * noiseGridW + clampedIx];
};
const p00 = C(gxi0, gyi0); // Value at grid point (i, j)
const p10 = C(gxi0 + 1, gyi0); // Value at grid point (i+1, j)
const p01 = C(gxi0, gyi0 + 1); // Value at grid point (i, j+1)
const p11 = C(gxi0 + 1, gyi0 + 1); // Value at grid point (i+1, j+1)
// Bilinear interpolation
return lerp(lerp(p00, p10, tx), lerp(p01, p11, tx), ty);
};
const normalizeVec3 = (v) => {
const len = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
if (len === 0) return [0, 0, 1]; // Default to a normal pointing straight out (Z-axis)
return [v[0]/len, v[1]/len, v[2]/len];
};
const dotVec3 = (v1, v2) => v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2];
// --- Lighting Constants ---
const diffusionFactor = 0.7; // How much diffuse light contributes to surface color
// Shininess: higher value = sharper, smaller highlights
const shininessExponent = 10 + (Math.max(0, Math.min(1, highlightIntensity)) * 60);
const lightVecZ = 0.5; // Z-component of light vector (0=horizontal, 1=directly overhead)
// --- Main Pixel Loop ---
for (let y = 0; y < imgHeight; y++) {
for (let x = 0; x < imgWidth; x++) {
// Calculate height map gradients for normal vector and displacement
const hL = getHeight(x - 1, y); // Height at (x-1, y)
const hR = getHeight(x + 1, y); // Height at (x+1, y)
const hU = getHeight(x, y - 1); // Height at (x, y-1)
const hD = getHeight(x, y + 1); // Height at (x, y+1)
// Gradients represent the slope of the height map
const gradX = (hR - hL); // Change in height along X
const gradY = (hD - hU); // Change in height along Y
// 1. Calculate Displacement
// Scale displacement by bumpDepth; 0.5 is an empirical factor for sensible displacement
const displacementX = gradX * bumpDepth * 0.5;
const displacementY = gradY * bumpDepth * 0.5;
// Determine source pixel coordinates after displacement, clamped to image bounds
const srcX = Math.max(0, Math.min(imgWidth - 1, Math.round(x + displacementX)));
const srcY = Math.max(0, Math.min(imgHeight - 1, Math.round(y + displacementY)));
const srcIdx = (srcY * imgWidth + srcX) * 4;
const rOrig = originalPixels[srcIdx];
const gOrig = originalPixels[srcIdx + 1];
const bOrig = originalPixels[srcIdx + 2];
const aOrig = originalPixels[srcIdx + 3];
// 2. Calculate Lighting Effects
// Surface Normal Vector (N)
// normalZStrength determines "flatness" of the surface. Higher bumpDepth -> smaller normalZ -> steeper surface.
const normalZStrength = Math.max(0.01, 2.5 / Math.max(1, bumpDepth));
const N = normalizeVec3([-gradX, -gradY, normalZStrength]);
// Light Vector (L)
const L = normalizeVec3([Math.cos(lightAngleRad), Math.sin(lightAngleRad), lightVecZ]);
// View Vector (V) - assuming viewer is looking straight at the surface (along Z-axis)
const V = [0, 0, 1];
// Diffuse Reflection (Lambertian model)
const diffuse = Math.max(0, dotVec3(N, L));
// Specular Reflection (Blinn-Phong model using Halfway Vector)
const Hx = L[0] + V[0];
const Hy = L[1] + V[1];
const Hz = L[2] + V[2];
const H = normalizeVec3([Hx, Hy, Hz]); // Halfway vector
const specAngle = Math.max(0, dotVec3(N, H));
const specular = Math.pow(specAngle, shininessExponent) * highlightIntensity;
// Combine lighting components
// Base color from original image, modulated by ambient and diffuse light
// Specular highlights are additive and colored by the light source
let r = rOrig * (ambientLight + diffuse * diffusionFactor) + specular * lightColorNorm[0] * 255;
let g = gOrig * (ambientLight + diffuse * diffusionFactor) + specular * lightColorNorm[1] * 255;
let b = bOrig * (ambientLight + diffuse * diffusionFactor) + specular * lightColorNorm[2] * 255;
// Write final pixel color to output image data, clamping values
const currentOutputIdx = (y * imgWidth + x) * 4;
outputPixels[currentOutputIdx] = Math.max(0, Math.min(255, r));
outputPixels[currentOutputIdx + 1] = Math.max(0, Math.min(255, g));
outputPixels[currentOutputIdx + 2] = Math.max(0, Math.min(255, b));
outputPixels[currentOutputIdx + 3] = aOrig; // Preserve original alpha
}
}
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes