You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, levels = 6, edgeThreshold = 80, edgeR = 0, edgeG = 0, edgeB = 0) {
// Helper function for color quantization
function quantizeChannel(value, numLevels) {
if (numLevels <= 1) { // Binary (black or white based on midpoint)
return value < 128 ? 0 : 255;
}
// Calculate the size of each quantization step
const step = 255.0 / (numLevels - 1);
// Quantize the value by finding the nearest step
return Math.round(value / step) * step;
}
// Sobel kernels for edge detection
const sobelXKernel = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
];
const sobelYKernel = [
[-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]
];
// 0. Parameter validation/sanitization
levels = Math.max(1, Math.floor(levels)); // Ensure levels is at least 1
edgeThreshold = Math.max(0, edgeThreshold); // Non-negative threshold
edgeR = Math.max(0, Math.min(255, Math.floor(edgeR))); // Clamp edge color components
edgeG = Math.max(0, Math.min(255, Math.floor(edgeG)));
edgeB = Math.max(0, Math.min(255, Math.floor(edgeB)));
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
if (width === 0 || height === 0) {
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = width; // which is 0
emptyCanvas.height = height; // which is 0
return emptyCanvas; // Return empty canvas for 0-size image
}
// 1. Canvas setup & Get Image Data
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d', { willReadFrequently: true }); // Optimization hint
ctx.drawImage(originalImg, 0, 0, width, height);
let imgData;
try {
imgData = ctx.getImageData(0, 0, width, height);
} catch (e) {
// Handle potential security errors if image is cross-origin and canvas is tainted
console.error("Error getting ImageData:", e);
// Fallback: return the original image drawn on a canvas if processing fails
const fallbackCanvas = document.createElement('canvas');
fallbackCanvas.width = width;
fallbackCanvas.height = height;
const fallbackCtx = fallbackCanvas.getContext('2d');
fallbackCtx.drawImage(originalImg, 0, 0, width, height);
return fallbackCanvas;
}
const data = imgData.data; // Uint8ClampedArray of [R,G,B,A, R,G,B,A, ...]
// 2. Grayscale Conversion
const grayData = new Uint8Array(width * height); // Stores intensity (0-255)
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i+1];
const b = data[i+2];
// Standard luminance calculation: (0.299*R + 0.587*G + 0.114*B)
grayData[i / 4] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
}
// 3. Sobel Edge Detection & Thresholding
const isEdge = new Uint8Array(width * height); // 1 if edge, 0 otherwise. Zero-initialized.
// Iterate, skipping 1-pixel border to avoid out-of-bounds access in kernel
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 centered at (x,y)
for (let ky = -1; ky <= 1; ky++) { // Kernel Y offset
for (let kx = -1; kx <= 1; kx++) { // Kernel X offset
// Pixel value from grayscale image at (x+kx, y+ky)
const currentPixelGrayscale = grayData[(y + ky) * width + (x + kx)];
gx += currentPixelGrayscale * sobelXKernel[ky + 1][kx + 1];
gy += currentPixelGrayscale * sobelYKernel[ky + 1][kx + 1];
}
}
const magnitude = Math.sqrt(gx * gx + gy * gy);
if (magnitude > edgeThreshold) {
isEdge[y * width + x] = 1;
}
}
}
// 4. Final Pixel Composition
const outputPixelData = new Uint8ClampedArray(data.length);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pixelIndex = y * width + x; // Index for 1D arrays (grayData, isEdge)
const dataIndex = pixelIndex * 4; // Index for RGBA pixel data arrays (data, outputPixelData)
if (isEdge[pixelIndex] === 1) { // Check if current pixel is marked as an edge
outputPixelData[dataIndex] = edgeR;
outputPixelData[dataIndex + 1] = edgeG;
outputPixelData[dataIndex + 2] = edgeB;
outputPixelData[dataIndex + 3] = 255; // Edges are opaque black (or specified edge color)
} else {
// Not an edge, apply color quantization
outputPixelData[dataIndex] = quantizeChannel(data[dataIndex], levels);
outputPixelData[dataIndex + 1] = quantizeChannel(data[dataIndex + 1], levels);
outputPixelData[dataIndex + 2] = quantizeChannel(data[dataIndex + 2], levels);
outputPixelData[dataIndex + 3] = data[dataIndex + 3]; // Preserve original alpha
}
}
}
// 5. Return Canvas
const outputCanvas = document.createElement('canvas');
outputCanvas.width = width;
outputCanvas.height = height;
const outputCtx = outputCanvas.getContext('2d');
const finalImageData = new ImageData(outputPixelData, width, height);
outputCtx.putImageData(finalImageData, 0, 0);
return outputCanvas;
}
Apply Changes