Please bookmark this page to avoid losing your image tool!

Image Roy Lichtenstein 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, dotSize = 8, outlineThreshold = 100, paletteColorsStr = "255,0,0;255,255,0;0,0,255;0,0,0;255,255,255;253,224,200", skinDotColorStr = "255,0,0", primaryDotColorStr = "0,0,0") {
    const w = originalImg.width;
    const h = originalImg.height;

    // 1. Initialize canvases and contexts
    const outputCanvas = document.createElement('canvas');
    outputCanvas.width = w;
    outputCanvas.height = h;
    const outputCtx = outputCanvas.getContext('2d');

    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = w;
    tempCanvas.height = h;
    const tempCtx = tempCanvas.getContext('2d');

    // Helper to parse color strings (e.g., "255,0,0") to {r,g,b}
    function parseColor(str) {
        const parts = str.split(',').map(Number);
        if (parts.length === 3 && parts.every(num => !isNaN(num) && num >= 0 && num <= 255)) {
            return { r: parts[0], g: parts[1], b: parts[2] };
        }
        // Fallback to black if parsing fails
        return { r: 0, g: 0, b: 0 }; 
    }
    
    // Helper to format {r,g,b} to "rgb(r,g,b)" string
    function toRGBString(colorObj) {
        return `rgb(${colorObj.r},${colorObj.g},${colorObj.b})`;
    }

    // Predefined named colors for special logic, using default values
    const DEFAULT_COLORS_NAMED_MAP = {
        "255,0,0": "red",
        "255,255,0": "yellow",
        "0,0,255": "blue",
        "0,0,0": "black",
        "255,255,255": "white",
        "253,224,200": "skin" // Default skin tone
    };

    // Parse input palette string
    const palette = paletteColorsStr.split(';').map(s => {
        const p = parseColor(s);
        const sNormalized = `${p.r},${p.g},${p.b}`; // Normalize for map lookup
        const name = DEFAULT_COLORS_NAMED_MAP[sNormalized] || "custom";
        return { ...p, name: name };
    });
    
    if (palette.length === 0) { // Ensure palette is not empty
        palette.push({ r:0, g:0, b:0, name: "black"}); // Default to black if palette is empty
    }


    const skinColorEntry = palette.find(c => c.name === "skin");
    const whiteColorEntry = palette.find(c => c.name === "white");
    const blackColorEntry = palette.find(c => c.name === "black");

    const skinDotRGB = parseColor(skinDotColorStr);
    const primaryDotRGB = parseColor(primaryDotColorStr);


    // Helper function: Euclidean distance for colors
    function colorDistance(c1, c2) {
        return Math.sqrt(Math.pow(c1.r - c2.r, 2) + Math.pow(c1.g - c2.g, 2) + Math.pow(c1.b - c2.b, 2));
    }

    // Helper function: Find closest color in palette
    function getClosestPaletteColor(r, g, b) {
        let closestColor = palette[0];
        let minDistance = Infinity;
        for (const pColor of palette) {
            const dist = colorDistance({r,g,b}, pColor);
            if (dist < minDistance) {
                minDistance = dist;
                closestColor = pColor;
            }
        }
        return closestColor;
    }

    // 2. Posterization and Grayscale original for intensity
    tempCtx.drawImage(originalImg, 0, 0, w, h);
    const originalImgData = tempCtx.getImageData(0, 0, w, h);
    const posterizedImgData = tempCtx.createImageData(w, h);
    const originalLuminanceMap = new Uint8ClampedArray(w * h); 

    for (let i = 0; i < originalImgData.data.length; i += 4) {
        const r = originalImgData.data[i];
        const g = originalImgData.data[i+1];
        const b = originalImgData.data[i+2];

        originalLuminanceMap[i / 4] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);

        const closest = getClosestPaletteColor(r, g, b);
        posterizedImgData.data[i]   = closest.r;
        posterizedImgData.data[i+1] = closest.g;
        posterizedImgData.data[i+2] = closest.b;
        posterizedImgData.data[i+3] = 255;
    }

    // 3. Halftone Dots
    for (let y = 0; y < h; y += dotSize) {
        for (let x = 0; x < w; x += dotSize) {
            const sampleX = Math.floor(Math.min(x + dotSize / 2, w - 1));
            const sampleY = Math.floor(Math.min(y + dotSize / 2, h - 1));
            
            const pIndex = (sampleY * w + sampleX) * 4;
            const cellPosterizedColor_r = posterizedImgData.data[pIndex];
            const cellPosterizedColor_g = posterizedImgData.data[pIndex+1];
            const cellPosterizedColor_b = posterizedImgData.data[pIndex+2];
            
            const currentPosterizedPaletteEntry = palette.find(p => p.r === cellPosterizedColor_r && p.g === cellPosterizedColor_g && p.b === cellPosterizedColor_b) 
                                               || { name: "custom", r: cellPosterizedColor_r, g: cellPosterizedColor_g, b: cellPosterizedColor_b };

            const originalCellLuminance = originalLuminanceMap[sampleY * w + sampleX];

            let cellBgFill = toRGBString(currentPosterizedPaletteEntry);
            let dotFill = toRGBString(primaryDotRGB);
            let drawThisDot = true;

            if (whiteColorEntry && currentPosterizedPaletteEntry.name === "white") {
                cellBgFill = toRGBString(whiteColorEntry);
                drawThisDot = false; 
            } else if (blackColorEntry && currentPosterizedPaletteEntry.name === "black") {
                cellBgFill = toRGBString(blackColorEntry);
                drawThisDot = false; 
            } else if (skinColorEntry && currentPosterizedPaletteEntry.name === "skin") {
                cellBgFill = whiteColorEntry ? toRGBString(whiteColorEntry) : 'rgb(255,255,255)';
                dotFill = toRGBString(skinDotRGB);
            }
            // Default: primary colors use posterized color as bg, primaryDotRGB for dots.

            outputCtx.fillStyle = cellBgFill;
            outputCtx.fillRect(x, y, dotSize, dotSize);

            if (drawThisDot) {
                const intensity = originalCellLuminance / 255.0; 
                let radius = (dotSize / 2.1) * (1.0 - intensity); 
                radius = Math.max(0, radius); 

                if (radius > dotSize * 0.05) { 
                    outputCtx.fillStyle = dotFill;
                    outputCtx.beginPath();
                    outputCtx.arc(x + dotSize / 2, y + dotSize / 2, radius, 0, 2 * Math.PI);
                    outputCtx.fill();
                }
            }
        }
    }

    // 4. Outlines (Sobel filter)
    const outlineCanvas = document.createElement('canvas');
    outlineCanvas.width = w;
    outlineCanvas.height = h;
    const outlineCtx = outlineCanvas.getContext('2d');

    tempCtx.drawImage(originalImg, 0, 0, w, h); 
    const sourceForSobelGray = tempCtx.getImageData(0, 0, w, h);
    const grayscaleImgDataForSobel = tempCtx.createImageData(w, h);

    for (let i = 0; i < sourceForSobelGray.data.length; i += 4) {
        const r = sourceForSobelGray.data[i];
        const g = sourceForSobelGray.data[i+1];
        const b = sourceForSobelGray.data[i+2];
        const avg = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
        grayscaleImgDataForSobel.data[i] = avg;
        grayscaleImgDataForSobel.data[i+1] = avg;
        grayscaleImgDataForSobel.data[i+2] = avg;
        grayscaleImgDataForSobel.data[i+3] = 255;
    }
    
    const sobelFinalData = outlineCtx.createImageData(w, h);
    const GxMatrix = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
    const GyMatrix = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];

    function getPixelGrayscale(imgData, x_coord, y_coord) {
        if (x_coord < 0 || x_coord >= imgData.width || y_coord < 0 || y_coord >= imgData.height) return 0;
        return imgData.data[(y_coord * imgData.width + x_coord) * 4];
    }

    for (let y_idx = 0; y_idx < h; y_idx++) {
        for (let x_idx = 0; x_idx < w; x_idx++) {
            let Gx = 0;
            let Gy = 0;
            for (let i = 0; i < 3; i++) {
                for (let j = 0; j < 3; j++) {
                    const pixelVal = getPixelGrayscale(grayscaleImgDataForSobel, x_idx + j - 1, y_idx + i - 1);
                    Gx += pixelVal * GxMatrix[i][j];
                    Gy += pixelVal * GyMatrix[i][j];
                }
            }
            const magnitude = Math.sqrt(Gx * Gx + Gy * Gy);
            const K_idx = (y_idx * w + x_idx) * 4;
            if (magnitude > outlineThreshold) {
                sobelFinalData.data[K_idx] = 0;     
                sobelFinalData.data[K_idx + 1] = 0; 
                sobelFinalData.data[K_idx + 2] = 0; 
                sobelFinalData.data[K_idx + 3] = 255; 
            } else {
                sobelFinalData.data[K_idx + 3] = 0; 
            }
        }
    }
    outlineCtx.putImageData(sobelFinalData, 0, 0);

    outputCtx.drawImage(outlineCanvas, 0, 0);

    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 Roy Lichtenstein Filter Effect Tool allows users to transform images into a stylized pop art version reminiscent of Roy Lichtenstein’s iconic work. By adjusting parameters like dot size and color palette, users can create vibrant images that use halftone dots to simulate color blending and outlines, making it ideal for artists, graphic designers, or anyone looking to give their images a unique, artistic flair. This tool can be used for various purposes including enhancing social media posts, creating eye-catching prints, or simply exploring creative image effects.

Leave a Reply

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