Please bookmark this page to avoid losing your image tool!

Image Lightning Bolt 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, 
    boltColor = "rgba(255, 255, 255, 0.9)", 
    glowColor = "rgba(170, 220, 255, 0.5)", 
    glowWidth = 15, // This is effectively shadowBlur radius for the glow
    boltWidth = 3,  // This is the width of the main bolt's core
    numBolts = 1, 
    jaggednessFactor = 0.3, // Displacement factor relative to segment length (0=straight, higher=more jagged)
    recursionDepth = 5,     // Number of recursive subdivisions for jaggedness (e.g., 5 means 2^5=32 segments)
    branchProbability = 0.3, // Likelihood of a branch forming at a segment node (0 to 1)
    branchAngleMax = Math.PI / 3, // Max angle deviation for a branch from parent segment (radians)
    branchLengthFactor = 0.6,   // Branch length relative to parent segment length
    branchWidthFactor = 0.6     // Branch width relative to parent bolt width
) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    // Ensure image is loaded for naturalWidth/Height
    if (!originalImg.complete || originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
        try {
            await new Promise((resolve, reject) => {
                // Check again in case it loaded between the if check and promise creation
                if (originalImg.complete && originalImg.naturalWidth !== 0 && originalImg.naturalHeight !== 0) {
                    resolve();
                    return;
                }
                originalImg.onload = () => resolve();
                originalImg.onerror = () => {
                    console.error("Image failed to load for lightning effect.");
                    // Resolve even on error to avoid unhandled promise rejection if caller doesn't catch.
                    // The function will then likely draw on a 0x0 or fallback canvas.
                    resolve(); 
                };
                // If src is set but not loading, or src is not set, onerror might not fire.
                // This mostly handles cases where src is set and loading is in progress.
            });
        } catch (e) {
            // If promise was rejected. Not currently implemented to reject.
            console.error("Error during image loading promise:", e);
            // Fallback: create an empty canvas of default size or return null
            canvas.width = 300; // Default size
            canvas.height = 150;
            ctx.fillStyle = "grey";
            ctx.fillRect(0,0,canvas.width, canvas.height);
            ctx.fillStyle = "black";
            ctx.fillText("Image load error", 10, 20);
            return canvas;
        }
    }
    
    canvas.width = originalImg.naturalWidth || 300; // Fallback width if naturalWidth is 0 after load attempt
    canvas.height = originalImg.naturalHeight || 150; // Fallback height

    // Draw the original image
    if (originalImg.complete && originalImg.naturalWidth > 0 && originalImg.naturalHeight > 0) {
        ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
    } else {
        // If image still not loaded or invalid, draw placeholder background
        ctx.fillStyle = "grey";
        ctx.fillRect(0,0,canvas.width, canvas.height);
        // No further drawing of bolts if image is missing
        return canvas; 
    }


    /**
     * Calculates the length of a line segment.
     * @param {object} p1 - Point {x, y}
     * @param {object} p2 - Point {x, y}
     * @returns {number} Length of the segment.
     */
    function getSegmentLength(p1, p2) {
        const dx = p2.x - p1.x;
        const dy = p2.y - p1.y;
        return Math.sqrt(dx * dx + dy * dy);
    }

    /**
     * Generates points for a lightning bolt segment and its branches.
     * @returns {{mainPath: Array<object>, branches: Array<Array<object>>}}
     */
    function generateBoltPath(
        startPoint, endPoint,
        jaggedness,
        currentRecDepth, maxRecDepth,
        branchProb, angleMax, lengthFactor, widthFactor,
        isBranchCall // True if this function call is for generating a branch segment
    ) {
        let segmentPoints = []; // Points for the main segment being processed by this call
        let allBranchPaths = []; // All branches generated from this segment and its children

        const currentSegmentLength = getSegmentLength(startPoint, endPoint);

        // Base case for recursion: if max depth reached or segment is too small
        if (currentRecDepth >= maxRecDepth || currentSegmentLength < 1) {
            segmentPoints.push(startPoint);
            segmentPoints.push(endPoint);
            return { mainPath: segmentPoints, branches: allBranchPaths };
        }

        // Calculate midpoint of the segment
        let midX = (startPoint.x + endPoint.x) / 2;
        let midY = (startPoint.y + endPoint.y) / 2;

        // Calculate displacement for the midpoint
        // Displacement is perpendicular to the segment, magnitude based on jaggedness and segment length
        const displacementMagnitude = (Math.random() * 2 - 1) * currentSegmentLength * jaggedness;
        
        let normalX = -(endPoint.y - startPoint.y); // Perpendicular vector component
        let normalY = (endPoint.x - startPoint.x);  // Perpendicular vector component
        
        const normLength = Math.sqrt(normalX * normalX + normalY * normalY);
        if (normLength > 0) { // Normalize the perpendicular vector
            normalX /= normLength;
            normalY /= normLength;
        } else { // Segment is essentially a point, choose random direction for displacement
            const randomAngle = Math.random() * 2 * Math.PI;
            normalX = Math.cos(randomAngle);
            normalY = Math.sin(randomAngle);
        }
        
        // Displaced midpoint
        const displacedMidX = midX + normalX * displacementMagnitude;
        const displacedMidY = midY + normalY * displacementMagnitude;
        const midPoint = { x: displacedMidX, y: displacedMidY };

        // Recursively generate paths for the two new sub-segments
        const result1 = generateBoltPath(startPoint, midPoint, jaggedness, currentRecDepth + 1, maxRecDepth, branchProb, angleMax, lengthFactor, widthFactor, isBranchCall);
        const result2 = generateBoltPath(midPoint, endPoint, jaggedness, currentRecDepth + 1, maxRecDepth, branchProb, angleMax, lengthFactor, widthFactor, isBranchCall);

        // Combine points from sub-segments (mainPath of result1 excluding its last point, plus all of mainPath of result2)
        segmentPoints = result1.mainPath.slice(0, -1).concat(result2.mainPath);
        
        // Collect all branches from sub-segments
        allBranchPaths.push(...result1.branches);
        allBranchPaths.push(...result2.branches);

        // Branching logic: potentially create a new branch from the displaced midpoint
        let currentBranchProb = isBranchCall ? branchProb / 2 : branchProb; // Optionally reduce probability for branches of branches
        if (currentRecDepth < maxRecDepth -1 && Math.random() < currentBranchProb && currentSegmentLength > 10 ) { // Conditions for branching
            const branchAngle = (Math.random() * 2 - 1) * angleMax; // Random angle for the branch
            const branchLen = currentSegmentLength * lengthFactor * (0.5 + Math.random() * 0.5); // Randomize branch length slightly

            // Determine base direction for the branch (e.g., along the parent segment, then rotated)
            let baseDirX = endPoint.x - midPoint.x; // Vector from new midpoint towards an end of original segment
            let baseDirY = endPoint.y - midPoint.y;
            let baseDirLen = Math.sqrt(baseDirX*baseDirX + baseDirY*baseDirY);

            if (baseDirLen === 0) { // If new midpoint is same as endPoint
                baseDirX = endPoint.x - startPoint.x; // Use full original segment direction
                baseDirY = endPoint.y - startPoint.y;
                baseDirLen = currentSegmentLength;
            }
            if (baseDirLen > 0) { // Normalize base direction vector
                baseDirX /= baseDirLen; baseDirY /= baseDirLen;
            } else { // Fallback: if segment was a point, use random direction
                const randomFallbackAngle = Math.random() * 2 * Math.PI;
                baseDirX = Math.cos(randomFallbackAngle);
                baseDirY = Math.sin(randomFallbackAngle);
            }
            
            // Rotate base direction to get new branch direction
            const newDirX = baseDirX * Math.cos(branchAngle) - baseDirY * Math.sin(branchAngle);
            const newDirY = baseDirX * Math.sin(branchAngle) + baseDirY * Math.cos(branchAngle);

            const branchEndPoint = {x: midPoint.x + newDirX * branchLen, y: midPoint.y + newDirY * branchLen};
            
            // Branches are generated with potentially less detail (max recursion depth)
            const branchMaxRec = Math.max(1, maxRecDepth - currentRecDepth - 1); 
            // Generate the branch path
            const branchResult = generateBoltPath(midPoint, branchEndPoint, jaggedness, 0, branchMaxRec, branchProb, angleMax, lengthFactor, widthFactor, true); 
            
            allBranchPaths.push(branchResult.mainPath); // Add the main path of the branch
            allBranchPaths.push(...branchResult.branches); // Add (sub)branches of this new branch
        }
        
        return { mainPath: segmentPoints, branches: allBranchPaths };
    }
    
    /**
     * Draws a path (array of points) with glow and core.
     */
    function drawSingleBoltPath(pathPoints, coreLnWidth, coreLnColor, glowLnColor, glowLnRadius) {
        if (pathPoints.length < 2 || coreLnWidth <= 0) return;
        
        ctx.beginPath();
        ctx.moveTo(pathPoints[0].x, pathPoints[0].y);
        for (let i = 1; i < pathPoints.length; i++) {
            ctx.lineTo(pathPoints[i].x, pathPoints[i].y);
        }
        
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';

        // Glow pass: line that casts shadow
        ctx.strokeStyle = glowLnColor;
        ctx.lineWidth = coreLnWidth; // Line casting shadow has coreWidth, shadow makes it appear wider
        ctx.shadowColor = glowLnColor;
        ctx.shadowBlur = glowLnRadius;
        ctx.stroke();

        // Core pass: brighter, thinner line on top
        ctx.strokeStyle = coreLnColor;
        ctx.lineWidth = Math.max(0.5, coreLnWidth * 0.5); // Core line is thinner than its glow base
        ctx.shadowBlur = 0; // No shadow for the core itself
        ctx.stroke();
        
        // Reset shadow properties for other drawing operations
        ctx.shadowColor = 'transparent';
        ctx.shadowBlur = 0;
    }

    // Generate and draw the specified number of lightning bolts
    for (let i = 0; i < numBolts; i++) {
        let startX, startY, endX, endY;
        // Determine random start/end points for the main bolt, typically across the image
        if (Math.random() > 0.5) { // Vertical orientation bias
            startX = Math.random() * canvas.width;
            startY = (Math.random() > 0.5) ? 0 : canvas.height; // Start from top or bottom edge
            endX = Math.random() * canvas.width;
            endY = (startY === 0) ? canvas.height : 0; // End on opposite edge
        } else { // Horizontal orientation bias
            startX = (Math.random() > 0.5) ? 0 : canvas.width; // Start from left or right edge
            startY = Math.random() * canvas.height;
            endX = (startX === 0) ? canvas.width : 0; // End on opposite edge
            endY = Math.random() * canvas.height;
        }
        
        const startPoint = { x: startX, y: startY };
        const endPoint = { x: endX, y: endY };

        // Generate the path structure (main bolt and its branches)
        const boltPathData = generateBoltPath(
            startPoint, endPoint,
            jaggednessFactor,
            0, // Initial recursion depth
            recursionDepth,
            branchProbability, branchAngleMax, branchLengthFactor, branchWidthFactor,
            false // This initial call is not for a branch
        );

        // Draw the main segment of the bolt
        drawSingleBoltPath(boltPathData.mainPath, boltWidth, boltColor, glowColor, glowWidth);

        // Draw all branches associated with this main bolt
        for (const branchPath of boltPathData.branches) {
            // Branches are typically thinner and might have less intense glow
            const currentBranchCoreWidth = Math.max(1, boltWidth * branchWidthFactor * (0.5 + Math.random() * 0.5));
            const currentBranchGlowRadius = Math.max(1, glowWidth * branchWidthFactor * (0.5 + Math.random() * 0.5));
            
            drawSingleBoltPath(branchPath, currentBranchCoreWidth, boltColor, glowColor, currentBranchGlowRadius);
        }
    }
    
    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 Lightning Bolt Filter Effect Tool allows users to add dramatic lightning bolt effects to their images. By generating realistic lightning paths with jagged edges and glowing effects, this tool enhances visual presentation, making it suitable for creative projects, artistic edits, or visualizations of storms and electrical themes. Users can customize parameters such as bolt color, glow intensity, jaggedness, and branching, enabling them to create unique lightning effects that can be applied for personal or professional use in graphic design, social media posts, or illustrations.

Leave a Reply

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