You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, filterType = "sobel", strength = 1.0) {
const canvas = document.createElement('canvas');
// Optimization hint for frequent readbacks, though support varies.
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
canvas.width = imgWidth;
canvas.height = imgHeight;
// Handle cases of 0-width/height images gracefully
if (imgWidth === 0 || imgHeight === 0) {
console.warn("Image has zero width or height. Returning empty canvas.");
return canvas; // Return empty canvas (0x0)
}
ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
let imageData;
try {
imageData = ctx.getImageData(0, 0, imgWidth, imgHeight);
} catch (e) {
// This can happen due to tainted canvas (e.g., cross-origin image without CORS)
console.error("Error getting ImageData: ", e);
// Return a new empty canvas of the same size, as processing is not possible.
const errorCanvas = document.createElement('canvas');
errorCanvas.width = imgWidth;
errorCanvas.height = imgHeight;
// Optionally, draw an error message on errorCanvas here.
return errorCanvas;
}
const data = imageData.data;
// Use imageData.width/height as source of truth after getImageData
const width = imageData.width;
const height = imageData.height;
// If image is too small for 3x3 kernel, convolution is not well-defined with current border handling.
// Return a copy of the original image on a new canvas.
if (width < 3 || height < 3) {
const smallCanvas = document.createElement('canvas');
smallCanvas.width = width;
smallCanvas.height = height;
const smallCtx = smallCanvas.getContext('2d');
smallCtx.drawImage(originalImg, 0, 0, width, height);
console.warn("Image is too small for 3x3 convolution filter. Returning original image copy.");
return smallCanvas;
}
const outputData = new Uint8ClampedArray(data.length);
// Kernels definition
const kernels = {
sobel_gx: [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
],
sobel_gy: [ // Standard Sobel Y (points down for positive gradient)
[-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]
],
sharpen: [
[ 0,-1, 0],
[-1, 5,-1],
[ 0,-1, 0]
],
blur: [ // Box blur 3x3
[1/9, 1/9, 1/9],
[1/9, 1/9, 1/9],
[1/9, 1/9, 1/9]
]
};
// Helper function to get pixel index from (x,y) coordinates
const getIndex = (x, y, w = width) => (y * w + x) * 4;
// Helper function to clamp a value between min and max (default 0-255)
const clamp = (value, min = 0, max = 255) => Math.max(min, Math.min(max, value));
if (filterType === "sobel" || filterType === "sobel_x" || filterType === "sobel_y") {
// Create a Float32Array for grayscale values for precision during calculation
const grayData = new Float32Array(width * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = getIndex(x, y);
// Standard luminance calculation
grayData[y * width + x] = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
}
}
const kx = kernels.sobel_gx;
const ky = kernels.sobel_gy;
// Iterate over interior pixels (kernel requires 1-pixel border)
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let sumGx = 0, sumGy = 0;
// Apply 3x3 kernel
for (let j = -1; j <= 1; j++) { // Kernel Y offset
for (let i = -1; i <= 1; i++) { // Kernel X offset
const grayVal = grayData[(y + j) * width + (x + i)];
sumGx += grayVal * kx[j + 1][i + 1];
sumGy += grayVal * ky[j + 1][i + 1];
}
}
let finalVal;
if (filterType === "sobel_x") {
finalVal = clamp(Math.abs(sumGx) * strength);
} else if (filterType === "sobel_y") {
finalVal = clamp(Math.abs(sumGy) * strength);
} else { // "sobel" (magnitude)
finalVal = clamp(Math.sqrt(sumGx * sumGx + sumGy * sumGy) * strength);
}
const outIndex = getIndex(x, y);
outputData[outIndex] = finalVal; // R
outputData[outIndex + 1] = finalVal; // G
outputData[outIndex + 2] = finalVal; // B
outputData[outIndex + 3] = 255; // Alpha (opaque)
}
}
// Border handling for Sobel-type filters: set border pixels to black and opaque
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (x === 0 || x === width - 1 || y === 0 || y === height - 1) {
const idx = getIndex(x, y);
outputData[idx] = 0; outputData[idx+1] = 0; outputData[idx+2] = 0; outputData[idx+3] = 255;
}
}
}
} else if (filterType === "sharpen" || filterType === "blur") {
const kernel = (filterType === "sharpen") ? kernels.sharpen : kernels.blur;
// Iterate over interior pixels
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let sumR = 0, sumG = 0, sumB = 0;
// Apply 3x3 kernel to each color channel
for (let j = -1; j <= 1; j++) { // Kernel Y offset
for (let i = -1; i <= 1; i++) { // Kernel X offset
const kVal = kernel[j + 1][i + 1];
const nIndex = getIndex(x + i, y + j); // Neighbor index
sumR += data[nIndex] * kVal;
sumG += data[nIndex + 1] * kVal;
sumB += data[nIndex + 2] * kVal;
}
}
const outIndex = getIndex(x, y);
// Apply strength using linear interpolation: result = original + strength * (filtered - original)
// This provides intuitive control: strength=0 is original, strength=1 is full filter.
outputData[outIndex] = clamp(data[outIndex] + strength * (sumR - data[outIndex]));
outputData[outIndex+1] = clamp(data[outIndex+1] + strength * (sumG - data[outIndex+1]));
outputData[outIndex+2] = clamp(data[outIndex+2] + strength * (sumB - data[outIndex+2]));
outputData[outIndex+3] = data[outIndex+3]; // Preserve original alpha
}
}
// Border handling for Sharpen/Blur: copy original pixels to avoid a black frame
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (x === 0 || x === width - 1 || y === 0 || y === height - 1) {
const idx = getIndex(x, y);
outputData[idx] = data[idx];
outputData[idx+1] = data[idx+1];
outputData[idx+2] = data[idx+2];
outputData[idx+3] = data[idx+3];
}
}
}
} else {
console.warn(`Unknown filterType: "${filterType}". Returning copy of original image.`);
// If filterType is unknown, copy original image data to outputData
for(let i=0; i < data.length; i++) {
outputData[i] = data[i];
}
}
// Create a new canvas for the output
const outputCanvas = document.createElement('canvas');
outputCanvas.width = width;
outputCanvas.height = height;
const outputCtx = outputCanvas.getContext('2d');
// Create ImageData for the output canvas
const newImageData = outputCtx.createImageData(width, height);
newImageData.data.set(outputData); // Set the processed pixel data
outputCtx.putImageData(newImageData, 0, 0); // Draw the ImageData onto the canvas
return outputCanvas;
}
Apply Changes