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!
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.