You can edit the below JavaScript code to customize the image tool.
/**
* Converts an image to a cel-shaded vector-like style.
*
* This function applies two main effects and composites them:
* 1. Posterization: Reduces the number of colors in the image to create flat, cartoon-like color areas.
* 2. Edge Detection: Uses a Sobel filter on the original image's grayscale version to find prominent
* edges, which are then drawn as outlines.
*
* The final result mimics the look of classic cel animation or vector art.
*
* @param {Image} originalImg The input Image object to process.
* @param {number} [levels=5] The number of color levels for posterization. Must be >= 2.
* @param {number} [edgeThreshold=80] The sensitivity for edge detection. Higher values find fewer, stronger edges.
* @param {string} [outlineColor='#000000'] The color of the outlines (any valid CSS color string).
* @param {number} [outlineWidth=2] The thickness of the outlines in pixels.
* @returns {Promise<HTMLCanvasElement>} A promise that resolves to a new canvas element containing the cel-shaded image.
*/
async function processImage(originalImg, levels = 5, edgeThreshold = 80, outlineColor = '#000000', outlineWidth = 2) {
// 1. SETUP
// Sanitize parameters to ensure they are within a valid range.
levels = Math.max(2, Math.floor(levels));
edgeThreshold = Math.max(0, edgeThreshold);
outlineWidth = Math.max(1, Math.floor(outlineWidth));
const width = originalImg.width;
const height = originalImg.height;
// Create a working canvas to read pixel data from the original image.
// This avoids tainting the original image object.
const workingCanvas = document.createElement('canvas');
workingCanvas.width = width;
workingCanvas.height = height;
const workingCtx = workingCanvas.getContext('2d', {
willReadFrequently: true // Optimization hint for frequent getImageData calls.
});
workingCtx.drawImage(originalImg, 0, 0, width, height);
const originalImageData = workingCtx.getImageData(0, 0, width, height);
// Prepare data arrays for processing.
const posterizedData = new Uint8ClampedArray(originalImageData.data.length);
const grayscaleData = new Uint8ClampedArray(width * height);
const edgeMap = new Uint8ClampedArray(width * height);
// 2. POSTERIZATION
// This step reduces the number of colors in the image to `levels`.
const factor = 255 / (levels - 1);
for (let i = 0; i < originalImageData.data.length; i += 4) {
posterizedData[i] = Math.round(originalImageData.data[i] / factor) * factor;
posterizedData[i + 1] = Math.round(originalImageData.data[i + 1] / factor) * factor;
posterizedData[i + 2] = Math.round(originalImageData.data[i + 2] / factor) * factor;
posterizedData[i + 3] = 255; // Set full alpha.
}
// 3. EDGE DETECTION (SOBEL OPERATOR)
// 3a. Convert the original image to grayscale for more accurate luminosity-based edge detection.
for (let i = 0, j = 0; i < originalImageData.data.length; i += 4, j++) {
const r = originalImageData.data[i];
const g = originalImageData.data[i + 1];
const b = originalImageData.data[i + 2];
// Use standard NTSC/PAL luminance calculation.
grayscaleData[j] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
}
// 3b. Apply Sobel filter to the grayscale data to generate an edge map.
// We skip the 1-pixel border of the image to simplify kernel application.
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
const idx = y * width + x;
// Get grayscale values of the 3x3 pixel neighborhood.
const g_tl = grayscaleData[idx - width - 1], g_t = grayscaleData[idx - width], g_tr = grayscaleData[idx - width + 1];
const g_l = grayscaleData[idx - 1], g_r = grayscaleData[idx + 1];
const g_bl = grayscaleData[idx + width - 1], g_b = grayscaleData[idx + width], g_br = grayscaleData[idx + width + 1];
// Apply Sobel kernels.
const pixelX = -g_tl - 2 * g_l - g_bl + g_tr + 2 * g_r + g_br;
const pixelY = -g_tl - 2 * g_t - g_tr + g_bl + 2 * g_b + g_br;
// Calculate gradient magnitude and threshold.
if (Math.sqrt(pixelX * pixelX + pixelY * pixelY) > edgeThreshold) {
edgeMap[idx] = 1;
}
}
}
// 3c. Apply dilation to the edge map to control the `outlineWidth`.
const dilatedEdgeMap = new Uint8ClampedArray(width * height);
if (outlineWidth > 1) {
const halfWidth = Math.floor(outlineWidth / 2);
// This process "stamps" a square of `outlineWidth` for each edge pixel.
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (edgeMap[y * width + x] === 1) {
for (let dy = -halfWidth; dy < outlineWidth - halfWidth; dy++) {
for (let dx = -halfWidth; dx < outlineWidth - halfWidth; dx++) {
const ny = y + dy;
const nx = x + dx;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
dilatedEdgeMap[ny * width + nx] = 1;
}
}
}
}
}
}
} else {
dilatedEdgeMap.set(edgeMap); // No dilation needed for 1px width.
}
// 4. COMPOSITING
// Create the final canvas that will be returned.
const finalCanvas = document.createElement('canvas');
finalCanvas.width = width;
finalCanvas.height = height;
const finalCtx = finalCanvas.getContext('2d');
// 4a. Parse the outlineColor string into RGB components.
// This robust method handles all valid CSS color formats (e.g., "red", "#f00", "rgb(255,0,0)").
const tempDiv = document.createElement('div');
tempDiv.style.color = outlineColor;
document.body.appendChild(tempDiv);
const computedColor = window.getComputedStyle(tempDiv).color;
document.body.removeChild(tempDiv);
const colorMatch = computedColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
const [outlineR, outlineG, outlineB] = colorMatch.slice(1).map(Number);
// 4b. Create the final image data by combining the posterized layer and the edge layer.
const finalImageData = finalCtx.createImageData(width, height);
for (let i = 0, j = 0; i < finalImageData.data.length; i += 4, j++) {
if (dilatedEdgeMap[j] === 1) {
// If the pixel is part of a dilated edge, use the outline color.
finalImageData.data[i] = outlineR;
finalImageData.data[i + 1] = outlineG;
finalImageData.data[i + 2] = outlineB;
finalImageData.data[i + 3] = 255;
} else {
// Otherwise, use the posterized color from the corresponding pixel.
finalImageData.data[i] = posterizedData[i];
finalImageData.data[i + 1] = posterizedData[i + 1];
finalImageData.data[i + 2] = posterizedData[i + 2];
finalImageData.data[i + 3] = 255;
}
}
// 4c. Draw the final composited data onto the canvas.
finalCtx.putImageData(finalImageData, 0, 0);
// 5. RETURN
return finalCanvas;
}
Free Image Tool Creator
Can't find the image tool you're looking for? Create one based on your own needs now!
The Image To Cel-Shaded Vector Converter is an online tool that transforms images into a cel-shaded, vector-like style reminiscent of classic animation. This tool utilizes posterization to simplify the color palette, creating flat, cartoon-like areas, combined with edge detection to highlight prominent outlines. Users can customize the number of color levels, edge sensitivity, outline color, and width to achieve their desired artistic effect. This tool is perfect for artists, graphic designers, and hobbyists looking to create stylized artwork, illustrations, or unique graphics for games and other media.