Please bookmark this page to avoid losing your image tool!

Image Damaged Film Filter Effect Tool

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
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;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Damaged Film Filter Effect Tool allows users to apply a vintage film effect to their images, simulating the appearance of aged and damaged film. This can include the application of a sepia tone, scratches, dust particles, film grain, and a vignette effect. This tool is ideal for photographers, graphic designers, or anyone looking to create nostalgic or artistic representations of their digital photos, making them appear as though they were taken with older film cameras.

Leave a Reply

Your email address will not be published. Required fields are marked *