Please bookmark this page to avoid losing your image tool!

Photo 16mm 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,
    grainIntensity = 0.1,      // 0 to 1: Strength of film grain.
    desaturationAmount = 0.3,   // 0 to 1: Amount of desaturation.
    sepiaAmount = 0.5,          // 0 to 1: Mix original with sepia.
    vignetteStrength = 0.4,   // 0 to 1: How dark and large the vignette is.
    scratchesCount = 5,         // Integer: Number of vertical scratches.
    dustParticleCount = 50,     // Integer: Number of dust particles.
    lightLeakStrength = 0.15    // 0 to 1: Overall opacity strength of light leaks.
) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    const width = originalImg.naturalWidth || originalImg.width;
    const height = originalImg.naturalHeight || originalImg.height;

    canvas.width = width;
    canvas.height = height;

    // 1. Draw the original image
    ctx.drawImage(originalImg, 0, 0, width, height);

    // 2. Get image data for pixel manipulation if any pixel-level effects are active
    if (desaturationAmount > 0 || sepiaAmount > 0 || grainIntensity > 0) {
        const imageData = ctx.getImageData(0, 0, width, height);
        const data = imageData.data;

        // 3. Apply desaturation, sepia, and grain pixel by pixel
        for (let i = 0; i < data.length; i += 4) {
            let r = data[i];
            let g = data[i + 1];
            let b = data[i + 2];

            // Desaturation
            if (desaturationAmount > 0) {
                const gray = 0.299 * r + 0.587 * g + 0.114 * b;
                r = r * (1 - desaturationAmount) + gray * desaturationAmount;
                g = g * (1 - desaturationAmount) + gray * desaturationAmount;
                b = b * (1 - desaturationAmount) + gray * desaturationAmount;
            }
            
            // Sepia
            if (sepiaAmount > 0) {
                const sr = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189);
                const sg = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168);
                const sb = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131);
                r = r * (1 - sepiaAmount) + sr * sepiaAmount;
                g = g * (1 - sepiaAmount) + sg * sepiaAmount;
                b = b * (1 - sepiaAmount) + sb * sepiaAmount;
            }
            
            // Film Grain
            if (grainIntensity > 0) {
                const noiseMagnitude = 30; // Max deviation (e.g., +/- 30 units) at full intensity
                // Add random noise independently to R, G, B for colored grain
                const rNoise = (Math.random() * 2 - 1) * grainIntensity * noiseMagnitude;
                const gNoise = (Math.random() * 2 - 1) * grainIntensity * noiseMagnitude;
                const bNoise = (Math.random() * 2 - 1) * grainIntensity * noiseMagnitude;

                r = Math.max(0, Math.min(255, r + rNoise));
                g = Math.max(0, Math.min(255, g + gNoise));
                b = Math.max(0, Math.min(255, b + bNoise));
            }

            data[i] = Math.round(r);
            data[i + 1] = Math.round(g);
            data[i + 2] = Math.round(b);
        }
        ctx.putImageData(imageData, 0, 0);
    }

    // 4. Vignette
    if (vignetteStrength > 0) {
        ctx.save();
        // Calculate radius to ensure vignette covers corners even for wide/tall images
        const cornerDist = Math.sqrt(Math.pow(width / 2, 2) + Math.pow(height / 2, 2));
        const outerRadius = cornerDist * 1.5; // Make it larger to ensure smooth falloff beyond corners
        
        // Inner radius: smaller for stronger vignette, larger for weaker
        const innerRadiusRatio = 0.35 + (1 - vignetteStrength) * 0.5; // Ranges from 0.35 (strong) to 0.85 (weak)
        const innerRadius = Math.min(width, height) * innerRadiusRatio;
        
        const gradV = ctx.createRadialGradient(
            width / 2, height / 2, innerRadius,
            width / 2, height / 2, outerRadius
        );
        gradV.addColorStop(0, 'rgba(0,0,0,0)');
        // Vignette alpha: stronger effect means more opaque black
        gradV.addColorStop(1, `rgba(0,0,0, ${Math.min(1, vignetteStrength * 1.2)})`);
        ctx.fillStyle = gradV;
        ctx.fillRect(0, 0, width, height);
        ctx.restore();
    }

    // 5. Scratches
    if (scratchesCount > 0) {
        for (let i = 0; i < scratchesCount; i++) {
            ctx.beginPath();
            const x = Math.random() * width;
            // Scratches are mostly vertical, varying start/end position and length
            const y1 = Math.random() * height * 0.3; // Start near top third
            const y2 = height - (Math.random() * height * 0.3); // End near bottom third
            
            ctx.moveTo(x, y1);
            // Slight horizontal deviation for a more natural scratch
            ctx.lineTo(x + (Math.random() * 8 - 4), y2); 
            
            ctx.strokeStyle = `rgba(230, 230, 230, ${Math.random() * 0.12 + 0.03})`; // Faint white/gray
            ctx.lineWidth = Math.random() * 1.2 + 0.4; // Thin lines
            ctx.stroke();
        }
    }

    // 6. Dust Particles
    if (dustParticleCount > 0) {
        for (let i = 0; i < dustParticleCount; i++) {
            const x = Math.random() * width;
            const y = Math.random() * height;
            const radius = (Math.random() * 1.0 + 0.3) / 2; // Diameter 0.3px to 1.3px
            ctx.beginPath();
            ctx.arc(x, y, radius, 0, Math.PI * 2);
            ctx.fillStyle = `rgba(200, 200, 200, ${Math.random() * 0.3 + 0.1})`; // Faint gray
            ctx.fill();
        }
    }

    // 7. Light Leaks
    if (lightLeakStrength > 0) {
        ctx.save();
        ctx.globalCompositeOperation = 'lighter'; // Additive blending for light effects

        const numLeaks = Math.floor(Math.random() * 2) + 1; // 1 to 2 leaks
        const leakColors = [ // Typical warm light leak colors
            { r: 255, g: 100, b: 50 }, { r: 255, g: 180, b: 30 }, { r: 230, g: 80, b: 40 }
        ];

        for (let i = 0; i < numLeaks; i++) {
            const colorIndex = Math.floor(Math.random() * leakColors.length);
            const baseColor = leakColors[colorIndex];
            
            const side = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left
            let leakX, leakY;
            
            const maxExtent = Math.min(width, height) * (Math.random() * 0.7 + 0.5); // How far leak spreads (50-120% of min_dim)
            const edgeOffset = (Math.random() * 0.5 - 0.25) * maxExtent; // Start near edge (-25% to +25% of extent)

            switch (side) {
                case 0: leakX = Math.random() * width; leakY = -edgeOffset; break;
                case 1: leakX = width + edgeOffset; leakY = Math.random() * height; break;
                case 2: leakX = Math.random() * width; leakY = height + edgeOffset; break;
                case 3: default: leakX = -edgeOffset; leakY = Math.random() * height; break;
            }
            
            const leakRadiusStart = Math.random() * maxExtent * 0.2; // Inner core of the leak
            const leakRadiusEnd = maxExtent;

            const leakGrad = ctx.createRadialGradient(
                leakX, leakY, leakRadiusStart,
                leakX, leakY, leakRadiusEnd
            );
            
            // Center of leak is brighter, fades out
            const leakCenterOpacity = lightLeakStrength * (Math.random() * 0.4 + 0.6); // 60-100% of main strength
            leakGrad.addColorStop(0, `rgba(${baseColor.r}, ${baseColor.g}, ${baseColor.b}, ${leakCenterOpacity})`);
            leakGrad.addColorStop(0.7, `rgba(${baseColor.r}, ${baseColor.g}, ${baseColor.b}, ${leakCenterOpacity * 0.25})`); // Softer falloff
            leakGrad.addColorStop(1, `rgba(${baseColor.r}, ${baseColor.g}, ${baseColor.b}, 0)`);
            
            ctx.fillStyle = leakGrad;
            ctx.fillRect(0, 0, width, height); // Apply gradient over the whole canvas
        }
        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 Photo 16mm Film Filter Effect Tool allows users to apply a vintage 16mm film style effect to their images. It features adjustable settings for film grain, desaturation, sepia tone, vignette effect, and imperfections like scratches and dust particles. Additionally, users can simulate light leaks for an authentic film look. This tool is perfect for photographers, designers, or anyone looking to enhance their digital images with a nostalgic film aesthetic, making it ideal for social media posts, art projects, or creative presentations.

Leave a Reply

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