You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, cellophaneColorStr = "255,230,150", opacity = 0.5, shininess = 0.7, detailAmount = 6, shadowIntensity = 0.4) {
// 0. Parameter sanitization
const saneOpacity = Math.max(0, Math.min(1, opacity));
const saneShininess = Math.max(0, Math.min(1, shininess));
// detailAmount: 1 (max blur, min detail) to 10 (min blur, max detail)
const saneDetailAmount = Math.max(1, Math.min(10, detailAmount));
const saneShadowIntensity = Math.max(0, Math.min(1, shadowIntensity));
const C_base = (() => { // IIFE for parsing cellophaneColorStr
try {
const parts = cellophaneColorStr.split(',');
if (parts.length === 3) {
const r = parseInt(parts[0], 10);
const g = parseInt(parts[1], 10);
const b = parseInt(parts[2], 10);
if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
return {
r: Math.max(0, Math.min(255, r)),
g: Math.max(0, Math.min(255, g)),
b: Math.max(0, Math.min(255, b))
};
}
}
} catch (e) { /* Fall through to default */ }
// Default color if parsing fails or invalid format
return { r: 255, g: 230, b: 150 };
})();
const w = originalImg.width;
const h = originalImg.height;
if (w === 0 || h === 0) {
// Handle empty image case: return an empty canvas or a specific error indicator
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = w;
emptyCanvas.height = h;
return emptyCanvas;
}
// 1. Main canvas for final output
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
// Draw the original image onto the main canvas to get its pixel data
ctx.drawImage(originalImg, 0, 0, w, h);
const originalImageData = ctx.getImageData(0, 0, w, h);
const P_data = originalImageData.data; // Original Pixel data
// 2. Create Light Map (blurred grayscale version of the image)
const lightMapCanvas = document.createElement('canvas');
lightMapCanvas.width = w;
lightMapCanvas.height = h;
const lightMapCtx = lightMapCanvas.getContext('2d', { willReadFrequently: true });
// Draw original image to lightMapCanvas for grayscale conversion
lightMapCtx.drawImage(originalImg, 0, 0, w, h);
// Convert lightMapCanvas to grayscale
const lmImageData = lightMapCtx.getImageData(0, 0, w, h);
const lmData = lmImageData.data;
for (let i = 0; i < lmData.length; i += 4) {
const avg = (lmData[i] + lmData[i+1] + lmData[i+2]) / 3;
lmData[i] = lmData[i+1] = lmData[i+2] = avg;
}
lightMapCtx.putImageData(lmImageData, 0, 0);
// Apply blur to the grayscaled lightMapCanvas
// blurRadius: 0.5px (for saneDetailAmount=10, max detail) up to 5px (for saneDetailAmount=1, min detail)
const blurRadius = (11 - saneDetailAmount) * 0.5;
if (blurRadius > 0) {
// To blur lightMapCanvas "in place", draw to a temporary canvas, then copy back
// This is because canvas filters apply to drawing operations, not existing content directly.
const tempBlurCanvas = document.createElement('canvas');
tempBlurCanvas.width = w;
tempBlurCanvas.height = h;
const tempBlurCtx = tempBlurCanvas.getContext('2d');
tempBlurCtx.filter = `blur(${blurRadius}px)`;
tempBlurCtx.drawImage(lightMapCanvas, 0, 0, w, h); // Draw blurred version onto temp canvas
tempBlurCtx.filter = 'none'; // Reset filter for this context
lightMapCtx.clearRect(0, 0, w, h); // Clear lightMapCanvas
lightMapCtx.drawImage(tempBlurCanvas, 0, 0, w, h); // Copy blurred image back
}
const lightMapFinalImageData = lightMapCtx.getImageData(0, 0, w, h);
const L_data = lightMapFinalImageData.data; // Luminance data (R channel of grayscale image)
// 3. Pixel processing loop
const outputImageData = ctx.createImageData(w, h); // Use main canvas's context to create ImageData
const outData = outputImageData.data;
// Define zones for highlights and shadows based on luminance
// saneShininess: 0 (less shiny, highlights start later) to 1 (very shiny, highlights start sooner)
// saneShadowIntensity: 0 (less intense shadows, shadows start later) to 1 (very intense shadows, shadows start sooner)
const highlightZoneStart = 0.5 + (1 - saneShininess) * 0.4; // Ranges from 0.9 down to 0.5
const shadowZoneEnd = 0.5 - (1 - saneShadowIntensity) * 0.4; // Ranges from 0.1 up to 0.5
const epsilon = 1e-6; // Small number to prevent division by zero
for (let i = 0; i < P_data.length; i += 4) {
const P_r = P_data[i];
const P_g = P_data[i+1];
const P_b = P_data[i+2];
const P_a = P_data[i+3];
const L_raw = L_data[i]; // Luminance value from blurred grayscale map (0-255)
const L = L_raw / 255.0; // Normalized luminance (0-1)
let reflectR, reflectG, reflectB; // These will be the cellophane's color modulated by light
if (L >= highlightZoneStart && highlightZoneStart < 1.0) { // Highlight zone
// Factor approaches 1 as L approaches 1.0
const factor = (L - highlightZoneStart) / (1.0 - highlightZoneStart + epsilon);
const intensity = saneShininess; // Use shininess to control highlight brightness
reflectR = C_base.r + (255 - C_base.r) * factor * intensity;
reflectG = C_base.g + (255 - C_base.g) * factor * intensity;
reflectB = C_base.b + (255 - C_base.b) * factor * intensity;
} else if (L <= shadowZoneEnd && shadowZoneEnd > 0.0) { // Shadow zone
// Factor approaches 1 as L approaches 0.0
const factor = (shadowZoneEnd - L) / (shadowZoneEnd + epsilon);
const intensity = saneShadowIntensity; // Use shadowIntensity to control shadow darkness
reflectR = C_base.r * (1 - factor * intensity);
reflectG = C_base.g * (1 - factor * intensity);
reflectB = C_base.b * (1 - factor * intensity);
} else { // Midtone zone
reflectR = C_base.r;
reflectG = C_base.g;
reflectB = C_base.b;
}
// Clamp reflected color components to 0-255 range
reflectR = Math.max(0, Math.min(255, reflectR));
reflectG = Math.max(0, Math.min(255, reflectG));
reflectB = Math.max(0, Math.min(255, reflectB));
// Blend the calculated cellophane color (reflectR,G,B) with the original pixel color (P_r,g,b)
outData[i] = (reflectR * saneOpacity) + (P_r * (1 - saneOpacity));
outData[i+1] = (reflectG * saneOpacity) + (P_g * (1 - saneOpacity));
outData[i+2] = (reflectB * saneOpacity) + (P_b * (1 - saneOpacity));
outData[i+3] = P_a; // Preserve original alpha
}
// Place the processed pixel data back onto the main canvas
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes