Please bookmark this page to avoid losing your image tool!

Image Toon Shading 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.
async function processImage(originalImg, edgeThreshold = 80, posterizationLevels = 4) {
    // Use naturalWidth/Height for actual image dimensions, fallback to width/height attributes
    const width = originalImg.naturalWidth || originalImg.width;
    const height = originalImg.naturalHeight || originalImg.height;

    // Handle cases where image dimensions are not valid (e.g., image not loaded)
    if (width === 0 || height === 0) {
        console.error("Image has zero width or height. Ensure the image is loaded and valid.");
        // Create a small placeholder canvas
        const emptyCanvas = document.createElement('canvas');
        emptyCanvas.width = Math.max(1, width); 
        emptyCanvas.height = Math.max(1, height);
        const ctx = emptyCanvas.getContext('2d');
        if (ctx) { // Context might be null in some environments
            ctx.fillStyle = 'lightgray';
            ctx.fillRect(0, 0, emptyCanvas.width, emptyCanvas.height);
        }
        return emptyCanvas;
    }

    // Create a temporary canvas to draw the original image and access its pixel data
    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = width;
    tempCanvas.height = height;
    // Add { willReadFrequently: true } for potential performance optimization when using getImageData repeatedly.
    const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); 
    
    if (!tempCtx) {
        console.error("Could not get 2D context from temporary canvas.");
        // Fallback: return a canvas with the original image if possible, or an empty one
        const fallbackCanvas = document.createElement('canvas');
        fallbackCanvas.width = width; fallbackCanvas.height = height;
        const fbCtx = fallbackCanvas.getContext('2d');
        if (fbCtx) try { fbCtx.drawImage(originalImg,0,0); } catch(e){}
        return fallbackCanvas;
    }
    tempCtx.drawImage(originalImg, 0, 0, width, height);
    
    let imageData;
    try {
        imageData = tempCtx.getImageData(0, 0, width, height);
    } catch (e) {
        // This can happen due to cross-origin restrictions if the image is from another domain
        // and lacks appropriate CORS headers.
        console.error("Could not get image data. Possible cross-origin issue.", e);
        const errorCanvas = document.createElement('canvas');
        errorCanvas.width = width; errorCanvas.height = height;
        const errCtx = errorCanvas.getContext('2d');
        if(errCtx) { // Try to draw original image as fallback
            try { errCtx.drawImage(originalImg, 0, 0, width, height); } catch(drawErr){}
        }
        return errorCanvas;
    }
    const data = imageData.data; // This is a Uint8ClampedArray: [r1,g1,b1,a1, r2,g2,b2,a2, ...]

    // 1. Convert to Grayscale (for edge detection)
    // Create a 1D array to store grayscale values for each pixel
    const grayData = new Uint8ClampedArray(width * height); 
    for (let i = 0; i < data.length; i += 4) {
        const r = data[i];
        const g = data[i + 1];
        const b = data[i + 2];
        // Using standard NTSC/PAL luminance calculation for grayscale conversion
        const gray = 0.299 * r + 0.587 * g + 0.114 * b;
        grayData[i / 4] = gray; // Store the grayscale value (one value per pixel)
    }

    // 2. Edge Detection (Sobel Operator)
    // Create a 1D array to store edge information (255 for edge, 0 for non-edge)
    const edgePixelData = new Uint8ClampedArray(width * height); 

    // Sobel kernels for detecting horizontal (Gx) and vertical (Gy) edges
    const Gx = [
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1]
    ];
    const Gy = [
        [-1, -2, -1],
        [0, 0, 0],
        [1, 2, 1]
    ];

    // Iterate through each pixel (excluding image borders to allow 3x3 kernel application)
    for (let y = 1; y < height - 1; y++) {
        for (let x = 1; x < width - 1; x++) {
            let sumX = 0; // Gradient in X direction
            let sumY = 0; // Gradient in Y direction

            // Apply Sobel kernels by convolving with the 3x3 neighborhood of the current pixel
            for (let ky = -1; ky <= 1; ky++) { // Kernel Y offset
                for (let kx = -1; kx <= 1; kx++) { // Kernel X offset
                    const pixelIndex = (y + ky) * width + (x + kx); // Index in 1D grayData array
                    const pixelVal = grayData[pixelIndex];
                    sumX += pixelVal * Gx[ky + 1][kx + 1];
                    sumY += pixelVal * Gy[ky + 1][kx + 1];
                }
            }

            const magnitude = Math.sqrt(sumX * sumX + sumY * sumY); // Gradient magnitude
            const currentIndex = y * width + x; // Index of the current pixel in 1D arrays
            if (magnitude > edgeThreshold) {
                edgePixelData[currentIndex] = 255; // Mark as an edge pixel (white simplifies later visualization if needed)
            } else {
                edgePixelData[currentIndex] = 0;   // Mark as a non-edge pixel (black)
            }
        }
    }
    // Note: Pixels on the very border (x=0, y=0, x=width-1, y=height-1) 
    // are not processed by the Sobel loop and will have edgePixelData[idx] = 0 by default.

    // 3. Posterization and Combining with Edges
    // Create the output canvas element that will be returned
    const outputCanvas = document.createElement('canvas');
    outputCanvas.width = width;
    outputCanvas.height = height;
    const outputCtx = outputCanvas.getContext('2d');

    if (!outputCtx) {
        console.error("Could not get 2D context from output canvas.");
        return tempCanvas; // Fallback to the temporary canvas (original image)
    }
    
    const finalImageData = outputCtx.createImageData(width, height);
    const finalData = finalImageData.data;

    // Ensure posterizationLevels is at least 1
    const pLevelsEffective = Math.max(1, Math.floor(posterizationLevels));

    const posterizeSingleChannel = (value, levels) => {
        if (levels <= 1) { 
            // If 1 level is specified, map all color values to a single representative value (e.g., 128 for mid-tone)
            return 128;
        }
        // For L levels (where L >= 2), we want L distinct output values.
        // These values are typically spread evenly, e.g., 0, 255/(L-1), 2*255/(L-1), ..., 255.
        // The formula Math.round(value / step) * step maps the input value to the nearest of these L values.
        const step = 255 / (levels - 1);
        return Math.max(0, Math.min(255, Math.round(value / step) * step)); // Clamp to 0-255
    };

    // Iterate through each pixel of the original image data
    for (let i = 0; i < data.length; i += 4) {
        const pixelArrayIndex = i / 4; // Index of the pixel (from 0 to width*height - 1)
        const y = Math.floor(pixelArrayIndex / width);
        const x = pixelArrayIndex % width;

        const r_orig = data[i];
        const g_orig = data[i + 1];
        const b_orig = data[i + 2];
        const a_orig = data[i + 3];

        // Apply posterization to each color channel (R, G, B)
        const pr = posterizeSingleChannel(r_orig, pLevelsEffective);
        const pg = posterizeSingleChannel(g_orig, pLevelsEffective);
        const pb = posterizeSingleChannel(b_orig, pLevelsEffective);

        // Check if the current pixel is an edge (based on Sobel operator results)
        let isEdgePixel = false;
        // Edges are only reliably detected for non-border pixels (where the 3x3 kernel fully fits)
        if (x > 0 && x < width - 1 && y > 0 && y < height - 1) {
            if (edgePixelData[y * width + x] === 255) { // Check the edge map
                isEdgePixel = true;
            }
        }
        
        // Set the final pixel data: black for edges, posterized color otherwise
        if (isEdgePixel) {
            finalData[i] = 0;          // Red channel - black for edge
            finalData[i + 1] = 0;      // Green channel - black for edge
            finalData[i + 2] = 0;      // Blue channel - black for edge
            finalData[i + 3] = a_orig; // Preserve original alpha for the edge
        } else {
            finalData[i] = pr;         // Posterized red channel
            finalData[i + 1] = pg;         // Posterized green channel
            finalData[i + 2] = pb;         // Posterized blue channel
            finalData[i + 3] = a_orig; // Preserve original alpha
        }
    }

    // Put the processed pixel data onto the output canvas
    outputCtx.putImageData(finalImageData, 0, 0);
    return outputCanvas; // Return the canvas element with the toon-shaded image
}

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 Toon Shading Filter is an online tool that transforms images to give them a cartoon-like appearance. It applies edge detection and color posterization to create a stylized effect, enhancing outlines and reducing color complexity. This tool is ideal for artists looking to create unique artwork, for social media users wishing to enhance their photos, or for anyone interested in adding a playful touch to their images.

Leave a Reply

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