Please bookmark this page to avoid losing your image tool!

Image Tattoo Art 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.
function processImage(originalImg, shadingLevels = 3, edgeThreshold = 80) {
    // Ensure shadingLevels is an integer >= 2 for meaningful posterization with distinct levels
    shadingLevels = Math.max(2, Math.floor(shadingLevels));
    // Ensure edgeThreshold is non-negative
    edgeThreshold = Math.max(0, edgeThreshold);

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

    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    // The 'willReadFrequently' hint can improve performance for frequent getImageData/putImageData calls
    const ctx = canvas.getContext('2d', { willReadFrequently: true });

    if (width === 0 || height === 0) {
        // Return an empty canvas for zero-dimension images
        return canvas; 
    }
    
    // Step 1: Draw the original image to the canvas
    try {
        ctx.drawImage(originalImg, 0, 0, width, height);
    } catch (e) {
        // If drawing the image fails (e.g., originalImg is not a valid image source)
        console.error("Error drawing original image:", e);
        // Return the blank canvas
        return canvas;
    }
    
    let imageData;
    try {
        imageData = ctx.getImageData(0, 0, width, height);
    } catch (e) {
        // This typically occurs due to canvas tainting (e.g., cross-origin image without CORS)
        console.error("Error getting ImageData (likely cross-origin issue):", e);
        // When the canvas is tainted, we cannot process its pixels.
        // Returning the canvas as-is will show the original image (drawn in the try block above).
        return canvas;
    }
    
    const data = imageData.data; // This is an Uint8ClampedArray view on imageData.buffer

    // Step 2: Prepare grayscale data from the original image
    // This will be used for both edge detection input and base for posterization
    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];
        // Standard luminosity method for grayscale conversion
        const gray = 0.299 * r + 0.587 * g + 0.114 * b;
        grayData[i / 4] = gray;
    }

    // Step 3: Perform Edge Detection using Sobel Operator
    const edgeMagnitudes = new Float32Array(width * height); // Use Float32Array for precision of magnitudes
    
    // Sobel kernels for X and Y gradients
    const kernelX = [
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1]
    ];
    const kernelY = [
        [-1, -2, -1],
        [ 0,  0,  0],
        [ 1,  2,  1]
    ];

    // Apply Sobel operator only if image is large enough for a 3x3 kernel
    // Otherwise, edgeMagnitudes will remain all zeros, resulting in no edges.
    if (width >= 3 && height >= 3) {
        for (let y = 1; y < height - 1; y++) { // Iterate y from 1 to height-2 (inclusive)
            for (let x = 1; x < width - 1; x++) { // Iterate x from 1 to width-2 (inclusive)
                let sumX = 0;
                let sumY = 0;
                // Apply 3x3 kernel to the neighborhood
                for (let ky = -1; ky <= 1; ky++) {
                    for (let kx = -1; kx <= 1; kx++) {
                        // Get grayscale value of the neighbor pixel
                        const neighborGrayValue = grayData[(y + ky) * width + (x + kx)];
                        sumX += neighborGrayValue * kernelX[ky + 1][kx + 1];
                        sumY += neighborGrayValue * kernelY[ky + 1][kx + 1];
                    }
                }
                // Calculate gradient magnitude
                const magnitude = Math.sqrt(sumX * sumX + sumY * sumY);
                edgeMagnitudes[y * width + x] = magnitude;
            }
        }
    }
    // Pixels on the border (x=0, y=0, x=width-1, y=height-1) will have edgeMagnitude = 0 by default

    // Step 4: Create the final image
    // Pixels are either black (if it's a strong edge) or a posterized grayscale value.
    // We modify the 'data' array (which is imageData.data) in place for efficiency.
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const i = (y * width + x) * 4;      // Index for RGBA components in 'data' array
            const grayIdx = y * width + x;      // Index for 'grayData' and 'edgeMagnitudes'

            let isStrongEdge = false;
            // Check if the edge magnitude at this pixel exceeds the threshold
            // Border pixels have magnitude 0, so they won't be edges if threshold > 0.
            if (edgeMagnitudes[grayIdx] > edgeThreshold) {
                isStrongEdge = true;
            }

            if (isStrongEdge) {
                data[i]     = 0; // Red = Black
                data[i + 1] = 0; // Green = Black
                data[i + 2] = 0; // Blue = Black
            } else {
                const originalGrayValue = grayData[grayIdx];
                let posterizedGrayValue;
                
                // Posterize the grayscale value to one of 'shadingLevels'
                // The formula maps 0-255 to 'shadingLevels' discrete values.
                // E.g., for shadingLevels=3, possible values are approx 0, 128, 255.
                // (shadingLevels - 1) is guaranteed to be >= 1 here.
                posterizedGrayValue = Math.round(originalGrayValue * (shadingLevels - 1) / 255) * (255 / (shadingLevels - 1));
                
                // Clamp to ensure value is strictly within [0, 255] after rounding and potential floating point inaccuracies
                posterizedGrayValue = Math.max(0, Math.min(255, posterizedGrayValue)); 
                
                data[i]     = posterizedGrayValue; // Red
                data[i + 1] = posterizedGrayValue; // Green
                data[i + 2] = posterizedGrayValue; // Blue
            }
            data[i + 3] = 255; // Alpha: Tattoo effect is generally fully opaque
        }
    }

    // Step 5: Put the modified image data back onto the canvas
    ctx.putImageData(imageData, 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 Tattoo Art Filter Effect Tool allows users to transform their images into a stylized tattoo art effect. By applying a posterization technique combined with edge detection, the tool enhances images to feature bold black outlines alongside a simplified gray shading style. This effect can be particularly useful for artists seeking to create tattoo designs or for anyone looking to generate unique visual interpretations of their photographs. The tool offers customizable options for shading levels and edge sensitivity, enabling a range of artistic expressions.

Leave a Reply

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