You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, sepiaIntensity = 0.7, noiseAmount = 25, vignetteStrength = 0.7, vignetteFalloff = 0.5, brightnessShift = -10, contrastFactor = 0.9, scratchDensity = 0.00005, scratchOpacity = 0.2, dustDensity = 0.0002, dustOpacity = 0.3) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true }); // Optimization hint for repeated getImageData/putImageData
const w = originalImg.naturalWidth || originalImg.width;
const h = originalImg.naturalHeight || originalImg.height;
canvas.width = w;
canvas.height = h;
// If image dimensions are zero, return an empty canvas
if (w === 0 || h === 0) {
return canvas;
}
ctx.drawImage(originalImg, 0, 0, w, h);
const imageData = ctx.getImageData(0, 0, w, h);
const data = imageData.data;
const centerX = w / 2;
const centerY = h / 2;
// Max distance from center to a corner, for vignette normalization
const maxDist = Math.sqrt(centerX * centerX + centerY * centerY);
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// 1. Brightness & Contrast
// Apply brightness shift
r += brightnessShift;
g += brightnessShift;
b += brightnessShift;
// Apply contrast (pivot around 127.5)
r = ((r / 255 - 0.5) * contrastFactor + 0.5) * 255;
g = ((g / 255 - 0.5) * contrastFactor + 0.5) * 255;
b = ((b / 255 - 0.5) * contrastFactor + 0.5) * 255;
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. Sepia
if (sepiaIntensity > 0) {
const origR = r, origG = g, origB = b;
const sr = (origR * 0.393) + (origG * 0.769) + (origB * 0.189);
const sg = (origR * 0.349) + (origG * 0.686) + (origB * 0.168);
const sb = (origR * 0.272) + (origG * 0.534) + (origB * 0.131);
r = origR * (1 - sepiaIntensity) + sr * sepiaIntensity;
g = origG * (1 - sepiaIntensity) + sg * sepiaIntensity;
b = origB * (1 - sepiaIntensity) + sb * sepiaIntensity;
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
}
// 3. Noise
if (noiseAmount > 0) {
const noise = (Math.random() - 0.5) * noiseAmount; // Noise range: -noiseAmount/2 to +noiseAmount/2
r += noise;
g += noise;
b += noise;
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
}
// 4. Vignette
if (vignetteStrength > 0 && maxDist > 0) {
const x = (i / 4) % w;
const y = Math.floor((i / 4) / w);
const dx = x - centerX;
const dy = y - centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
const normalizedDist = dist / maxDist;
// vignetteFalloff: 0.1 (wide bright area) to 2 (small bright area)
// Power curve for falloff: lower falloff value means slower darkening from center.
const vgnFactor = Math.pow(normalizedDist, vignetteFalloff);
const reduction = vignetteStrength * vgnFactor;
r *= (1 - reduction);
g *= (1 - reduction);
b *= (1 - reduction);
r = Math.max(0, Math.min(255, r)); // Clamp again after vignette
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
}
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
}
ctx.putImageData(imageData, 0, 0);
// 5. Scratches (Drawn on top of pixel manipulations)
if (scratchDensity > 0 && scratchOpacity > 0) {
const numScratches = Math.floor(w * h * scratchDensity);
// Lighter scratches
ctx.strokeStyle = `rgba(220, 220, 220, ${scratchOpacity * 0.7})`;
for (let k = 0; k < numScratches / 2; k++) {
ctx.lineWidth = Math.random() * 1.0 + 0.5; // Width: 0.5px to 1.5px
const x1 = Math.random() * w;
const y1 = Math.random() * h;
const len = (0.05 + Math.random() * 0.15) * Math.min(w, h); // Length: 5% to 20% of min image dimension
const angle = Math.random() * Math.PI * 2;
const x2 = x1 + Math.cos(angle) * len;
const y2 = y1 + Math.sin(angle) * len;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
// Darker scratches
ctx.strokeStyle = `rgba(30, 30, 30, ${scratchOpacity})`;
for (let k = 0; k < numScratches / 2; k++) {
ctx.lineWidth = Math.random() * 0.6 + 0.2; // Width: 0.2px to 0.8px
const x1 = Math.random() * w;
const y1 = Math.random() * h;
const len = (0.03 + Math.random() * 0.12) * Math.min(w, h); // Length: 3% to 15% of min image dimension
const angle = Math.random() * Math.PI * 2;
const x2 = x1 + Math.cos(angle) * len;
const y2 = y1 + Math.sin(angle) * len;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
}
// 6. Dust (Drawn on top)
if (dustDensity > 0 && dustOpacity > 0) {
const numDustParticles = Math.floor(w * h * dustDensity);
for (let k = 0; k < numDustParticles; k++) {
const x = Math.random() * w;
const y = Math.random() * h;
const size = Math.random() * 1.5 + 0.5; // Dust particle size: 0.5px to 2px
// Dust color: 60% light specks, 40% dark specks
const colorVal = Math.random() > 0.4 ? Math.floor(Math.random() * 55) + 200 : Math.floor(Math.random() * 100);
// Vary opacity slightly per particle
const particleOpacity = dustOpacity * (Math.random() * 0.5 + 0.5); // 50% to 100% of base dustOpacity
ctx.fillStyle = `rgba(${colorVal}, ${colorVal}, ${colorVal}, ${particleOpacity})`;
ctx.fillRect(x - size / 2, y - size / 2, size, size); // Center the particle
}
}
return canvas;
}
Apply Changes