Please bookmark this page to avoid losing your image tool!

Image Dither 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, paletteString = "#000000,#FFFFFF", algorithm = "FloydSteinberg", thresholdVal = 128, bayerMatrixSize = 4, bayerLevels = 2) {

    // Helper function: Clamp value to a range
    function clamp(value, min, max) {
        return Math.min(Math.max(value, min), max);
    }

    // Helper function: Parse palette string (comma-separated hex colors)
    function parsePalette(pString) {
        const colors = pString.split(',');
        if (colors.length === 0 || pString.trim() === "") {
            // Default to Black & White if palette string is empty
            return [{ r: 0, g: 0, b: 0 }, { r: 255, g: 255, b: 255 }];
        }
        return colors.map(hex => {
            hex = hex.trim().replace('#', '');
            if (hex.length === 3) { // Handle shorthand hex like #RGB
                hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
            }
            if (hex.length !== 6) {
                 return { r: 0, g: 0, b: 0 }; // Default to black on invalid length
            }
            const r = parseInt(hex.substring(0, 2), 16);
            const g = parseInt(hex.substring(2, 4), 16);
            const b = parseInt(hex.substring(4, 6), 16);

            if (isNaN(r) || isNaN(g) || isNaN(b)) {
                return { r: 0, g: 0, b: 0 }; // Default to black on parsing error
            }
            return { r, g, b };
        });
    }

    // Helper function: Find closest color in palette
    function findClosestColor(r, g, b, currentPalette) {
        let closestColor = currentPalette[0];
        let minDistanceSquared = Infinity;

        for (const color of currentPalette) {
            const distR = r - color.r;
            const distG = g - color.g;
            const distB = b - color.b;
            const distanceSquared = distR * distR + distG * distG + distB * distB;
            
            if (distanceSquared < minDistanceSquared) {
                minDistanceSquared = distanceSquared;
                closestColor = color;
            }
        }
        return closestColor;
    }

    // Bayer matrices definition
    const bayerMatrices = {
        2: [
            [0, 2],
            [3, 1]
        ],
        4: [
            [ 0,  8,  2, 10],
            [12,  4, 14,  6],
            [ 3, 11,  1,  9],
            [15,  7, 13,  5]
        ],
        8: [
            [ 0, 32,  8, 40,  2, 34, 10, 42],
            [48, 16, 56, 24, 50, 18, 58, 26],
            [12, 44,  4, 36, 14, 46,  6, 38],
            [60, 28, 52, 20, 62, 30, 54, 22],
            [ 3, 35, 11, 43,  1, 33,  9, 41],
            [51, 19, 59, 27, 49, 17, 57, 25],
            [15, 47,  7, 39, 13, 45,  5, 37],
            [63, 31, 55, 23, 61, 29, 53, 21]
        ]
    };

    function getBayerMatrix(size) {
        if (bayerMatrices[size]) {
            return bayerMatrices[size];
        }
        console.warn(`Bayer matrix of size ${size} not available. Using size 4.`);
        return bayerMatrices[4];
    }
    
    // Dithering Algorithm: Threshold
    function applyThreshold(imgData, imgWidth, imgHeight, currentPalette, currentThresholdVal) {
        const data = imgData.data;
        const color0 = currentPalette[0] || {r:0, g:0, b:0}; // Default to black if palette is empty
        const color1 = currentPalette[1] || (currentPalette[0] || {r:255,g:255,b:255}); // Default to white or first color

        for (let i = 0; i < data.length; i += 4) {
            const r = data[i];
            const g = data[i + 1];
            const b = data[i + 2];
            const luminance = 0.299 * r + 0.587 * g + 0.114 * b;

            let newColor;
            if (luminance < currentThresholdVal) {
                newColor = color0;
            } else {
                newColor = color1;
            }
            data[i] = newColor.r;
            data[i + 1] = newColor.g;
            data[i + 2] = newColor.b;
        }
    }

    // Dithering Algorithm: Floyd-Steinberg
    function applyFloydSteinberg(imgData, imgWidth, imgHeight, currentPalette) {
        const data = imgData.data;
        const floatData = new Float32Array(imgWidth * imgHeight * 3);

        for (let y = 0; y < imgHeight; y++) {
            for (let x = 0; x < imgWidth; x++) {
                const i = (y * imgWidth + x) * 4;
                const fIdx = (y * imgWidth + x) * 3;
                floatData[fIdx] = data[i];         // R
                floatData[fIdx + 1] = data[i + 1]; // G
                floatData[fIdx + 2] = data[i + 2]; // B
            }
        }

        for (let y = 0; y < imgHeight; y++) {
            for (let x = 0; x < imgWidth; x++) {
                const fIdx = (y * imgWidth + x) * 3;
                const oldR = floatData[fIdx];
                const oldG = floatData[fIdx + 1];
                const oldB = floatData[fIdx + 2];

                const newColor = findClosestColor(oldR, oldG, oldB, currentPalette);

                const dataIdx = (y * imgWidth + x) * 4;
                data[dataIdx]     = newColor.r;
                data[dataIdx + 1] = newColor.g;
                data[dataIdx + 2] = newColor.b;

                const errR = oldR - newColor.r;
                const errG = oldG - newColor.g;
                const errB = oldB - newColor.b;

                // Distribute error (Indices are for floatData)
                // Right: (x+1, y)
                if (x + 1 < imgWidth) {
                    const i = (y * imgWidth + (x + 1)) * 3;
                    floatData[i]     = floatData[i] + errR * 7 / 16;
                    floatData[i + 1] = floatData[i+1] + errG * 7 / 16;
                    floatData[i + 2] = floatData[i+2] + errB * 7 / 16;
                }
                // Bottom-left: (x-1, y+1)
                if (x - 1 >= 0 && y + 1 < imgHeight) {
                    const i = ((y + 1) * imgWidth + (x - 1)) * 3;
                    floatData[i]     = floatData[i] + errR * 3 / 16;
                    floatData[i + 1] = floatData[i+1] + errG * 3 / 16;
                    floatData[i + 2] = floatData[i+2] + errB * 3 / 16;
                }
                // Bottom: (x, y+1)
                if (y + 1 < imgHeight) {
                    const i = ((y + 1) * imgWidth + x) * 3;
                    floatData[i]     = floatData[i] + errR * 5 / 16;
                    floatData[i + 1] = floatData[i+1] + errG * 5 / 16;
                    floatData[i + 2] = floatData[i+2] + errB * 5 / 16;
                }
                // Bottom-right: (x+1, y+1)
                if (x + 1 < imgWidth && y + 1 < imgHeight) {
                    const i = ((y + 1) * imgWidth + (x + 1)) * 3;
                    floatData[i]     = floatData[i] + errR * 1 / 16;
                    floatData[i + 1] = floatData[i+1] + errG * 1 / 16;
                    floatData[i + 2] = floatData[i+2] + errB * 1 / 16;
                }
            }
        }
    }

    // Dithering Algorithm: Bayer (Ordered Dithering)
    function applyBayerDithering(imgData, imgWidth, imgHeight, currentPalette, matrixSize, levels) {
        const data = imgData.data;
        const bayerMatrix = getBayerMatrix(matrixSize);
        const M_sq = matrixSize * matrixSize;
        
        const bayerThresholds = bayerMatrix.map(row => row.map(val => val / M_sq)); // Normalized to [0,1)

        if (levels < 1) levels = 1; // Prevent division by zero if levels is < 1

        for (let y = 0; y < imgHeight; y++) {
            for (let x = 0; x < imgWidth; x++) {
                const i = (y * imgWidth + x) * 4;
                const r_orig = data[i];
                const g_orig = data[i + 1];
                const b_orig = data[i + 2];

                const threshold = bayerThresholds[y % matrixSize][x % matrixSize];
                
                let r_temp, g_temp, b_temp;

                if (levels === 1) { // Quantize to a single level (average or first color)
                    // This means the intermediate color is fixed before picking from palette
                    r_temp = 128; g_temp = 128; b_temp = 128; // Or use 0, or average of palette, behavior can be tuned
                } else {
                    // Apply Bayer formula to get N-level intermediate color components
                    r_temp = Math.floor( (r_orig / 255.0) * (levels - 1) + threshold ) / (levels - 1) * 255.0;
                    g_temp = Math.floor( (g_orig / 255.0) * (levels - 1) + threshold ) / (levels - 1) * 255.0;
                    b_temp = Math.floor( (b_orig / 255.0) * (levels - 1) + threshold ) / (levels - 1) * 255.0;
                }
                
                // Clamp intermediate values (should inherently be within range, but good practice)
                r_temp = clamp(r_temp, 0, 255);
                g_temp = clamp(g_temp, 0, 255);
                b_temp = clamp(b_temp, 0, 255);

                // Find the closest color in the final palette for this dithered intermediate color
                const newColor = findClosestColor(r_temp, g_temp, b_temp, currentPalette);
                
                data[i] = newColor.r;
                data[i + 1] = newColor.g;
                data[i + 2] = newColor.b;
            }
        }
    }

    // Main function logic
    const canvas = document.createElement('canvas');
    const width = originalImg.naturalWidth || originalImg.width;
    const height = originalImg.naturalHeight || originalImg.height;
    
    canvas.width = width;
    canvas.height = height;

    if (width === 0 || height === 0) {
        return canvas; // Return empty (0x0) canvas if image has no dimensions
    }

    const ctx = canvas.getContext('2d');
    ctx.drawImage(originalImg, 0, 0, width, height);
    const imageData = ctx.getImageData(0, 0, width, height);
    
    const finalPalette = parsePalette(paletteString);
    if (finalPalette.length === 0) { // Should be handled by parsePalette, but as a safeguard
        console.warn("Palette is empty. Using default black and white.");
        finalPalette.push({r:0,g:0,b:0}, {r:255,g:255,b:255});
    }

    const algo = algorithm.toLowerCase();

    if (algo === "floydsteinberg") {
        applyFloydSteinberg(imageData, width, height, finalPalette);
    } else if (algo === "bayer") {
        applyBayerDithering(imageData, width, height, finalPalette, bayerMatrixSize, bayerLevels);
    } else if (algo === "threshold") {
        applyThreshold(imageData, width, height, finalPalette, thresholdVal);
    } else {
        console.warn(`Unknown algorithm: ${algorithm}. Defaulting to FloydSteinberg.`);
        applyFloydSteinberg(imageData, width, height, finalPalette);
    }

    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 Dither Filter Application is a versatile online tool designed to apply dithering effects to images, transforming them into visually distinct styles that utilize a limited color palette. Users can customize the palette of colors from which the dithering will select, enabling a range of creative effects. The tool supports several dithering algorithms, including Floyd-Steinberg, Bayer, and simple thresholding, allowing for different visual results depending on the selected method. This application is useful for graphic designers, artists, or anyone looking to reduce image colors or create stylized graphics for web use, prints, or retro aesthetics, providing a unique way to enhance image presentations.

Leave a Reply

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