You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg,
desaturation = 0.6,
contrast = 1.4,
grain = 25,
sepia = 0.3,
brightness = -15,
tintOverlayColor = "rgba(40, 25, 10, 0.1)", // Subtle dark brownish overlay
vignette = 0.6
) {
// Helper function for clamping values
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
// Ensure parameters are of correct type and apply defaults if necessary.
// Store processed parameters in new variables to avoid modifying originals if they are used later in a wider scope.
let pDesaturation = Number(desaturation);
let pContrast = Number(contrast);
let pGrain = Number(grain);
let pSepia = Number(sepia);
let pBrightness = Number(brightness);
let pVignette = Number(vignette);
let pTintOverlayColor = (typeof tintOverlayColor === 'string') ? tintOverlayColor : "rgba(40, 25, 10, 0.1)";
// Use default values if conversion to Number resulted in NaN
pDesaturation = isNaN(pDesaturation) ? 0.6 : pDesaturation;
pContrast = isNaN(pContrast) ? 1.4 : pContrast;
pGrain = isNaN(pGrain) ? 25 : pGrain;
pSepia = isNaN(pSepia) ? 0.3 : pSepia;
pBrightness = isNaN(pBrightness) ? -15 : pBrightness;
pVignette = isNaN(pVignette) ? 0.6 : pVignette;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
if (imgWidth === 0 || imgHeight === 0) {
// Handle case where image might not be loaded or has no dimensions
// Return a 1x1 transparent canvas to avoid errors downsteam.
canvas.width = 1;
canvas.height = 1;
return canvas;
}
canvas.width = imgWidth;
canvas.height = imgHeight;
// Initial draw of the image
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const len = data.length;
// Clamp numeric parameters to their operational ranges
pDesaturation = clamp(pDesaturation, 0, 1);
pContrast = clamp(pContrast, 0, 5); // 0-1 reduces contrast, 1 no change, >1 increases
pGrain = clamp(pGrain, 0, 100);
pSepia = clamp(pSepia, 0, 1);
pBrightness = clamp(pBrightness, -255, 255);
pVignette = clamp(pVignette, 0, 1);
for (let i = 0; i < len; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// 1. Brightness adjustment
r += pBrightness;
g += pBrightness;
b += pBrightness;
// 2. Desaturation
// Mix current color with its grayscale equivalent
if (pDesaturation > 0) {
const gray = 0.299 * r + 0.587 * g + 0.114 * b; // Luminosity method
r = gray * pDesaturation + r * (1 - pDesaturation);
g = gray * pDesaturation + g * (1 - pDesaturation);
b = gray * pDesaturation + b * (1 - pDesaturation);
}
// 3. Sepia
// Mix current color with its sepia equivalent
if (pSepia > 0) {
const R_ = r; // Use current r,g,b (possibly desaturated and brightened)
const G_ = g;
const B_ = b;
// Standard sepia weights
const sr = (R_ * 0.393) + (G_ * 0.769) + (B_ * 0.189);
const sg = (R_ * 0.349) + (G_ * 0.686) + (B_ * 0.168);
const sb = (R_ * 0.272) + (G_ * 0.534) + (B_ * 0.131);
r = (1 - pSepia) * R_ + pSepia * sr;
g = (1 - pSepia) * G_ + pSepia * sg;
b = (1 - pSepia) * B_ + pSepia * sb;
}
// 4. Contrast
// Adjust contrast by stretching/compressing values relative to midpoint (128)
if (pContrast !== 1.0) {
r = (((r / 255.0 - 0.5) * pContrast) + 0.5) * 255.0;
g = (((g / 255.0 - 0.5) * pContrast) + 0.5) * 255.0;
b = (((b / 255.0 - 0.5) * pContrast) + 0.5) * 255.0;
}
// 5. Grain
// Add random noise
if (pGrain > 0) {
const noise = (Math.random() - 0.5) * pGrain;
r += noise;
g += noise;
b += noise;
}
// Clamp final RGB values to [0, 255]
data[i] = clamp(r, 0, 255);
data[i + 1] = clamp(g, 0, 255);
data[i + 2] = clamp(b, 0, 255);
// Alpha (data[i+3]) remains unchanged
}
ctx.putImageData(imageData, 0, 0); // Apply pixel manipulations
// 6. Tint Overlay (applied on top of pixel-processed image)
if (pTintOverlayColor && pTintOverlayColor.trim() !== "") {
try {
ctx.fillStyle = pTintOverlayColor; // Browser parses color string
ctx.fillRect(0, 0, canvas.width, canvas.height);
} catch (e) {
// Silently ignore if tint color string is invalid to prevent breaking
}
}
// 7. Vignette (applied on top of everything else)
if (pVignette > 0) {
const w = canvas.width;
const h = canvas.height;
const gradCenterX = w / 2;
const gradCenterY = h / 2;
// Outer radius should reach the furthest corner from the center to cover image
const gradOuterRadius = Math.sqrt(Math.pow(w / 2, 2) + Math.pow(h / 2, 2));
// Inner radius becomes smaller as vignette strength increases, creating a larger dark area
// The 0.95 factor makes the vignette effect start a bit more sharply/closer to the center.
const gradInnerRadius = gradOuterRadius * (1.0 - pVignette * 0.95);
const gradient = ctx.createRadialGradient(
gradCenterX, gradCenterY, gradInnerRadius,
gradCenterX, gradCenterY, gradOuterRadius
);
gradient.addColorStop(0, 'rgba(0,0,0,0)'); // Center is transparent
gradient.addColorStop(1, `rgba(0,0,0,${pVignette})`); // Edges are dark with opacity pVignette
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
}
return canvas;
}
Apply Changes