You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
grayscale = 0.7, // 0.0 (original color) to 1.0 (fully grayscale)
contrast = 1.5, // e.g., 0.0 (gray), 1.0 (original), >1.0 (more contrast)
sepia = 0.3, // 0.0 (none) to 1.0 (full sepia)
noise = 25, // 0 (none) to e.g., 50 (heavy noise/grain)
vignette = 0.6, // 0.0 (none) to 1.0 (strongest vignette, black edges)
numScratches = 3, // Number of scratch lines to draw
numDirtSpots = 50 // Number of dirt spots to draw
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const w = originalImg.naturalWidth || originalImg.width;
const h = originalImg.naturalHeight || originalImg.height;
if (!w || !h) {
console.error("Image has zero dimensions or is not loaded properly.");
// Return a small canvas with an error message
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 200;
errorCanvas.height = 50;
const errorCtx = errorCanvas.getContext('2d');
if (errorCtx) {
errorCtx.fillStyle = "red";
errorCtx.font = "12px Arial";
errorCtx.fillText("Error: Invalid image source.", 10, 20);
errorCtx.fillText("Ensure image is loaded & has dimensions.", 10, 40);
}
return errorCanvas;
}
canvas.width = w;
canvas.height = h;
// 1. Apply core filters: grayscale, contrast, sepia using canvas context filter property
let filterString = "";
if (grayscale > 0) { // grayscale(0) is original colors
filterString += `grayscale(${grayscale}) `;
}
if (sepia > 0) { // sepia(0) is no effect
filterString += `sepia(${sepia}) `;
}
if (contrast !== 1.0) { // contrast(1) is original contrast
filterString += `contrast(${contrast}) `;
}
if (filterString) {
ctx.filter = filterString.trim();
}
ctx.drawImage(originalImg, 0, 0, w, h);
ctx.filter = 'none'; // Reset filter for subsequent manual operations
// 2. Apply noise and vignette through pixel manipulation
if (noise > 0 || vignette > 0) {
const imageData = ctx.getImageData(0, 0, w, h);
const data = imageData.data;
const centerX = w / 2;
const centerY = h / 2;
// Use diagonal distance to a corner as maxDist for vignette normalization
const maxDist = Math.sqrt(centerX * centerX + centerY * centerY);
const truncate = (value) => Math.max(0, Math.min(255, Math.round(value)));
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i+1];
let b = data[i+2];
// Apply noise
if (noise > 0) {
// Generate noise value between -noise and +noise
const noiseVal = (Math.random() - 0.5) * noise * 2;
r = truncate(r + noiseVal);
g = truncate(g + noiseVal);
b = truncate(b + noiseVal);
}
// Apply vignette
if (vignette > 0 && maxDist > 0) { // maxDist > 0 to avoid division by zero for 1x1px images etc.
const pixelY = Math.floor((i / 4) / w);
const pixelX = (i / 4) % w;
const dx = pixelX - centerX;
const dy = pixelY - centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
// Normalized distance from center (0 at center, 1 at maxDist or further)
const normDist = dist / maxDist;
// vignette parameter (0 to 1) controls the strength of darkening at edges.
// Using Math.pow(normDist, 2.0) for a quadratic falloff (common for vignettes)
const vignetteEffectAmount = vignette * Math.pow(normDist, 2.0);
const vignetteMultiplier = Math.max(0, 1.0 - vignetteEffectAmount);
r = truncate(r * vignetteMultiplier);
g = truncate(g * vignetteMultiplier);
b = truncate(b * vignetteMultiplier);
}
data[i] = r;
data[i+1] = g;
data[i+2] = b;
}
ctx.putImageData(imageData, 0, 0);
}
// 3. Draw scratches on top
if (numScratches > 0) {
for (let i = 0; i < numScratches; i++) {
const x1 = Math.random() * w;
const y1 = Math.random() * h;
const angle = Math.random() * Math.PI * 2;
// Scratch length relative to image size, e.g., 10% to 40% of the smaller dimension
const length = (Math.random() * 0.3 + 0.1) * Math.min(w, h);
const x2 = x1 + Math.cos(angle) * length;
const y2 = y1 + Math.sin(angle) * length;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
// Scratches can be dark or light with varying opacity for subtlety
if (Math.random() > 0.5) { // Light scratch
ctx.strokeStyle = `rgba(200, 200, 200, ${Math.random() * 0.15 + 0.05})`; // opacity 0.05 to 0.2
} else { // Dark scratch
ctx.strokeStyle = `rgba(50, 50, 50, ${Math.random() * 0.25 + 0.1})`; // opacity 0.1 to 0.35
}
ctx.lineWidth = Math.random() * 1.5 + 0.5; // Thickness from 0.5px to 2px
ctx.stroke();
}
}
// 4. Draw dirt spots on top
if (numDirtSpots > 0) {
for (let i = 0; i < numDirtSpots; i++) {
const x = Math.random() * w;
const y = Math.random() * h;
const radius = Math.random() * 2.0 + 1.0; // Radius from 1px to 3px
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
// Dirt spots are usually darkish and semi-transparent
ctx.fillStyle = `rgba(30, 30, 30, ${Math.random() * 0.2 + 0.05})`; // opacity 0.05 to 0.25
ctx.fill();
}
}
return canvas;
}
Apply Changes