You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Converts an image into a graphic novel style by applying posterization,
* halftone shading, and edge detection outlines. This function combines
* several image processing techniques to mimic the look of comic book art.
*
* @param {HTMLImageElement} originalImg The original image element to process.
* @param {number} [posterizationLevels=5] The number of color levels per channel for posterization (e.g., 2-16). Lower values create a flatter, more stylized look.
* @param {number} [edgeThreshold=80] The sensitivity for edge detection which creates the "ink" outlines (e.g., 50-150). A lower value detects more lines.
* @param {number} [halftoneSize=4] The size of the grid for the halftone dot effect. Larger values create bigger dots. Set to 0 to disable this effect.
* @param {number} [halftoneOpacity=0.2] The opacity of the halftone shading layer (from 0.0 to 1.0).
* @returns {HTMLCanvasElement} A new canvas element with the graphic novel effect applied.
*/
function processImage(originalImg, posterizationLevels = 5, edgeThreshold = 80, halftoneSize = 4, halftoneOpacity = 0.2) {
const width = originalImg.width;
const height = originalImg.height;
// Create the final canvas that will be returned
const finalCanvas = document.createElement('canvas');
finalCanvas.width = width;
finalCanvas.height = height;
const finalCtx = finalCanvas.getContext('2d');
// Create a temporary canvas to get the original image data and for intermediate processing
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(originalImg, 0, 0);
const originalImageData = tempCtx.getImageData(0, 0, width, height);
const originalData = originalImageData.data;
// --- STEP 1: Apply Posterization for the color base layer ---
const posterizedData = new Uint8ClampedArray(originalData.length);
const levels = Math.max(2, Math.floor(posterizationLevels)); // Ensure integer levels >= 2
const step = 255 / (levels - 1);
for (let i = 0; i < originalData.length; i += 4) {
posterizedData[i] = Math.round(originalData[i] / step) * step; // R
posterizedData[i + 1] = Math.round(originalData[i + 1] / step) * step; // G
posterizedData[i + 2] = Math.round(originalData[i + 2] / step) * step; // B
posterizedData[i + 3] = originalData[i + 3]; // A
}
const posterizedImageData = new ImageData(posterizedData, width, height);
// Draw the posterized base onto the final canvas
tempCtx.putImageData(posterizedImageData, 0, 0);
finalCtx.drawImage(tempCanvas, 0, 0);
// --- Prepare Grayscale Data (needed for both halftone and outlines) ---
const grayscaleData = new Uint8ClampedArray(width * height);
for (let i = 0, j = 0; i < originalData.length; i += 4, j++) {
const r = originalData[i];
const g = originalData[i + 1];
const b = originalData[i + 2];
grayscaleData[j] = 0.299 * r + 0.587 * g + 0.114 * b; // Luminosity formula
}
// --- STEP 2: Apply Halftone Shading Layer ---
if (halftoneSize > 0) {
const halftoneCanvas = document.createElement('canvas');
halftoneCanvas.width = width;
halftoneCanvas.height = height;
const halftoneCtx = halftoneCanvas.getContext('2d');
halftoneCtx.fillStyle = 'black';
for (let y = 0; y < height; y += halftoneSize) {
for (let x = 0; x < width; x += halftoneSize) {
let totalBrightness = 0;
let count = 0;
// Calculate the average brightness of the grid cell
for (let j = 0; j < halftoneSize; j++) {
for (let i = 0; i < halftoneSize; i++) {
const pixelX = x + i;
const pixelY = y + j;
if (pixelX < width && pixelY < height) {
totalBrightness += grayscaleData[pixelY * width + pixelX];
count++;
}
}
}
const avgBrightness = totalBrightness / count;
// The radius is inversely proportional to brightness (darker = bigger dot)
const radius = (1 - avgBrightness / 255) * (halftoneSize / 2);
if (radius > 0.1) { // Only draw if the dot is somewhat visible
halftoneCtx.beginPath();
halftoneCtx.arc(x + halftoneSize / 2, y + halftoneSize / 2, radius, 0, Math.PI * 2);
halftoneCtx.fill();
}
}
}
// Composite the halftone layer onto the final canvas using a 'multiply' blend mode
finalCtx.globalAlpha = Math.max(0, Math.min(1, halftoneOpacity));
finalCtx.globalCompositeOperation = 'multiply';
finalCtx.drawImage(halftoneCanvas, 0, 0);
finalCtx.globalAlpha = 1.0; // Reset alpha
finalCtx.globalCompositeOperation = 'source-over'; // Reset blend mode
}
// --- STEP 3: Apply "Ink" Outline Layer using Sobel Edge Detection ---
const outlineCanvas = document.createElement('canvas');
outlineCanvas.width = width;
outlineCanvas.height = height;
const outlineCtx = outlineCanvas.getContext('2d');
const outlineImageData = outlineCtx.createImageData(width, height);
const outlineData = outlineImageData.data;
const Gx = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
];
const Gy = [
[1, 2, 1],
[0, 0, 0],
[-1, -2, -1]
];
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let sumX = 0;
let sumY = 0;
// Apply kernels to 3x3 neighborhood
for (let j = -1; j <= 1; j++) {
for (let i = -1; i <= 1; i++) {
const grayVal = grayscaleData[(y + j) * width + (x + i)];
sumX += grayVal * Gx[j + 1][i + 1];
sumY += grayVal * Gy[j + 1][i + 1];
}
}
const magnitude = Math.sqrt(sumX * sumX + sumY * sumY);
const index = (y * width + x) * 4;
if (magnitude > edgeThreshold) {
// This is an edge, draw it in black
outlineData[index] = 0;
outlineData[index + 1] = 0;
outlineData[index + 2] = 0;
outlineData[index + 3] = 255;
} else {
// Not an edge, make it fully transparent
outlineData[index + 3] = 0;
}
}
}
// Draw the final outline layer on top of everything
outlineCtx.putImageData(outlineImageData, 0, 0);
finalCtx.drawImage(outlineCanvas, 0, 0);
return finalCanvas;
}
Apply Changes