You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg,
shadowColorStr = "40,30,20", // Dark brown for deep shadows
midLowColorStr = "100,70,50", // Medium brown
midHighColorStr = "180,140,100", // Light brown/tan
highlightColorStr = "230,200,170", // Pale orange/yellow for highlights
edgeStrength = 0.6, // 0.0 (no edge darkening) to 1.0 (edges become much darker)
edgeThreshold = 50 // Gradient magnitude (0-~1442) to detect an edge
) {
// Helper: Parse "r,g,b" string to [r, g, b] array
function parseColor(colorStr, defaultColor = [0,0,0]) {
try {
const colors = colorStr.split(',').map(s => parseInt(s.trim(), 10));
if (colors.length !== 3 || colors.some(isNaN)) {
console.warn(`Invalid color string format: "${colorStr}". Using default.`);
return defaultColor.map(c => Math.max(0, Math.min(255, c)));
}
return colors.map(c => Math.max(0, Math.min(255, c))); // Clamp to 0-255
} catch (e) {
console.error(`Error parsing color string "${colorStr}": ${e.message}. Using default.`);
return defaultColor.map(c => Math.max(0, Math.min(255, c)));
}
}
const shadowCol = parseColor(shadowColorStr, [40,30,20]);
const midLowCol = parseColor(midLowColorStr, [100,70,50]);
const midHighCol = parseColor(midHighColorStr, [180,140,100]);
const highlightCol = parseColor(highlightColorStr, [230,200,170]);
// Helper: Linear interpolation for colors
function lerpColor(color1, color2, t) {
const t_clamped = Math.max(0, Math.min(1, t));
return [
Math.round(color1[0] * (1 - t_clamped) + color2[0] * t_clamped),
Math.round(color1[1] * (1 - t_clamped) + color2[1] * t_clamped),
Math.round(color1[2] * (1 - t_clamped) + color2[2] * t_clamped)
];
}
// Helper: Get luminance (Rec. 709) from RGB values
function getLuminance(r, g, b) {
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
const canvas = document.createElement('canvas');
// Add willReadFrequently for potential performance optimization if supported.
const ctx = canvas.getContext('2d', { willReadFrequently: true });
if (!originalImg.complete || originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
console.error("Original image is not loaded or has no dimensions.");
canvas.width = 200;
canvas.height = 100;
ctx.fillStyle = "pink";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.textAlign = "center";
ctx.fillText("Error: Image not loaded.", canvas.width / 2, canvas.height / 2);
return canvas;
}
canvas.width = originalImg.naturalWidth;
canvas.height = originalImg.naturalHeight;
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
let imageData;
try {
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
console.error("Error getting ImageData (possibly CORS issue):", e);
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = "gray";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.font = "14px Arial";
ctx.fillText("Error: Could not process image.", canvas.width / 2, canvas.height / 2 - 10);
ctx.fillText("(May be a cross-origin issue)", canvas.width / 2, canvas.height / 2 + 10);
return canvas;
}
const data = imageData.data;
const width = canvas.width;
const height = canvas.height;
const outputImageData = ctx.createImageData(width, height);
const outputData = outputImageData.data;
// Precompute luminance for all pixels
const luminances = new Float32Array(width * height);
for (let i = 0; i < data.length; i += 4) {
luminances[i / 4] = getLuminance(data[i], data[i + 1], data[i + 2]);
}
const T1_3 = 1/3; // Threshold for first segment
const T2_3 = 2/3; // Threshold for second segment
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pixelArrIdx = y * width + x; // Index for 1D luminance array
const dataArrIdx = pixelArrIdx * 4; // Index for 1D pixel data array (R value)
const L = luminances[pixelArrIdx];
const t_norm = L / 255.0; // Normalized luminance [0, 1]
// 1. Color Mapping based on Luminance
let mappedR, mappedG, mappedB;
if (t_norm < T1_3) {
const local_t = t_norm * 3; // Scale [0, 1/3) to [0, 1)
[mappedR, mappedG, mappedB] = lerpColor(shadowCol, midLowCol, local_t);
} else if (t_norm < T2_3) {
const local_t = (t_norm - T1_3) * 3; // Scale [1/3, 2/3) to [0, 1)
[mappedR, mappedG, mappedB] = lerpColor(midLowCol, midHighCol, local_t);
} else {
const local_t = (t_norm - T2_3) * 3; // Scale [2/3, 1] to [0, 1]
[mappedR, mappedG, mappedB] = lerpColor(midHighCol, highlightCol, local_t);
}
// 2. Edge Detection (Sobel operator)
let isEdgePixel = false;
if (x > 0 && x < width - 1 && y > 0 && y < height - 1) {
// Get indices of 3x3 neighborhood in the 1D luminances array
const tl = (y - 1) * width + (x - 1); const tc = (y - 1) * width + x; const tr = (y - 1) * width + (x + 1);
const ml = y * width + (x - 1); /* mc is current */ const mr = y * width + (x + 1);
const bl = (y + 1) * width + (x - 1); const bc = (y + 1) * width + x; const br = (y + 1) * width + (x + 1);
const Gx = -luminances[tl] + luminances[tr]
-2 * luminances[ml] + 2 * luminances[mr]
-luminances[bl] + luminances[br];
const Gy = -luminances[tl] - 2 * luminances[tc] - luminances[tr]
+luminances[bl] + 2 * luminances[bc] + luminances[br];
const gradientMagnitude = Math.sqrt(Gx * Gx + Gy * Gy);
if (gradientMagnitude > edgeThreshold) {
isEdgePixel = true;
}
}
// 3. Apply Edge Effect (darken edges)
let finalR = mappedR;
let finalG = mappedG;
let finalB = mappedB;
if (isEdgePixel && edgeStrength > 0) {
const darkeningFactor = Math.max(0, 1 - edgeStrength);
finalR = Math.round(mappedR * darkeningFactor);
finalG = Math.round(mappedG * darkeningFactor);
finalB = Math.round(mappedB * darkeningFactor);
}
outputData[dataArrIdx] = Math.max(0, Math.min(255, finalR));
outputData[dataArrIdx + 1] = Math.max(0, Math.min(255, finalG));
outputData[dataArrIdx + 2] = Math.max(0, Math.min(255, finalB));
outputData[dataArrIdx + 3] = data[dataArrIdx + 3]; // Preserve original alpha channel
}
}
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes