Please bookmark this page to avoid losing your image tool!

Photo Invisible Ink 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, particleColorRGB = '255,255,255', particleOpacity = 0.7, minRadius = 10, maxRadius = 30, density = 0.8, blurAmount = 15) {

    let imgWidth = originalImg.naturalWidth || originalImg.width;
    let imgHeight = originalImg.naturalHeight || originalImg.height;

    // Handle HTMLImageElement loading if it's not complete or has no dimensions
    if (originalImg instanceof HTMLImageElement && (!originalImg.complete || originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0)) {
        // If the image is 'complete' but has no width/height, it might be broken or has no src.
        if (originalImg.complete && (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0)) {
            if (!originalImg.src) {
                console.warn("Image has no source. Returning 1x1 canvas indicating error.");
            } else {
                console.warn("Image is marked complete but has zero dimensions (possibly broken). Returning 1x1 canvas.");
            }
            const errorCanvas = document.createElement('canvas');
            errorCanvas.width = 1; errorCanvas.height = 1;
            return errorCanvas;
        }
        
        // If not complete, or src is set but not yet loaded (naturalWidth is 0), try to load
        if (!originalImg.src) {
             console.warn("Image has no source and is not complete. Returning 1x1 canvas.");
             const errorCanvas = document.createElement('canvas');
             errorCanvas.width = 1; errorCanvas.height = 1;
             return errorCanvas;
        }

        try {
            await new Promise((resolve, reject) => {
                // Assign new handlers
                const loadHandler = () => {
                    imgWidth = originalImg.naturalWidth;
                    imgHeight = originalImg.naturalHeight;
                    cleanup();
                    resolve();
                };
                const errorHandler = () => {
                    cleanup();
                    reject(new Error("Image failed to load."));
                };
                const cleanup = () => {
                    originalImg.removeEventListener('load', loadHandler);
                    originalImg.removeEventListener('error', errorHandler);
                };

                originalImg.addEventListener('load', loadHandler);
                originalImg.addEventListener('error', errorHandler);

                // In case the image completes loading after the 'complete' check but before handlers are attached
                if (originalImg.complete && originalImg.naturalWidth > 0) {
                    loadHandler();
                }
            });
        } catch (e) {
            console.error("Error during image loading:", e.message);
            const errorCanvas = document.createElement('canvas');
            errorCanvas.width = 1; errorCanvas.height = 1; 
            return errorCanvas;
        }
    }
    
    // Final check on dimensions after any loading attempt
    if (!imgWidth || !imgHeight || imgWidth === 0 || imgHeight === 0) {
        console.warn("Image has invalid or zero dimensions after loading attempts. Returning 1x1 canvas.");
        const errorCanvas = document.createElement('canvas');
        errorCanvas.width = 1; errorCanvas.height = 1;
        return errorCanvas;
    }

    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    canvas.width = imgWidth;
    canvas.height = imgHeight;

    // Draw the original image
    ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);

    // Parameter validation and sanitization
    // Coerce to string then to number for robustness against mixed types
    const pOpacity = Math.max(0, Math.min(1, parseFloat(String(particleOpacity))));
    
    let pMinRadius = Math.max(1, parseInt(String(minRadius), 10));
    let pMaxRadius = Math.max(1, parseInt(String(maxRadius), 10));
    if (pMinRadius > pMaxRadius) { // Ensure minRadius is not greater than maxRadius
        [pMinRadius, pMaxRadius] = [pMaxRadius, pMinRadius]; 
    }

    const pDensity = Math.max(0.01, parseFloat(String(density))); // Density factor, avoid zero or negative
    const pBlurAmount = Math.max(0, parseInt(String(blurAmount), 10));

    // Parse particleColorRGB string (e.g., "255,255,255")
    const rgbStrings = String(particleColorRGB).split(',');
    const r_val = parseInt(rgbStrings[0] ? rgbStrings[0].trim() : '255', 10);
    const g_val = parseInt(rgbStrings[1] ? rgbStrings[1].trim() : '255', 10);
    const b_val = parseInt(rgbStrings[2] ? rgbStrings[2].trim() : '255', 10);
    
    // Default to white if parsing fails for any component
    const R = !isNaN(r_val) ? r_val : 255;
    const G = !isNaN(g_val) ? g_val : 255;
    const B = !isNaN(b_val) ? b_val : 255;

    const finalParticleColor = `rgba(${R},${G},${B},${pOpacity})`;
    // Shadow color can be same as particle or slightly adjusted (e.g., more transparent)
    const shadowParticleColor = `rgba(${R},${G},${B},${pOpacity * 0.75})`; 

    // Setup particle drawing style
    ctx.fillStyle = finalParticleColor;
    if (pBlurAmount > 0) {
        ctx.shadowColor = shadowParticleColor; 
        ctx.shadowBlur = pBlurAmount;
        ctx.shadowOffsetX = 0; // Particle glow should be centered
        ctx.shadowOffsetY = 0;
    }

    // Determine particle placement strategy
    // avgRadius is used to scale the step size based on particle dimensions and density
    const avgRadius = (pMinRadius + pMaxRadius) / 2;
    // Step size for iterating grid. Inversely proportional to density.
    // Higher density = smaller step = more particles.
    const step = Math.max(1, avgRadius / pDensity);

    // Loop through a grid and draw particles with jitter
    // Extend loop bounds by pMaxRadius to ensure particles cover image edges
    for (let y = -pMaxRadius; y < imgHeight + pMaxRadius; y += step) {
        for (let x = -pMaxRadius; x < imgWidth + pMaxRadius; x += step) {
            
            // Jitter position: random offset from grid point for a more natural look
            // Jitter magnitude is proportional to step size.
            const jitterX = (Math.random() - 0.5) * step;
            const jitterY = (Math.random() - 0.5) * step;
            
            // Base position is center of grid cell, then jitter is applied
            const currentX = x + step / 2 + jitterX; 
            const currentY = y + step / 2 + jitterY;
            
            // Randomize particle radius within the defined min/max range
            const radius = pMinRadius + Math.random() * (pMaxRadius - pMinRadius);

            // Optimization: skip drawing if particle is entirely off-canvas
            if (currentX + radius < 0 || currentX - radius > imgWidth ||
                currentY + radius < 0 || currentY - radius > imgHeight) {
                continue;
            }

            ctx.beginPath();
            ctx.arc(currentX, currentY, radius, 0, Math.PI * 2);
            ctx.fill();
        }
    }

    // Reset shadow properties to avoid affecting subsequent drawings if context is reused elsewhere
    if (pBlurAmount > 0) {
        ctx.shadowColor = 'transparent'; // Or 'rgba(0,0,0,0)'
        ctx.shadowBlur = 0;
        ctx.shadowOffsetX = 0;
        ctx.shadowOffsetY = 0;
    }

    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 Photo Invisible Ink Filter Effect Tool allows users to apply a unique filter effect to images, simulating the appearance of invisible ink using customizable particles. Users can adjust particle color, opacity, size, density, and blur to create visually appealing renditions of their images. This tool is useful for graphic designers, artists, or anyone looking to create stylized images for social media posts, presentations, or personal projects, adding an artistic touch that can evoke intrigue and creativity.

Leave a Reply

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