You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
desaturation = 0.4, // 0.0 (no change) to 1.0 (grayscale)
contrast = 1.3, // 1.0 is no change. <1 reduces, >1 increases.
brightnessOffset = -10, // Pixel value offset. Affects overall brightness. Range typically -255 to 255.
tint = "rgba(0, 40, 80, 0.2)", // CSS color string for tint layer (e.g., "rgba(0,50,100,0.1)" for a cool blueish tint)
tintBlendMode = "soft-light", // Blend mode for tint: 'soft-light', 'overlay', 'multiply', 'color', etc.
vignetteIntensity = 0.6, // 0.0 (none) to 1.0 (strong black vignette)
vignetteSmoothness = 0.5,// 0.01 (sharp edge) to 1.0 (very soft, fades from center)
grain = 0.05 // 0.0 (none) to 1.0 (very noisy)
) {
const canvas = document.createElement('canvas');
// Use naturalWidth/Height for intrinsic image size, fallback to width/height
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
if (width === 0 || height === 0) {
// Return a minimal (e.g., 1x1 transparent) canvas if image dimensions are invalid
canvas.width = 1;
canvas.height = 1;
// Optional: fill with transparent color
// const tempCtx = canvas.getContext('2d');
// if (tempCtx) { tempCtx.fillStyle = 'rgba(0,0,0,0)'; tempCtx.fillRect(0,0,1,1); }
return canvas;
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
// This should ideally not happen in modern browsers for 2D context
console.error("Failed to get 2D context from canvas.");
return canvas; // Return the canvas, which will be blank
}
// 1. Draw original image onto the canvas
ctx.drawImage(originalImg, 0, 0, width, height);
// 2. Attempt to get image data for pixel manipulation
// This can fail if the canvas is tainted (e.g., cross-origin image without CORS)
let imageData = null;
try {
imageData = ctx.getImageData(0, 0, width, height);
} catch (e) {
console.warn("Could not get image data for pixel manipulation. " +
"Effects like desaturation, contrast, and grain will be skipped. " +
"This is often due to cross-origin image restrictions.", e);
}
if (imageData) {
const data = imageData.data;
// Clamp and prepare parameter values for pixel operations
const actualDesaturation = Math.max(0, Math.min(1, desaturation));
const actualContrast = Math.max(0, contrast); // Contrast factor (0 = solid gray, 1 = no change)
const actualGrain = Math.max(0, Math.min(1, grain));
const grainAmount = actualGrain * 50; // Max noise amplitude (e.g., +/-50 for grain=1.0)
// 3. Process pixels: desaturation, contrast, brightness, grain
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i+1];
let b = data[i+2];
// Apply Desaturation
// L = 0.299R + 0.587G + 0.114B
// R' = R + (L - R) * desaturationAmount
if (actualDesaturation > 0) {
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
r += (luminance - r) * actualDesaturation;
g += (luminance - g) * actualDesaturation;
b += (luminance - b) * actualDesaturation;
}
// Apply Contrast and Brightness
// Contrast formula: NewValue = factor * (OldValue - 128) + 128
// Then add brightness offset
if (actualContrast !== 1 || brightnessOffset !== 0) {
r = actualContrast * (r - 128) + 128 + brightnessOffset;
g = actualContrast * (g - 128) + 128 + brightnessOffset;
b = actualContrast * (b - 128) + 128 + brightnessOffset;
}
// Apply Grain
if (grainAmount > 0) {
// Generate monochromatic noise
const noise = (Math.random() * 2 - 1) * grainAmount;
r += noise;
g += noise;
b += noise;
}
// Clamp values to [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));
}
// 4. Put modified imageData back onto the canvas
ctx.putImageData(imageData, 0, 0);
} // End of pixel manipulation block
// Store original context settings to restore them later
const originalGCO = ctx.globalCompositeOperation;
const originalFillStyle = ctx.fillStyle;
// 5. Apply Tint layer (drawing operation, works even if imageData failed)
// Ensure tint is a non-empty string. `ctx.fillStyle` handles "transparent" or rgba(...,0) correctly.
if (typeof tint === 'string' && tint.trim() !== "") {
try {
ctx.globalCompositeOperation = tintBlendMode;
ctx.fillStyle = tint;
ctx.fillRect(0, 0, width, height);
} catch (e) {
console.warn("Failed to apply tint. Invalid tint color or blend mode?", e);
} finally {
// Reset to avoid affecting subsequent drawing operations outside this function potentially
ctx.globalCompositeOperation = originalGCO;
ctx.fillStyle = originalFillStyle;
}
}
// 6. Apply Vignette (drawing operation, works even if imageData failed)
const actualVignetteIntensity = Math.max(0, Math.min(1, vignetteIntensity));
if (actualVignetteIntensity > 0) {
const centerX = width / 2;
const centerY = height / 2;
// Calculate outer radius to cover corners
const outerRadius = Math.hypot(centerX, centerY);
// vignetteSmoothness defines the size of the transparent center relative to outerRadius
// Clamp to [0.01, 1.0] for sensible results
const actualVignetteSmoothness = Math.max(0.01, Math.min(1, vignetteSmoothness));
const innerRadius = outerRadius * (1 - actualVignetteSmoothness);
try {
const gradient = ctx.createRadialGradient(centerX, centerY, innerRadius, centerX, centerY, outerRadius);
gradient.addColorStop(0, 'rgba(0,0,0,0)'); // Transparent center
gradient.addColorStop(1, `rgba(0,0,0,${actualVignetteIntensity})`); // Dark edges
ctx.fillStyle = gradient;
// Ensure vignette is drawn using source-over blending on top of existing content
ctx.globalCompositeOperation = 'source-over';
ctx.fillRect(0, 0, width, height);
} catch (e) {
console.warn("Failed to apply vignette.", e);
} finally {
// Reset properties
ctx.fillStyle = originalFillStyle;
ctx.globalCompositeOperation = originalGCO;
}
}
return canvas;
}
Apply Changes