Please bookmark this page to avoid losing your image tool!

Image String Theory Visualization Filter

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
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!

Description

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.

Leave a Reply

Your email address will not be published. Required fields are marked *