You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, sepiaLevelParam = 0.7, grainLevelParam = 0.2, scratchesCountParam = 10, vignetteStrengthParam = 0.6) {
// Ensure parameters are numbers and clamped/validated
const sepiaLevel = Math.max(0, Math.min(1, Number(sepiaLevelParam)));
const grainLevel = Math.max(0, Math.min(1, Number(grainLevelParam)));
const scratchesCount = Math.max(0, Math.floor(Number(scratchesCountParam)));
const vignetteStrength = Math.max(0, Math.min(1, Number(vignetteStrengthParam)));
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const w = originalImg.naturalWidth || originalImg.width;
const h = originalImg.naturalHeight || originalImg.height;
canvas.width = w;
canvas.height = h;
if (!ctx || w === 0 || h === 0) {
console.error("Canvas setup failed or image dimensions are zero.");
// Return an empty (but valid) canvas in case of issues with image/context
if (canvas.width === 0) canvas.width = 1; // Ensure minimum dimensions
if (canvas.height === 0) canvas.height = 1;
// Attempt to get context again if it failed initially and dimensions are now set
const fallbackCtx = canvas.getContext('2d');
if (fallbackCtx) {
fallbackCtx.fillStyle = '#808080'; // Draw gray so it's not transparent
fallbackCtx.fillRect(0, 0, canvas.width, canvas.height);
}
return canvas;
}
// 1. Draw the original image
ctx.drawImage(originalImg, 0, 0, w, h);
// 2. Apply Sepia and Grain (Pixel manipulation)
if (sepiaLevel > 0 || grainLevel > 0) {
const imageData = ctx.getImageData(0, 0, w, h);
const data = imageData.data;
// grainLevel 0 to 1 maps to noise amplitude. E.g. 0.2 * 25 -> noise up to +/- 5 RGB units.
const grainAmount = grainLevel * 25;
for (let i = 0; i < data.length; i += 4) {
let r_orig = data[i];
let g_orig = data[i + 1];
let b_orig = data[i + 2];
let r_eff = r_orig;
let g_eff = g_orig;
let b_eff = b_orig;
// Apply Sepia
if (sepiaLevel > 0) {
// Standard sepia matrix coefficients for full sepia
const sr = 0.393 * r_orig + 0.769 * g_orig + 0.189 * b_orig;
const sg = 0.349 * r_orig + 0.686 * g_orig + 0.168 * b_orig;
const sb = 0.272 * r_orig + 0.534 * g_orig + 0.131 * b_orig;
// Blend original with sepia based on sepiaLevel
r_eff = (1 - sepiaLevel) * r_eff + sepiaLevel * sr;
g_eff = (1 - sepiaLevel) * g_eff + sepiaLevel * sg;
b_eff = (1 - sepiaLevel) * b_eff + sepiaLevel * sb;
}
// Apply Grain
if (grainLevel > 0) {
// noise is in [-grainAmount, +grainAmount]
const noise = (Math.random() - 0.5) * 2 * grainAmount;
r_eff += noise;
g_eff += noise;
b_eff += noise;
}
data[i] = Math.max(0, Math.min(255, r_eff));
data[i + 1] = Math.max(0, Math.min(255, g_eff));
data[i + 2] = Math.max(0, Math.min(255, b_eff));
}
ctx.putImageData(imageData, 0, 0);
}
// 3. Apply Scratches
if (scratchesCount > 0) {
for (let i = 0; i < scratchesCount; i++) {
ctx.beginPath();
// Main light scratches
const x = Math.random() * w;
const y1 = Math.random() * h * 0.2; // Start near top third
const y2 = h - (Math.random() * h * 0.2); // End near bottom third
const alphaLight = 0.05 + Math.random() * 0.25; // Opacity: 0.05 to 0.3
const lineWidthLight = Math.random() * 1.0 + 0.5; // Width: 0.5px to 1.5px
ctx.strokeStyle = `rgba(230, 230, 230, ${alphaLight})`; // Lighter gray for more subtle scratches
ctx.lineWidth = lineWidthLight;
ctx.moveTo(x, y1);
const midX = x + (Math.random() - 0.5) * 10; // Wobble control point X
const midY = (y1 + y2) / 2 + (Math.random() - 0.5) * 40; // Wobble control point Y
const endX = x + (Math.random() - 0.5) * 5; // End point X deviation
ctx.quadraticCurveTo(midX, midY, endX, y2);
ctx.stroke();
// Occasional darker/thicker scratch (less frequent)
if (Math.random() < 0.2) { // 20% chance for a darker scratch
ctx.beginPath();
const sx = Math.random() * w;
const sy1 = Math.random() * h * 0.5; // Can start more towards middle
const sy2 = sy1 + Math.random() * (h * 0.3) + h*0.05; // Shorter, variable length
const alphaDark = 0.03 + Math.random() * 0.07; // Opacity: 0.03 to 0.1 (very subtle)
const lineWidthDark = Math.random() * 1.5 + 0.6; // Width: 0.6px to 2.1px
ctx.strokeStyle = `rgba(20, 20, 20, ${alphaDark})`; // Dark gray, almost black
ctx.lineWidth = lineWidthDark;
ctx.moveTo(sx, sy1);
ctx.lineTo(sx + (Math.random() - 0.5) * 20, sy2); // More horizontal deviation & potential slant
ctx.stroke();
}
}
}
// 4. Apply Vignette
if (vignetteStrength > 0) {
ctx.save(); // Save context state (like globalCompositeOperation)
const centerX = w / 2;
const centerY = h / 2;
// featherStartRatio: how far from center the vignette effect starts to become visible.
// Range approx: 0.1 (vignette starts very close to center for high strength)
// to 0.7 (vignette starts further out for low strength).
const featherStartRatio = 0.1 + (1 - vignetteStrength) * 0.6;
const innerR = Math.min(w, h) / 2 * featherStartRatio;
// Outer radius should always cover the canvas corners
const outerR = Math.sqrt(centerX*centerX + centerY*centerY);
const vigGradient = ctx.createRadialGradient(centerX, centerY, innerR, centerX, centerY, outerR);
// Vignette darkens by multiplying with a gray color.
// The edgeRGB determines how dark the multiplier color is at the edges.
// vignetteStrength 1.0 -> edgeDarknessMultiplier 0.2 (color is 255*0.2 = 51, very dark)
// vignetteStrength 0.5 -> edgeDarknessMultiplier 0.6 (color is 255*0.6 = 153, medium dark)
// vignetteStrength 0.0 -> edgeDarknessMultiplier 1.0 (color is 255, effectively no change)
const edgeDarknessMultiplier = 1.0 - (vignetteStrength * 0.8);
const edgeRGB = Math.max(0, Math.floor(255 * edgeDarknessMultiplier));
vigGradient.addColorStop(0, 'rgba(255,255,255,1)'); // Center is white (no effect with multiply)
vigGradient.addColorStop(1, `rgba(${edgeRGB},${edgeRGB},${edgeRGB},1)`); // Edges are darker gray
ctx.globalCompositeOperation = 'multiply';
ctx.fillStyle = vigGradient;
ctx.fillRect(0, 0, w, h);
ctx.restore(); // Restore context state
}
return canvas;
}
Apply Changes