You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Applies a cartoon/comic book effect to an image.
* This is achieved by reducing the color palette (posterization) and adding outlines around edges.
*
* @param {HTMLImageElement} originalImg The original image object.
* @param {number} posterizationLevels The number of color levels per channel (e.g., 4 gives 4^3 = 64 colors in total). A value between 2 and 255.
* @param {number} outlineThreshold The sensitivity for edge detection. A lower value means more sensitive (more lines). Range is roughly 0-255.
* @param {string} outlineColor The color of the outlines (e.g., 'black', '#FF0000', 'rgb(0,0,255)').
* @param {number} outlineWidth The thickness of the outlines in pixels. A value of 0 means no outlines.
* @returns {HTMLCanvasElement} A canvas element with the cartoonized image.
*/
function processImage(originalImg, posterizationLevels = 4, outlineThreshold = 80, outlineColor = 'black', outlineWidth = 2) {
// 0. SETUP
const width = originalImg.width;
const height = originalImg.height;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
// Use { willReadFrequently: true } for performance hint when using getImageData frequently.
const ctx = canvas.getContext('2d', {
willReadFrequently: true
});
// Draw image to a temporary canvas to read its pixel data
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d', {
willReadFrequently: true
});
tempCtx.drawImage(originalImg, 0, 0);
const originalData = tempCtx.getImageData(0, 0, width, height);
// Helper to parse the outline color string into an [r, g, b] array
const outlineRgba = (() => {
const c = document.createElement('canvas');
c.width = c.height = 1;
const cCtx = c.getContext('2d', {
willReadFrequently: true
});
cCtx.fillStyle = outlineColor;
cCtx.fillRect(0, 0, 1, 1);
return cCtx.getImageData(0, 0, 1, 1).data;
})();
// 1. POSTERIZATION
// This reduces the number of colors in the image.
const levels = Math.max(2, Math.min(255, Math.floor(posterizationLevels)));
const step = 255 / (levels - 1);
const posterizedData = tempCtx.createImageData(width, height);
for (let i = 0; i < originalData.data.length; i += 4) {
posterizedData.data[i] = Math.round(originalData.data[i] / step) * step;
posterizedData.data[i + 1] = Math.round(originalData.data[i + 1] / step) * step;
posterizedData.data[i + 2] = Math.round(originalData.data[i + 2] / step) * step;
posterizedData.data[i + 3] = originalData.data[i + 3];
}
if (outlineWidth < 1) {
// If no outline is needed, just draw the posterized image and return.
ctx.putImageData(posterizedData, 0, 0);
return canvas;
}
// 2. EDGE DETECTION (SOBEL OPERATOR)
// First, create a grayscale representation of the image.
const grayscaleData = new Uint8ClampedArray(width * height);
for (let i = 0; i < originalData.data.length; i += 4) {
const r = originalData.data[i];
const g = originalData.data[i + 1];
const b = originalData.data[i + 2];
grayscaleData[i / 4] = 0.299 * r + 0.587 * g + 0.114 * b;
}
const sobelX = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
];
const sobelY = [
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]
];
const isEdge = new Uint8ClampedArray(width * height);
// Apply Sobel filter to find edges.
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let px = 0;
let py = 0;
for (let j = 0; j < 3; j++) {
for (let i = 0; i < 3; i++) {
const gray = grayscaleData[(y + j - 1) * width + (x + i - 1)];
px += gray * sobelX[j][i];
py += gray * sobelY[j][i];
}
}
const magnitude = Math.sqrt(px * px + py * py);
if (magnitude > outlineThreshold) {
isEdge[y * width + x] = 1;
}
}
}
// 3. COMBINE POSTERIZED IMAGE AND OUTLINES
const finalData = tempCtx.createImageData(width, height);
const halfWidth = Math.floor(outlineWidth / 2);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = (y * width + x) * 4;
let isOutlinePixel = false;
// Check a square neighborhood for any edge pixels to create thick lines.
// This is the most computationally expensive part of the function.
for (let j = -halfWidth; j <= halfWidth; j++) {
for (let i = -halfWidth; i <= halfWidth; i++) {
const checkY = y + j;
const checkX = x + i;
if (checkY >= 0 && checkY < height && checkX >= 0 && checkX < width) {
if (isEdge[checkY * width + checkX] === 1) {
isOutlinePixel = true;
break;
}
}
}
if (isOutlinePixel) break;
}
if (isOutlinePixel) {
finalData.data[index] = outlineRgba[0];
finalData.data[index + 1] = outlineRgba[1];
finalData.data[index + 2] = outlineRgba[2];
finalData.data[index + 3] = 255;
} else {
finalData.data[index] = posterizedData.data[index];
finalData.data[index + 1] = posterizedData.data[index + 1];
finalData.data[index + 2] = posterizedData.data[index + 2];
finalData.data[index + 3] = posterizedData.data[index + 3];
}
}
}
// 4. DRAW FINAL IMAGE TO CANVAS
ctx.putImageData(finalData, 0, 0);
return canvas;
}
Apply Changes