You can edit the below JavaScript code to customize the image tool.
async function processImage(originalImg, edgeThreshold = 80, posterizationLevels = 4) {
// Use naturalWidth/Height for actual image dimensions, fallback to width/height attributes
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
// Handle cases where image dimensions are not valid (e.g., image not loaded)
if (width === 0 || height === 0) {
console.error("Image has zero width or height. Ensure the image is loaded and valid.");
// Create a small placeholder canvas
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = Math.max(1, width);
emptyCanvas.height = Math.max(1, height);
const ctx = emptyCanvas.getContext('2d');
if (ctx) { // Context might be null in some environments
ctx.fillStyle = 'lightgray';
ctx.fillRect(0, 0, emptyCanvas.width, emptyCanvas.height);
}
return emptyCanvas;
}
// Create a temporary canvas to draw the original image and access its pixel data
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
// Add { willReadFrequently: true } for potential performance optimization when using getImageData repeatedly.
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
if (!tempCtx) {
console.error("Could not get 2D context from temporary canvas.");
// Fallback: return a canvas with the original image if possible, or an empty one
const fallbackCanvas = document.createElement('canvas');
fallbackCanvas.width = width; fallbackCanvas.height = height;
const fbCtx = fallbackCanvas.getContext('2d');
if (fbCtx) try { fbCtx.drawImage(originalImg,0,0); } catch(e){}
return fallbackCanvas;
}
tempCtx.drawImage(originalImg, 0, 0, width, height);
let imageData;
try {
imageData = tempCtx.getImageData(0, 0, width, height);
} catch (e) {
// This can happen due to cross-origin restrictions if the image is from another domain
// and lacks appropriate CORS headers.
console.error("Could not get image data. Possible cross-origin issue.", e);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = width; errorCanvas.height = height;
const errCtx = errorCanvas.getContext('2d');
if(errCtx) { // Try to draw original image as fallback
try { errCtx.drawImage(originalImg, 0, 0, width, height); } catch(drawErr){}
}
return errorCanvas;
}
const data = imageData.data; // This is a Uint8ClampedArray: [r1,g1,b1,a1, r2,g2,b2,a2, ...]
// 1. Convert to Grayscale (for edge detection)
// Create a 1D array to store grayscale values for each pixel
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];
// Using standard NTSC/PAL luminance calculation for grayscale conversion
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
grayData[i / 4] = gray; // Store the grayscale value (one value per pixel)
}
// 2. Edge Detection (Sobel Operator)
// Create a 1D array to store edge information (255 for edge, 0 for non-edge)
const edgePixelData = new Uint8ClampedArray(width * height);
// Sobel kernels for detecting horizontal (Gx) and vertical (Gy) edges
const Gx = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
];
const Gy = [
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]
];
// Iterate through each pixel (excluding image borders to allow 3x3 kernel application)
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let sumX = 0; // Gradient in X direction
let sumY = 0; // Gradient in Y direction
// Apply Sobel kernels by convolving with the 3x3 neighborhood of the current pixel
for (let ky = -1; ky <= 1; ky++) { // Kernel Y offset
for (let kx = -1; kx <= 1; kx++) { // Kernel X offset
const pixelIndex = (y + ky) * width + (x + kx); // Index in 1D grayData array
const pixelVal = grayData[pixelIndex];
sumX += pixelVal * Gx[ky + 1][kx + 1];
sumY += pixelVal * Gy[ky + 1][kx + 1];
}
}
const magnitude = Math.sqrt(sumX * sumX + sumY * sumY); // Gradient magnitude
const currentIndex = y * width + x; // Index of the current pixel in 1D arrays
if (magnitude > edgeThreshold) {
edgePixelData[currentIndex] = 255; // Mark as an edge pixel (white simplifies later visualization if needed)
} else {
edgePixelData[currentIndex] = 0; // Mark as a non-edge pixel (black)
}
}
}
// Note: Pixels on the very border (x=0, y=0, x=width-1, y=height-1)
// are not processed by the Sobel loop and will have edgePixelData[idx] = 0 by default.
// 3. Posterization and Combining with Edges
// Create the output canvas element that will be returned
const outputCanvas = document.createElement('canvas');
outputCanvas.width = width;
outputCanvas.height = height;
const outputCtx = outputCanvas.getContext('2d');
if (!outputCtx) {
console.error("Could not get 2D context from output canvas.");
return tempCanvas; // Fallback to the temporary canvas (original image)
}
const finalImageData = outputCtx.createImageData(width, height);
const finalData = finalImageData.data;
// Ensure posterizationLevels is at least 1
const pLevelsEffective = Math.max(1, Math.floor(posterizationLevels));
const posterizeSingleChannel = (value, levels) => {
if (levels <= 1) {
// If 1 level is specified, map all color values to a single representative value (e.g., 128 for mid-tone)
return 128;
}
// For L levels (where L >= 2), we want L distinct output values.
// These values are typically spread evenly, e.g., 0, 255/(L-1), 2*255/(L-1), ..., 255.
// The formula Math.round(value / step) * step maps the input value to the nearest of these L values.
const step = 255 / (levels - 1);
return Math.max(0, Math.min(255, Math.round(value / step) * step)); // Clamp to 0-255
};
// Iterate through each pixel of the original image data
for (let i = 0; i < data.length; i += 4) {
const pixelArrayIndex = i / 4; // Index of the pixel (from 0 to width*height - 1)
const y = Math.floor(pixelArrayIndex / width);
const x = pixelArrayIndex % width;
const r_orig = data[i];
const g_orig = data[i + 1];
const b_orig = data[i + 2];
const a_orig = data[i + 3];
// Apply posterization to each color channel (R, G, B)
const pr = posterizeSingleChannel(r_orig, pLevelsEffective);
const pg = posterizeSingleChannel(g_orig, pLevelsEffective);
const pb = posterizeSingleChannel(b_orig, pLevelsEffective);
// Check if the current pixel is an edge (based on Sobel operator results)
let isEdgePixel = false;
// Edges are only reliably detected for non-border pixels (where the 3x3 kernel fully fits)
if (x > 0 && x < width - 1 && y > 0 && y < height - 1) {
if (edgePixelData[y * width + x] === 255) { // Check the edge map
isEdgePixel = true;
}
}
// Set the final pixel data: black for edges, posterized color otherwise
if (isEdgePixel) {
finalData[i] = 0; // Red channel - black for edge
finalData[i + 1] = 0; // Green channel - black for edge
finalData[i + 2] = 0; // Blue channel - black for edge
finalData[i + 3] = a_orig; // Preserve original alpha for the edge
} else {
finalData[i] = pr; // Posterized red channel
finalData[i + 1] = pg; // Posterized green channel
finalData[i + 2] = pb; // Posterized blue channel
finalData[i + 3] = a_orig; // Preserve original alpha
}
}
// Put the processed pixel data onto the output canvas
outputCtx.putImageData(finalImageData, 0, 0);
return outputCanvas; // Return the canvas element with the toon-shaded image
}
Free Image Tool Creator
Can't find the image tool you're looking for? Create one based on your own needs now!
The Image Toon Shading Filter is an online tool that transforms images to give them a cartoon-like appearance. It applies edge detection and color posterization to create a stylized effect, enhancing outlines and reducing color complexity. This tool is ideal for artists looking to create unique artwork, for social media users wishing to enhance their photos, or for anyone interested in adding a playful touch to their images.