Please bookmark this page to avoid losing your image tool!

ANSI Art Video Generator

(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.
function processImage(originalImg, columns = 100, fontSize = 14, animSpeedMs = 5000) {
    // Create and style the main visible canvas output
    const canvas = document.createElement('canvas');
    canvas.style.maxWidth = '100%';
    canvas.style.backgroundColor = 'black';
    canvas.style.display = 'block';

    // Validate image load state
    if (!originalImg || !originalImg.width || !originalImg.height) return canvas;

    // Parse and constrain parameters
    const cols = Math.max(10, Math.min(300, parseInt(columns) || 100));
    const fSize = Math.max(8, Math.min(48, parseInt(fontSize) || 14));
    const speed = Math.max(500, Math.min(30000, parseInt(animSpeedMs) || 5000));

    // Dynamic font measurement for accurate character aspect ratio mapping
    const testCtx = document.createElement('canvas').getContext('2d');
    testCtx.font = `bold ${fSize}px monospace`;
    const measuredW = testCtx.measureText('M').width;
    const charW = measuredW > 0 ? measuredW : (fSize * 0.6);
    const charH = fSize;

    // Maintain aspect ratio factoring in the font dimensions
    const rows = Math.round((originalImg.height / originalImg.width) * cols * (charW / charH));
    
    if (rows < 1) return canvas;

    canvas.width = cols * charW;
    canvas.height = rows * charH;

    // Render image to a small offscreen canvas to sample the pixel data
    const offCanvas = document.createElement('canvas');
    offCanvas.width = cols;
    offCanvas.height = rows;
    const offCtx = offCanvas.getContext('2d', { willReadFrequently: true });
    offCtx.drawImage(originalImg, 0, 0, cols, rows);
    const imgData = offCtx.getImageData(0, 0, cols, rows).data;

    // Classic 16-color ANSI DOS Palette mapped to RGB
    const ansiPalette = [
        [0,0,0], [0,0,170], [0,170,0], [0,170,170], [170,0,0], [170,0,170], [170,85,0], [170,170,170],
        [85,85,85], [85,85,255], [85,255,85], [85,255,255], [255,85,85], [255,85,255], [255,255,85], [255,255,255]
    ];

    function getNearestColor(r, g, b) {
        let minDist = Infinity;
        let pColor = ansiPalette[0];
        for (let i = 0; i < ansiPalette.length; i++) {
            const p = ansiPalette[i];
            const dist = (r - p[0]) ** 2 + (g - p[1]) ** 2 + (b - p[2]) ** 2;
            if (dist < minDist) {
                minDist = dist;
                pColor = p;
            }
        }
        return `rgb(${pColor[0]},${pColor[1]},${pColor[2]})`;
    }

    // Extended ASCII set mapped from darkest to brightest
    const asciiStr = ' .\'`^",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';
    
    // Group characters by exact color to heavily optimize rendering
    const charGroups = {};

    for (let y = 0; y < rows; y++) {
        for (let x = 0; x < cols; x++) {
            const i = (y * cols + x) * 4;
            const r = imgData[i];
            const g = imgData[i + 1];
            const b = imgData[i + 2];
            const a = imgData[i + 3];

            let char = ' ';
            let colorKey = 'rgb(0,0,0)';

            // Ignore transparent pixels
            if (a >= 128) {
                colorKey = getNearestColor(r, g, b);
                const luma = 0.299 * r + 0.587 * g + 0.114 * b;
                const charIdx = Math.floor((luma / 255) * (asciiStr.length - 1));
                char = asciiStr[charIdx];
            }

            if (!charGroups[colorKey]) charGroups[colorKey] = [];
            charGroups[colorKey].push({ x, y, char });
        }
    }

    // Pre-render the fully revealed image (Static Image Buffer for extreme performance) 
    const finalCanvas = document.createElement('canvas');
    finalCanvas.width = canvas.width;
    finalCanvas.height = canvas.height;
    const fctx = finalCanvas.getContext('2d');
    
    fctx.fillStyle = 'black';
    fctx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
    fctx.font = `bold ${fSize}px monospace`;
    fctx.textBaseline = 'top';

    for (const color in charGroups) {
        fctx.fillStyle = color;
        for (const item of charGroups[color]) {
            if (item.char !== ' ') {
                fctx.fillText(item.char, item.x * charW, item.y * charH);
            }
        }
    }

    // Interactive Animation setup (Decryption CRT Scanline Loop)
    const ctx = canvas.getContext('2d');
    const startTime = Date.now();

    function draw() {
        // Automatically isolate memory leak / rogue loops if the canvas gets completely discarded from the DOM
        if (!canvas.isConnected) {
            if (canvas.dataset.attached === 'true') return;
        } else {
            canvas.dataset.attached = 'true';
        }

        const now = Date.now();
        const phase = ((now - startTime) % speed) / speed;

        let lockY = 0;
        let showNoise = true;

        // Timeline progression
        if (phase < 0.15) {
            lockY = 0; // Stage 1: Establishing connection (Full Noise)
        } else if (phase >= 0.15 && phase < 0.75) {
            const sweepProgress = (phase - 0.15) / 0.60;
            lockY = canvas.height * sweepProgress; // Stage 2: Decoder scanline down
        } else {
            lockY = canvas.height; // Stage 3: Hold final revealed ANSI Art
            showNoise = false;
        }

        // Wipe current main frame
        ctx.fillStyle = 'black';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // Draw successfully decoded "locked" portion directly from our pre-rendered buffer
        const yCrop = Math.floor(lockY);
        if (yCrop > 0) {
            ctx.drawImage(finalCanvas, 0, 0, canvas.width, yCrop, 0, 0, canvas.width, yCrop);
        }

        // Render the bright scanning CRT strip separating memory zones
        if (yCrop > 0 && yCrop < canvas.height) {
            ctx.fillStyle = 'rgba(0, 255, 0, 0.4)';
            ctx.fillRect(0, yCrop - 4, canvas.width, 8);
            ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
            ctx.fillRect(0, yCrop - 1, canvas.width, 2);
        }

        // Render live hacker/terminal matrix noise in uncleared spaces
        if (showNoise) {
            ctx.font = `bold ${fSize}px monospace`;
            ctx.textBaseline = 'top';
            const startRow = Math.max(0, Math.floor(yCrop / charH));
            
            for (let y = startRow; y < rows; y++) {
                for (let x = 0; x < cols; x++) {
                    // Introduce sparse random gaps via simple prob distribution limit
                    if (Math.random() > 0.4) {
                        ctx.fillStyle = Math.random() > 0.8 ? '#cfc' : '#0c0';
                        const char = asciiStr[Math.floor(Math.random() * asciiStr.length)];
                        ctx.fillText(char, x * charW, y * charH);
                    }
                }
            }
        }

        requestAnimationFrame(draw);
    }

    // Bootstrap loop
    requestAnimationFrame(draw);

    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 ANSI Art Video Generator transforms standard images into stylized, animated ANSI-style art. It converts images into a grid of ASCII characters mapped to a classic 16-color DOS-inspired palette. The tool features a dynamic ‘decryption’ animation effect that mimics a CRT terminal scanline sweeping down the screen to reveal the final image amidst terminal-style noise. This tool is ideal for creators looking to generate retro-themed digital assets, hacker-style aesthetic videos, or unique stylized visual content for social media and gaming projects.

Leave a Reply

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