Please bookmark this page to avoid losing your image tool!

Image Cartoon 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, levels = 6, edgeThreshold = 80, edgeColor = "black", edgeThickness = 1) {
    // 1. Create a canvas and draw the original image to access its pixel data
    const canvas = document.createElement('canvas');
    // Use willReadFrequently for potential performance gain as per MDN.
    const ctx = canvas.getContext('2d', { willReadFrequently: true }); 
    
    // Use naturalWidth/Height to ensure we use the original image dimensions
    canvas.width = originalImg.naturalWidth || originalImg.width;
    canvas.height = originalImg.naturalHeight || originalImg.height;
    
    // Draw the image onto the canvas
    ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);

    // Get ImageData for processing
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const originalData = imageData.data;
    const width = canvas.width;
    const height = canvas.height;

    // 2. Posterization (Color Quantization)
    const posterizedData = new Uint8ClampedArray(originalData.length);
    if (levels <= 1) { // Handle 1 level (or less) as a single color (e.g., black)
        for (let i = 0; i < originalData.length; i += 4) {
            posterizedData[i] = 0; // R
            posterizedData[i + 1] = 0; // G
            posterizedData[i + 2] = 0; // B
            posterizedData[i + 3] = originalData[i + 3]; // Preserve original alpha
        }
    } else {
        const step = 255 / (levels - 1);
        for (let i = 0; i < originalData.length; i += 4) {
            posterizedData[i]     = Math.round(originalData[i] / step) * step;
            posterizedData[i + 1] = Math.round(originalData[i + 1] / step) * step;
            posterizedData[i + 2] = Math.round(originalData[i + 2] / step) * step;
            posterizedData[i + 3] = originalData[i + 3]; // Preserve original alpha
        }
    }

    // 3. Edge Detection (Using Sobel operator)
    // Initialize edgeMap with all zeros (no edges)
    const edgeMap = new Uint8ClampedArray(width * height); 

    const sobelXKernel = [
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1]
    ];
    const sobelYKernel = [
        [-1, -2, -1],
        [ 0,  0,  0],
        [ 1,  2,  1]
    ];

    // Iterate image pixels (excluding 1-pixel border for Sobel kernel)
    // If image is smaller than 3x3, this loop won't run, and edgeMap remains all zeros.
    for (let y = 1; y < height - 1; y++) {
        for (let x = 1; x < width - 1; x++) {
            let gx = 0;
            let gy = 0;

            // Apply Sobel kernels to 3x3 neighborhood
            for (let ky = -1; ky <= 1; ky++) {
                for (let kx = -1; kx <= 1; kx++) {
                    const pixelIndexOffset = ((y + ky) * width + (x + kx)) * 4;
                    // Calculate grayscale intensity of the neighbor pixel from original data
                    const intensity = originalData[pixelIndexOffset] * 0.299 + 
                                      originalData[pixelIndexOffset + 1] * 0.587 + 
                                      originalData[pixelIndexOffset + 2] * 0.114;
                    
                    gx += intensity * sobelXKernel[ky + 1][kx + 1];
                    gy += intensity * sobelYKernel[ky + 1][kx + 1];
                }
            }

            const magnitude = Math.sqrt(gx * gx + gy * gy);
            
            if (magnitude > edgeThreshold) {
                edgeMap[y * width + x] = 255; // Mark as edge
            }
            // No 'else' needed as edgeMap is initialized to 0
        }
    }

    // 4. Combine Posterized Image and Edges
    // Start with a copy of the posterized data for the final image
    const finalPixelData = new Uint8ClampedArray(posterizedData);

    // Parse edgeColor string to RGB values
    let ecR = 0, ecG = 0, ecB = 0;
    const tempElem = document.createElement('div');
    tempElem.style.color = edgeColor;
    // Element must be in DOM to compute style, hide it to prevent layout shifts
    tempElem.style.position = 'fixed';
    tempElem.style.display = 'none'; 
    document.body.appendChild(tempElem); // Add to DOM
    const computedColor = getComputedStyle(tempElem).color;
    document.body.removeChild(tempElem); // Remove from DOM

    // Extract R, G, B from computedColor string (e.g., "rgb(r, g, b)")
    const colorParts = computedColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d\.]+)?\)/);
    if (colorParts && colorParts.length >= 4) {
        ecR = parseInt(colorParts[1]);
        ecG = parseInt(colorParts[2]);
        ecB = parseInt(colorParts[3]);
    } // Defaults to black (0,0,0) if parsing fails or color is invalid

    // Apply edges to the final pixel data
    if (edgeThickness > 0) { // Only apply edges if thickness is positive
        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                if (edgeMap[y * width + x] === 255) { // If this pixel is an edge point
                    // Apply edge color with specified thickness around (x,y)
                    // The loop creates a square of 'edgeThickness' pixels centered around (x,y)
                    // For T=1: dx,dy = 0. Colors 1 pixel. (x,y)
                    // For T=2: dx,dy = 0,1. Colors 2x2 block. Top-left is (x,y). Centered at (x+0.5, y+0.5)
                    // For T=3: dx,dy = -1,0,1. Colors 3x3 block. Center is (x,y)
                    const startOffset = -Math.floor((edgeThickness - 1) / 2);
                    const endOffset = Math.ceil((edgeThickness - 1) / 2);

                    for (let dy = startOffset; dy < startOffset + edgeThickness; dy++) {
                         for (let dx = startOffset; dx < startOffset + edgeThickness; dx++) {
                            const currentX = x + dx;
                            const currentY = y + dy;

                            if (currentX >= 0 && currentX < width && currentY >= 0 && currentY < height) {
                                const pixelIndex = (currentY * width + currentX) * 4;
                                finalPixelData[pixelIndex]     = ecR;
                                finalPixelData[pixelIndex + 1] = ecG;
                                finalPixelData[pixelIndex + 2] = ecB;
                                finalPixelData[pixelIndex + 3] = 255; // Edges are opaque
                            }
                        }
                    }
                }
            }
        }
    }


    // Create new ImageData from the final pixel data and put it onto the canvas
    const finalImageData = new ImageData(finalPixelData, width, height);
    ctx.putImageData(finalImageData, 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 Cartoon Filter Application allows users to transform images into a cartoon-like style. This tool achieves the cartoon effect by applying two main processes: posterization, which reduces the number of colors in the image, and edge detection, which outlines the prominent features in the image. Users can customize parameters such as the number of color levels, edge detection thresholds, edge color, and edge thickness. This tool is perfect for creating fun, stylized images for social media, graphic design, or personal projects.

Leave a Reply

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