Please bookmark this page to avoid losing your image tool!

Image Sonar Scan 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,
    scanColorRGB = "0,255,0",      // e.g., "0,255,0" for green
    trailDegrees = 90,         // 0-360, length of the visible trail sector
    scanSpeedDPS = 60,         // degrees per second for the sweep
    backgroundColorRGB = "0,0,0",// e.g., "0,0,0" for black
    beamWidthPixels = 2,       // width of the scanning line
    centerOffsetXRatio = 0.5,  // 0 to 1, 0.5 is center of image
    centerOffsetYRatio = 0.5,  // 0 to 1, 0.5 is center of image
    gridLinesCount = 5,        // number of concentric circles in the grid
    gridSpokesCount = 8,       // number of radial lines in the grid
    gridLineOpacity = 0.3      // 0 to 1, opacity of grid lines
) {

    const createErrorCanvas = (message, width = 300, height = 150) => {
        const errCanvas = document.createElement('canvas');
        errCanvas.width = width;
        errCanvas.height = height;
        const errCtx = errCanvas.getContext('2d');
        errCtx.fillStyle = '#CCCCCC'; // Light gray background
        errCtx.fillRect(0, 0, errCanvas.width, errCanvas.height);
        errCtx.fillStyle = '#FF0000'; // Red text
        errCtx.textAlign = 'center';
        errCtx.textBaseline = 'middle';
        errCtx.font = '16px Arial';
        
        // Simple text wrapping
        const words = message.split(' ');
        let line = '';
        let y = errCanvas.height / 2 - ( (message.split('\n').length -1) * 10 ); // Adjust based on potential newlines

        if (message.includes('\n')) { // Manual newlines if any
             const lines = message.split('\n');
             const lineHeight = 20;
             y = errCanvas.height / 2 - (lines.length -1) * lineHeight / 2;
             for(let i = 0; i < lines.length; i++) {
                errCtx.fillText(lines[i], errCanvas.width / 2, y + (i * lineHeight));
             }
        } else { // Auto-wrap short messages
            const maxWidth = errCanvas.width - 20; // Padding
            for (let n = 0; n < words.length; n++) {
                const testLine = line + words[n] + ' ';
                const metrics = errCtx.measureText(testLine);
                const testWidth = metrics.width;
                if (testWidth > maxWidth && n > 0) {
                    errCtx.fillText(line, errCanvas.width / 2, y);
                    line = words[n] + ' ';
                    y += 20; // Line height
                } else {
                    line = testLine;
                }
            }
            errCtx.fillText(line, errCanvas.width / 2, y);
        }
        return errCanvas;
    };

    if (!originalImg || !(originalImg instanceof HTMLImageElement) || !originalImg.src) {
        console.error("Sonar Scan: Original image is not a valid HTMLImageElement or src is missing.");
        return createErrorCanvas("Invalid Image Object");
    }

    try {
        if (!originalImg.complete) {
            await new Promise((resolve, reject) => {
                originalImg.onload = resolve;
                originalImg.onerror = (err) => reject(new Error("Image failed to load. Check URL or CORS policy."));
                // Add timeout for loading, e.g. 10 seconds
                setTimeout(() => reject(new Error("Image load timed out")), 10000);
            });
        }
        if (originalImg.decode) { // decode() is a method on HTMLImageElement
            await originalImg.decode();
        }
    } catch (error) {
        console.error("Sonar Scan: Error loading or decoding image:", error);
        return createErrorCanvas(`Image Load Error:\n${error.message}`);
    }
    
    if (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
        console.error("Sonar Scan: Image has zero dimensions after loading.");
        return createErrorCanvas("Image Has Zero Dimensions");
    }

    const canvas = document.createElement('canvas');
    canvas.width = originalImg.naturalWidth;
    canvas.height = originalImg.naturalHeight;
    const ctx = canvas.getContext('2d');

    const [scanR, scanG, scanB] = scanColorRGB.split(',').map(s => parseInt(s.trim(), 10));
    const [bgR, bgG, bgB] = backgroundColorRGB.split(',').map(s => parseInt(s.trim(), 10));
    
    const finalScanColor = `rgb(${scanR},${scanG},${scanB})`;
    const finalBackgroundColor = `rgb(${bgR},${bgG},${bgB})`;
    const finalGridColor = `rgba(${scanR},${scanG},${scanB},${Math.max(0, Math.min(1, gridLineOpacity))})`;

    // Pre-process image (monochrome tint)
    const preprocessedCanvas = document.createElement('canvas');
    preprocessedCanvas.width = canvas.width;
    preprocessedCanvas.height = canvas.height;
    const pCtx = preprocessedCanvas.getContext('2d', { willReadFrequently: true });
    pCtx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);

    try {
        const imageData = pCtx.getImageData(0, 0, preprocessedCanvas.width, preprocessedCanvas.height);
        const data = imageData.data;
        for (let i = 0; i < data.length; i += 4) {
            const r = data[i];
            const g = data[i+1];
            const b = data[i+2];
            const gray = 0.299 * r + 0.587 * g + 0.114 * b; // Standard grayscale
            data[i]   = (gray / 255) * scanR;
            data[i+1] = (gray / 255) * scanG;
            data[i+2] = (gray / 255) * scanB;
            // data[i+3] is alpha, remains unchanged
        }
        pCtx.putImageData(imageData, 0, 0);
    } catch (e) {
        console.error("Sonar Scan: Failed to process image data, possibly due to CORS. Using original image for trail.", e);
        // If getImageData fails, preprocessedCanvas will still contain the original image,
        // so the effect will partially work but without the green tint on the trail.
    }


    let currentAngle = 0; // In degrees, 0 is 3 o'clock position
    let lastTimestamp = 0;
    let animationFrameId = null;

    const cX = canvas.width * Math.max(0, Math.min(1, centerOffsetXRatio));
    const cY = canvas.height * Math.max(0, Math.min(1, centerOffsetYRatio));
    
    const W = canvas.width;
    const H = canvas.height;
    const distToCorners = [
        Math.sqrt(Math.pow(0 - cX, 2) + Math.pow(0 - cY, 2)),         // Top-left
        Math.sqrt(Math.pow(W - cX, 2) + Math.pow(0 - cY, 2)),         // Top-right
        Math.sqrt(Math.pow(0 - cX, 2) + Math.pow(H - cY, 2)),         // Bottom-left
        Math.sqrt(Math.pow(W - cX, 2) + Math.pow(H - cY, 2))          // Bottom-right
    ];
    const maxRadius = Math.max(...distToCorners);

    function drawFrame(angleDegrees) {
        ctx.fillStyle = finalBackgroundColor;
        ctx.fillRect(0, 0, W, H);

        // Draw grid
        if (gridLinesCount > 0 || gridSpokesCount > 0) {
            ctx.strokeStyle = finalGridColor;
            ctx.lineWidth = 1;

            if (gridLinesCount > 0) {
                for (let i = 1; i <= gridLinesCount; i++) {
                    const radius = maxRadius * (i / gridLinesCount);
                    ctx.beginPath();
                    ctx.arc(cX, cY, radius, 0, 2 * Math.PI);
                    ctx.stroke();
                }
            }
            if (gridSpokesCount > 0) {
                for (let i = 0; i < gridSpokesCount; i++) {
                    const spokeAngleRad = (i / gridSpokesCount) * 2 * Math.PI;
                    ctx.beginPath();
                    ctx.moveTo(cX, cY);
                    ctx.lineTo(cX + maxRadius * Math.cos(spokeAngleRad), cY + maxRadius * Math.sin(spokeAngleRad));
                    ctx.stroke();
                }
            }
        }

        // Draw revealed image part (trail)
        if (trailDegrees > 0) {
            ctx.save();
            ctx.beginPath();
            ctx.moveTo(cX, cY);
            const_safeTrailDegrees = Math.max(0, Math.min(360, trailDegrees));
            const startAngleRad = (angleDegrees - _safeTrailDegrees) * Math.PI / 180;
            const endAngleRad = angleDegrees * Math.PI / 180;
            ctx.arc(cX, cY, maxRadius, startAngleRad, endAngleRad);
            ctx.closePath();
            ctx.clip();
            
            ctx.drawImage(preprocessedCanvas, 0, 0, W, H);
            ctx.restore();
        }

        // Draw scanning beam
        if (beamWidthPixels > 0) {
            ctx.strokeStyle = finalScanColor;
            ctx.lineWidth = beamWidthPixels;
            ctx.beginPath();
            ctx.moveTo(cX, cY);
            const beamRadAngle = angleDegrees * Math.PI / 180;
            ctx.lineTo(cX + maxRadius * Math.cos(beamRadAngle), cY + maxRadius * Math.sin(beamRadAngle));
            ctx.stroke();
        }
    }
    
    // Add a method to stop animation if needed externally
    canvas.stopAnimation = () => {
        if (animationFrameId) {
            cancelAnimationFrame(animationFrameId);
            animationFrameId = null;
        }
    };

    function animate(timestamp) {
        if (lastTimestamp === 0) {
            lastTimestamp = timestamp;
        }
        const deltaTime = (timestamp - lastTimestamp) / 1000; // seconds
        currentAngle = (currentAngle + scanSpeedDPS * deltaTime);
        while(currentAngle < 0) currentAngle += 360; // Ensure positive angle
        currentAngle %= 360;
        
        lastTimestamp = timestamp;

        drawFrame(currentAngle);
        
        if (canvas.isConnected) {
            animationFrameId = requestAnimationFrame(animate);
        } else {
            // Canvas removed from DOM, stop animation
            console.log("Sonar Scan: Canvas removed from DOM, stopping animation.");
            canvas.stopAnimation();
        }
    }

    drawFrame(currentAngle); // Initial frame
    if (scanSpeedDPS !== 0) { // Only animate if speed is not zero
      animationFrameId = requestAnimationFrame(animate);
    }

    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 Sonar Scan Filter Effect Tool allows users to transform images with a sonar scan effect. By specifying parameters such as scan color, speed, and background color, users can create visually engaging effects that simulate a scanning beam revealing parts of the image. This tool can be utilized in various contexts, including enhancing presentations, creating unique graphics for websites, designing game UI elements, or producing artistic effects for personal projects.

Leave a Reply

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