Please bookmark this page to avoid losing your image tool!

Chorded Image Effect Generator

(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, 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;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Chorded Image Effect Generator transforms standard photographs into unique, stylized geometric artworks using a chord-based line algorithm. The tool works by mapping an image into a circular frame and generating a series of connecting lines (chords) between points on the circle’s perimeter, strategically placing them to represent the shadows and details of the original image. Users can customize the effect by adjusting the number of pins, the quantity of lines, line weight, opacity, and color. This tool is ideal for creating artistic profile pictures, minimalist geometric portraits, or unique graphic design elements for digital media and print.

Leave a Reply

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