You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
grainIntensity = 0.1, // 0 to 1: Strength of film grain.
desaturationAmount = 0.3, // 0 to 1: Amount of desaturation.
sepiaAmount = 0.5, // 0 to 1: Mix original with sepia.
vignetteStrength = 0.4, // 0 to 1: How dark and large the vignette is.
scratchesCount = 5, // Integer: Number of vertical scratches.
dustParticleCount = 50, // Integer: Number of dust particles.
lightLeakStrength = 0.15 // 0 to 1: Overall opacity strength of light leaks.
) {
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;
// 1. Draw the original image
ctx.drawImage(originalImg, 0, 0, width, height);
// 2. Get image data for pixel manipulation if any pixel-level effects are active
if (desaturationAmount > 0 || sepiaAmount > 0 || grainIntensity > 0) {
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
// 3. Apply desaturation, sepia, and grain pixel by pixel
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// Desaturation
if (desaturationAmount > 0) {
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
r = r * (1 - desaturationAmount) + gray * desaturationAmount;
g = g * (1 - desaturationAmount) + gray * desaturationAmount;
b = b * (1 - desaturationAmount) + gray * desaturationAmount;
}
// Sepia
if (sepiaAmount > 0) {
const sr = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189);
const sg = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168);
const sb = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131);
r = r * (1 - sepiaAmount) + sr * sepiaAmount;
g = g * (1 - sepiaAmount) + sg * sepiaAmount;
b = b * (1 - sepiaAmount) + sb * sepiaAmount;
}
// Film Grain
if (grainIntensity > 0) {
const noiseMagnitude = 30; // Max deviation (e.g., +/- 30 units) at full intensity
// Add random noise independently to R, G, B for colored grain
const rNoise = (Math.random() * 2 - 1) * grainIntensity * noiseMagnitude;
const gNoise = (Math.random() * 2 - 1) * grainIntensity * noiseMagnitude;
const bNoise = (Math.random() * 2 - 1) * grainIntensity * noiseMagnitude;
r = Math.max(0, Math.min(255, r + rNoise));
g = Math.max(0, Math.min(255, g + gNoise));
b = Math.max(0, Math.min(255, b + bNoise));
}
data[i] = Math.round(r);
data[i + 1] = Math.round(g);
data[i + 2] = Math.round(b);
}
ctx.putImageData(imageData, 0, 0);
}
// 4. Vignette
if (vignetteStrength > 0) {
ctx.save();
// Calculate radius to ensure vignette covers corners even for wide/tall images
const cornerDist = Math.sqrt(Math.pow(width / 2, 2) + Math.pow(height / 2, 2));
const outerRadius = cornerDist * 1.5; // Make it larger to ensure smooth falloff beyond corners
// Inner radius: smaller for stronger vignette, larger for weaker
const innerRadiusRatio = 0.35 + (1 - vignetteStrength) * 0.5; // Ranges from 0.35 (strong) to 0.85 (weak)
const innerRadius = Math.min(width, height) * innerRadiusRatio;
const gradV = ctx.createRadialGradient(
width / 2, height / 2, innerRadius,
width / 2, height / 2, outerRadius
);
gradV.addColorStop(0, 'rgba(0,0,0,0)');
// Vignette alpha: stronger effect means more opaque black
gradV.addColorStop(1, `rgba(0,0,0, ${Math.min(1, vignetteStrength * 1.2)})`);
ctx.fillStyle = gradV;
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
// 5. Scratches
if (scratchesCount > 0) {
for (let i = 0; i < scratchesCount; i++) {
ctx.beginPath();
const x = Math.random() * width;
// Scratches are mostly vertical, varying start/end position and length
const y1 = Math.random() * height * 0.3; // Start near top third
const y2 = height - (Math.random() * height * 0.3); // End near bottom third
ctx.moveTo(x, y1);
// Slight horizontal deviation for a more natural scratch
ctx.lineTo(x + (Math.random() * 8 - 4), y2);
ctx.strokeStyle = `rgba(230, 230, 230, ${Math.random() * 0.12 + 0.03})`; // Faint white/gray
ctx.lineWidth = Math.random() * 1.2 + 0.4; // Thin lines
ctx.stroke();
}
}
// 6. Dust Particles
if (dustParticleCount > 0) {
for (let i = 0; i < dustParticleCount; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
const radius = (Math.random() * 1.0 + 0.3) / 2; // Diameter 0.3px to 1.3px
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(200, 200, 200, ${Math.random() * 0.3 + 0.1})`; // Faint gray
ctx.fill();
}
}
// 7. Light Leaks
if (lightLeakStrength > 0) {
ctx.save();
ctx.globalCompositeOperation = 'lighter'; // Additive blending for light effects
const numLeaks = Math.floor(Math.random() * 2) + 1; // 1 to 2 leaks
const leakColors = [ // Typical warm light leak colors
{ r: 255, g: 100, b: 50 }, { r: 255, g: 180, b: 30 }, { r: 230, g: 80, b: 40 }
];
for (let i = 0; i < numLeaks; i++) {
const colorIndex = Math.floor(Math.random() * leakColors.length);
const baseColor = leakColors[colorIndex];
const side = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left
let leakX, leakY;
const maxExtent = Math.min(width, height) * (Math.random() * 0.7 + 0.5); // How far leak spreads (50-120% of min_dim)
const edgeOffset = (Math.random() * 0.5 - 0.25) * maxExtent; // Start near edge (-25% to +25% of extent)
switch (side) {
case 0: leakX = Math.random() * width; leakY = -edgeOffset; break;
case 1: leakX = width + edgeOffset; leakY = Math.random() * height; break;
case 2: leakX = Math.random() * width; leakY = height + edgeOffset; break;
case 3: default: leakX = -edgeOffset; leakY = Math.random() * height; break;
}
const leakRadiusStart = Math.random() * maxExtent * 0.2; // Inner core of the leak
const leakRadiusEnd = maxExtent;
const leakGrad = ctx.createRadialGradient(
leakX, leakY, leakRadiusStart,
leakX, leakY, leakRadiusEnd
);
// Center of leak is brighter, fades out
const leakCenterOpacity = lightLeakStrength * (Math.random() * 0.4 + 0.6); // 60-100% of main strength
leakGrad.addColorStop(0, `rgba(${baseColor.r}, ${baseColor.g}, ${baseColor.b}, ${leakCenterOpacity})`);
leakGrad.addColorStop(0.7, `rgba(${baseColor.r}, ${baseColor.g}, ${baseColor.b}, ${leakCenterOpacity * 0.25})`); // Softer falloff
leakGrad.addColorStop(1, `rgba(${baseColor.r}, ${baseColor.g}, ${baseColor.b}, 0)`);
ctx.fillStyle = leakGrad;
ctx.fillRect(0, 0, width, height); // Apply gradient over the whole canvas
}
ctx.restore();
}
return canvas;
}
Apply Changes