You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
sepiaAmount = 1.0,
noiseIntensity = 20,
vignetteOpacity = 0.6,
vignetteColorStr = "0,0,0", // Comma-separated R,G,B string e.g., "0,0,0" for black
vignetteExtent = 0.3 // Defines the proportion of the image's diagonal radius for the clear central area (0-1).
// 0 means vignette starts darkening from the very center.
// 1 means the entire image is clear (no dark vignette from this gradient setting).
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Determine image dimensions. naturalWidth/Height are for <img> elements after they've loaded.
// Fallback to width/height for other canvas elements or if naturalWidth/Height aren't available.
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
// Handle cases where the image might not be fully loaded or is invalid.
if (width === 0 || height === 0) {
// console.error("Image not loaded or dimensions are zero.");
// Return a small canvas with an error message.
canvas.width = 200; // Increased size for better message visibility
canvas.height = 50;
ctx.fillStyle = "rgb(200, 200, 200)"; // Light gray background
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "red";
ctx.font = "16px Arial";
ctx.textAlign = "center";
ctx.fillText("Error: Invalid Image", canvas.width / 2, canvas.height / 2 + 6); // Centered text
return canvas;
}
canvas.width = width;
canvas.height = height;
// Draw the original image onto the canvas
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// Get image pixel data
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// Sanitize and clamp input parameters to their expected ranges and convert to numbers
const effectiveSepiaAmount = Math.max(0, Math.min(1, Number(sepiaAmount)));
const effectiveNoiseIntensity = Math.max(0, Number(noiseIntensity)); // Can be > 255, but effect saturates
const effectiveVignetteOpacity = Math.max(0, Math.min(1, Number(vignetteOpacity)));
const effectiveVignetteExtent = Math.max(0, Math.min(1, Number(vignetteExtent)));
// Parse the vignetteColorStr (e.g., "R,G,B") into usable RGB values
const colorParts = vignetteColorStr.split(',').map(s => parseInt(s.trim(), 10));
const V_R = (colorParts.length === 3 && !isNaN(colorParts[0])) ? Math.max(0, Math.min(255, colorParts[0])) : 0;
const V_G = (colorParts.length === 3 && !isNaN(colorParts[1])) ? Math.max(0, Math.min(255, colorParts[1])) : 0;
const V_B = (colorParts.length === 3 && !isNaN(colorParts[2])) ? Math.max(0, Math.min(255, colorParts[2])) : 0;
const vignetteColorRgbaBase = `rgba(${V_R},${V_G},${V_B},`; // Base for "rgba(R,G,B," string
// Iterate over each pixel (RGBA)
for (let i = 0; i < data.length; i += 4) {
let r_orig = data[i];
let g_orig = data[i + 1];
let b_orig = data[i + 2];
let r = r_orig;
let g = g_orig;
let b = b_orig;
// 1. Apply Sepia effect
// Mixes original color with sepia-toned color based on effectiveSepiaAmount
if (effectiveSepiaAmount > 0) {
// Standard sepia transformation coefficients
const tr = 0.393 * r_orig + 0.769 * g_orig + 0.189 * b_orig;
const tg = 0.349 * r_orig + 0.686 * g_orig + 0.168 * b_orig;
const tb = 0.272 * r_orig + 0.534 * g_orig + 0.131 * b_orig;
// Blend original with sepia version
r = (tr * effectiveSepiaAmount) + (r_orig * (1 - effectiveSepiaAmount));
g = (tg * effectiveSepiaAmount) + (g_orig * (1 - effectiveSepiaAmount));
b = (tb * effectiveSepiaAmount) + (b_orig * (1 - effectiveSepiaAmount));
}
// Clamp R, G, B values to [0, 255] after sepia and blending
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// 2. Add Noise
// Adds random variation to R,G,B channels to simulate film grain
if (effectiveNoiseIntensity > 0) {
const noiseR_val = (Math.random() - 0.5) * effectiveNoiseIntensity;
const noiseG_val = (Math.random() - 0.5) * effectiveNoiseIntensity;
const noiseB_val = (Math.random() - 0.5) * effectiveNoiseIntensity;
r = Math.max(0, Math.min(255, r + noiseR_val));
g = Math.max(0, Math.min(255, g + noiseG_val));
b = Math.max(0, Math.min(255, b + noiseB_val));
}
// Update pixel data
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
// Alpha channel (data[i+3]) is preserved from the original image
}
// Write the modified pixel data back to the canvas
ctx.putImageData(imageData, 0, 0);
// 3. Apply Vignette effect
// Overlays a radial gradient to darken the edges/corners of the image
if (effectiveVignetteOpacity > 0) {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// The outer radius of the gradient extends to the furthest corner of the canvas
const outerRadius = Math.sqrt(Math.pow(centerX, 2) + Math.pow(centerY, 2));
// The inner radius defines the start of the vignette fade; effectively, the "clear" central area
const innerRadius = outerRadius * effectiveVignetteExtent;
const gradient = ctx.createRadialGradient(centerX, centerY, innerRadius, centerX, centerY, outerRadius);
// The gradient starts transparent at innerRadius (or from center if innerRadius is 0)
gradient.addColorStop(0, 'rgba(0,0,0,0)');
// It fades to the specified vignette color and opacity at outerRadius
gradient.addColorStop(1, vignetteColorRgbaBase + effectiveVignetteOpacity + ')');
ctx.fillStyle = gradient;
// Ensure vignette is drawn using default composition mode (source-over)
ctx.globalCompositeOperation = 'source-over';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
return canvas;
}
Apply Changes