You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, levels = 6, edgeThreshold = 80, edgeColor = "black", edgeThickness = 1) {
// 1. Create a canvas and draw the original image to access its pixel data
const canvas = document.createElement('canvas');
// Use willReadFrequently for potential performance gain as per MDN.
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Use naturalWidth/Height to ensure we use the original image dimensions
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
// Draw the image onto the canvas
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// Get ImageData for processing
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const originalData = imageData.data;
const width = canvas.width;
const height = canvas.height;
// 2. Posterization (Color Quantization)
const posterizedData = new Uint8ClampedArray(originalData.length);
if (levels <= 1) { // Handle 1 level (or less) as a single color (e.g., black)
for (let i = 0; i < originalData.length; i += 4) {
posterizedData[i] = 0; // R
posterizedData[i + 1] = 0; // G
posterizedData[i + 2] = 0; // B
posterizedData[i + 3] = originalData[i + 3]; // Preserve original alpha
}
} else {
const step = 255 / (levels - 1);
for (let i = 0; i < originalData.length; i += 4) {
posterizedData[i] = Math.round(originalData[i] / step) * step;
posterizedData[i + 1] = Math.round(originalData[i + 1] / step) * step;
posterizedData[i + 2] = Math.round(originalData[i + 2] / step) * step;
posterizedData[i + 3] = originalData[i + 3]; // Preserve original alpha
}
}
// 3. Edge Detection (Using Sobel operator)
// Initialize edgeMap with all zeros (no edges)
const edgeMap = new Uint8ClampedArray(width * height);
const sobelXKernel = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
];
const sobelYKernel = [
[-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]
];
// Iterate image pixels (excluding 1-pixel border for Sobel kernel)
// If image is smaller than 3x3, this loop won't run, and edgeMap remains all zeros.
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let gx = 0;
let gy = 0;
// Apply Sobel kernels to 3x3 neighborhood
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const pixelIndexOffset = ((y + ky) * width + (x + kx)) * 4;
// Calculate grayscale intensity of the neighbor pixel from original data
const intensity = originalData[pixelIndexOffset] * 0.299 +
originalData[pixelIndexOffset + 1] * 0.587 +
originalData[pixelIndexOffset + 2] * 0.114;
gx += intensity * sobelXKernel[ky + 1][kx + 1];
gy += intensity * sobelYKernel[ky + 1][kx + 1];
}
}
const magnitude = Math.sqrt(gx * gx + gy * gy);
if (magnitude > edgeThreshold) {
edgeMap[y * width + x] = 255; // Mark as edge
}
// No 'else' needed as edgeMap is initialized to 0
}
}
// 4. Combine Posterized Image and Edges
// Start with a copy of the posterized data for the final image
const finalPixelData = new Uint8ClampedArray(posterizedData);
// Parse edgeColor string to RGB values
let ecR = 0, ecG = 0, ecB = 0;
const tempElem = document.createElement('div');
tempElem.style.color = edgeColor;
// Element must be in DOM to compute style, hide it to prevent layout shifts
tempElem.style.position = 'fixed';
tempElem.style.display = 'none';
document.body.appendChild(tempElem); // Add to DOM
const computedColor = getComputedStyle(tempElem).color;
document.body.removeChild(tempElem); // Remove from DOM
// Extract R, G, B from computedColor string (e.g., "rgb(r, g, b)")
const colorParts = computedColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d\.]+)?\)/);
if (colorParts && colorParts.length >= 4) {
ecR = parseInt(colorParts[1]);
ecG = parseInt(colorParts[2]);
ecB = parseInt(colorParts[3]);
} // Defaults to black (0,0,0) if parsing fails or color is invalid
// Apply edges to the final pixel data
if (edgeThickness > 0) { // Only apply edges if thickness is positive
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (edgeMap[y * width + x] === 255) { // If this pixel is an edge point
// Apply edge color with specified thickness around (x,y)
// The loop creates a square of 'edgeThickness' pixels centered around (x,y)
// For T=1: dx,dy = 0. Colors 1 pixel. (x,y)
// For T=2: dx,dy = 0,1. Colors 2x2 block. Top-left is (x,y). Centered at (x+0.5, y+0.5)
// For T=3: dx,dy = -1,0,1. Colors 3x3 block. Center is (x,y)
const startOffset = -Math.floor((edgeThickness - 1) / 2);
const endOffset = Math.ceil((edgeThickness - 1) / 2);
for (let dy = startOffset; dy < startOffset + edgeThickness; dy++) {
for (let dx = startOffset; dx < startOffset + edgeThickness; dx++) {
const currentX = x + dx;
const currentY = y + dy;
if (currentX >= 0 && currentX < width && currentY >= 0 && currentY < height) {
const pixelIndex = (currentY * width + currentX) * 4;
finalPixelData[pixelIndex] = ecR;
finalPixelData[pixelIndex + 1] = ecG;
finalPixelData[pixelIndex + 2] = ecB;
finalPixelData[pixelIndex + 3] = 255; // Edges are opaque
}
}
}
}
}
}
}
// Create new ImageData from the final pixel data and put it onto the canvas
const finalImageData = new ImageData(finalPixelData, width, height);
ctx.putImageData(finalImageData, 0, 0);
return canvas;
}
Apply Changes