You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, stainColor = "100,20,50", strength = 0.6, darkenFactor = 0.15) {
const canvas = document.createElement('canvas');
// Use naturalWidth/Height for intrinsic image dimensions, fallback to width/height
const W = originalImg.naturalWidth || originalImg.width;
const H = originalImg.naturalHeight || originalImg.height;
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
// If context cannot be obtained (highly unlikely for '2d')
// an error will be thrown by subsequent ctx operations, which is acceptable.
// Or, to be very robust and ensure a canvas is returned:
if (!ctx) {
console.error("Unable to get 2D context. Returning an empty canvas or a copy of the original.");
// Fallback: try to return a canvas with the original image drawn,
// or at least an empty canvas of the correct size.
const fallbackCanvas = document.createElement('canvas');
fallbackCanvas.width = W;
fallbackCanvas.height = H;
const fallbackCtx = fallbackCanvas.getContext('2d');
if (fallbackCtx) {
try {
fallbackCtx.drawImage(originalImg, 0, 0, W, H);
return fallbackCanvas;
} catch (e) {
// If drawing originalImg fails, return the empty fallback canvas
console.error("Error drawing original image in fallback.", e);
return fallbackCanvas;
}
}
// If even creating a fallback context fails, return the first empty canvas
return canvas;
}
ctx.drawImage(originalImg, 0, 0, W, H);
let stainRGBArray;
try {
stainRGBArray = stainColor.split(',').map(c => {
const val = parseInt(c.trim(), 10);
if (isNaN(val) || val < 0 || val > 255) {
throw new Error(`Invalid color component: ${c.trim()}`);
}
return val;
});
if (stainRGBArray.length !== 3) {
throw new Error("Color string must have 3 components (R,G,B).");
}
} catch (e) {
console.warn(`Invalid stainColor string: "${stainColor}". Using default "100,20,50". Error: ${e.message}`);
stainRGBArray = [100, 20, 50]; // Default wine color (dark red-purple)
}
const [targetR, targetG, targetB] = stainRGBArray;
// Clamp strength to [0, 1]
const s = Math.max(0, Math.min(1, Number(strength)));
// Clamp darkenFactor to [0, 1]
const d = Math.max(0, Math.min(1, Number(darkenFactor)));
if (W === 0 || H === 0) { // Handle 0-dimension images
return canvas; // Return empty canvas
}
const imageData = ctx.getImageData(0, 0, W, H);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const r_orig = data[i];
const g_orig = data[i + 1];
const b_orig = data[i + 2];
// Apply tint: linear interpolation from original color towards target stain color
// strength (s) controls the mix: 0 = original, 1 = full targetColor
let r_tinted = r_orig * (1 - s) + targetR * s;
let g_tinted = g_orig * (1 - s) + targetG * s;
let b_tinted = b_orig * (1 - s) + targetB * s;
// Apply darkening: darkenFactor (d) reduces brightness
// d = 0 means no darkening, d = 1 means fully black
const darkeningMultiplier = (1 - d);
r_tinted *= darkeningMultiplier;
g_tinted *= darkeningMultiplier;
b_tinted *= darkeningMultiplier;
// Clamp values to the 0-255 range and round them
data[i] = Math.max(0, Math.min(255, Math.round(r_tinted)));
data[i + 1] = Math.max(0, Math.min(255, Math.round(g_tinted)));
data[i + 2] = Math.max(0, Math.min(255, Math.round(b_tinted)));
// Alpha channel (data[i+3]) remains unchanged
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes