You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, gridSize = 50, gridLineWidth = 0.5, detailThreshold = 20, outlineThreshold = 40, blurRadius = 2, detailLineWidth = 1, outlineLineWidth = 2) {
/**
* Helper function to convert RGBA image data to a grayscale array (1 byte per pixel).
* @param {Uint8ClampedArray} rgbaData - The source image data array.
* @param {number} width - The width of the image.
* @param {number} height - The height of the image.
* @returns {Uint8ClampedArray} A new array containing grayscale values.
*/
const toGrayscale = (rgbaData, width, height) => {
const grayData = new Uint8ClampedArray(width * height);
for (let i = 0; i < rgbaData.length; i += 4) {
// Using the luminosity method for a standard grayscale conversion
const gray = rgbaData[i] * 0.299 + rgbaData[i + 1] * 0.587 + rgbaData[i + 2] * 0.114;
grayData[i / 4] = gray;
}
return grayData;
};
/**
* Helper function for a separable box blur, which is more efficient than a 2D convolution.
* @param {Uint8ClampedArray} grayData - The source grayscale data.
* @param {number} width - The width of the image.
* @param {number} height - The height of the image.
* @param {number} radius - The blur radius.
* @returns {Uint8ClampedArray} A new array containing the blurred grayscale data.
*/
const boxBlur = (grayData, width, height, radius) => {
const tempBlurData = new Uint8ClampedArray(grayData.length);
const finalBlurData = new Uint8ClampedArray(grayData.length);
// Horizontal pass
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let sum = 0;
let count = 0;
for (let kx = -radius; kx <= radius; kx++) {
const ix = x + kx;
if (ix >= 0 && ix < width) {
sum += grayData[y * width + ix];
count++;
}
}
tempBlurData[y * width + x] = sum / count;
}
}
// Vertical pass
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let sum = 0;
let count = 0;
for (let ky = -radius; ky <= radius; ky++) {
const iy = y + ky;
if (iy >= 0 && iy < height) {
sum += tempBlurData[iy * width + x];
count++;
}
}
finalBlurData[y * width + x] = sum / count;
}
}
return finalBlurData;
};
/**
* Helper function to apply the Sobel operator for edge detection.
* @param {Uint8ClampedArray} grayData - The source grayscale data.
* @param {number} width - The width of the image.
* @param {number} height - The height of the image.
* @returns {Uint8ClampedArray} A new array containing edge magnitude values.
*/
const sobelOperator = (grayData, width, height) => {
const magnitudeData = new Uint8ClampedArray(width * height);
// Sobel kernels for detecting horizontal and vertical edges
const Gx = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
];
const Gy = [
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]
];
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let sumX = 0;
let sumY = 0;
// Apply 3x3 kernels by convolving with the image patch
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const val = grayData[(y + ky) * width + (x + kx)];
sumX += val * Gx[ky + 1][kx + 1];
sumY += val * Gy[ky + 1][kx + 1];
}
}
// Calculate the gradient magnitude and store it
const magnitude = Math.sqrt(sumX * sumX + sumY * sumY);
magnitudeData[y * width + x] = magnitude;
}
}
return magnitudeData;
};
// --- Main Function Logic ---
const width = originalImg.naturalWidth;
const height = originalImg.naturalHeight;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// 1. Draw Blueprint Background with specified color
ctx.fillStyle = '#0A3D91';
ctx.fillRect(0, 0, width, height);
// 2. Draw Grid
ctx.strokeStyle = `rgba(255, 255, 255, 0.15)`;
ctx.lineWidth = gridLineWidth;
ctx.beginPath();
for (let x = gridLineWidth / 2; x <= width; x += gridSize) {
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
}
for (let y = gridLineWidth / 2; y <= height; y += gridSize) {
ctx.moveTo(0, y);
ctx.lineTo(width, y);
}
ctx.stroke();
// 3. Perform Edge Detection
// Draw the image to a temporary canvas to get its pixel data for processing
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
tempCtx.drawImage(originalImg, 0, 0, width, height);
const imageData = tempCtx.getImageData(0, 0, width, height);
// Image processing pipeline: Grayscale -> Blur -> Sobel
const grayData = toGrayscale(imageData.data, width, height);
const blurredGrayData = boxBlur(grayData, width, height, blurRadius);
// Get two edge maps: one for fine details (from original) and one for outlines (from blurred)
const detailEdges = sobelOperator(grayData, width, height);
const outlineEdges = sobelOperator(blurredGrayData, width, height);
// 4. Draw Detected Edges onto the Canvas
ctx.fillStyle = 'white';
// First, draw fine details with a thinner line width
if (detailLineWidth > 0) {
const detailOffset = Math.floor(detailLineWidth / 2);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (detailEdges[y * width + x] > detailThreshold) {
ctx.fillRect(x - detailOffset, y - detailOffset, detailLineWidth, detailLineWidth);
}
}
}
}
// Second, draw main outlines with a thicker line width.
// Drawing this last ensures outlines are visually dominant.
if (outlineLineWidth > 0) {
const outlineOffset = Math.floor(outlineLineWidth / 2);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (outlineEdges[y * width + x] > outlineThreshold) {
ctx.fillRect(x - outlineOffset, y - outlineOffset, outlineLineWidth, outlineLineWidth);
}
}
}
}
return canvas;
}
Apply Changes