You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, levels = 4, edgeThreshold = 120, edgeColor = "black") {
// Ensure parameters are of correct type
levels = Number(levels);
edgeThreshold = Number(edgeThreshold);
// edgeColor is already string by default
const canvas = document.createElement('canvas');
// Use willReadFrequently: true for performance hint if available
const ctxOptions = { willReadFrequently: true };
// Firefox < 9willReadFrequently> throws if alpha is false and willReadFrequently is true
// So, ensure alpha is not explicitly false if willReadFrequently is true. Default is true.
// ctxOptions.alpha = true; // Or let it be default
const ctx = canvas.getContext('2d', ctxOptions);
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
if (width === 0 || height === 0) {
console.warn("Image has zero width or height. Returning empty canvas.");
canvas.width = 0;
canvas.height = 0;
return canvas;
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(originalImg, 0, 0, width, height);
let imageData;
try {
imageData = ctx.getImageData(0, 0, width, height);
} catch (e) {
// This can happen if the image is tainted (e.g., cross-origin)
console.error("Could not get image data, possibly due to cross-origin restrictions:", e);
// Fallback: return the canvas with the original image drawn, unprocessed.
// The user will see the original image instead of an error or blank canvas.
return canvas;
}
const data = imageData.data;
// Helper to parse edgeColor to {r, g, b} components
let ecR, ecG, ecB;
const tempCanvasForColor = document.createElement('canvas');
tempCanvasForColor.width = 1;
tempCanvasForColor.height = 1;
const tempCtxForColor = tempCanvasForColor.getContext('2d');
ecR = 0; ecG = 0; ecB = 0; // Default to black
try {
tempCtxForColor.fillStyle = edgeColor; // Use the provided edgeColor string
tempCtxForColor.fillRect(0, 0, 1, 1);
const colorData = tempCtxForColor.getImageData(0, 0, 1, 1).data;
ecR = colorData[0];
ecG = colorData[1];
ecB = colorData[2];
} catch (e) {
console.warn(`Failed to parse edgeColor "${edgeColor}", defaulting to black. Error: ${e.message}`);
// ecR, ecG, ecB are already set to black as a fallback
}
// Ensure at least 2 levels for quantization; otherwise, step calculation would involve division by zero.
const numLevels = Math.max(2, levels);
const step = 255 / (numLevels - 1);
// 1. Color Quantization
// Create a new array for quantized pixel data. Uint8ClampedArray automatically clamps values to 0-255.
const quantizedData = new Uint8ClampedArray(data.length);
for (let i = 0; i < data.length; i += 4) {
quantizedData[i] = Math.round(data[i] / step) * step; // R
quantizedData[i+1] = Math.round(data[i+1] / step) * step; // G
quantizedData[i+2] = Math.round(data[i+2] / step) * step; // B
quantizedData[i+3] = data[i+3]; // Alpha (preserve original)
}
// 2. Edge Detection (Sobel Operator)
// First, create a grayscale luminance map from the quantized color data.
const luminanceMap = new Float32Array(width * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
const r = quantizedData[i];
const g = quantizedData[i+1];
const b = quantizedData[i+2];
// Standard NTSC luminance calculation
luminanceMap[y * width + x] = 0.299 * r + 0.587 * g + 0.114 * b;
}
}
// Prepare output image data, initially filled with quantized colors.
// Edges detected later will overwrite these pixels.
const outputImageData = ctx.createImageData(width, height);
const outputData = outputImageData.data;
for (let i = 0; i < quantizedData.length; i++) {
outputData[i] = quantizedData[i];
}
// Sobel kernels for edge detection
const Gx_kernel = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
];
const Gy_kernel = [
[-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]
];
// Apply Sobel operator
// Iterate from 1 to width/height - 1 to avoid border issues with 3x3 kernel
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let gx = 0;
let gy = 0;
// Apply 3x3 kernel
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const Gx_val = Gx_kernel[ky + 1][kx + 1];
const Gy_val = Gy_kernel[ky + 1][kx + 1];
// Get luminance of the neighboring pixel
const L_val = luminanceMap[(y + ky) * width + (x + kx)];
gx += L_val * Gx_val;
gy += L_val * Gy_val;
}
}
// Calculate gradient magnitude
const magnitude = Math.sqrt(gx * gx + gy * gy);
// If magnitude exceeds threshold, mark as an edge pixel
if (magnitude > edgeThreshold) {
const outputIndex = (y * width + x) * 4;
outputData[outputIndex] = ecR; // Edge R
outputData[outputIndex + 1] = ecG; // Edge G
outputData[outputIndex + 2] = ecB; // Edge B
outputData[outputIndex + 3] = 255; // Edges are opaque
}
// Else: pixel retains its quantized color (already set)
}
}
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes