You can edit the below JavaScript code to customize the image tool.
Apply Changes
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;
}
Apply Changes