You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, edgeThreshold = 50, invertOutput = "false") {
const canvas = document.createElement('canvas');
// Use { willReadFrequently: true } for potential performance improvement if available
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
if (width === 0 || height === 0) {
// Handle case where image might not be loaded or is empty
// Create a minimal canvas to avoid errors downstream
canvas.width = 1;
canvas.height = 1;
console.warn("Image To Line Art Converter: Input image has zero width or height.");
// Optionally, draw a small indicator or leave blank
ctx.fillStyle = "gray";
ctx.fillRect(0,0,1,1);
return canvas;
}
canvas.width = width;
canvas.height = height;
try {
ctx.drawImage(originalImg, 0, 0, width, height);
} catch (e) {
// This can happen for various reasons, including if originalImg isn't a valid image source
console.error("Image To Line Art Converter: Could not draw image.", e);
ctx.fillStyle = "lightgray";
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = "red";
ctx.font = "16px Arial";
ctx.textAlign = "center";
ctx.fillText("Error drawing input image", width / 2, height / 2);
return canvas;
}
let imageData;
try {
imageData = ctx.getImageData(0, 0, width, height);
} catch (e) {
// This can happen if the image is from a different origin (tainted canvas)
console.error("Image To Line Art Converter: Could not get image data. Canvas may be tainted by cross-origin data.", e);
// Draw an error message on the canvas
ctx.fillStyle = "lightgray"; // Clear previous content
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = "red";
ctx.font = "16px Arial";
ctx.textAlign = "center";
ctx.fillText("Error: Cannot process cross-origin image", width / 2, height / 2);
return canvas;
}
const data = imageData.data;
// 1. Grayscale conversion
const grayValues = new Uint8Array(width * height); // 1 byte per pixel for grayscale value
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Using luminance formula (Rec. 709): L = 0.2126*R + 0.7152*G + 0.0722*B
grayValues[i / 4] = 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
// 2. Sobel Edge Detection
// Sobel kernels
const Gx = [
-1, 0, 1,
-2, 0, 2,
-1, 0, 1
];
const Gy = [
-1, -2, -1,
0, 0, 0,
1, 2, 1
];
const edgeData = new Uint8ClampedArray(data.length); // Output data for the new image
const gradientMagnitudes = new Float32Array(width * height); // Stores magnitude for each pixel
// Iterate through each pixel (excluding 1-pixel border for Sobel operator)
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let sumX = 0;
let sumY = 0;
let kernelIndex = 0;
// Apply 3x3 Sobel kernel
for (let ky = -1; ky <= 1; ky++) { // Kernel y-offset
for (let kx = -1; kx <= 1; kx++) { // Kernel x-offset
// Calculate index for the grayValues array (1 value per pixel)
const pixelIndex = (y + ky) * width + (x + kx);
const grayVal = grayValues[pixelIndex];
sumX += grayVal * Gx[kernelIndex];
sumY += grayVal * Gy[kernelIndex];
kernelIndex++;
}
}
// Calculate gradient magnitude: sqrt(Gx^2 + Gy^2)
const magnitude = Math.sqrt(sumX * sumX + sumY * sumY);
gradientMagnitudes[y * width + x] = magnitude;
}
}
// 3. Thresholding and constructing the output image
let thresholdValue = Number(edgeThreshold);
if (isNaN(thresholdValue) || thresholdValue < 0) {
// Default threshold if parsing failed or value is invalid
thresholdValue = 50;
}
// Determine if colors should be inverted (e.g., white lines on black vs. black lines on white)
const shouldInvert = String(invertOutput).toLowerCase() === "true";
const lineColor = shouldInvert ? 255 : 0; // e.g., white if inverted, black otherwise
const bgColor = shouldInvert ? 0 : 255; // e.g., black if inverted, white otherwise
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const outputPixelIndex = (y * width + x) * 4; // Index for edgeData (RGBA)
const magnitudeIndex = y * width + x; // Index for gradientMagnitudes
// For border pixels (where Sobel wasn't applied), magnitude is 0.
// Using || 0 ensures that if gradientMagnitudes[magnitudeIndex] is undefined/NaN somehow, it defaults to 0.
const magnitude = gradientMagnitudes[magnitudeIndex] || 0;
if (magnitude > thresholdValue) {
edgeData[outputPixelIndex] = lineColor;
edgeData[outputPixelIndex + 1] = lineColor;
edgeData[outputPixelIndex + 2] = lineColor;
} else {
edgeData[outputPixelIndex] = bgColor;
edgeData[outputPixelIndex + 1] = bgColor;
edgeData[outputPixelIndex + 2] = bgColor;
}
edgeData[outputPixelIndex + 3] = 255; // Full alpha (opaque)
}
}
// Create new ImageData object from the processed pixel data
const newImageData = new ImageData(edgeData, width, height);
// Put the new image data back onto the canvas
ctx.putImageData(newImageData, 0, 0);
return canvas;
}
Apply Changes