You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, contrast = 1.5, saturation = 0.2, warmth = 0.15, vignette = 0.4) {
// Helper function: RGB to HSL
// r, g, b are in [0, 255]
// Returns 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 function: HSL to RGB
// h, s, l are in [0, 1]
// Returns 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)];
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Use naturalWidth/Height to get intrinsic dimensions of the image
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
if (canvas.width === 0 || canvas.height === 0) {
console.error("Image has zero width or height. Ensure the image is loaded and valid.");
// Return an empty canvas or a canvas with a small size indicating an error
canvas.width = canvas.width || 1; // Avoid 0x0 canvas if original dimensions were 0
canvas.height = canvas.height || 1;
return canvas;
}
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 maxDist = Math.sqrt(centerX * centerX + centerY * centerY); // Dist from center to a corner
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// 1. Contrast adjustment
// contrast = 1.0 means no change. > 1 increases contrast, < 1 decreases.
if (contrast !== 1.0) {
r = (r - 128) * contrast + 128;
g = (g - 128) * contrast + 128;
b = (b - 128) * contrast + 128;
}
// Clamp values after contrast
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// 2. Saturation adjustment
// saturation = 0 means no change. Positive values increase saturation, negative values decrease.
// e.g., saturation = 0.5 means 50% boost, saturation = -0.5 means 50% desaturation.
if (saturation !== 0) {
let [h_hsl, s_hsl, l_hsl] = rgbToHsl(r, g, b);
s_hsl = s_hsl * (1 + saturation);
s_hsl = Math.max(0, Math.min(1, s_hsl)); // Clamp saturation to [0, 1]
[r, g, b] = hslToRgb(h_hsl, s_hsl, l_hsl);
}
// hslToRgb result is already [0, 255] due to Math.round(X*255)
// 3. Warmth (Sepia-like tint)
// warmth = 0 means no change. warmth = 1 means full sepia.
if (warmth > 0) {
const originalR = r, originalG = g, originalB = b;
const sepiaR = 0.393 * originalR + 0.769 * originalG + 0.189 * originalB;
const sepiaG = 0.349 * originalR + 0.686 * originalG + 0.168 * originalB;
const sepiaB = 0.272 * originalR + 0.534 * originalG + 0.131 * originalB;
r = (1 - warmth) * originalR + warmth * sepiaR;
g = (1 - warmth) * originalG + warmth * sepiaG;
b = (1 - warmth) * originalB + warmth * sepiaB;
}
// Clamp values after warmth
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
// vignette = 0 means no vignette. vignette = 1 means edges can be fully black.
if (vignette > 0 && maxDist > 0) { // maxDist check for 1x1 or invalid tiny images
const x = (i / 4) % width; // Current pixel's x coordinate
const y = Math.floor((i / 4) / width); // Current pixel's y coordinate
const dx = x - centerX;
const dy = y - centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
// normDist is 0 at center, 1 at the furthest corner
const normDist = dist / maxDist;
// Vignette intensity has a quadratic falloff (power=2)
// `vignette` parameter controls the maximum darkening effect at the edges
const vignettePower = 2;
const vignetteAmount = Math.pow(normDist, vignettePower) * vignette;
const vignetteMultiplier = Math.max(0, 1.0 - vignetteAmount); // Ensure multiplier is not negative
r *= vignetteMultiplier;
g *= vignetteMultiplier;
b *= vignetteMultiplier;
}
// Final clamp and assignment
data[i] = Math.round(Math.max(0, Math.min(255, r)));
data[i + 1] = Math.round(Math.max(0, Math.min(255, g)));
data[i + 2] = Math.round(Math.max(0, Math.min(255, b)));
// Alpha channel (data[i+3]) is preserved
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes