You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, wireColor = "#333333", wireThickness = 1, edgeThreshold = 70, numWires = 300, maxWireSegments = 20, segmentLength = 8, turnRandomness = Math.PI / 8) {
const width = originalImg.width;
const height = originalImg.height;
if (width === 0 || height === 0) {
console.warn("Image has zero width or height.");
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = width;
emptyCanvas.height = height;
return emptyCanvas;
}
// 1. Create temp canvas for image processing (grayscale, blur)
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
try {
tempCtx.drawImage(originalImg, 0, 0, width, height);
} catch (e) {
console.error("Error drawing original image to temporary canvas:", e);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = width;
errorCanvas.height = height;
// Optionally draw something on errorCanvas or leave blank
return errorCanvas;
}
let imageData;
try {
imageData = tempCtx.getImageData(0, 0, width, height);
} catch (e) {
console.error("Error getting image data (cross-origin issue?):", e);
// Fallback: return canvas with original image if pixel manipulation is not possible
const fallbackCanvas = document.createElement('canvas');
fallbackCanvas.width = width;
fallbackCanvas.height = height;
const fallbackCtx = fallbackCanvas.getContext('2d');
try {
fallbackCtx.drawImage(originalImg, 0, 0, width, height);
} catch (drawError) {
console.error("Error drawing original image to fallback canvas:", drawError);
}
return fallbackCanvas;
}
// 2. Grayscale
const grayData = new Uint8ClampedArray(width * height);
for (let i = 0; i < imageData.data.length; i += 4) {
const r = imageData.data[i];
const g = imageData.data[i + 1];
const b = imageData.data[i + 2];
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
grayData[i / 4] = gray;
}
// 3. Box Blur (on grayData) - simple 3x3 blur
const blurredGrayData = new Uint8ClampedArray(width * height);
if (width < 3 || height < 3) {
// For very small images, skip blur or copy grayData
for(let i=0; i < grayData.length; i++) blurredGrayData[i] = grayData[i];
} else {
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let sum = 0;
let count = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
sum += grayData[ny * width + nx]; // Blur from original grayData
count++;
}
}
}
blurredGrayData[y * width + x] = count > 0 ? sum / count : grayData[y * width + x];
}
}
}
// 4. Sobel Edge Detection (on blurredGrayData)
const magnitudes = new Float32Array(width * height); // Initialized to 0
const directions = new Float32Array(width * height); // Initialized to 0
const Gx_kernel = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
const Gy_kernel = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
if (width >= 3 && height >= 3) { // Sobel needs at least 3x3 image
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let sumX = 0;
let sumY = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const val = blurredGrayData[(y + ky) * width + (x + kx)];
sumX += val * Gx_kernel[ky + 1][kx + 1];
sumY += val * Gy_kernel[ky + 1][kx + 1];
}
}
magnitudes[y * width + x] = Math.sqrt(sumX * sumX + sumY * sumY);
directions[y * width + x] = Math.atan2(sumY, sumX); // Angle in radians: -PI to PI
}
}
}
// 5. Prepare output canvas
const outputCanvas = document.createElement('canvas');
outputCanvas.width = width;
outputCanvas.height = height;
const ctx = outputCanvas.getContext('2d');
try {
ctx.drawImage(originalImg, 0, 0, width, height); // Draw original image as background
} catch(e) {
// Should not happen if tempCanvas drawImage succeeded, but as a safeguard
console.error("Error drawing original image to output canvas:", e);
}
// 6. Draw Wires
if (width < 3 || height < 3) { // Not enough data for wires if image is too small
return outputCanvas; // Return original image drawn on canvas
}
ctx.strokeStyle = wireColor;
ctx.lineWidth = wireThickness;
ctx.lineCap = "round";
ctx.lineJoin = "round";
// Attempt to find start points for wires
// Max attempts: 10% of numWires, but at least 10, and not more than numWires itself
const maxStartAttempts = Math.min(numWires, Math.max(10, Math.floor(numWires * 0.2)));
for (let i = 0; i < numWires; i++) {
let startX = 0, startY = 0;
let foundStart = false;
for (let attempt = 0; attempt < maxStartAttempts; attempt++) {
// Pick random point, ensuring it's within the Sobel-processed area [1, width-2]x[1, height-2]
startX = Math.floor(Math.random() * (width - 2)) + 1;
startY = Math.floor(Math.random() * (height - 2)) + 1;
if (magnitudes[startY * width + startX] > edgeThreshold) {
foundStart = true;
break;
}
}
if (!foundStart) continue;
let currentX = startX;
let currentY = startY;
// Initial angle is one of the two edge directions at start point
let currentAngle = directions[startY * width + startX] + Math.PI / 2; // Edge direction
if (Math.random() < 0.5) {
currentAngle += Math.PI; // Randomly pick one of the two opposite directions
}
ctx.beginPath();
ctx.moveTo(currentX, currentY);
let segmentsDrawn = 0;
for (let j = 0; j < maxWireSegments; j++) {
// Ensure current coordinates are within valid Sobel range for reading direction
const ix = Math.max(1, Math.min(width - 2, Math.round(currentX)));
const iy = Math.max(1, Math.min(height - 2, Math.round(currentY)));
const gradAngle = directions[iy * width + ix]; // Gradient direction at current point
// Determine two possible edge directions (perpendicular to gradient)
let edgeDir1 = gradAngle + Math.PI / 2;
let edgeDir2 = gradAngle - Math.PI / 2;
// Normalize angles to [0, 2*PI) for consistent comparison
const normalizeAngle = (angle) => (angle % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI);
let cAngleNorm = normalizeAngle(currentAngle);
edgeDir1 = normalizeAngle(edgeDir1);
edgeDir2 = normalizeAngle(edgeDir2);
// Calculate angular difference to choose path of least resistance (inertia)
let diff1 = Math.abs(cAngleNorm - edgeDir1);
if (diff1 > Math.PI) diff1 = 2 * Math.PI - diff1; // Ensure shortest angle
let diff2 = Math.abs(cAngleNorm - edgeDir2);
if (diff2 > Math.PI) diff2 = 2 * Math.PI - diff2;
let chosenEdgeAngle = (diff1 < diff2) ? edgeDir1 : edgeDir2;
// Add random perturbation to the chosen angle
const randomPerturbation = (Math.random() - 0.5) * 2 * turnRandomness;
currentAngle = chosenEdgeAngle + randomPerturbation;
const nextX = currentX + segmentLength * Math.cos(currentAngle);
const nextY = currentY + segmentLength * Math.sin(currentAngle);
const nextIx = Math.round(nextX);
const nextIy = Math.round(nextY);
// Boundary checks for the next point
if (nextIx <= 0 || nextIx >= width - 1 || nextIy <= 0 || nextIy >= height - 1 ) {
break; // Stop if wire goes out of Sobel-valid bounds
}
// Stop if edge strength at next point is too low
if (magnitudes[nextIy * width + nextIx] < edgeThreshold * 0.5) {
break;
}
ctx.lineTo(nextX, nextY);
segmentsDrawn++;
currentX = nextX;
currentY = nextY;
}
if (segmentsDrawn > 0) { // Only stroke if the path has at least one segment
ctx.stroke();
}
}
return outputCanvas;
}
Apply Changes