Please bookmark this page to avoid losing your image tool!

Image Geode Crystal 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.
async function processImage(originalImg, numCrystals = 50, edgeColor = 'rgba(255, 255, 255, 0.5)', edgeWidth = 1) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    const width = originalImg.naturalWidth || originalImg.width;
    const height = originalImg.naturalHeight || originalImg.height;

    if (width === 0 || height === 0) {
        canvas.width = 0;
        canvas.height = 0;
        return canvas;
    }

    canvas.width = width;
    canvas.height = height;

    // Parameter sanitization
    let pNumCrystals = parseInt(String(numCrystals));
    if (isNaN(pNumCrystals) || pNumCrystals <= 0) {
        pNumCrystals = 50; // Default if invalid
    }
    // Cap numCrystals at the number of pixels in the image, and ensure at least 1.
    pNumCrystals = Math.min(pNumCrystals, width * height);
    pNumCrystals = Math.max(1, pNumCrystals);


    let pEdgeWidth = parseFloat(String(edgeWidth));
    if (isNaN(pEdgeWidth) || pEdgeWidth < 0) {
        pEdgeWidth = 1; // Default if invalid
    }
    
    const pEdgeColor = typeof edgeColor === 'string' ? edgeColor : 'rgba(255, 255, 255, 0.5)';

    // 1. Draw original image to a temporary canvas to get its imageData.
    // This is a robust way to access pixel data, especially if the image is from a different origin (CORS permitting)
    // or if it's not a simple bitmap (e.g. SVG).
    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = width;
    tempCanvas.height = height;
    // Add { willReadFrequently: true } for potential performance optimization if supported.
    const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); 
    tempCtx.drawImage(originalImg, 0, 0, width, height);
    const originalImageData = tempCtx.getImageData(0, 0, width, height);
    const originalPixels = originalImageData.data;

    // 2. Generate Seed Points randomly across the image.
    // These points will be the centers of the "crystal" regions.
    const seeds = [];
    for (let i = 0; i < pNumCrystals; i++) {
        seeds.push({
            x: Math.floor(Math.random() * width),
            y: Math.floor(Math.random() * height),
            sumR: 0,  // Sum of red components of pixels in this seed's region
            sumG: 0,  // Sum of green components
            sumB: 0,  // Sum of blue components
            count: 0, // Number of pixels in this seed's region
            avgR: 0,  // Average red component
            avgG: 0,  // Average green component
            avgB: 0   // Average blue component
        });
    }

    // Helper function for squared Euclidean distance (faster than actual distance as sqrt is avoided)
    function distanceSq(x1, y1, x2, y2) {
        const dx = x1 - x2;
        const dy = y1 - y2;
        return dx * dx + dy * dy;
    }

    // 3. Assign Pixels to Regions (Voronoi-like segmentation) and accumulate colors.
    // For each pixel, find the closest seed point.
    // pixelMap will store the index of the seed that each pixel belongs to.
    const pixelMap = []; 
    for(let i = 0; i < height; i++) {
        // Using Uint16Array for pixelMap rows for memory efficiency if numCrystals is large.
        // This supports up to 65535 crystals. If fewer (<256), Uint8Array could be used.
        pixelMap.push(new Uint16Array(width));
    }

    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            let minDistSq = Infinity;
            let closestSeedIndex = 0; // Default to the first seed

            for (let i = 0; i < seeds.length; i++) {
                const dSq = distanceSq(x, y, seeds[i].x, seeds[i].y);
                if (dSq < minDistSq) {
                    minDistSq = dSq;
                    closestSeedIndex = i;
                }
            }
            
            pixelMap[y][x] = closestSeedIndex; // Assign pixel (x,y) to the closest seed
            
            // Accumulate color data for the assigned seed
            const pixelIndexInOriginal = (y * width + x) * 4; // R, G, B, A components
            seeds[closestSeedIndex].sumR += originalPixels[pixelIndexInOriginal];
            seeds[closestSeedIndex].sumG += originalPixels[pixelIndexInOriginal + 1];
            seeds[closestSeedIndex].sumB += originalPixels[pixelIndexInOriginal + 2];
            seeds[closestSeedIndex].count++;
        }
    }
    
    // 4. Calculate Average Colors for each seed region.
    for (let i = 0; i < seeds.length; i++) {
        const seed = seeds[i];
        if (seed.count > 0) {
            seed.avgR = Math.floor(seed.sumR / seed.count);
            seed.avgG = Math.floor(seed.sumG / seed.count);
            seed.avgB = Math.floor(seed.sumB / seed.count);
        } else {
            // Fallback for a seed that didn't get any pixels assigned
            // (e.g., if pNumCrystals is extremely high or seeds are pathologically clustered).
            // Use the color of the original image at the seed's own location.
            if (seed.x >= 0 && seed.x < width && seed.y >= 0 && seed.y < height) {
                 const seedPixelIndex = (seed.y * width + seed.x) * 4;
                 seed.avgR = originalPixels[seedPixelIndex];
                 seed.avgG = originalPixels[seedPixelIndex + 1];
                 seed.avgB = originalPixels[seedPixelIndex + 2];
            } else { // Should not happen with proper seed generation
                 seed.avgR = 0; seed.avgG = 0; seed.avgB = 0;
            }
        }
    }

    // 5. Draw Crystal Regions with average colors onto the main output canvas.
    const outputImageData = ctx.createImageData(width, height);
    const outputPixels = outputImageData.data;

    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const seedIndex = pixelMap[y][x]; // Get the region this pixel belongs to
            const avgColor = seeds[seedIndex]; // Get the average color for that region
            
            const outputPixelIndex = (y * width + x) * 4;
            outputPixels[outputPixelIndex]     = avgColor.avgR;
            outputPixels[outputPixelIndex + 1] = avgColor.avgG;
            outputPixels[outputPixelIndex + 2] = avgColor.avgB;
            outputPixels[outputPixelIndex + 3] = 255; // Set alpha to fully opaque
        }
    }
    ctx.putImageData(outputImageData, 0, 0);

    // 6. Draw Crystal Edges (lines between regions).
    if (pEdgeWidth > 0 && pEdgeColor && pEdgeColor !== 'transparent' && pEdgeColor !== '') {
        ctx.strokeStyle = pEdgeColor;
        ctx.lineWidth = pEdgeWidth;
        
        ctx.beginPath();
        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                const currentSeedIndex = pixelMap[y][x];

                // Check boundary to the right: if pixel (x,y) and (x+1,y) are in different regions.
                if (x < width - 1) { // Ensure there is a pixel to the right.
                    if (pixelMap[y][x+1] !== currentSeedIndex) {
                        // Draw a vertical line segment along the boundary.
                        // This line is at x-coordinate (x+1), from y to y+1.
                        ctx.moveTo(x + 1, y);
                        ctx.lineTo(x + 1, y + 1);
                    }
                }
                // Check boundary below: if pixel (x,y) and (x,y+1) are in different regions.
                if (y < height - 1) { // Ensure there is a pixel below.
                    if (pixelMap[y+1][x] !== currentSeedIndex) {
                        // Draw a horizontal line segment along the boundary.
                        // This line is at y-coordinate (y+1), from x to x+1.
                        ctx.moveTo(x, y + 1);
                        ctx.lineTo(x + 1, y + 1);
                    }
                }
            }
        }
        ctx.stroke(); // Apply all queued line segments
    }
    
    return canvas;
}

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 Geode Crystal Filter Effect Tool is designed to apply a unique crystal filter effect to images. Users can generate stunning visuals by utilizing adjustable parameters, including the number of crystal regions, edge color, and width. This tool is ideal for enhancing digital artwork, creating eye-catching social media posts, and adding artistic flair to photographs. Whether for personal projects or professional use, this tool enables users to transform ordinary images into striking, crystal-inspired compositions.

Leave a Reply

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