You can edit the below JavaScript code to customize the image tool.
async function processImage(originalImg, numStrings = 2000, numPins = 256, stringThickness = 0.5, stringOpacity = 0.15, backgroundColor = "black") {
const w = originalImg.width;
const h = originalImg.height;
const outputCanvas = document.createElement('canvas');
// Ensure width and height are at least 0, not negative.
outputCanvas.width = Math.max(0, w);
outputCanvas.height = Math.max(0, h);
const outputCtx = outputCanvas.getContext('2d');
// Fill background
outputCtx.fillStyle = backgroundColor;
outputCtx.fillRect(0, 0, outputCanvas.width, outputCanvas.height);
// Early exit if image dimensions are invalid or no pins/strings requested
if (w <= 0 || h <= 0 || numPins <= 0 || numStrings <= 0) {
return outputCanvas;
}
// Source canvas for pixel data
const sourceCanvas = document.createElement('canvas');
sourceCanvas.width = w;
sourceCanvas.height = h;
const sourceCtx = sourceCanvas.getContext('2d');
sourceCtx.drawImage(originalImg, 0, 0);
let imgData;
try {
imgData = sourceCtx.getImageData(0, 0, w, h);
} catch (e) {
console.error("Error getting image data (possibly due to CORS tainted canvas if image is from another domain without CORS headers):", e);
// Return the background-filled canvas as a fallback
return outputCanvas;
}
// Helper function to get pixel color from image data
function getPixelColor(x, y, data, imageWidth, imageHeight) {
// Clamp coordinates to be within image bounds
const clampedX = Math.max(0, Math.min(Math.floor(x), imageWidth - 1));
const clampedY = Math.max(0, Math.min(Math.floor(y), imageHeight - 1));
const index = (clampedY * imageWidth + clampedX) * 4;
if (!data || index < 0 || index + 3 >= data.length) {
return [0, 0, 0, 0]; // Return transparent black if out of bounds or data is invalid
}
return [data[index], data[index + 1], data[index + 2], data[index + 3]];
}
const pins = [];
// Generate perimeter points (all unique pixel coordinates on the border)
const perimeterPixelCoords = [];
if (w > 0 && h > 0) {
for (let i = 0; i < w; i++) perimeterPixelCoords.push({ x: i, y: 0 }); // Top edge
for (let i = 1; i < h; i++) perimeterPixelCoords.push({ x: w - 1, y: i }); // Right edge (skip top-right)
if (h > 1) { // Add bottom edge only if height > 1 (avoids duplicate points for h=1)
for (let i = w - 2; i >= 0; i--) perimeterPixelCoords.push({ x: i, y: h - 1 }); // Bottom edge (skip bottom-right)
}
if (w > 1) { // Add left edge only if width > 1 (avoids duplicate points for w=1)
for (let i = h - 2; i > 0; i--) perimeterPixelCoords.push({ x: 0, y: i }); // Left edge (skip bottom-left and top-left)
}
}
// If perimeterPixelCoords is empty (e.g. 1x1 image where above logic might miss it or for very small images)
if (perimeterPixelCoords.length === 0 && w > 0 && h > 0) {
perimeterPixelCoords.push({x: Math.floor(w/2), y: Math.floor(h/2)}); // Add a center pin as fallback
}
if (perimeterPixelCoords.length === 0) {
return outputCanvas; // No points to place pins
}
// Select numPins from perimeterPixelCoords, distributing them
for (let i = 0; i < numPins; i++) {
const index = Math.floor(i * (perimeterPixelCoords.length / numPins));
pins.push(perimeterPixelCoords[index % perimeterPixelCoords.length]); // Modulo for safety
}
if (pins.length < 2) {
// Not enough pins to draw strings (e.g. if numPins was 1)
return outputCanvas;
}
// Clamp parameters
const clampedStringOpacity = Math.max(0, Math.min(1, stringOpacity));
const clampedStringThickness = Math.max(0.1, stringThickness); // Ensure thickness is at least 0.1
outputCtx.lineCap = "round"; // Nicer line ends
for (let i = 0; i < numStrings; i++) {
const pinIndex1 = Math.floor(Math.random() * pins.length);
let pinIndex2 = Math.floor(Math.random() * pins.length);
let attempts = 0;
const maxAttempts = pins.length * 2; // Heuristic to prevent infinite loop
// Ensure two distinct pins if possible
while (pinIndex1 === pinIndex2 && pins.length > 1 && attempts < maxAttempts) {
pinIndex2 = Math.floor(Math.random() * pins.length);
attempts++;
}
// If we couldn't find a distinct pin (e.g., only 1 unique pin location, or bad luck with random)
// and pins.length > 1, we might skip or just draw. If pins.length==1, this loop is skipped.
// if (pinIndex1 === pinIndex2 && pins.length > 1) continue; // Option: skip if same pin chosen
const p1 = pins[pinIndex1];
const p2 = pins[pinIndex2];
// Get color from the average of the two pin points in the original image
const color1 = getPixelColor(p1.x, p1.y, imgData.data, w, h);
const color2 = getPixelColor(p2.x, p2.y, imgData.data, w, h);
const r = Math.floor((color1[0] + color2[0]) / 2);
const g = Math.floor((color1[1] + color2[1]) / 2);
const b = Math.floor((color1[2] + color2[2]) / 2);
// Alpha could be averaged too, e.g. const avgAlpha = (color1[3] + color2[3]) / 2;
// For simplicity, we use the parameter stringOpacity for the drawn string.
outputCtx.beginPath();
// Offset by 0.5 for potentially sharper lines on pixel grid
outputCtx.moveTo(p1.x + 0.5, p1.y + 0.5);
outputCtx.lineTo(p2.x + 0.5, p2.y + 0.5);
outputCtx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${clampedStringOpacity})`;
outputCtx.lineWidth = clampedStringThickness;
outputCtx.stroke();
}
return outputCanvas;
}
Free Image Tool Creator
Can't find the image tool you're looking for? Create one based on your own needs now!
The Image String Theory Visualization Filter allows users to transform images into artistic visualizations that simulate strings connecting points on the image. This tool generates a new image by drawing multiple semi-transparent strings between selected points on the perimeter of the original image, using color information from those points. It can be particularly useful for creating unique and abstract art pieces, enhancing visual presentations, or generating eye-catching graphics for web and social media. Users can customize parameters such as the number of strings, string thickness, and opacity to achieve their desired aesthetic effect.