Please bookmark this page to avoid losing your image tool!

Image Cel 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.
function processImage(originalImg, levels = 4, edgeThreshold = 120, edgeColor = "black") {
    // Ensure parameters are of correct type
    levels = Number(levels);
    edgeThreshold = Number(edgeThreshold);
    // edgeColor is already string by default

    const canvas = document.createElement('canvas');
    // Use willReadFrequently: true for performance hint if available
    const ctxOptions = { willReadFrequently: true };
    // Firefox < 9willReadFrequently> throws if alpha is false and willReadFrequently is true
    // So, ensure alpha is not explicitly false if willReadFrequently is true. Default is true.
    // ctxOptions.alpha = true; // Or let it be default
    const ctx = canvas.getContext('2d', ctxOptions);
    
    const width = originalImg.naturalWidth || originalImg.width;
    const height = originalImg.naturalHeight || originalImg.height;

    if (width === 0 || height === 0) {
        console.warn("Image has zero width or height. Returning empty canvas.");
        canvas.width = 0;
        canvas.height = 0;
        return canvas;
    }

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

    ctx.drawImage(originalImg, 0, 0, width, height);
    let imageData;
    try {
        imageData = ctx.getImageData(0, 0, width, height);
    } catch (e) {
        // This can happen if the image is tainted (e.g., cross-origin)
        console.error("Could not get image data, possibly due to cross-origin restrictions:", e);
        // Fallback: return the canvas with the original image drawn, unprocessed.
        // The user will see the original image instead of an error or blank canvas.
        return canvas; 
    }
    const data = imageData.data;

    // Helper to parse edgeColor to {r, g, b} components
    let ecR, ecG, ecB;
    const tempCanvasForColor = document.createElement('canvas');
    tempCanvasForColor.width = 1;
    tempCanvasForColor.height = 1;
    const tempCtxForColor = tempCanvasForColor.getContext('2d'); 
    
    ecR = 0; ecG = 0; ecB = 0; // Default to black
    try {
        tempCtxForColor.fillStyle = edgeColor; // Use the provided edgeColor string
        tempCtxForColor.fillRect(0, 0, 1, 1);
        const colorData = tempCtxForColor.getImageData(0, 0, 1, 1).data;
        ecR = colorData[0];
        ecG = colorData[1];
        ecB = colorData[2];
    } catch (e) {
        console.warn(`Failed to parse edgeColor "${edgeColor}", defaulting to black. Error: ${e.message}`);
        // ecR, ecG, ecB are already set to black as a fallback
    }

    // Ensure at least 2 levels for quantization; otherwise, step calculation would involve division by zero.
    const numLevels = Math.max(2, levels); 
    const step = 255 / (numLevels - 1);

    // 1. Color Quantization
    // Create a new array for quantized pixel data. Uint8ClampedArray automatically clamps values to 0-255.
    const quantizedData = new Uint8ClampedArray(data.length);
    for (let i = 0; i < data.length; i += 4) {
        quantizedData[i]   = Math.round(data[i] / step) * step;   // R
        quantizedData[i+1] = Math.round(data[i+1] / step) * step; // G
        quantizedData[i+2] = Math.round(data[i+2] / step) * step; // B
        quantizedData[i+3] = data[i+3];                           // Alpha (preserve original)
    }

    // 2. Edge Detection (Sobel Operator)
    // First, create a grayscale luminance map from the quantized color data.
    const luminanceMap = new Float32Array(width * height);
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const i = (y * width + x) * 4;
            const r = quantizedData[i];
            const g = quantizedData[i+1];
            const b = quantizedData[i+2];
            // Standard NTSC luminance calculation
            luminanceMap[y * width + x] = 0.299 * r + 0.587 * g + 0.114 * b;
        }
    }

    // Prepare output image data, initially filled with quantized colors.
    // Edges detected later will overwrite these pixels.
    const outputImageData = ctx.createImageData(width, height);
    const outputData = outputImageData.data;
    for (let i = 0; i < quantizedData.length; i++) {
        outputData[i] = quantizedData[i];
    }

    // Sobel kernels for edge detection
    const Gx_kernel = [
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1]
    ];
    const Gy_kernel = [
        [-1, -2, -1],
        [ 0,  0,  0],
        [ 1,  2,  1]
    ];

    // Apply Sobel operator
    // Iterate from 1 to width/height - 1 to avoid border issues with 3x3 kernel
    for (let y = 1; y < height - 1; y++) {
        for (let x = 1; x < width - 1; x++) {
            let gx = 0;
            let gy = 0;

            // Apply 3x3 kernel
            for (let ky = -1; ky <= 1; ky++) {
                for (let kx = -1; kx <= 1; kx++) {
                    const Gx_val = Gx_kernel[ky + 1][kx + 1];
                    const Gy_val = Gy_kernel[ky + 1][kx + 1];
                    
                    // Get luminance of the neighboring pixel
                    const L_val = luminanceMap[(y + ky) * width + (x + kx)];
                    
                    gx += L_val * Gx_val;
                    gy += L_val * Gy_val;
                }
            }

            // Calculate gradient magnitude
            const magnitude = Math.sqrt(gx * gx + gy * gy);
            
            // If magnitude exceeds threshold, mark as an edge pixel
            if (magnitude > edgeThreshold) {
                const outputIndex = (y * width + x) * 4;
                outputData[outputIndex]     = ecR;       // Edge R
                outputData[outputIndex + 1] = ecG;       // Edge G
                outputData[outputIndex + 2] = ecB;       // Edge B
                outputData[outputIndex + 3] = 255;       // Edges are opaque
            }
            // Else: pixel retains its quantized color (already set)
        }
    }

    ctx.putImageData(outputImageData, 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 Cel Shading Filter is a web-based tool that applies a cel shading effect to images, giving them a stylized appearance often seen in animations and comic art. Users can customize the level of color quantization, edge detection sensitivity, and edge color to create unique visuals. This tool is ideal for artists, designers, and hobbyists looking to transform their images into graphics with a cartoon-like quality, suitable for illustrations, video game art, or creative projects.

Leave a Reply

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