You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
tintColorStr = "255,230,200", // Default: Warm yellow-ish tint (R,G,B string)
tintOpacity = 0.15, // Default: 0.15 (0-1 range, how much tint to apply)
saturation = 0.85, // Default: 0.85 (0=grayscale, 1=original, >1 more saturated)
contrast = 1.1, // Default: 1.1 (1=original, >1 more contrast, <1 less contrast)
brightness = 1.05, // Default: 1.05 (1=original, >1 brighter, <1 darker)
vignetteStrength = 0.5, // Default: 0.5 (0-1 range, intensity of vignette)
vignetteStart = 0.3, // Default: 0.3 (0=center, 1=image edge, where vignette begins)
vignetteEnd = 0.85, // Default: 0.85 (0=center, 1=image edge, where vignette is full strength, must be > vignetteStart)
grainAmount = 12 // Default: 12 (0 for no grain, typical range 0-50 for subtle to noticeable grain)
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true }); // Opt-in for performance if available
// Ensure image dimensions are from natural size for accuracy
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
// Draw the original image onto the canvas
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// Get image data to manipulate pixels
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const width = canvas.width;
const height = canvas.height;
// Parse tint color string into RGB components
const [tintR, tintG, tintB] = tintColorStr.split(',').map(Number);
// Pre-calculate values for vignette
const centerX = width / 2;
const centerY = height / 2;
// Maximum distance from center to a corner, used for normalizing distances
const maxRadialDist = Math.sqrt(centerX * centerX + centerY * centerY);
// Helper function to clamp color values between 0 and 255
const clamp = (value) => Math.max(0, Math.min(255, Math.round(value)));
// Iterate over each pixel (RGBA components)
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 Brightness
if (brightness !== 1.0) {
r *= brightness;
g *= brightness;
b *= brightness;
}
// 2. Apply Tint
if (tintOpacity > 0 && !isNaN(tintR) && !isNaN(tintG) && !isNaN(tintB)) {
r = r * (1 - tintOpacity) + tintR * tintOpacity;
g = g * (1 - tintOpacity) + tintG * tintOpacity;
b = b * (1 - tintOpacity) + tintB * tintOpacity;
}
// 3. Apply Saturation
if (saturation !== 1.0) {
// Calculate luminance (standard weights for perceived brightness)
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
r = lum + saturation * (r - lum);
g = lum + saturation * (g - lum);
b = lum + saturation * (b - lum);
}
// 4. Apply Contrast
if (contrast !== 1.0) {
// Formula: NewValue = (((OldValue/255 - 0.5) * ContrastFactor) + 0.5) * 255
r = (r / 255 - 0.5) * contrast + 0.5;
g = (g / 255 - 0.5) * contrast + 0.5;
b = (b / 255 - 0.5) * contrast + 0.5;
r *= 255;
g *= 255;
b *= 255;
}
// Clamp intermediate color values to prevent overflow/underflow before spatial effects
r = clamp(r);
g = clamp(g);
b = clamp(b);
// 5. Apply Vignette
// Ensure vignetteStrength is positive and vignetteEnd is meaningfully greater than vignetteStart
if (vignetteStrength > 0 && vignetteEnd > vignetteStart && maxRadialDist > 0) {
const pixelX = (i / 4) % width;
const pixelY = Math.floor((i / 4) / width);
const deltaX = pixelX - centerX;
const deltaY = pixelY - centerY;
const distFromCenter = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// Normalize distance: 0 at center, 1 at maxRadialDist (corners)
const normalizedDist = distFromCenter / maxRadialDist;
if (normalizedDist >= vignetteStart) {
// Calculate how far into the vignette effect region (from vignetteStart to vignetteEnd) this pixel is
// progress will be 0 at vignetteStart, and 1 at or beyond vignetteEnd
const progress = Math.min(1, Math.max(0, (normalizedDist - vignetteStart) / (vignetteEnd - vignetteStart)));
// Apply a power to the progress for a smoother falloff (exponent > 1 means faster falloff near vignetteEnd)
const reduction = vignetteStrength * Math.pow(progress, 1.5);
const vignetteFactor = 1.0 - reduction;
r *= vignetteFactor;
g *= vignetteFactor;
b *= vignetteFactor;
}
}
// 6. Apply Grain
if (grainAmount > 0) {
// Add a random value (monochrome noise) to each channel
const grain = (Math.random() - 0.5) * grainAmount;
r += grain;
g += grain;
b += grain;
}
// Final Clamp for all color values
data[i] = clamp(r);
data[i + 1] = clamp(g);
data[i + 2] = clamp(b);
// Alpha channel (data[i + 3]) remains unchanged
}
// Put the modified pixel data back onto the canvas
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes