Please bookmark this page to avoid losing your image tool!

Image Analog Film Grain Filter

(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, intensity = 20, grainSize = 1, monochrome = "false") {
    const canvas = document.createElement('canvas');
    // Set willReadFrequently to true as we use getImageData and putImageData,
    // which can be a performance hint for some browsers.
    const ctx = canvas.getContext('2d', { willReadFrequently: true });

    // Determine image dimensions. Use naturalWidth/Height for intrinsic dimensions.
    const imgWidth = originalImg.naturalWidth || originalImg.width;
    const imgHeight = originalImg.naturalHeight || originalImg.height;

    // Handle cases where the image might not be loaded or has no dimensions
    if (imgWidth === 0 || imgHeight === 0) {
        console.error("Image has no dimensions. Ensure it is loaded and valid. Returning a 1x1 placeholder canvas.");
        // Return a tiny, effectively empty, canvas to avoid errors downstream
        canvas.width = 1;
        canvas.height = 1;
        // Optionally, fill it with a color to indicate an issue
        // ctx.fillStyle = 'red'; 
        // ctx.fillRect(0, 0, 1, 1); 
        return canvas;
    }

    canvas.width = imgWidth;
    canvas.height = imgHeight;

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

    // Parameter parsing and sanitization
    const parsedIntensity = Number(intensity);
    // Ensure grainSize is an integer and at least 1
    let parsedGrainSize = Math.max(1, Math.floor(Number(grainSize))); 
    const parsedMonochrome = String(monochrome).toLowerCase() === "true";

    // If intensity is 0, no grain needs to be applied.
    if (parsedIntensity === 0) {
        return canvas; // Return the canvas with the original image drawn
    }

    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const data = imageData.data;
    // Use dimensions from imageData as it's the source of truth for the pixel array
    const width = imageData.width; 
    const height = imageData.height;

    // Helper function to clamp color values between 0 and 255 and round them
    function clamp(value) {
        return Math.max(0, Math.min(255, Math.round(value)));
    }

    if (parsedGrainSize === 1) {
        // Per-pixel noise: most "fine-grained"
        for (let i = 0; i < data.length; i += 4) {
            // data[i] is R, data[i+1] is G, data[i+2] is B, data[i+3] is Alpha
            // Optionally, one might choose to not apply grain to fully transparent pixels:
            // if (data[i+3] === 0) continue;
            
            let noiseR, noiseG, noiseB;
            if (parsedMonochrome) {
                // Generate a single noise value for R, G, B for monochrome grain
                // Noise value ranges from -parsedIntensity to +parsedIntensity
                const monoNoise = (Math.random() * 2 - 1) * parsedIntensity;
                noiseR = monoNoise;
                noiseG = monoNoise;
                noiseB = monoNoise;
            } else {
                // Generate separate noise values for R, G, B for colored grain
                noiseR = (Math.random() * 2 - 1) * parsedIntensity;
                noiseG = (Math.random() * 2 - 1) * parsedIntensity;
                noiseB = (Math.random() * 2 - 1) * parsedIntensity;
            }

            data[i]     = clamp(data[i] + noiseR);
            data[i + 1] = clamp(data[i + 1] + noiseG);
            data[i + 2] = clamp(data[i + 2] + noiseB);
            // Alpha channel (data[i + 3]) remains unchanged
        }
    } else {
        // Block-based noise: simulates larger grain particles
        // Calculate number of noise blocks needed
        const numBlocksX = Math.ceil(width / parsedGrainSize);
        const numBlocksY = Math.ceil(height / parsedGrainSize);
        
        // Create a 2D array to store noise values for each block
        const blockNoises = new Array(numBlocksY);
        for(let i = 0; i < numBlocksY; ++i) {
            blockNoises[i] = new Array(numBlocksX);
        }
    
        // Pre-calculate noise for each block
        for (let by = 0; by < numBlocksY; by++) {
            for (let bx = 0; bx < numBlocksX; bx++) {
                let noiseR_block, noiseG_block, noiseB_block;
                if (parsedMonochrome) {
                    const monoNoise_block = (Math.random() * 2 - 1) * parsedIntensity;
                    noiseR_block = monoNoise_block;
                    noiseG_block = monoNoise_block;
                    noiseB_block = monoNoise_block;
                } else {
                    noiseR_block = (Math.random() * 2 - 1) * parsedIntensity;
                    noiseG_block = (Math.random() * 2 - 1) * parsedIntensity;
                    noiseB_block = (Math.random() * 2 - 1) * parsedIntensity;
                }
                blockNoises[by][bx] = { r: noiseR_block, g: noiseG_block, b: noiseB_block };
            }
        }

        // Apply block noise to pixels
        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                const pixelIndex = (y * width + x) * 4;
                // if (data[pixelIndex+3] === 0) continue; // Optional alpha check
                
                // Determine which block this pixel belongs to
                const blockXCoord = Math.floor(x / parsedGrainSize);
                const blockYCoord = Math.floor(y / parsedGrainSize);
                
                // Get the pre-calculated noise for this block
                const currentBlockNoise = blockNoises[blockYCoord][blockXCoord];

                data[pixelIndex]     = clamp(data[pixelIndex]     + currentBlockNoise.r);
                data[pixelIndex + 1] = clamp(data[pixelIndex + 1] + currentBlockNoise.g);
                data[pixelIndex + 2] = clamp(data[pixelIndex + 2] + currentBlockNoise.b);
                // Alpha channel (data[pixelIndex + 3]) remains unchanged
            }
        }
    }

    // Put the modified image data back onto the canvas
    ctx.putImageData(imageData, 0, 0);

    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 Analog Film Grain Filter tool allows users to add a vintage film grain effect to their images, simulating the texture and aesthetic of classic film photography. Users can adjust the intensity and size of the grain, as well as choose between color or monochrome versions of the effect. This tool is ideal for photographers, graphic designers, and artists looking to give their digital images a nostalgic, film-like quality, enhancing visual storytelling or creating mood in their artwork.

Leave a Reply

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