You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, contrast = 1.3, desaturation = 0.2, vignetteStrength = 0.4, vignettePower = 2.0, tintColorStr = "255,230,200", tintAmount = 0.1) {
// Helper function to clamp values between min and max
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
// Parse tintColorStr and set up tint RGB values
let parsedTintR = 255, parsedTintG = 230, parsedTintB = 200; // Default tint color components
if (typeof tintColorStr === 'string') {
const parts = tintColorStr.split(',');
if (parts.length === 3) {
const r_p = parseInt(parts[0].trim(), 10);
const g_p = parseInt(parts[1].trim(), 10);
const b_p = parseInt(parts[2].trim(), 10);
if (!isNaN(r_p) && !isNaN(g_p) && !isNaN(b_p)) {
parsedTintR = clamp(r_p, 0, 255);
parsedTintG = clamp(g_p, 0, 255);
parsedTintB = clamp(b_p, 0, 255);
}
}
}
// Sanitize and prepare effective parameter values
const effectiveContrast = Math.max(0, Number(contrast)); // Contrast should not be negative
const effectiveDesaturation = clamp(Number(desaturation), 0, 1);
const effectiveVignetteStrength = clamp(Number(vignetteStrength), 0, 1);
// Vignette power must be positive; 0.1 is a small positive to avoid issues with Math.pow if 0 or negative.
const effectiveVignettePower = Math.max(0.1, Number(vignettePower));
const effectiveTintAmount = clamp(Number(tintAmount), 0, 1);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Ensure image is loaded before trying to use its dimensions or draw it
if (!originalImg.complete || originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
try {
await new Promise((resolve, reject) => {
// Store original handlers to restore them later if needed, though not strictly necessary here.
const oldOnload = originalImg.onload;
const oldOnerror = originalImg.onerror;
const oldOnabort = originalImg.onabort;
originalImg.onload = () => {
originalImg.onload = oldOnload; // Restore
originalImg.onerror = oldOnerror;
originalImg.onabort = oldOnabort;
resolve();
};
originalImg.onerror = (err) => {
originalImg.onload = oldOnload;
originalImg.onerror = oldOnerror;
originalImg.onabort = oldOnabort;
reject(new Error('Image failed to load.'));
};
originalImg.onabort = () => {
originalImg.onload = oldOnload;
originalImg.onerror = oldOnerror;
originalImg.onabort = oldOnabort;
reject(new Error('Image loading aborted.'));
};
// Check again in case the image loaded between the initial check and setting handlers.
if (originalImg.complete && originalImg.naturalWidth !== 0 && originalImg.naturalHeight !== 0) {
resolve();
return;
}
// If complete but no width/height, it's an error state.
if (originalImg.complete && (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0)) {
reject(new Error('Image is complete but has zero dimensions, indicating an error.'));
return;
}
// If src is not set, it will never load. This is an upstream issue.
// If using `new Image()` user must set `img.src`. If from DOM, src should be there.
});
} catch (error) {
console.error("Image loading error in processImage:", error.message);
throw error; // Re-throw the error for a higher-level handler
}
}
canvas.width = originalImg.naturalWidth;
canvas.height = originalImg.naturalHeight;
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const width = canvas.width;
const height = canvas.height;
const centerX = width / 2;
const centerY = height / 2;
const maxDistToCorner = Math.sqrt(centerX * centerX + centerY * centerY);
// Prevent division by zero for 0x0 or 1x1 images. Default to 1 if maxDistToCorner is 0.
const safeMaxDist = maxDistToCorner === 0 ? 1 : maxDistToCorner;
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i+1];
let b = data[i+2];
// 1. Apply Contrast
if (effectiveContrast !== 1.0) { // No change if contrast is 1.0
r = clamp(effectiveContrast * (r - 128) + 128, 0, 255);
g = clamp(effectiveContrast * (g - 128) + 128, 0, 255);
b = clamp(effectiveContrast * (b - 128) + 128, 0, 255);
}
// 2. Apply Desaturation
if (effectiveDesaturation > 0) {
const gray = 0.299 * r + 0.587 * g + 0.114 * b; // Standard luminosity calculation
r = clamp(r * (1 - effectiveDesaturation) + gray * effectiveDesaturation, 0, 255);
g = clamp(g * (1 - effectiveDesaturation) + gray * effectiveDesaturation, 0, 255);
b = clamp(b * (1 - effectiveDesaturation) + gray * effectiveDesaturation, 0, 255);
}
// 3. Apply Tint
if (effectiveTintAmount > 0) {
r = clamp(r * (1 - effectiveTintAmount) + parsedTintR * effectiveTintAmount, 0, 255);
g = clamp(g * (1 - effectiveTintAmount) + parsedTintG * effectiveTintAmount, 0, 255);
b = clamp(b * (1 - effectiveTintAmount) + parsedTintB * effectiveTintAmount, 0, 255);
}
// 4. Apply Vignette
if (effectiveVignetteStrength > 0) {
const pixelX = (i / 4) % width;
const pixelY = Math.floor((i / 4) / width);
const dx = pixelX - centerX;
const dy = pixelY - centerY;
const distCurrentPixel = Math.sqrt(dx * dx + dy * dy);
const normalizedDistance = distCurrentPixel / safeMaxDist; // 0 at center, 1 at corners
// vignetteMultiplier decreases from 1 (center) towards (1 - vignetteStrength) at edges.
const vignetteMultiplier = 1.0 - effectiveVignetteStrength * Math.pow(normalizedDistance, effectiveVignettePower);
r = clamp(r * vignetteMultiplier, 0, 255);
g = clamp(g * vignetteMultiplier, 0, 255);
b = clamp(b * vignetteMultiplier, 0, 255);
}
// Ensure pixel values are integers
data[i] = Math.round(r);
data[i+1] = Math.round(g);
data[i+2] = Math.round(b);
// Alpha channel (data[i+3]) remains unchanged
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes