You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, saturationAdjustment = 0.8, contrastAdjustment = 1.1, grainAmount = 0.05, vignetteStrength = 0.4, colorTint = "255,240,220", tintOpacity = 0.08) {
const canvas = document.createElement('canvas');
// Using { willReadFrequently: true } can be a performance hint for browsers when using getImageData/putImageData frequently.
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Ensure dimensions are from natural size of the image, not potentially styled size
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
// Handle cases of 0-dimension images to prevent errors
if (canvas.width === 0 || canvas.height === 0) {
// Return the 0-dimension canvas. The caller can decide how to handle this.
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;
// Parse colorTint string "R,G,B" into individual numeric components
const tintColorsStr = colorTint.split(',').map(s => s.trim());
const parsedTintR = parseInt(tintColorsStr[0], 10);
const parsedTintG = parseInt(tintColorsStr[1], 10);
const parsedTintB = parseInt(tintColorsStr[2], 10);
// Use default tint values if parsing fails (e.g., malformed string, missing components)
const finalTintR = isNaN(parsedTintR) ? 255 : parsedTintR;
const finalTintG = isNaN(parsedTintG) ? 240 : parsedTintG;
const finalTintB = isNaN(parsedTintB) ? 220 : parsedTintB;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// Max distance from center to a corner, used for vignette normalization.
// This ensures the vignette effect reaches the corners properly.
const maxDistVignette = 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. Saturation Adjustment
// Skip if saturationAdjustment is 1.0 (no change)
if (saturationAdjustment !== 1.0) {
const lum = 0.299 * r + 0.587 * g + 0.114 * b; // Calculate luminance
r = lum + saturationAdjustment * (r - lum);
g = lum + saturationAdjustment * (g - lum);
b = lum + saturationAdjustment * (b - lum);
}
// 2. Contrast Adjustment
// Skip if contrastAdjustment is 1.0 (no change)
if (contrastAdjustment !== 1.0) {
r = ((r / 255 - 0.5) * contrastAdjustment + 0.5) * 255;
g = ((g / 255 - 0.5) * contrastAdjustment + 0.5) * 255;
b = ((b / 255 - 0.5) * contrastAdjustment + 0.5) * 255;
}
// Clamp intermediate results to [0, 255] range before applying additive effects
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// 3. Color Tint
if (tintOpacity > 0) {
r = r * (1 - tintOpacity) + finalTintR * tintOpacity;
g = g * (1 - tintOpacity) + finalTintG * tintOpacity;
b = b * (1 - tintOpacity) + finalTintB * tintOpacity;
}
// 4. Grain
if (grainAmount > 0) {
// Generate monochrome noise for a classic film grain look
// Noise is centered around 0, scaled by grainAmount and color range.
const noise = (Math.random() - 0.5) * 255 * grainAmount;
r += noise;
g += noise;
b += noise;
}
// Clamp again after tint and grain which are additive
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. Vignette
// Apply vignette if strength is positive and image has dimensions
if (vignetteStrength > 0 && maxDistVignette > 0) {
// Calculate current pixel's coordinates from its index
const currentPixelX = (i / 4) % canvas.width;
const currentPixelY = Math.floor((i / 4) / canvas.width);
const dx = currentPixelX - centerX;
const dy = currentPixelY - centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
// Normalized distance from center (0 at center, 1 at corners)
const percentDist = dist / maxDistVignette;
// Vignette falloff power: 2.0 for quadratic (standard), higher for sharper falloff
const vignettePower = 2.0;
// vignetteStrength (0-1) determines darkness at edges.
// 0 = no vignette, 1 = black edges.
const vignetteMultiplier = 1.0 - (vignetteStrength * Math.pow(percentDist, vignettePower));
// Apply vignette by multiplying color values. Clamp multiplier to be >= 0.
r *= Math.max(0, vignetteMultiplier);
g *= Math.max(0, vignetteMultiplier);
b *= Math.max(0, vignetteMultiplier);
}
// Final assignment after all effects. Clamp values to ensure they are valid.
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