Please bookmark this page to avoid losing your image tool!

Video To Gif Converter

(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, frameRate = "10", maxDimension = "320") {
    // Construct the UI container
    const container = document.createElement('div');
    container.style.fontFamily = 'system-ui, -apple-system, sans-serif';
    container.style.maxWidth = '600px';
    container.style.margin = '20px auto';
    container.style.padding = '25px';
    container.style.borderRadius = '12px';
    container.style.border = '1px solid #e2e8f0';
    container.style.boxShadow = '0 10px 15px -3px rgba(0, 0, 0, 0.1)';
    container.style.backgroundColor = '#ffffff';
    container.style.color = '#333333';

    // Header
    const title = document.createElement('h2');
    title.textContent = '🎥 Video to GIF Converter';
    title.style.margin = '0 0 20px 0';
    title.style.fontSize = '1.5rem';
    container.appendChild(title);

    // Form settings
    const form = document.createElement('div');
    form.style.display = 'flex';
    form.style.flexDirection = 'column';
    form.style.gap = '15px';
    container.appendChild(form);

    // File input
    const fileLabel = document.createElement('label');
    fileLabel.innerHTML = '<span style="font-weight: 600; font-size: 0.95rem;">Select Video File (MP4, WebM, etc.):</span>';
    const fileInput = document.createElement('input');
    fileInput.type = 'file';
    fileInput.accept = 'video/*';
    fileInput.style.display = 'block';
    fileInput.style.marginTop = '8px';
    fileInput.style.width = '100%';
    fileInput.style.padding = '8px';
    fileInput.style.border = '1px solid #ccc';
    fileInput.style.borderRadius = '6px';
    fileLabel.appendChild(fileInput);
    form.appendChild(fileLabel);

    // Frame rate input
    const fpsLabel = document.createElement('label');
    fpsLabel.innerHTML = '<span style="font-weight: 600; font-size: 0.95rem;">Frame Rate (FPS):</span>';
    const fpsInput = document.createElement('input');
    fpsInput.type = 'number';
    fpsInput.value = frameRate;
    fpsInput.min = '1';
    fpsInput.max = '30';
    fpsInput.style.display = 'block';
    fpsInput.style.marginTop = '8px';
    fpsInput.style.width = '100%';
    fpsInput.style.padding = '10px';
    fpsInput.style.border = '1px solid #ccc';
    fpsInput.style.borderRadius = '6px';
    fpsInput.style.boxSizing = 'border-box';
    fpsLabel.appendChild(fpsInput);
    form.appendChild(fpsLabel);

    // Dimension size setup
    const sizeLabel = document.createElement('label');
    sizeLabel.innerHTML = '<span style="font-weight: 600; font-size: 0.95rem;">Maximum Dimension (px):</span>';
    const sizeInput = document.createElement('input');
    sizeInput.type = 'number';
    sizeInput.value = maxDimension;
    sizeInput.min = '100';
    sizeInput.max = '800';
    sizeInput.style.display = 'block';
    sizeInput.style.marginTop = '8px';
    sizeInput.style.width = '100%';
    sizeInput.style.padding = '10px';
    sizeInput.style.border = '1px solid #ccc';
    sizeInput.style.borderRadius = '6px';
    sizeInput.style.boxSizing = 'border-box';
    sizeLabel.appendChild(sizeInput);
    form.appendChild(sizeLabel);

    // Submission Context
    const convertBtn = document.createElement('button');
    convertBtn.textContent = 'Generate GIF';
    convertBtn.style.padding = '12px';
    convertBtn.style.backgroundColor = '#007BFF';
    convertBtn.style.color = '#ffffff';
    convertBtn.style.border = 'none';
    convertBtn.style.borderRadius = '6px';
    convertBtn.style.cursor = 'pointer';
    convertBtn.style.fontWeight = 'bold';
    convertBtn.style.fontSize = '1.05rem';
    convertBtn.style.transition = 'background-color 0.2s';
    
    // Hover effects
    convertBtn.onmouseover = () => { if (!convertBtn.disabled) convertBtn.style.backgroundColor = '#0056b3'; };
    convertBtn.onmouseout = () => { if (!convertBtn.disabled) convertBtn.style.backgroundColor = '#007BFF'; };
    form.appendChild(convertBtn);

    // Progress Section
    const statusCont = document.createElement('div');
    statusCont.style.display = 'none';
    statusCont.style.marginTop = '20px';

    const statusText = document.createElement('div');
    statusText.style.fontWeight = '600';
    statusText.style.marginBottom = '8px';
    statusText.style.fontSize = '0.95rem';
    statusText.textContent = 'Processing... 0%';

    const progressBarDiv = document.createElement('div');
    progressBarDiv.style.width = '100%';
    progressBarDiv.style.height = '14px';
    progressBarDiv.style.backgroundColor = '#eef2f5';
    progressBarDiv.style.borderRadius = '10px';
    progressBarDiv.style.overflow = 'hidden';

    const progressFill = document.createElement('div');
    progressFill.style.height = '100%';
    progressFill.style.width = '0%';
    progressFill.style.backgroundColor = '#28a745';
    progressFill.style.transition = 'width 0.2s linear';

    progressBarDiv.appendChild(progressFill);
    statusCont.appendChild(statusText);
    statusCont.appendChild(progressBarDiv);
    container.appendChild(statusCont);

    // Result Section
    const outputCont = document.createElement('div');
    outputCont.style.marginTop = '25px';
    outputCont.style.display = 'none';
    outputCont.style.textAlign = 'center';

    const outputInfo = document.createElement('p');
    outputInfo.innerHTML = '<strong style="color: #28a745;">✓ Conversion Complete!</strong> Your GIF is ready:';

    const outputImg = document.createElement('img');
    outputImg.style.maxWidth = '100%';
    outputImg.style.border = '1px solid #e2e8f0';
    outputImg.style.borderRadius = '8px';
    outputImg.style.boxShadow = '0 4px 6px -1px rgba(0,0,0,0.1)';

    const downloadLink = document.createElement('a');
    downloadLink.textContent = '↓ Download Final GIF';
    downloadLink.style.display = 'inline-block';
    downloadLink.style.marginTop = '15px';
    downloadLink.style.padding = '10px 18px';
    downloadLink.style.backgroundColor = '#28a745';
    downloadLink.style.color = '#ffffff';
    downloadLink.style.textDecoration = 'none';
    downloadLink.style.borderRadius = '6px';
    downloadLink.style.fontWeight = 'bold';

    outputCont.appendChild(outputInfo);
    outputCont.appendChild(outputImg);
    outputCont.appendChild(document.createElement('br'));
    outputCont.appendChild(downloadLink);
    container.appendChild(outputCont);

    const updateBtnState = (disabled, text) => {
        convertBtn.disabled = disabled;
        convertBtn.textContent = text;
        convertBtn.style.opacity = disabled ? '0.7' : '1';
        convertBtn.style.cursor = disabled ? 'not-allowed' : 'pointer';
        convertBtn.style.backgroundColor = disabled ? '#6c757d' : '#007BFF';
    };

    convertBtn.addEventListener('click', async () => {
        if (!fileInput.files.length) {
            alert('Please select a video file to convert.');
            return;
        }

        const videoFile = fileInput.files[0];
        const fps = parseInt(fpsInput.value, 10) || 10;
        const maxD = parseInt(sizeInput.value, 10) || 320;

        updateBtnState(true, 'Converting...');
        statusCont.style.display = 'block';
        outputCont.style.display = 'none';
        
        let videoAppended = false;
        const video = document.createElement('video');
        const videoUrl = URL.createObjectURL(videoFile);

        try {
            statusText.textContent = 'Loading Encoding Libraries...';
            
            // Dynamically import lightweight exact library 'gifenc' via esm.sh CDN
            // Falls back to direct default or named resolution
            let gifencModule;
            try {
                gifencModule = await import('https://esm.sh/gifenc@1.0.3');
            } catch (err) {
                throw new Error("Ensure you have an active internet connection to load the GIF encoder module.");
            }
            
            const GIFEncoder = gifencModule.GIFEncoder || gifencModule.default.GIFEncoder;
            const quantize = gifencModule.quantize || gifencModule.default.quantize;
            const applyPalette = gifencModule.applyPalette || gifencModule.default.applyPalette;

            statusText.textContent = 'Initializing Video Container...';
            video.src = videoUrl;
            video.muted = true;
            video.playsInline = true;
            video.style.display = 'none';
            document.body.appendChild(video);
            videoAppended = true;

            await new Promise((resolve, reject) => {
                video.onloadeddata = resolve;
                video.onerror = () => reject(new Error("The provided file could not be parsed as a video."));
                video.load();
            });

            // Adjust sizing while keeping aspect ratio intact
            let width = video.videoWidth;
            let height = video.videoHeight;
            if (width > maxD || height > maxD) {
                if (width > height) {
                    height = Math.floor(height * (maxD / width));
                    width = maxD;
                } else {
                    width = Math.floor(width * (maxD / height));
                    height = maxD;
                }
            }

            // Fallback for missing WebM duration metadata lengths 
            let duration = video.duration;
            if (!duration || duration === Infinity) {
                video.currentTime = 100000;
                await new Promise(r => {
                    let fired = false;
                    const h = () => { if(fired) return; fired = true; video.removeEventListener('seeked', h); r(); };
                    video.addEventListener('seeked', h);
                    setTimeout(h, 1500); // safety fallback
                });
                duration = video.currentTime;
                video.currentTime = 0;
                await new Promise(r => {
                    let fired = false;
                    const h = () => { if(fired) return; fired = true; video.removeEventListener('seeked', h); r(); };
                    video.addEventListener('seeked', h);
                    setTimeout(h, 800);
                });
                if (!duration || duration === Infinity) duration = 5; 
            }

            let totalFrames = Math.floor(duration * fps);
            
            // Limit to prevent huge browser memory blowup (e.g., limits to 20 seconds at 15fps)
            if (totalFrames > 300) {
                totalFrames = 300;
                alert(`The video is lengthy! To respect your browser's memory capabilities, only the first ${parseInt(300 / fps)} seconds will be processed.`);
            }

            const delayMs = Math.floor(1000 / fps);
            
            statusText.textContent = 'Processing Initial Frame... 0%';
            
            const gif = GIFEncoder();
            const canvas = document.createElement('canvas');
            canvas.width = width;
            canvas.height = height;
            const ctx = canvas.getContext('2d', { willReadFrequently: true });

            // Ensure background is cleared consistently
            ctx.fillStyle = '#000000';
            ctx.fillRect(0, 0, width, height);

            for (let i = 0; i <= totalFrames; i++) {
                const time = i / fps;
                if (time > duration) break;
                
                // Seek to frame and wait
                video.currentTime = time;
                await new Promise(r => {
                    let fired = false;
                    const handler = () => { 
                        if(fired) return; 
                        fired = true; 
                        video.removeEventListener('seeked', handler); 
                        r(); 
                    };
                    video.addEventListener('seeked', handler);
                    // 500ms safety timeout boundary lock
                    setTimeout(handler, 500);
                });

                ctx.drawImage(video, 0, 0, width, height);
                const imageData = ctx.getImageData(0, 0, width, height);

                // rgb444 reduces processing time significantly while preserving acceptable visual clarity globally per frame
                const palette = quantize(imageData.data, 256, { format: 'rgb444' });
                const index = applyPalette(imageData.data, palette, 'rgb444');

                // Enqueue encoded frame onto the array 
                gif.writeFrame(index, width, height, { palette, delay: delayMs });

                // UI Status Update Sync to main thread via requestAnimationFrame
                const pct = Math.round((i / totalFrames) * 100);
                statusText.textContent = `Extracting frames & composing GIF... ${pct}%`;
                progressFill.style.width = `${pct}%`;
                
                await new Promise(r => requestAnimationFrame(r));
            }

            statusText.textContent = 'Finalizing and Wrapping File Encoding...';
            await new Promise(r => requestAnimationFrame(r));

            gif.finish();
            const buffer = gif.bytes();
            const blob = new Blob([buffer], { type: 'image/gif' });
            const finalUrl = URL.createObjectURL(blob);

            // Publish visually and stage for download
            outputImg.src = finalUrl;
            downloadLink.href = finalUrl;
            
            // Generate clean filename
            const pureFileName = videoFile.name.replace(/\.[^/.]+$/, "");
            downloadLink.download = `${pureFileName}_snippet.gif`;

            statusCont.style.display = 'none';
            outputCont.style.display = 'block';

        } catch (err) {
            console.error('GIF Translation Error Context: ', err);
            alert(`Oops! An error occurred during conversion: ${err.message}`);
        } finally {
            // Clean up heavy DOM presence efficiently
            if (videoAppended && video.parentNode) {
                video.parentNode.removeChild(video);
            }
            URL.revokeObjectURL(videoUrl);
            
            updateBtnState(false, 'Generate Another GIF');
            progressFill.style.width = '0%';
            statusText.textContent = 'Processing... 0%';
        }
    });

    return container;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

This tool allows you to convert video files into high-quality GIF animations. Users can customize the output by adjusting the frame rate (FPS) and setting a maximum dimension to control the resolution and file size. It is ideal for creating short, looping clips for social media, adding memes to presentations, or extracting quick visual snippets from longer video content.

Leave a Reply

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