You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, saturation = 1.1, contrast = 1.3, warmth = 0.25, vignetteDarkness = 0.7, vignetteSharpness = 2.0) {
// Helper: RGB to HSL. Input r,g,b in [0,255]. Output h,s,l in [0,1].
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
}
// Helper: HSL to RGB. Input h,s,l in [0,1]. Output r,g,b in [0,255].
function hslToRgb(h, s, l) {
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
// Helper to create an error canvas
const createErrorCanvas = (message1, message2 = "") => {
const errCanvas = document.createElement('canvas');
errCanvas.width = 350; errCanvas.height = 80;
const errCtx = errCanvas.getContext('2d');
errCtx.fillStyle = '#FADBD8'; // Light pink/red
errCtx.fillRect(0, 0, errCanvas.width, errCanvas.height);
errCtx.fillStyle = '#7B241C'; // Dark red
errCtx.font = '14px Arial';
errCtx.textAlign = 'center';
const T1Y = message2 ? 30 : 45;
errCtx.fillText(message1, errCanvas.width/2, T1Y);
if (message2) {
errCtx.font = '12px Arial';
errCtx.fillText(message2, errCanvas.width/2, 55);
}
return errCanvas;
};
if (originalImg instanceof HTMLImageElement) {
if (!originalImg.complete || originalImg.naturalWidth === 0) {
if (originalImg.src || originalImg.currentSrc) { // Image has a source to load
try {
if (typeof originalImg.decode === 'function') {
await originalImg.decode();
} else { // Fallback for older browsers
await new Promise((resolve, reject) => {
const oldOnload = originalImg.onload;
const oldOnerror = originalImg.onerror;
originalImg.onload = () => { originalImg.onload = oldOnload; originalImg.onerror = oldOnerror; resolve(); };
originalImg.onerror = (errEvent) => {
originalImg.onload = oldOnload; originalImg.onerror = oldOnerror;
reject(new Error('Image loading failed: ' + (typeof errEvent === 'string' ? errEvent : errEvent.type)));
};
// This condition handles cases where the image might already be 'complete' but broken
if (originalImg.complete && originalImg.naturalWidth === 0) {
reject(new Error('Image is in a broken state (complete but zero dimensions).'));
}
});
}
} catch (e) {
console.error("Image loading/decoding error:", e.message);
return createErrorCanvas("Image loading or decoding failed.", e.message);
}
} else {
console.error('Image element has no src.');
return createErrorCanvas('Image has no src property.');
}
}
}
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
if (!imgWidth || !imgHeight || imgWidth === 0 || imgHeight === 0) {
console.error('Image has invalid dimensions (0x0).');
return createErrorCanvas('Image has invalid dimensions (0x0).');
}
const canvas = document.createElement('canvas');
canvas.width = imgWidth;
canvas.height = imgHeight;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
try {
ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
} catch (e) {
console.error("Error drawing image to canvas:", e);
return createErrorCanvas("Error drawing image to canvas.", e.message);
}
let imageData;
try {
imageData = ctx.getImageData(0, 0, imgWidth, imgHeight);
} catch (e) {
console.error("Error getting ImageData (possibly CORS issue):", e);
return createErrorCanvas("Could not get image data.", "This may be due to a cross-origin image.");
}
const data = imageData.data;
const centerX = imgWidth / 2;
const centerY = imgHeight / 2;
const maxDist = Math.sqrt(centerX * centerX + centerY * centerY) || 1; // Ensure maxDist is not 0
// Target tint color for Venetian warmth (rich goldish-brown)
const R_TINT_TARGET = 200;
const G_TINT_TARGET = 150;
const B_TINT_TARGET = 50;
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i+1];
let b = data[i+2];
// 1. Saturation adjustment
if (saturation !== 1.0) {
const hsl = rgbToHsl(r, g, b);
hsl[1] *= saturation; // Adjust saturation
hsl[1] = Math.max(0, Math.min(1, hsl[1])); // Clamp saturation to [0, 1]
const [newR, newG, newB] = hslToRgb(hsl[0], hsl[1], hsl[2]);
r = newR; g = newG; b = newB;
}
// 2. Contrast adjustment
if (contrast !== 1.0) {
// Formula: f = (259 * (C + 255)) / (255 * (259 - C)) where C is -255 to 255
// Simpler formula for factor based contrast:
r = ((r / 255 - 0.5) * contrast + 0.5) * 255;
g = ((g / 255 - 0.5) * contrast + 0.5) * 255;
b = ((b / 255 - 0.5) * contrast + 0.5) * 255;
}
// 3. Warmth/Tint application (mix with Venetian target color)
if (warmth > 0 && warmth <= 1) { // warmth is the mixing factor [0, 1]
r = r * (1 - warmth) + R_TINT_TARGET * warmth;
g = g * (1 - warmth) + G_TINT_TARGET * warmth;
b = b * (1 - warmth) + B_TINT_TARGET * warmth;
}
// Clamp RGB values after saturation, contrast, and warmth adjustments
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// 4. Vignette effect
if (vignetteDarkness > 0 && vignetteDarkness <= 1) {
const pixelX = (i / 4) % imgWidth;
const pixelY = Math.floor((i / 4) / imgWidth);
const dx = centerX - pixelX;
const dy = centerY - pixelY;
const dist = Math.sqrt(dx * dx + dy * dy);
// Calculate vignette falloff
let falloff = (dist / maxDist); // Normalized distance [0, 1]
falloff = Math.pow(falloff, vignetteSharpness); // Apply sharpness
// Calculate vignette factor (1 = no change, <1 = darken)
const vignetteFactor = Math.max(0, 1.0 - falloff * vignetteDarkness);
r *= vignetteFactor;
g *= vignetteFactor;
b *= vignetteFactor;
// No need to clamp again here as vignetteFactor <= 1 and r,g,b >=0
}
// Assign final values. Uint8ClampedArray handles final clamping and rounding.
// Explicit Math.round for consistent rounding behavior across environments.
data[i] = Math.round(r);
data[i+1] = Math.round(g);
data[i+2] = Math.round(b);
// Alpha channel (data[i+3]) is preserved
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes