Please bookmark this page to avoid losing your image tool!

Image Comic Panel Filter Effect Tool

(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,
    outlineThreshold = 60,
    outlineColorStr = "black",
    posterizeLevels = 5,
    enableHalftoneStr = "true",
    halftoneGridSize = 8,
    halftoneMaxDotRadius = 3,
    halftoneDotColorStr = "rgba(0,0,0,0.3)"
) {
    // 1. Parameter Parsing and Validation
    const enableHalftone = String(enableHalftoneStr).toLowerCase() === 'true' || String(enableHalftoneStr) === '1';
    posterizeLevels = Math.max(2, Math.floor(Number(posterizeLevels)));
    outlineThreshold = Number(outlineThreshold);
    halftoneGridSize = Math.max(1, Math.floor(Number(halftoneGridSize)));
    halftoneMaxDotRadius = Math.max(0, Number(halftoneMaxDotRadius));

    const width = originalImg.naturalWidth || originalImg.width;
    const height = originalImg.naturalHeight || originalImg.height;

    // 2. Canvas Setup
    const outputCanvas = document.createElement('canvas');
    outputCanvas.width = width;
    outputCanvas.height = height;
    const ctx = outputCanvas.getContext('2d');
    if (!ctx) {
        console.error("Could not get 2D context from output canvas.");
        return outputCanvas; // Return empty canvas or throw error
    }
    
    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = width;
    tempCanvas.height = height;
    const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });

    if (!tempCtx) {
        console.error("Could not get 2D context from temporary canvas.");
        ctx.drawImage(originalImg, 0, 0, width, height); // Draw original as fallback
        return outputCanvas;
    }
    
    tempCtx.drawImage(originalImg, 0, 0, width, height);
    
    let originalImageData;
    try {
        originalImageData = tempCtx.getImageData(0, 0, width, height);
    } catch (e) {
        console.error("Error getting image data. This might be due to cross-origin restrictions if the image isn't hosted on the same domain or lacks CORS headers.", e);
        // Fallback: draw original image and return
        ctx.drawImage(originalImg, 0, 0, width, height);
        return outputCanvas;
    }
    const originalPixels = originalImageData.data;

    // Helper function for color parsing
    function getRGBA(colorStr) {
        const c = document.createElement('canvas');
        c.width = 1;
        c.height = 1;
        const tinyCtx = c.getContext('2d', { willReadFrequently: true });
        if (!tinyCtx) return { r:0, g:0, b:0, a:255 }; // Fallback color
        tinyCtx.fillStyle = colorStr;
        tinyCtx.fillRect(0, 0, 1, 1);
        const data = tinyCtx.getImageData(0, 0, 1, 1).data;
        return { r: data[0], g: data[1], b: data[2], a: data[3] };
    }

    // 3. Posterization
    const posterizedPixels = new Uint8ClampedArray(originalPixels.length);
    if (posterizeLevels <= 1) posterizeLevels = 2; // Should be caught by Math.max earlier
    const سلم = 255 / (posterizeLevels - 1); // 'سلم' (sullam) means 'ladder' or 'step' in Arabic
    for (let i = 0; i < originalPixels.length; i += 4) {
        posterizedPixels[i]   = Math.round(originalPixels[i]   / سلم) * سلم;
        posterizedPixels[i+1] = Math.round(originalPixels[i+1] / سلم) * سلم;
        posterizedPixels[i+2] = Math.round(originalPixels[i+2] / سلم) * سلم;
        posterizedPixels[i+3] = originalPixels[i+3]; // Alpha
    }
    ctx.putImageData(new ImageData(posterizedPixels, width, height), 0, 0);

    // 4. Halftone Effect (if enabled)
    if (enableHalftone && halftoneMaxDotRadius > 0 && halftoneGridSize > 0) {
        ctx.fillStyle = halftoneDotColorStr;
        for (let y = 0; y < height; y += halftoneGridSize) {
            for (let x = 0; x < width; x += halftoneGridSize) {
                let sumLuminance = 0;
                let numBlockPixels = 0;
                
                const blockStartX = x;
                const blockStartY = y;
                const blockEndX = Math.min(width, x + halftoneGridSize);
                const blockEndY = Math.min(height, y + halftoneGridSize);

                for (let by = blockStartY; by < blockEndY; by++) {
                    for (let bx = blockStartX; bx < blockEndX; bx++) {
                        const idx = (by * width + bx) * 4;
                        const r = posterizedPixels[idx];
                        const g = posterizedPixels[idx+1];
                        const b = posterizedPixels[idx+2];
                        sumLuminance += (0.299 * r + 0.587 * g + 0.114 * b);
                        numBlockPixels++;
                    }
                }

                if (numBlockPixels > 0) {
                    const avgLuminance = sumLuminance / numBlockPixels;
                    const dotRadius = (1 - (avgLuminance / 255)) * halftoneMaxDotRadius;
                    if (dotRadius > 0.1) { // Avoid drawing tiny/invisible dots
                        ctx.beginPath();
                        ctx.arc(
                            x + halftoneGridSize / 2,
                            y + halftoneGridSize / 2,
                            dotRadius,
                            0,
                            2 * Math.PI
                        );
                        ctx.fill();
                    }
                }
            }
        }
    }

    // 5. Outlines (Sobel)
    const grayscaleMap = new Uint8Array(width * height);
    for (let i = 0; i < posterizedPixels.length; i += 4) {
        const r = posterizedPixels[i];
        const g = posterizedPixels[i+1];
        const b = posterizedPixels[i+2];
        grayscaleMap[i / 4] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
    }

    const outlinePixelData = new Uint8ClampedArray(originalPixels.length).fill(0); // Initialize with transparent
    const outlineActualColor = getRGBA(outlineColorStr);

    const kernelX = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
    const kernelY = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];

    for (let y = 1; y < height - 1; y++) {
        for (let x = 1; x < width - 1; x++) {
            let gx = 0;
            let gy = 0;
            for (let ky = -1; ky <= 1; ky++) {
                for (let kx = -1; kx <= 1; kx++) {
                    const R_idx = ((y + ky) * width + (x + kx));
                    const grayVal = grayscaleMap[R_idx];
                    gx += grayVal * kernelX[ky + 1][kx + 1];
                    gy += grayVal * kernelY[ky + 1][kx + 1];
                }
            }
            const magnitude = Math.sqrt(gx * gx + gy * gy);
            if (magnitude > outlineThreshold) {
                const idx = (y * width + x) * 4;
                outlinePixelData[idx]     = outlineActualColor.r;
                outlinePixelData[idx + 1] = outlineActualColor.g;
                outlinePixelData[idx + 2] = outlineActualColor.b;
                outlinePixelData[idx + 3] = outlineActualColor.a; 
            }
        }
    }
    
    const outlineCanvas = document.createElement('canvas');
    outlineCanvas.width = width;
    outlineCanvas.height = height;
    const outlineCtx = outlineCanvas.getContext('2d');
    
    if (outlineCtx) {
        outlineCtx.putImageData(new ImageData(outlinePixelData, width, height), 0, 0);
        ctx.drawImage(outlineCanvas, 0, 0);
    } else {
        console.error("Could not get 2D context for outline canvas.");
    }

    return outputCanvas;
}

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 Comic Panel Filter Effect Tool allows users to transform their images into comic-style visuals. It applies a combination of posterization to reduce color depth, adds outlines to create a comic-like effect, and can produce halftone patterns for a unique, stylized appearance. This tool is perfect for artists, graphic designers, and anyone looking to create comic-like images for social media, graphic novels, or personal projects.

Leave a Reply

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