Please bookmark this page to avoid losing your image tool!

Image Old Movie 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.
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;
}

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 Old Movie Filter Effect Tool allows users to transform their images to resemble the appearance of vintage films. This tool applies several effects including sepia tones for a classic look, grain to simulate the texture of aged film, random scratches to mimic wear and tear, and a vignette effect to draw focus to the center of the image. This can be particularly useful for creative projects, nostalgia-driven media, and enhancing photography with an old-fashioned aesthetic.

Leave a Reply

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