You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, contrast = 50, brightness = -10, grainAmount = 25, vignetteStrength = 0.4) {
const canvas = document.createElement('canvas');
// Using { willReadFrequently: true } can hint the browser to optimize for frequent getImageData/putImageData calls.
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
canvas.width = imgWidth;
canvas.height = imgHeight;
// Handle cases where image dimensions are not available or are zero.
if (imgWidth === 0 || imgHeight === 0) {
console.error("Image dimensions are zero. Image might not be loaded or is invalid.");
// Ensure canvas has at least 1x1 dimension to be drawable.
canvas.width = Math.max(1, imgWidth);
canvas.height = Math.max(1, imgHeight);
ctx.fillStyle = "#7f7f7f"; // Gray background for the placeholder
ctx.fillRect(0, 0, canvas.width, canvas.height);
// If dimensions were indeed 0,0, add a small text.
if (imgWidth === 0 || imgHeight === 0) {
ctx.fillStyle = "white";
ctx.font = "bold 8px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("N/A", canvas.width / 2, canvas.height / 2);
}
return canvas;
}
ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
let imageData;
try {
imageData = ctx.getImageData(0, 0, imgWidth, imgHeight);
} catch (e) {
// This can happen due to a tainted canvas (e.g., cross-origin image loaded without CORS headers)
console.error("Could not get image data: ", e);
// Fallback: draw an error message on the canvas.
ctx.clearRect(0, 0, imgWidth, imgHeight);
ctx.fillStyle = "rgba(0,0,0,0.7)"; // Dark overlay
ctx.fillRect(0, 0, imgWidth, imgHeight);
ctx.fillStyle = "white";
const fontSize = Math.max(12, Math.min(24, imgWidth / 20, imgHeight / 10)); // Adaptive font size
ctx.font = `bold ${fontSize}px Arial`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Error: Could not process image.", imgWidth / 2, imgHeight / 2 - fontSize * 0.6);
ctx.font = `${fontSize * 0.8}px Arial`;
ctx.fillText("(May be a cross-origin image issue)", imgWidth / 2, imgHeight / 2 + fontSize * 0.6);
return canvas;
}
const data = imageData.data;
// Pre-calculate contrast factor.
// The typical contrast formula: Factor = (259 * (level + 255)) / (255 * (259 - level))
// 'contrast' parameter (e.g. -100 to 100) is used as 'level'.
// A value of 0 means factor=1 (no change). Positive values increase contrast.
const contrastFactor = (259 * (contrast + 255)) / (255 * (259 - contrast));
const centerX = imgWidth / 2;
const centerY = imgHeight / 2;
// Max distance from center to a corner, used for vignette normalization.
const maxDist = Math.sqrt(centerX * centerX + centerY * centerY);
function clamp(value, min = 0, max = 255) {
return Math.max(min, Math.min(max, value));
}
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Alpha (data[i+3]) is preserved.
// 1. Convert to Grayscale (Luminosity method for perceptual accuracy)
let gray = 0.299 * r + 0.587 * g + 0.114 * b;
// 2. Apply Brightness adjustment
gray += brightness;
gray = clamp(gray);
// 3. Apply Contrast adjustment
// Formula: NewColor = Factor * (OldColor - Midpoint) + Midpoint
// Midpoint for 0-255 range is 128.
gray = contrastFactor * (gray - 128) + 128;
gray = clamp(gray);
// 4. Add Film Grain / Noise
if (grainAmount > 0) {
// Generate noise: a random value between -grainAmount and +grainAmount.
const noise = (Math.random() * 2 - 1) * grainAmount;
gray += noise;
gray = clamp(gray);
}
// 5. Apply Vignette effect
// vignetteStrength: 0 means no vignette, 1.0 means edges are black.
if (vignetteStrength > 0 && maxDist > 0) { // maxDist > 0 avoids division by zero for 1x1px or smaller.
// Current pixel's coordinates
const x = (i / 4) % imgWidth;
const y = Math.floor((i / 4) / imgWidth);
// Distance of current pixel from the center
const dx = x - centerX;
const dy = y - centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
// Normalized distance (0 at center, typically 1 at corners)
const normalizedDist = dist / maxDist;
// Vignette effect factor (0.0 to 1.0). Multiplied with pixel brightness.
// Factor of 1.0 means no change; 0.0 means black.
// (1.0 - normalizedDist * vignetteStrength):
// - At center (normalizedDist=0), factor is 1.0.
// - At maxDist (normalizedDist=1), factor is (1.0 - vignetteStrength).
// e.g., if vignetteStrength = 0.4, factor at edge is 0.6.
const vignetteMultiplier = clamp(1.0 - (normalizedDist * vignetteStrength), 0.0, 1.0);
gray *= vignetteMultiplier;
gray = clamp(gray); // Clamp again after vignette multiplication
}
// Update pixel data with the new grayscale value
data[i] = gray; // Red channel
data[i + 1] = gray; // Green channel
data[i + 2] = gray; // Blue channel
// data[i+3] (alpha) remains unchanged.
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes