You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
sepiaAmount = 1.0, // Number: 0 (original color) to 1.0 (full sepia).
scratchCount = 25, // Number: Number of major scratches.
scratchColor = 'rgba(230, 230, 230, 0.15)', // String: Color and alpha of scratches.
maxScratchWidth = 2, // Number: Max width of a scratch (pixels).
minScratchLengthFactor = 0.2, // Number: Min length of scratch (factor of image height, 0-1).
maxScratchLengthFactor = 0.9, // Number: Max length of scratch (factor of image height, 0-1).
dustCount = 1000, // Number: Number of dust particles.
dustColor = 'rgba(30, 30, 30, 0.25)', // String: Color and alpha of dust. (Alpha slightly reduced from 0.3).
maxDustSize = 2, // Number: Maximum size of a dust particle (pixels).
grainAmount = 0.08, // Number: Intensity of grain (0 to 1).
grainMonochrome = 'true', // String: 'true' for monochrome grain, 'false' for colored.
vignetteStrength = 0.6, // Number: Strength of vignette effect (0 to 1 opacity for black).
vignetteStartFactor = 0.3 // Number: How far from center vignette starts (0 center, 1 edge).
) {
// Parameter parsing and validation
const numSepiaAmount = Math.max(0, Math.min(1, parseFloat(sepiaAmount) || 0));
const numScratchCount = Math.max(0, parseInt(scratchCount) || 0);
const strScratchColor = String(scratchColor);
const numMaxScratchWidth = Math.max(0.5, parseFloat(maxScratchWidth) || 0.5);
const numMinScratchLengthFactor = Math.max(0, Math.min(1, parseFloat(minScratchLengthFactor) || 0));
const numMaxScratchLengthFactor = Math.max(numMinScratchLengthFactor, Math.min(1, parseFloat(maxScratchLengthFactor) || 0));
const numDustCount = Math.max(0, parseInt(dustCount) || 0);
const strDustColor = String(dustColor);
const numMaxDustSize = Math.max(0.5, parseFloat(maxDustSize) || 0.5);
const numGrainAmount = Math.max(0, Math.min(1, parseFloat(grainAmount) || 0));
const boolGrainMonochrome = String(grainMonochrome).toLowerCase() === 'true';
const numVignetteStrength = Math.max(0, Math.min(1, parseFloat(vignetteStrength) || 0));
const numVignetteStartFactor = Math.max(0, Math.min(1, parseFloat(vignetteStartFactor) || 0));
const ensureImageLoaded = (img) => {
return new Promise((resolve, reject) => {
if (img.complete && img.naturalWidth !== 0) {
resolve(); // Already loaded and valid
} else if (img.complete && img.naturalWidth === 0) {
// Loaded but failed (e.g. invalid src, broken image)
reject(new Error("Image loaded but has zero dimensions. Source: " + (img.src || img.currentSrc || "unknown")));
} else if (img.src || img.currentSrc) { // src is set, but maybe not yet loaded
const originalOnload = img.onload;
const originalOnerror = img.onerror;
img.onload = () => {
img.onload = originalOnload;
img.onerror = originalOnerror;
if (img.naturalWidth === 0) { // Check again on load
reject(new Error("Image loaded with zero dimensions. Source: " + (img.src || img.currentSrc)));
} else {
resolve();
}
};
img.onerror = () => {
img.onload = originalOnload;
img.onerror = originalOnerror;
reject(new Error("Image failed to load. Source: " + (img.src || img.currentSrc)));
};
} else {
reject(new Error("Image source not set."));
}
});
};
try {
await ensureImageLoaded(originalImg);
} catch (error) {
console.error("ImageDamagedFilmFilter Error:", error.message);
const errorCanvas = document.createElement('canvas');
const errWidth = typeof originalImg.width === 'number' && originalImg.width > 0 ? originalImg.width : 200;
const errHeight = typeof originalImg.height === 'number' && originalImg.height > 0 ? originalImg.height : 150;
errorCanvas.width = errWidth;
errorCanvas.height = errHeight;
const ctx = errorCanvas.getContext('2d');
ctx.fillStyle = 'rgba(200, 200, 200, 0.5)';
ctx.fillRect(0, 0, errorCanvas.width, errorCanvas.height);
ctx.fillStyle = 'red';
ctx.font = `bold ${Math.min(16, errWidth / 15)}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const lines = ("Error: " + error.message).match(new RegExp(`.{1,${Math.floor(errWidth/ (ctx.measureText('M').width * 0.8))}}`, 'g')) || ["Error processing image"];
lines.forEach((line, index) => {
ctx.fillText(line, errorCanvas.width / 2, errorCanvas.height / 2 + (index - (lines.length -1)/2) * (Math.min(18, errHeight / lines.length * 0.8)));
});
return errorCanvas;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = originalImg.naturalWidth;
canvas.height = originalImg.naturalHeight;
// 1. Draw original image
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// 2. Apply Sepia and Grain (pixel manipulation)
if (numSepiaAmount > 0 || numGrainAmount > 0) {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const grainStrengthMapping = 250; // Higher = stronger grain for same numGrainAmount
const sr = [0.393, 0.769, 0.189];
const sg = [0.349, 0.686, 0.168];
const sb = [0.272, 0.534, 0.131];
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
if (numSepiaAmount > 0) {
const rOrig = r, gOrig = g, bOrig = b;
const rSepia = Math.min(255, (rOrig * sr[0]) + (gOrig * sr[1]) + (bOrig * sr[2]));
const gSepia = Math.min(255, (rOrig * sg[0]) + (gOrig * sg[1]) + (bOrig * sg[2]));
const bSepia = Math.min(255, (rOrig * sb[0]) + (gOrig * sb[1]) + (bOrig * sb[2]));
r = rOrig * (1 - numSepiaAmount) + rSepia * numSepiaAmount;
g = gOrig * (1 - numSepiaAmount) + gSepia * numSepiaAmount;
b = bOrig * (1 - numSepiaAmount) + bSepia * numSepiaAmount;
}
if (numGrainAmount > 0) {
const noiseBase = (Math.random() - 0.5) * grainStrengthMapping * numGrainAmount;
if (boolGrainMonochrome) {
r = Math.max(0, Math.min(255, r + noiseBase));
g = Math.max(0, Math.min(255, g + noiseBase));
b = Math.max(0, Math.min(255, b + noiseBase));
} else {
r = Math.max(0, Math.min(255, r + (Math.random() - 0.5) * grainStrengthMapping * numGrainAmount));
g = Math.max(0, Math.min(255, g + (Math.random() - 0.5) * grainStrengthMapping * numGrainAmount));
b = Math.max(0, Math.min(255, b + (Math.random() - 0.5) * grainStrengthMapping * numGrainAmount));
}
}
data[i] = Math.round(r);
data[i+1] = Math.round(g);
data[i+2] = Math.round(b);
}
ctx.putImageData(imageData, 0, 0);
}
// 3. Draw Scratches
if (numScratchCount > 0) {
ctx.save();
for (let i = 0; i < numScratchCount; i++) {
const xStart = Math.random() * canvas.width;
const yStart = Math.random() * canvas.height * (1 - numMinScratchLengthFactor);
const scratchLength = (numMinScratchLengthFactor + Math.random() * (numMaxScratchLengthFactor - numMinScratchLengthFactor)) * canvas.height;
if (scratchLength < 1) continue; // Skip tiny scratches
ctx.beginPath();
ctx.moveTo(xStart, yStart);
const segments = Math.max(1, Math.floor(scratchLength / 20)); // More segments for longer scratches
let currentX = xStart;
const yEnd = Math.min(canvas.height, yStart + scratchLength);
for (let j = 1; j <= segments; j++) {
const segmentEndY = yStart + (j / segments) * (yEnd - yStart);
const deviation = (Math.random() * 2 - 1) * (numMaxScratchWidth + 1);
currentX = xStart + deviation;
ctx.lineTo(currentX, segmentEndY);
}
ctx.lineWidth = (Math.random() * (numMaxScratchWidth - 0.5) + 0.5);
ctx.strokeStyle = strScratchColor;
ctx.stroke();
}
ctx.restore();
}
// 4. Draw Dust & Speckles
if (numDustCount > 0) {
ctx.save();
ctx.fillStyle = strDustColor;
for (let i = 0; i < numDustCount; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const size = (Math.random() * (numMaxDustSize - 0.5) + 0.5);
ctx.globalAlpha = Math.random() * 0.7 + 0.1; // Vary speckle visibility (0.1 to 0.8)
ctx.fillRect(x - size / 2, y - size / 2, size, size);
}
ctx.restore();
}
// 5. Apply Vignette
if (numVignetteStrength > 0) {
ctx.save();
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const outerRadius = Math.sqrt(Math.pow(centerX, 2) + Math.pow(centerY, 2));
const innerRadius = outerRadius * numVignetteStartFactor;
const gradient = ctx.createRadialGradient(
centerX, centerY, innerRadius,
centerX, centerY, outerRadius
);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, `rgba(0,0,0,${numVignetteStrength})`);
ctx.fillStyle = gradient;
ctx.globalCompositeOperation = 'source-over';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
return canvas;
}
Apply Changes