You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg,
desaturation = 1.0, // 0 (original color) to 1 (grayscale). Default: full B&W.
sepiaAmount = 0.20, // 0 (none) to 1 (full sepia tint). Default: light sepia.
vignetteStrength = 0.6, // 0 (none) to 1 (strong vignette). Default: medium-strong.
grainAmount = 0.08, // 0 (none) to 1 (max intensity for noise). Default: subtle grain.
contrast = 1.4, // Multiplier, 1.0 is no change. E.g., 0.5 to 2.0. Default: good punch.
brightness = 5 // Additive, e.g., -100 to 100. Default: slight lift.
) {
// Default values for internal use if parsed params are NaN or out of typical range
const defaults = {
desaturation: 1.0, sepiaAmount: 0.20, vignetteStrength: 0.6,
grainAmount: 0.08, contrast: 1.4, brightness: 5
};
// Ensure parameters are numbers and handle potential NaN or out-of-range values
desaturation = Number(desaturation);
if (isNaN(desaturation) || desaturation < 0 || desaturation > 1) desaturation = defaults.desaturation;
sepiaAmount = Number(sepiaAmount);
if (isNaN(sepiaAmount) || sepiaAmount < 0 || sepiaAmount > 1) sepiaAmount = defaults.sepiaAmount;
vignetteStrength = Number(vignetteStrength);
if (isNaN(vignetteStrength) || vignetteStrength < 0 || vignetteStrength > 1) {
vignetteStrength = defaults.vignetteStrength;
}
grainAmount = Number(grainAmount);
if (isNaN(grainAmount) || grainAmount < 0 || grainAmount > 1) {
grainAmount = defaults.grainAmount;
}
contrast = Number(contrast);
if (isNaN(contrast)) contrast = defaults.contrast;
brightness = Number(brightness);
if (isNaN(brightness)) brightness = defaults.brightness;
const canvas = document.createElement('canvas');
// Optimization hint for frequent getImageData/putImageData calls
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
// Handle cases where image dimensions are not available (e.g. image not loaded fully)
if (canvas.width === 0 || canvas.height === 0) {
// Return a minimal canvas to avoid errors downstream if image is invalid
canvas.width = 1;
canvas.height = 1;
ctx.fillRect(0,0,1,1); // make it black or some color
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;
// Max distance from center to a corner, used for vignette calculation
const maxDistCanvas = Math.sqrt(centerX * centerX + centerY * centerY);
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// 1. Desaturation
if (desaturation > 0) {
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
r = r * (1 - desaturation) + gray * desaturation;
g = g * (1 - desaturation) + gray * desaturation;
b = b * (1 - desaturation) + gray * desaturation;
}
// 2. Brightness
if (brightness !== 0) {
r += brightness;
g += brightness;
b += brightness;
}
// 3. Contrast
if (contrast !== 1.0) {
// Formula: NewVal = (OldVal - 128) * ContrastFactor + 128
r = (r - 128) * contrast + 128;
g = (g - 128) * contrast + 128;
b = (b - 128) * contrast + 128;
}
// 4. Sepia Tint
if (sepiaAmount > 0) {
// Store current r,g,b (after desat/bright/contrast) for sepia calculation
const currentR = r;
const currentG = g;
const currentB = b;
// Standard sepia conversion coefficients
const sr = (currentR * 0.393) + (currentG * 0.769) + (currentB * 0.189);
const sg = (currentR * 0.349) + (currentG * 0.686) + (currentB * 0.168);
const sb = (currentR * 0.272) + (currentG * 0.534) + (currentB * 0.131);
// Blend current color with its sepia version based on sepiaAmount
r = currentR * (1 - sepiaAmount) + sr * sepiaAmount;
g = currentG * (1 - sepiaAmount) + sg * sepiaAmount;
b = currentB * (1 - sepiaAmount) + sb * sepiaAmount;
}
// Clamp intermediate values (before grain and vignette which can be additive/multiplicative)
// This prevents out-of-bounds values from causing strange artifacts with grain/vignette
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// 5. Grain
if (grainAmount > 0) {
// grainAmount (0-1) scales the intensity. Max intensity of +/-15 for RGB channels.
const grainIntensityValue = grainAmount * 30;
const noise = (Math.random() - 0.5) * grainIntensityValue; // Symmetrical noise around 0
r += noise;
g += noise;
b += noise;
}
// 6. Vignette
if (vignetteStrength > 0) {
const currentPixelX = (i / 4) % width;
const currentPixelY = Math.floor((i / 4) / width);
const deltaX = currentPixelX - centerX;
const deltaY = currentPixelY - centerY;
const distFromCenter = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// Normalized distance: 0 at center, 1 at corners. Add epsilon for 1x1px image.
const normalizedDist = distFromCenter / (maxDistCanvas + 0.00001);
// Vignette power: higher strength leads to a more focused vignette (sharper falloff)
// Adjusts the steepness of the vignette curve.
const vignettePower = 1.5 + vignetteStrength * 2.5; // Ranges from ~1.5 to 4.0
// Reduction factor: how much to darken the pixel. Max reduction is vignetteStrength.
const reduction = Math.pow(normalizedDist, vignettePower) * vignetteStrength;
const vFactor = Math.max(0, 1.0 - reduction); // Ensure factor is not negative
r *= vFactor;
g *= vFactor;
b *= vFactor;
}
// Final Clamping for RGB values to be within [0, 255]
data[i] = Math.max(0, Math.min(255, r));
data[i + 1] = Math.max(0, Math.min(255, g));
data[i + 2] = Math.max(0, Math.min(255, b));
// Alpha channel (data[i+3]) remains unchanged
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes