You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, numNodes = '7', tangleLevel = '15', themeColor = '#e74c3c', bgColor = '#1e272e') {
// Parse parameters
const safeNumNodes = Math.min(Math.max(Number(numNodes) || 7, 2), 20);
const safeTangle = Math.min(Math.max(Number(tangleLevel) || 15, 1), 50);
const pinColor = typeof themeColor === 'string' ? themeColor : '#e74c3c';
const boardColor = typeof bgColor === 'string' ? bgColor : '#1e272e';
// Canvas setup
const canvas = document.createElement('canvas');
const bW = 1400; // Board Width
const bH = 1000; // Board Height
canvas.width = bW;
canvas.height = bH;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
// 1. Draw Background (Detective Board)
ctx.fillStyle = boardColor;
ctx.fillRect(0, 0, bW, bH);
// Board Gradient
const grad = ctx.createRadialGradient(bW/2, bH/2, bH/3, bW/2, bH/2, bW);
grad.addColorStop(0, 'rgba(0,0,0,0)');
grad.addColorStop(1, 'rgba(0,0,0,0.6)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, bW, bH);
// Board Grid
ctx.strokeStyle = 'rgba(255,255,255,0.03)';
ctx.lineWidth = 1;
for(let i=0; i<bW; i+=40) { ctx.beginPath(); ctx.moveTo(i,0); ctx.lineTo(i,bH); ctx.stroke(); }
for(let i=0; i<bH; i+=40) { ctx.beginPath(); ctx.moveTo(0,i); ctx.lineTo(bW,i); ctx.stroke(); }
// Random Background Text (Clues)
ctx.fillStyle = 'rgba(255,255,255,0.05)';
ctx.font = 'bold 28px Courier New, Courier, monospace';
const clues = ["WHO?", "WHERE?", "CONFIDENTIAL", "REPORT", "EVIDENCE", "CONNECT THE DOTS", "CLASSIFIED", "FILE", "?!"];
for(let k=0; k<12; k++) {
ctx.save();
ctx.translate(Math.random() * bW, Math.random() * bH);
ctx.rotate((Math.random() - 0.5) * 2);
ctx.fillText(clues[Math.floor(Math.random() * clues.length)], 0, 0);
ctx.restore();
}
// Math Utility
function rotatePoint(cx, cy, x, y, angle) {
const cos = Math.cos(angle), sin = Math.sin(angle);
return {
x: (cos * (x - cx)) - (sin * (y - cy)) + cx,
y: (sin * (x - cx)) + (cos * (y - cy)) + cy
};
}
// 2. Setup Dimensions for Main Polaroid (Center Image)
const centerX = bW / 2;
const centerY = bH / 2;
let mainWidth = 650;
let mainImgHeight = mainWidth * (originalImg.height / originalImg.width);
// Scale down if image is extremely tall
if (mainImgHeight > bH * 0.6) {
mainImgHeight = bH * 0.6;
mainWidth = mainImgHeight * (originalImg.width / originalImg.height);
}
const mainPh = mainImgHeight + 90; // Polarid outer height
const mainAngle = (Math.random() - 0.5) * 0.08;
// 3. Generate Small Nodes (Image Crops)
const nodes = [];
for (let i = 0; i < safeNumNodes; i++) {
let placed = false;
let nx, ny;
let attempts = 0;
// Spread nodes nicely around the main center image
while (!placed && attempts < 150) {
nx = 200 + Math.random() * (bW - 400);
ny = 200 + Math.random() * (bH - 400);
if (Math.abs(nx - centerX) > mainWidth/2 + 70 || Math.abs(ny - centerY) > mainPh/2 + 70) {
placed = true;
}
attempts++;
}
// Random square harvest from the original
let minDim = Math.min(originalImg.width, originalImg.height);
let cropSize = minDim * (0.15 + Math.random() * 0.2); // 15% to 35%
let sx = Math.random() * (originalImg.width - cropSize);
let sy = Math.random() * (originalImg.height - cropSize);
// Find location of this crop center on the MAIN polaroid (canvas coordinates)
let imgTopLeftX = centerX - mainWidth/2 + 20;
let imgTopLeftY = centerY - mainPh/2 + 20;
let cropCenterXOnMain = imgTopLeftX + (sx + cropSize/2) / originalImg.width * (mainWidth - 40);
let cropCenterYOnMain = imgTopLeftY + (sy + cropSize/2) / originalImg.height * mainImgHeight;
let origPos = rotatePoint(centerX, centerY, cropCenterXOnMain, cropCenterYOnMain, mainAngle);
nodes.push({
x: nx, y: ny,
angle: (Math.random() - 0.5) * 0.5,
pw: 160 + Math.random() * 40,
ph: 200 + Math.random() * 40,
sx: sx, sy: sy, cropSize: cropSize,
origX: origPos.x, origY: origPos.y,
// Calculate coordinates later for exact pin
pinPos: { x: 0, y: 0 }
});
}
// Compute Exact Pin Anchor Coordinates for Nodes
nodes.forEach(node => {
node.pinPos = rotatePoint(node.x, node.y, node.x, node.y - node.ph/2 + 12, node.angle);
});
// 4. Draw Main Target Center Image
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate(mainAngle);
// Shadow
ctx.shadowColor = 'rgba(0,0,0,0.8)';
ctx.shadowBlur = 25;
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
// Paper
ctx.fillStyle = '#f5f6fa';
ctx.fillRect(-mainWidth/2, -mainPh/2, mainWidth, mainPh);
// Image
ctx.shadowColor = 'transparent'; // stop shadow for inner operations
ctx.drawImage(originalImg, -mainWidth/2 + 20, -mainPh/2 + 20, mainWidth - 40, mainImgHeight);
// Tape/Pin
ctx.fillStyle = '#2c3e50';
ctx.beginPath(); ctx.arc(0, -mainPh/2 + 10, 6, 0, Math.PI*2); ctx.fill();
// Inner Pin reflection
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.beginPath(); ctx.arc(-2, -mainPh/2 + 8, 2, 0, Math.PI*2); ctx.fill();
// Text Label Main
ctx.fillStyle = '#2d3436';
ctx.font = 'bold 20px Courier New, Courier, monospace';
ctx.fillText("SUBJECT PROFILE", -mainWidth/2 + 20, mainPh/2 - 25);
ctx.restore();
// 4.5 Draw Target Pins directly on the Main Image First
nodes.forEach(node => {
ctx.fillStyle = pinColor;
ctx.beginPath();
ctx.arc(node.origX, node.origY, 4.5, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.beginPath();
ctx.arc(node.origX - 1.5, node.origY - 1.5, 1.5, 0, Math.PI*2);
ctx.fill();
});
// 5. Draw Tangled "Red" Strings
ctx.strokeStyle = pinColor;
// Primary Strings: From Nodes to Main Image Targets
for (let node of nodes) {
for (let t = 0; t < safeTangle; t++) {
ctx.globalAlpha = 0.2 + Math.random() * 0.3;
ctx.lineWidth = 0.5 + Math.random() * 1.2;
ctx.beginPath();
ctx.moveTo(node.pinPos.x, node.pinPos.y);
// Random chaotic curve to the target point
let cp1x = node.pinPos.x + (node.origX - node.pinPos.x)*0.33 + (Math.random()-0.5)*150;
let cp1y = node.pinPos.y + (node.origY - node.pinPos.y)*0.33 + (Math.random()-0.5)*150;
let cp2x = node.pinPos.x + (node.origX - node.pinPos.x)*0.66 + (Math.random()-0.5)*150;
let cp2y = node.pinPos.y + (node.origY - node.pinPos.y)*0.66 + (Math.random()-0.5)*150;
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, node.origX, node.origY);
ctx.stroke();
}
}
// Secondary Strings: Random crazy connections between small nodes
ctx.globalAlpha = 0.2;
for (let i = 0; i < nodes.length; i++) {
let n1 = nodes[i];
let n2 = nodes[(i + 1) % nodes.length];
if(Math.random() > 0.3) {
for(let t=0; t < safeTangle/2; t++) {
ctx.lineWidth = 0.5 + Math.random();
ctx.beginPath();
ctx.moveTo(n1.pinPos.x, n1.pinPos.y);
let cx1 = n1.pinPos.x + (Math.random()-0.5)*300;
let cy1 = n1.pinPos.y + (Math.random()-0.5)*300;
let cx2 = n2.pinPos.x + (Math.random()-0.5)*300;
let cy2 = n2.pinPos.y + (Math.random()-0.5)*300;
ctx.bezierCurveTo(cx1, cy1, cx2, cy2, n2.pinPos.x, n2.pinPos.y);
ctx.stroke();
}
}
}
ctx.globalAlpha = 1.0;
// 6. Draw Small Nodes (Evidences / Focus points)
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i];
ctx.save();
ctx.translate(node.x, node.y);
ctx.rotate(node.angle);
// Polaroid Shadow
ctx.shadowColor = 'rgba(0,0,0,0.6)';
ctx.shadowBlur = 15;
ctx.shadowOffsetX = 6;
ctx.shadowOffsetY = 6;
// Paper
ctx.fillStyle = '#fdfdfd';
ctx.fillRect(-node.pw/2, -node.ph/2, node.pw, node.ph);
ctx.shadowColor = 'transparent';
// Draw Cropped Image
const borderMargin = 12;
const imgCanvasSize = node.pw - borderMargin * 2;
ctx.drawImage(
originalImg,
node.sx, node.sy, node.cropSize, node.cropSize,
-imgCanvasSize/2, -node.ph/2 + borderMargin + 15,
imgCanvasSize, imgCanvasSize
);
// Node Pin
ctx.fillStyle = pinColor; // nodes and strings share color theme
ctx.beginPath();
ctx.arc(0, -node.ph/2 + 12, 5.5, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.beginPath();
ctx.arc(-1.5, -node.ph/2 + 10, 2, 0, Math.PI*2);
ctx.fill();
// Scribble Label
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.font = 'bold 16px Courier New, Courier, monospace';
ctx.fillText(`FIG-0${i+1}`, -node.pw/2 + 15, node.ph/2 - 15);
ctx.restore();
}
return canvas;
}
Apply Changes