You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg,
blur = 0.3, // number: blur radius in pixels, 0 for no blur
sepia = 0.8, // number: 0-1, intensity of sepia effect. 0 for original colors (still subject to grain etc.)
grain = 20, // number: 0-50 (approx), intensity of film grain. 0 for no grain.
scratches = 0.5, // number: 0-1, intensity/density of scratches. 0 for no scratches.
dust = 0.5, // number: 0-1, intensity/density of dust particles. 0 for no dust.
vignette = 0.6 // number: 0-1, intensity of vignette darkening. 0 for no vignette.
) {
// Ensure parameters are numbers
const blurAmount = Number(blur);
const sepiaIntensity = Math.max(0, Math.min(1, Number(sepia)));
const grainAmount = Math.max(0, Number(grain));
const scratchesIntensity = Math.max(0, Math.min(1, Number(scratches)));
const dustIntensity = Math.max(0, Math.min(1, Number(dust)));
const vignetteIntensity = Math.max(0, Math.min(1, Number(vignette)));
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const width = originalImg.naturalWidth || originalImg.width || 0;
const height = originalImg.naturalHeight || originalImg.height || 0;
canvas.width = width;
canvas.height = height;
if (width === 0 || height === 0) {
// Return an empty canvas if image dimensions are zero
return canvas;
}
// --- Step 1: Initial slight blur (if any) ---
if (blurAmount > 0) {
ctx.filter = `blur(${blurAmount}px)`;
}
ctx.drawImage(originalImg, 0, 0, width, height);
ctx.filter = 'none'; // Reset filter for subsequent drawing operations
// --- Step 2: Pixel Manipulation for Sepia and Grain ---
if (sepiaIntensity > 0 || grainAmount > 0) {
let imageData = ctx.getImageData(0, 0, width, height);
let data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
let finalR = r, finalG = g, finalB = b;
// Apply Sepia
if (sepiaIntensity > 0) {
const sr = 0.393 * r + 0.769 * g + 0.189 * b;
const sg = 0.349 * r + 0.686 * g + 0.168 * b;
const sb = 0.272 * r + 0.534 * g + 0.131 * b;
finalR = (1 - sepiaIntensity) * finalR + sepiaIntensity * sr;
finalG = (1 - sepiaIntensity) * finalG + sepiaIntensity * sg;
finalB = (1 - sepiaIntensity) * finalB + sepiaIntensity * sb;
}
// Apply Grain
if (grainAmount > 0) {
const noise = (Math.random() - 0.5) * grainAmount;
finalR += noise;
finalG += noise;
finalB += noise;
}
data[i] = Math.max(0, Math.min(255, finalR));
data[i + 1] = Math.max(0, Math.min(255, finalG));
data[i + 2] = Math.max(0, Math.min(255, finalB));
}
ctx.putImageData(imageData, 0, 0);
}
// --- Step 3: Adding Scratches ---
if (scratchesIntensity > 0) {
// Adjust scratch count based on image size and intensity
const numScratches = Math.floor((width / 80 + Math.random() * 2 + 2) * scratchesIntensity * 2);
for (let i = 0; i < numScratches; i++) {
const x = Math.random() * width;
const y1 = Math.random() * height * 0.2; // Start near top
const y2 = height - (Math.random() * height * 0.2); // End near bottom
const midY = (y1 + y2) / 2 + (Math.random() - 0.5) * height * 0.1; // Midpoint y-wobble
const midX = x + (Math.random() - 0.5) * 10; // Midpoint x-wobble
const xEnd = x + (Math.random() - 0.5) * 8; // End x-wobble
ctx.beginPath();
ctx.moveTo(x, y1 + (Math.random() - 0.5) * 10); // Start y jitter
ctx.quadraticCurveTo(midX, midY, xEnd, y2 + (Math.random() - 0.5) * 10); // End y jitter
const scratchOpacity = Math.random() * 0.15 * scratchesIntensity + 0.03; // Min opacity 0.03
// Scratches are typically lighter
ctx.strokeStyle = `rgba(230, 230, 230, ${Math.min(1, scratchOpacity)})`;
// Line width slightly influenced by intensity
ctx.lineWidth = (Math.random() * 1.0 + 0.25) * (0.75 + scratchesIntensity * 0.5);
if (ctx.lineWidth > 0.01 && scratchOpacity > 0.001) {
ctx.stroke();
}
}
}
// --- Step 4: Adding Dust Particles ---
if (dustIntensity > 0) {
const baseDustDensityFactor = 0.00012; // Slightly increased density
const secondaryDustFactor = 0.025; // Slightly increased for smaller images
const numDustSpecks = Math.floor(width * height * baseDustDensityFactor * dustIntensity) +
Math.floor(Math.sqrt(width * height) * secondaryDustFactor * dustIntensity);
for (let i = 0; i < numDustSpecks; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
// Particle size slightly influenced by intensity
const particleSize = (Math.random() * 1.2 + 0.5) * (0.75 + dustIntensity * 0.5);
const particleOpacity = (Math.random() * 0.35 * dustIntensity + 0.05); // Min opacity 0.05
if (particleSize > 0.1 && particleOpacity > 0.01) {
const colorVal = Math.random() > 0.4 ? 245 : 35; // Slightly more light dust
ctx.fillStyle = `rgba(${colorVal}, ${colorVal}, ${colorVal}, ${Math.min(1, particleOpacity)})`;
ctx.beginPath();
ctx.arc(x, y, particleSize / 2, 0, Math.PI * 2); // Draw as small circles
ctx.fill();
}
}
}
// --- Step 5: Adding Vignette ---
if (vignetteIntensity > 0) {
const centerX = width / 2;
const centerY = height / 2;
const cornerDist = Math.sqrt(centerX * centerX + centerY * centerY);
// Vignette falloff starts gently from about 30% of center-to-corner distance
const gradientInnerRadius = cornerDist * 0.3;
const gradientOuterRadius = cornerDist;
const gradient = ctx.createRadialGradient(centerX, centerY, gradientInnerRadius, centerX, centerY, gradientOuterRadius);
// Start of darkening effect (at innerRadius)
gradient.addColorStop(0, `rgba(0, 0, 0, ${vignetteIntensity * 0.1})`);
// Full darkening effect (at outerRadius/corners)
gradient.addColorStop(1, `rgba(0, 0, 0, ${Math.min(0.9, vignetteIntensity * 0.85)})`); // Max vignette alpha capped
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
}
return canvas;
}
Apply Changes