Please bookmark this page to avoid losing your image tool!

Photo Grunge Filter Application

(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, desaturationLevelStr = "0.7", contrastLevelStr = "1.3", noiseIntensityStr = "25", vignetteStrengthStr = "0.7", sepiaToneStr = "0.25", grainIntensityStr = "0.08", dirtAndScratchesStr = "40") {

    // Helper function to clamp values
    const clamp = (value, min, max) => Math.max(min, Math.min(max, value));

    // Parse parameters and provide robust defaults
    const desaturationLevel = clamp(parseFloat(desaturationLevelStr) || 0.7, 0, 1);
    const contrastLevel = Math.max(0, parseFloat(contrastLevelStr) || 1.3); // contrast can be > 1
    const noiseIntensity = clamp(parseInt(noiseIntensityStr) || 25, 0, 255);
    const vignetteStrength = clamp(parseFloat(vignetteStrengthStr) || 0.7, 0, 1);
    const sepiaTone = clamp(parseFloat(sepiaToneStr) || 0.25, 0, 1);
    const grainIntensity = clamp(parseFloat(grainIntensityStr) || 0.08, 0, 1);
    const dirtAndScratchesAmount = Math.max(0, parseInt(dirtAndScratchesStr) || 40);

    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;

    if (width === 0 || height === 0) {
        // Avoid errors with 0-dimension images
        console.error("Image has zero width or height.");
        return canvas; // Return empty canvas
    }

    ctx.drawImage(originalImg, 0, 0, width, height);

    const imageData = ctx.getImageData(0, 0, width, height);
    const data = imageData.data;
    
    const centerX = width / 2;
    const centerY = height / 2;
    // Max distance from center to a corner, used for vignette
    const maxDist = Math.sqrt(centerX * centerX + centerY * centerY); 

    // Process each pixel
    for (let i = 0; i < data.length; i += 4) {
        let r = data[i];
        let g = data[i + 1];
        let b = data[i + 2];

        // 1. Desaturation
        if (desaturationLevel > 0) {
            const gray = 0.299 * r + 0.587 * g + 0.114 * b;
            r = r * (1 - desaturationLevel) + gray * desaturationLevel;
            g = g * (1 - desaturationLevel) + gray * desaturationLevel;
            b = b * (1 - desaturationLevel) + gray * desaturationLevel;
        }

        // 2. Sepia Tone
        if (sepiaTone > 0) {
            const sr = (r * 0.393) + (g * 0.769) + (b * 0.189);
            const sg = (r * 0.349) + (g * 0.686) + (b * 0.168);
            const sb = (r * 0.272) + (g * 0.534) + (b * 0.131);
            r = r * (1 - sepiaTone) + sr * sepiaTone;
            g = g * (1 - sepiaTone) + sg * sepiaTone;
            b = b * (1 - sepiaTone) + sb * sepiaTone;
        }
        
        // 3. Contrast
        if (contrastLevel !== 1.0) {
            r = 128 + contrastLevel * (r - 128);
            g = 128 + contrastLevel * (g - 128);
            b = 128 + contrastLevel * (b - 128);
        }

        // 4. Noise
        if (noiseIntensity > 0) {
            const randomNoise = (Math.random() - 0.5) * noiseIntensity;
            r += randomNoise;
            g += randomNoise;
            b += randomNoise;
        }

        // Clamp final RGB values
        data[i] = clamp(r, 0, 255);
        data[i + 1] = clamp(g, 0, 255);
        data[i + 2] = clamp(b, 0, 255);
    }
    ctx.putImageData(imageData, 0, 0);

    // 5. Vignette (as an overlay drawing)
    if (vignetteStrength > 0 && maxDist > 0) {
        ctx.globalCompositeOperation = 'source-over';
        const outerRadius = maxDist;
        // Inner radius calculation: smaller inner radius means vignette starts closer to center
        // vignetteStrength = 1 means innerRadius is small. vignetteStrength = 0 means innerRadius = outerRadius
        const innerRadiusPercent = 1 - Math.min(1, vignetteStrength * 1.1); // Adjust 1.1 for desired falloff start
        const innerRadius = outerRadius * innerRadiusPercent;
        
        const vignetteGradient = ctx.createRadialGradient(centerX, centerY, innerRadius, centerX, centerY, outerRadius);
        vignetteGradient.addColorStop(0, 'rgba(0,0,0,0)'); // Transparent center
        vignetteGradient.addColorStop(1, `rgba(0,0,0, ${clamp(vignetteStrength,0,1)})`); // Dark edges

        ctx.fillStyle = vignetteGradient;
        ctx.fillRect(0, 0, width, height);
    }
	ctx.globalCompositeOperation = 'source-over'; // Reset blend mode

    // 6. Grain Overlay (simulated film grain)
    if (grainIntensity > 0 && grainIntensity <= 1) {
        const grainCanvas = document.createElement('canvas');
        grainCanvas.width = width;
        grainCanvas.height = height;
        const grainCtx = grainCanvas.getContext('2d');
        
        // Create an ImageData manually for grain for performance on large images
        const grainImgData = grainCtx.createImageData(width, height);
        const grainData = grainImgData.data;

        for (let j = 0; j < grainData.length; j += 4) {
            const grainValue = Math.random() * 255; // Grayscale noise
            grainData[j] = grainValue;
            grainData[j + 1] = grainValue;
            grainData[j + 2] = grainValue;
            grainData[j + 3] = 255; // Grain pixels are opaque; layer opacity controls overall visibility
        }
        grainCtx.putImageData(grainImgData, 0, 0);
        
        ctx.globalAlpha = grainIntensity; 
        ctx.globalCompositeOperation = 'overlay'; // 'overlay' or 'soft-light' work well
        ctx.drawImage(grainCanvas, 0, 0);
        ctx.globalAlpha = 1.0; // Reset alpha
        ctx.globalCompositeOperation = 'source-over'; // Reset blend mode
    }

    // 7. Dirt Spots & Scratches (simple procedural)
    if (dirtAndScratchesAmount > 0) {
        ctx.globalCompositeOperation = 'overlay'; // 'overlay' or 'multiply'
        
        // Dirt Spots
        const numSpots = dirtAndScratchesAmount;
        for (let k = 0; k < numSpots; k++) {
            const x = Math.random() * width;
            const y = Math.random() * height;
            // Spot size relative to image diagonal for somewhat consistent appearance
            const spotSize = Math.random() * (maxDist * 0.015) + 1; 
            
            ctx.fillStyle = `rgba(0,0,0, ${Math.random() * 0.4 + 0.1})`; // Dark spots
            ctx.beginPath();
            ctx.arc(x, y, spotSize, 0, Math.PI * 2);
            ctx.fill();

            // Occasional lighter spots
            if (k % 7 === 0) { 
                 ctx.fillStyle = `rgba(255,255,255, ${Math.random() * 0.25 + 0.05})`;
                 ctx.beginPath();
                 ctx.arc(Math.random() * width, Math.random() * height, spotSize * 0.6, 0, Math.PI * 2);
                 ctx.fill();
            }
        }

        // Scratches
        const numScratches = Math.floor(dirtAndScratchesAmount / 4); // Fewer scratches
        for (let k = 0; k < numScratches; k++) {
            const sx1 = Math.random() * width;
            const sy1 = Math.random() * height;
            // Scratches are short, max 10% of width/height
            const sx2 = sx1 + (Math.random() - 0.5) * (width * 0.1); 
            const sy2 = sy1 + (Math.random() - 0.5) * (height * 0.1);
            
            ctx.strokeStyle = `rgba(0,0,0, ${Math.random() * 0.25 + 0.05})`;
            ctx.lineWidth = Math.random() * 1.5 + 0.5; // Thin scratches
            ctx.beginPath();
            ctx.moveTo(sx1, sy1);
            ctx.lineTo(sx2, sy2);
            ctx.stroke();

             // Occasional lighter scratches
            if (k % 5 === 0) {
                const lsx1 = Math.random() * width;
                const lsy1 = Math.random() * height;
                const lsx2 = lsx1 + (Math.random() - 0.5) * (width * 0.08); 
                const lsy2 = lsy1 + (Math.random() - 0.5) * (height * 0.08);
                ctx.strokeStyle = `rgba(255,255,255, ${Math.random() * 0.15 + 0.03})`;
                ctx.lineWidth = Math.random() * 1.0 + 0.3;
                ctx.beginPath();
                ctx.moveTo(lsx1, lsy1);
                ctx.lineTo(lsx2, lsy2);
                ctx.stroke();
            }
        }
        ctx.globalCompositeOperation = 'source-over'; // Reset blend mode
    }

    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 Grunge Filter Application is a versatile online tool designed to enhance images with a vintage or artistic grunge effect. Users can adjust various parameters to create custom filters, including desaturation, contrast, noise intensity, vignette strength, sepia tone, grain intensity, and the presence of dirt and scratches. This tool is ideal for photographers, artists, and social media enthusiasts looking to stylize their images or achieve a specific aesthetic for digital artwork, presentations, or personal projects.

Leave a Reply

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