You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Converts an image to a CAD-style blueprint by performing edge detection.
* The function renders the image with white lines on a gridded blue background,
* simulating a technical drawing.
*
* @param {Image} originalImg The original image object to process.
* @param {number} lowThreshold Lower threshold for edge detection hysteresis. Default is 20.
* @param {number} highThreshold Higher threshold for edge detection hysteresis. Default is 50.
* @param {number} gridSize The size of grid squares in pixels for the background. Default is 40.
* @param {number} gridOpacity The opacity of the background grid lines (0 to 1). Default is 0.2.
* @param {number} lineThicknessFactor A multiplier affecting the thickness of stronger lines. Default is 1.5.
* @returns {HTMLCanvasElement} A canvas element displaying the blueprint image.
*/
async function processImage(originalImg, lowThreshold = 20, highThreshold = 50, gridSize = 40, gridOpacity = 0.2, lineThicknessFactor = 2.0) {
// 1. --- Canvas and Background Setup ---
const outputSize = 2048;
const canvas = document.createElement('canvas');
canvas.width = outputSize;
canvas.height = outputSize;
const ctx = canvas.getContext('2d');
const bgColor = '#0A3D91';
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, outputSize, outputSize);
// Draw a subtle grid on the background
ctx.strokeStyle = `rgba(255, 255, 255, ${gridOpacity})`;
ctx.lineWidth = 1;
for (let i = gridSize; i < outputSize; i += gridSize) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, outputSize);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i);
ctx.lineTo(outputSize, i);
ctx.stroke();
}
// 2. --- Image Scaling and Positioning ---
// Calculate dimensions to fit image within output size while maintaining aspect ratio
const hRatio = outputSize / originalImg.width;
const vRatio = outputSize / originalImg.height;
const ratio = Math.min(hRatio, vRatio);
const scaledWidth = Math.floor(originalImg.width * ratio);
const scaledHeight = Math.floor(originalImg.height * ratio);
const offsetX = Math.floor((outputSize - scaledWidth) / 2);
const offsetY = Math.floor((outputSize - scaledHeight) / 2);
// 3. --- Image Processing (Canny Edge Detection) ---
// Draw the scaled image onto a temporary canvas for pixel processing
const tempCanvas = document.createElement('canvas');
tempCanvas.width = scaledWidth;
tempCanvas.height = scaledHeight;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(originalImg, 0, 0, scaledWidth, scaledHeight);
const imageData = tempCtx.getImageData(0, 0, scaledWidth, scaledHeight);
const { data, width, height } = imageData;
// Helper for applying a convolution kernel
const applyKernel = (src, dst, w, h, kernel) => {
const kernelSize = Math.sqrt(kernel.length);
const half = Math.floor(kernelSize / 2);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let sum = 0;
let k_i = 0;
for (let ky = -half; ky <= half; ky++) {
for (let kx = -half; kx <= half; kx++) {
const px = x + kx;
const py = y + ky;
if (px >= 0 && px < w && py >= 0 && py < h) {
sum += src[py * w + px] * kernel[k_i];
}
k_i++;
}
}
dst[y * w + x] = sum;
}
}
};
// 3.1: Grayscale Conversion
const grayData = new Uint8ClampedArray(width * height);
for (let i = 0; i < data.length; i += 4) {
grayData[i / 4] = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
}
// 3.2: Gaussian Blur (to reduce noise)
const blurKernel = [2, 4, 5, 4, 2, 4, 9, 12, 9, 4, 5, 12, 15, 12, 5, 4, 9, 12, 9, 4, 2, 4, 5, 4, 2].map(v => v / 159);
const blurredData = new Uint8ClampedArray(width * height);
applyKernel(grayData, blurredData, width, height, blurKernel);
// 3.3: Sobel Operator (to find gradient intensity and direction)
const sobelXKernel = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
const sobelYKernel = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
const Gx = new Float32Array(width * height);
const Gy = new Float32Array(width * height);
applyKernel(blurredData, Gx, width, height, sobelXKernel);
applyKernel(blurredData, Gy, width, height, sobelYKernel);
const gradientMagnitude = new Float32Array(width * height);
const gradientDirection = new Float32Array(width * height);
let maxMagnitude = 0;
for (let i = 0; i < gradientMagnitude.length; i++) {
const mag = Math.sqrt(Gx[i] ** 2 + Gy[i] ** 2);
gradientMagnitude[i] = mag;
if (mag > maxMagnitude) maxMagnitude = mag;
gradientDirection[i] = Math.atan2(Gy[i], Gx[i]);
}
const normalizedMagnitude = new Float32Array(width * height);
if (maxMagnitude > 0) {
for (let i = 0; i < gradientMagnitude.length; i++) {
normalizedMagnitude[i] = gradientMagnitude[i] / maxMagnitude;
}
}
// 3.4: Non-Maximum Suppression (to thin edges)
const nmsData = new Float32Array(width * height);
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
const i = y * width + x;
const angle = gradientDirection[i] * (180 / Math.PI);
const mag = gradientMagnitude[i];
let q = 255, r = 255;
if ((0 <= angle && angle < 22.5) || (157.5 <= angle && angle <= 180) || (-22.5 <= angle && angle < 0) || (-180 <= angle && angle < -157.5)) {
q = gradientMagnitude[i + 1]; r = gradientMagnitude[i - 1];
} else if ((22.5 <= angle && angle < 67.5) || (-157.5 <= angle && angle < -112.5)) {
q = gradientMagnitude[i - width + 1]; r = gradientMagnitude[i + width - 1];
} else if ((67.5 <= angle && angle < 112.5) || (-112.5 <= angle && angle < -67.5)) {
q = gradientMagnitude[i - width]; r = gradientMagnitude[i + width];
} else if ((112.5 <= angle && angle < 157.5) || (-67.5 <= angle && angle < -22.5)) {
q = gradientMagnitude[i - width - 1]; r = gradientMagnitude[i + width + 1];
}
if (mag >= q && mag >= r) nmsData[i] = mag;
}
}
// 3.5: Double Thresholding and Hysteresis (to connect edges)
const edges = new Uint8ClampedArray(width * height); // 0 no, 128 weak, 255 strong
const finalEdges = new Uint8ClampedArray(width * height);
for (let i = 0; i < nmsData.length; i++) {
if (nmsData[i] > highThreshold) edges[i] = 255;
else if (nmsData[i] > lowThreshold) edges[i] = 128;
}
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (edges[y * width + x] === 255 && finalEdges[y * width + x] === 0) {
// Use an iterative approach (DFS) to avoid stack overflow on complex images
const stack = [[x, y]];
while (stack.length > 0) {
const [cx, cy] = stack.pop();
const currentIdx = cy * width + cx;
if (cx >= 0 && cx < width && cy >= 0 && cy < height && finalEdges[currentIdx] === 0 && edges[currentIdx] > 0) {
finalEdges[currentIdx] = 255;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue;
stack.push([cx + dx, cy + dy]);
}
}
}
}
}
}
}
// 4. --- Drawing the Final Result ---
// Draw edges with variable thickness based on gradient magnitude
ctx.fillStyle = '#FFFFFF';
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = y * width + x;
if (finalEdges[i] === 255) {
const diameter = 1 + (normalizedMagnitude[i] * lineThicknessFactor);
ctx.beginPath();
ctx.arc(offsetX + x, offsetY + y, diameter / 2, 0, Math.PI * 2);
ctx.fill();
}
}
}
return canvas;
}
Apply Changes