You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, numPins = "250", numLines = "2500", lineOpacity = "0.1", lineWeight = "1", color = "#000000", canvasSize = "600") {
// Parse parameters
const pinsCount = parseInt(numPins, 10);
const chordsCount = parseInt(numLines, 10);
const opacity = parseFloat(lineOpacity);
const weight = parseFloat(lineWeight);
const size = parseInt(canvasSize, 10);
// Create working canvas for extracting image data
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Fill background with white
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, size, size);
// Calculate dimensions to scale the image so it covers the circular frame
const radius = size / 2 - 2;
const cx = size / 2;
const cy = size / 2;
const minDim = Math.min(originalImg.width, originalImg.height);
const scale = (radius * 2) / minDim;
const dw = originalImg.width * scale;
const dh = originalImg.height * scale;
const dx = cx - dw / 2;
const dy = cy - dh / 2;
// Draw the image clipped to a circle
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.clip();
ctx.drawImage(originalImg, dx, dy, dw, dh);
ctx.restore();
// Extract pixel data and build error map (inverted grayscale)
const imgData = ctx.getImageData(0, 0, size, size);
const data = imgData.data;
const errorMap = new Float32Array(size * size);
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const gray = r * 0.299 + g * 0.587 + b * 0.114;
const idx = i / 4;
const px = idx % size;
const py = Math.floor(idx / size);
// Prevent drawing outside the circular boundary
const dist = Math.hypot(px - cx, py - cy);
if (dist > radius) {
errorMap[idx] = 0;
} else {
errorMap[idx] = 255 - gray; // Dark parts yield higher "error"
}
}
// Generate pins around the circle
const pins = [];
for (let i = 0; i < pinsCount; i++) {
const angle = (i * Math.PI * 2) / pinsCount;
pins.push({
x: Math.round(cx + radius * Math.cos(angle)),
y: Math.round(cy + radius * Math.sin(angle))
});
}
// Cache to prevent re-computing identical line segments (Memory optimized)
const linesMap = new Array(pinsCount);
for (let i = 0; i < pinsCount; i++) {
linesMap[i] = new Array(pinsCount);
}
function getLine(p1, p2) {
const cacheP1 = p1 < p2 ? p1 : p2;
const cacheP2 = p1 < p2 ? p2 : p1;
if (linesMap[cacheP1][cacheP2]) {
return linesMap[cacheP1][cacheP2];
}
// Standard Bresenham's line algorithm
const x0 = pins[cacheP1].x;
const y0 = pins[cacheP1].y;
const x1 = pins[cacheP2].x;
const y1 = pins[cacheP2].y;
const pixels = [];
const dxLine = Math.abs(x1 - x0);
const sx = x0 < x1 ? 1 : -1;
const dyLine = -Math.abs(y1 - y0);
const sy = y0 < y1 ? 1 : -1;
let err = dxLine + dyLine;
let cx_i = x0;
let cy_i = y0;
while (true) {
pixels.push(cy_i * size + cx_i);
if (cx_i === x1 && cy_i === Math.round(y1)) break;
const e2 = 2 * err;
if (e2 >= dyLine) {
err += dyLine;
cx_i += sx;
}
if (e2 <= dxLine) {
err += dxLine;
cy_i += sy;
}
}
linesMap[cacheP1][cacheP2] = pixels;
return pixels;
}
// Algorithm: Find chords that reduce the maximum "error"
let currentPin = 0;
const path = [currentPin];
const errorReduction = 255 * opacity; // Remove visual equivalent darkness from map
for (let step = 0; step < chordsCount; step++) {
let bestPin = -1;
let bestScore = -1;
// Skip immediately adjacent pins to prevent dense edge rings
const minJump = Math.max(2, Math.floor(pinsCount * 0.05));
for (let nextPin = 0; nextPin < pinsCount; nextPin++) {
if (nextPin === currentPin) continue;
let dist = Math.abs(nextPin - currentPin);
if (dist > pinsCount / 2) dist = pinsCount - dist;
if (dist < minJump) continue;
const linePixels = getLine(currentPin, nextPin);
let score = 0;
const len = linePixels.length;
for (let i = 0; i < len; i++) {
score += errorMap[linePixels[i]];
}
const avgScore = score / len;
if (avgScore > bestScore) {
bestScore = avgScore;
bestPin = nextPin;
}
}
if (bestPin === -1) break;
const bestLineParams = getLine(currentPin, bestPin);
for (let i = 0; i < bestLineParams.length; i++) {
const idx = bestLineParams[i];
errorMap[idx] = Math.max(0, errorMap[idx] - errorReduction);
}
path.push(bestPin);
currentPin = bestPin;
// Yield to browser periodically to keep UI responsive
if (step % 50 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
// Convert hex color to rgba for drawing
function hexToRgba(hexStr, alpha) {
hexStr = hexStr.replace(/^#/, '');
let r = 0, g = 0, b = 0;
if (hexStr.length === 3) {
r = parseInt(hexStr[0] + hexStr[0], 16);
g = parseInt(hexStr[1] + hexStr[1], 16);
b = parseInt(hexStr[2] + hexStr[2], 16);
} else if (hexStr.length === 6) {
r = parseInt(hexStr.substring(0, 2), 16);
g = parseInt(hexStr.substring(2, 4), 16);
b = parseInt(hexStr.substring(4, 6), 16);
}
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
// Create Result Canvas
const outCanvas = document.createElement('canvas');
outCanvas.width = size;
outCanvas.height = size;
const outCtx = outCanvas.getContext('2d');
outCtx.fillStyle = '#ffffff';
outCtx.fillRect(0, 0, size, size);
outCtx.strokeStyle = hexToRgba(color, opacity);
outCtx.lineWidth = weight;
outCtx.lineCap = "round";
// Draw the chords
for (let i = 1; i < path.length; i++) {
outCtx.beginPath();
outCtx.moveTo(pins[path[i - 1]].x, pins[path[i - 1]].y);
outCtx.lineTo(pins[path[i]].x, pins[path[i]].y);
outCtx.stroke();
}
return outCanvas;
}
Apply Changes