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!
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.