You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, desaturationLevelStr = "0.7", contrastLevelStr = "1.3", noiseIntensityStr = "25", vignetteStrengthStr = "0.7", sepiaToneStr = "0.25", grainIntensityStr = "0.08", dirtAndScratchesStr = "40") {
// Helper function to clamp values
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
// Parse parameters and provide robust defaults
const desaturationLevel = clamp(parseFloat(desaturationLevelStr) || 0.7, 0, 1);
const contrastLevel = Math.max(0, parseFloat(contrastLevelStr) || 1.3); // contrast can be > 1
const noiseIntensity = clamp(parseInt(noiseIntensityStr) || 25, 0, 255);
const vignetteStrength = clamp(parseFloat(vignetteStrengthStr) || 0.7, 0, 1);
const sepiaTone = clamp(parseFloat(sepiaToneStr) || 0.25, 0, 1);
const grainIntensity = clamp(parseFloat(grainIntensityStr) || 0.08, 0, 1);
const dirtAndScratchesAmount = Math.max(0, parseInt(dirtAndScratchesStr) || 40);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
canvas.width = width;
canvas.height = height;
if (width === 0 || height === 0) {
// Avoid errors with 0-dimension images
console.error("Image has zero width or height.");
return canvas; // Return empty canvas
}
ctx.drawImage(originalImg, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const centerX = width / 2;
const centerY = height / 2;
// Max distance from center to a corner, used for vignette
const maxDist = Math.sqrt(centerX * centerX + centerY * centerY);
// Process each pixel
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// 1. Desaturation
if (desaturationLevel > 0) {
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
r = r * (1 - desaturationLevel) + gray * desaturationLevel;
g = g * (1 - desaturationLevel) + gray * desaturationLevel;
b = b * (1 - desaturationLevel) + gray * desaturationLevel;
}
// 2. Sepia Tone
if (sepiaTone > 0) {
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 = r * (1 - sepiaTone) + sr * sepiaTone;
g = g * (1 - sepiaTone) + sg * sepiaTone;
b = b * (1 - sepiaTone) + sb * sepiaTone;
}
// 3. Contrast
if (contrastLevel !== 1.0) {
r = 128 + contrastLevel * (r - 128);
g = 128 + contrastLevel * (g - 128);
b = 128 + contrastLevel * (b - 128);
}
// 4. Noise
if (noiseIntensity > 0) {
const randomNoise = (Math.random() - 0.5) * noiseIntensity;
r += randomNoise;
g += randomNoise;
b += randomNoise;
}
// Clamp final RGB values
data[i] = clamp(r, 0, 255);
data[i + 1] = clamp(g, 0, 255);
data[i + 2] = clamp(b, 0, 255);
}
ctx.putImageData(imageData, 0, 0);
// 5. Vignette (as an overlay drawing)
if (vignetteStrength > 0 && maxDist > 0) {
ctx.globalCompositeOperation = 'source-over';
const outerRadius = maxDist;
// Inner radius calculation: smaller inner radius means vignette starts closer to center
// vignetteStrength = 1 means innerRadius is small. vignetteStrength = 0 means innerRadius = outerRadius
const innerRadiusPercent = 1 - Math.min(1, vignetteStrength * 1.1); // Adjust 1.1 for desired falloff start
const innerRadius = outerRadius * innerRadiusPercent;
const vignetteGradient = ctx.createRadialGradient(centerX, centerY, innerRadius, centerX, centerY, outerRadius);
vignetteGradient.addColorStop(0, 'rgba(0,0,0,0)'); // Transparent center
vignetteGradient.addColorStop(1, `rgba(0,0,0, ${clamp(vignetteStrength,0,1)})`); // Dark edges
ctx.fillStyle = vignetteGradient;
ctx.fillRect(0, 0, width, height);
}
ctx.globalCompositeOperation = 'source-over'; // Reset blend mode
// 6. Grain Overlay (simulated film grain)
if (grainIntensity > 0 && grainIntensity <= 1) {
const grainCanvas = document.createElement('canvas');
grainCanvas.width = width;
grainCanvas.height = height;
const grainCtx = grainCanvas.getContext('2d');
// Create an ImageData manually for grain for performance on large images
const grainImgData = grainCtx.createImageData(width, height);
const grainData = grainImgData.data;
for (let j = 0; j < grainData.length; j += 4) {
const grainValue = Math.random() * 255; // Grayscale noise
grainData[j] = grainValue;
grainData[j + 1] = grainValue;
grainData[j + 2] = grainValue;
grainData[j + 3] = 255; // Grain pixels are opaque; layer opacity controls overall visibility
}
grainCtx.putImageData(grainImgData, 0, 0);
ctx.globalAlpha = grainIntensity;
ctx.globalCompositeOperation = 'overlay'; // 'overlay' or 'soft-light' work well
ctx.drawImage(grainCanvas, 0, 0);
ctx.globalAlpha = 1.0; // Reset alpha
ctx.globalCompositeOperation = 'source-over'; // Reset blend mode
}
// 7. Dirt Spots & Scratches (simple procedural)
if (dirtAndScratchesAmount > 0) {
ctx.globalCompositeOperation = 'overlay'; // 'overlay' or 'multiply'
// Dirt Spots
const numSpots = dirtAndScratchesAmount;
for (let k = 0; k < numSpots; k++) {
const x = Math.random() * width;
const y = Math.random() * height;
// Spot size relative to image diagonal for somewhat consistent appearance
const spotSize = Math.random() * (maxDist * 0.015) + 1;
ctx.fillStyle = `rgba(0,0,0, ${Math.random() * 0.4 + 0.1})`; // Dark spots
ctx.beginPath();
ctx.arc(x, y, spotSize, 0, Math.PI * 2);
ctx.fill();
// Occasional lighter spots
if (k % 7 === 0) {
ctx.fillStyle = `rgba(255,255,255, ${Math.random() * 0.25 + 0.05})`;
ctx.beginPath();
ctx.arc(Math.random() * width, Math.random() * height, spotSize * 0.6, 0, Math.PI * 2);
ctx.fill();
}
}
// Scratches
const numScratches = Math.floor(dirtAndScratchesAmount / 4); // Fewer scratches
for (let k = 0; k < numScratches; k++) {
const sx1 = Math.random() * width;
const sy1 = Math.random() * height;
// Scratches are short, max 10% of width/height
const sx2 = sx1 + (Math.random() - 0.5) * (width * 0.1);
const sy2 = sy1 + (Math.random() - 0.5) * (height * 0.1);
ctx.strokeStyle = `rgba(0,0,0, ${Math.random() * 0.25 + 0.05})`;
ctx.lineWidth = Math.random() * 1.5 + 0.5; // Thin scratches
ctx.beginPath();
ctx.moveTo(sx1, sy1);
ctx.lineTo(sx2, sy2);
ctx.stroke();
// Occasional lighter scratches
if (k % 5 === 0) {
const lsx1 = Math.random() * width;
const lsy1 = Math.random() * height;
const lsx2 = lsx1 + (Math.random() - 0.5) * (width * 0.08);
const lsy2 = lsy1 + (Math.random() - 0.5) * (height * 0.08);
ctx.strokeStyle = `rgba(255,255,255, ${Math.random() * 0.15 + 0.03})`;
ctx.lineWidth = Math.random() * 1.0 + 0.3;
ctx.beginPath();
ctx.moveTo(lsx1, lsy1);
ctx.lineTo(lsx2, lsy2);
ctx.stroke();
}
}
ctx.globalCompositeOperation = 'source-over'; // Reset blend mode
}
return canvas;
}
Apply Changes