Please bookmark this page to avoid losing your image tool!

Image Stained Glass Window Filter Effect Tool

(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.
function processImage(originalImg, numCells = 200, lineColor = "black", lineWidth = 2) {
    const width = originalImg.width;
    const height = originalImg.height;

    // 1. Setup canvases
    const outputCanvas = document.createElement('canvas');
    outputCanvas.width = width;
    outputCanvas.height = height;
    const ctx = outputCanvas.getContext('2d');

    // Handle edge cases for dimensions
    if (width === 0 || height === 0) {
        return outputCanvas; // Return empty canvas for 0-sized image
    }

    // Draw original image to a temporary canvas to get pixel data
    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = width;
    tempCanvas.height = height;
    const tempCtx = tempCanvas.getContext('2d');
    tempCtx.drawImage(originalImg, 0, 0, width, height);
    let originalImageData;
    try {
        originalImageData = tempCtx.getImageData(0, 0, width, height);
    } catch (e) {
        // This can happen due to tainted canvas if image is cross-origin and server lacks CORS headers
        console.error("Could not get image data, possibly due to CORS policy. Returning original image drawn on canvas.", e);
        ctx.drawImage(originalImg, 0, 0);
        return outputCanvas;
    }
    const originalPixels = originalImageData.data;

    // 2. Generate Cell Points (Voronoi Sites)
    const cellPoints = [];
    if (numCells <= 0) numCells = 1; // Ensure at least one cell point for logic
    if (lineWidth < 0) lineWidth = 0; // Ensure non-negative line width

    for (let i = 0; i < numCells; i++) {
        const x = Math.floor(Math.random() * width);
        const y = Math.floor(Math.random() * height);
        const pixelIndex = (y * width + x) * 4; // Clamp coordinates for safety, though Math.random()*width should be < width
        
        const safeX = Math.max(0, Math.min(x, width - 1));
        const safeY = Math.max(0, Math.min(y, height - 1));
        const safePixelIndex = (safeY * width + safeX) * 4;

        cellPoints.push({
            x: safeX,
            y: safeY,
            r: originalPixels[safePixelIndex],
            g: originalPixels[safePixelIndex + 1],
            b: originalPixels[safePixelIndex + 2],
            id: i // Unique ID for the cell, used for ownership check
        });
    }
    
    // If somehow cellPoints ended up empty (e.g. numCells was manipulated to 0 after initial check)
    if (cellPoints.length === 0) {
        ctx.drawImage(originalImg, 0, 0); // Draw original and return
        return outputCanvas;
    }


    // 3. Assign Pixels to Cells (Coloring Pass)
    // This creates the colored "panes" of the stained glass.
    const outputImageData = ctx.createImageData(width, height);
    const outputPixels = outputImageData.data;
    // pixelOwnerMap stores the ID of the cell each pixel belongs to.
    // This is used later to detect boundaries for drawing lines.
    const pixelOwnerMap = new Array(width * height); 

    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            let minDistSq = Number.MAX_VALUE;
            let closestCellId = -1;
            let r = 0, g = 0, b = 0; // Color of the closest cell

            for (let i = 0; i < cellPoints.length; i++) {
                const cell = cellPoints[i];
                const dx = cell.x - x;
                const dy = cell.y - y;
                const distSq = dx * dx + dy * dy; // Squared Euclidean distance

                if (distSq < minDistSq) {
                    minDistSq = distSq;
                    closestCellId = cell.id;
                    r = cell.r;
                    g = cell.g;
                    b = cell.b;
                }
            }

            const targetPixelIndex = (y * width + x) * 4;
            outputPixels[targetPixelIndex] = r;
            outputPixels[targetPixelIndex + 1] = g;
            outputPixels[targetPixelIndex + 2] = b;
            outputPixels[targetPixelIndex + 3] = 255; // Full alpha

            pixelOwnerMap[y * width + x] = closestCellId;
        }
    }
    ctx.putImageData(outputImageData, 0, 0); // Draw the colored panes onto the canvas

    // 4. Draw Leading Lines (if lineWidth > 0)
    // These lines are drawn on top of the colored panes.
    if (lineWidth > 0) {
        ctx.fillStyle = lineColor;
        const effectiveLineWidth = Math.round(lineWidth); // Use rounded integer for pixel operations

        if (effectiveLineWidth > 0) { // Only draw if lines have thickness
             for (let y = 0; y < height; y++) {
                for (let x = 0; x < width; x++) {
                    const currentOwner = pixelOwnerMap[y * width + x];

                    // Check right neighbor for a potential vertical line
                    if (x < width - 1) {
                        const rightOwner = pixelOwnerMap[y * width + (x + 1)];
                        if (currentOwner !== rightOwner) {
                            // A boundary exists between pixel (x,y) and (x+1,y).
                            // Draw a vertical line segment.
                            // The line is centered at the boundary (x + 0.5).
                            // For fillRect, calculate starting x-coordinate to center the line.
                            const lineX = x + 1 - Math.ceil(effectiveLineWidth / 2);
                            ctx.fillRect(lineX, y, effectiveLineWidth, 1);
                        }
                    }

                    // Check bottom neighbor for a potential horizontal line
                    if (y < height - 1) {
                        const bottomOwner = pixelOwnerMap[(y + 1) * width + x];
                        if (currentOwner !== bottomOwner) {
                            // A boundary exists between pixel (x,y) and (x,y+1).
                            // Draw a horizontal line segment.
                            // The line is centered at the boundary (y + 0.5).
                            const lineY = y + 1 - Math.ceil(effectiveLineWidth / 2);
                             ctx.fillRect(x, lineY, 1, effectiveLineWidth);
                        }
                    }
                }
            }
        }
    }

    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 Stained Glass Window Filter Effect Tool allows users to transform their images into beautiful stained glass window effects. By applying this filter, images are divided into colorful ‘panes’ created from random cell points, mimicking the look of traditional stained glass. Users can adjust the number of panes, the color of the leading lines, and their width to customize the effect. This tool can be useful for artists, designers, or anyone looking to create visually striking images for use in prints, digital art, social media, or personalized gifts.

Leave a Reply

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