You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Applies an anime-style cel-shading effect to an image.
* This is achieved by combining two techniques:
* 1. Posterization: Reduces the number of colors in the image to create a "flat" look.
* 2. Edge Detection: Uses a Sobel filter to find edges and draw outlines.
*
* @param {Image} originalImg The original javascript Image object to process.
* @param {number} [levels=4] The number of color levels for posterization. Fewer levels result in a flatter, more cartoonish look. Must be 2 or greater.
* @param {number} [threshold=60] The sensitivity for edge detection (0-255). A higher value detects only stronger edges.
* @param {string} [lineColor='0,0,0'] A comma-separated RGB string for the outlines (e.g., '0,0,0' for black).
* @returns {Promise<HTMLCanvasElement>} A canvas element with the cel-shaded image.
*/
async function processImage(originalImg, levels = 4, threshold = 60, lineColor = '0,0,0') {
// 1. Setup: Create canvas and get image data
const canvas = document.createElement('canvas');
// Use { willReadFrequently: true } for performance optimization with frequent getImageData calls.
const ctx = canvas.getContext('2d', {
willReadFrequently: true
});
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
canvas.width = width;
canvas.height = height;
ctx.drawImage(originalImg, 0, 0, width, height);
const sourceImageData = ctx.getImageData(0, 0, width, height);
const sourceData = sourceImageData.data;
// 2. Prepare data structures for processing
const resultImageData = ctx.createImageData(width, height);
const resultData = resultImageData.data;
const [lineR, lineG, lineB] = lineColor.split(',').map(Number);
// 3. Convert the source image to grayscale for edge detection
// Using the luminosity method for perceived brightness: (0.299*R + 0.587*G + 0.114*B)
const grayscaleData = new Uint8ClampedArray(width * height);
for (let i = 0; i < sourceData.length; i += 4) {
const r = sourceData[i];
const g = sourceData[i + 1];
const b = sourceData[i + 2];
grayscaleData[i / 4] = 0.299 * r + 0.587 * g + 0.114 * b;
}
// 4. Detect edges using the Sobel operator
const edgeMap = new Uint8Array(grayscaleData.length);
const sobelX = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
];
const sobelY = [
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]
];
// Iterate over each pixel (excluding borders) to apply the Sobel filter
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let gx = 0;
let gy = 0;
// Apply 3x3 Sobel kernels
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const idx = (y + ky) * width + (x + kx);
const pixelValue = grayscaleData[idx];
gx += pixelValue * sobelX[ky + 1][kx + 1];
gy += pixelValue * sobelY[ky + 1][kx + 1];
}
}
const magnitude = Math.sqrt(gx * gx + gy * gy);
// If magnitude is above the threshold, mark it as an edge
if (magnitude > threshold) {
edgeMap[y * width + x] = 1;
}
}
}
// 5. Generate the final image by combining posterization and edges
const numLevels = Math.max(2, Math.floor(levels)); // Ensure at least 2 levels
const posterizeStep = 255 / (numLevels - 1);
for (let i = 0; i < sourceData.length; i += 4) {
const pixelIndex = i / 4;
// If the pixel is an edge, draw it with the line color
if (edgeMap[pixelIndex] === 1) {
resultData[i] = lineR;
resultData[i + 1] = lineG;
resultData[i + 2] = lineB;
} else { // Otherwise, apply posterization
const r = sourceData[i];
const g = sourceData[i + 1];
const b = sourceData[i + 2];
resultData[i] = Math.round(r / posterizeStep) * posterizeStep;
resultData[i + 1] = Math.round(g / posterizeStep) * posterizeStep;
resultData[i + 2] = Math.round(b / posterizeStep) * posterizeStep;
}
// Preserve original alpha channel
resultData[i + 3] = sourceData[i + 3];
}
// 6. Put the final processed data back onto the canvas
ctx.putImageData(resultImageData, 0, 0);
return canvas;
}
Apply Changes