const GIF_WORKER_URL = "https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.worker.js"; function loadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error("Failed to load generated frame.")); img.src = src; }); } export async function createGifFromFrames(frameUrls, { fps = 4 } = {}) { if (!Array.isArray(frameUrls) || !frameUrls.length) { throw new Error("No frames available for GIF encoding."); } if (typeof window.GIF !== "function") { throw new Error("GIF encoder is not loaded."); } const images = await Promise.all(frameUrls.map(loadImage)); const first = images[0]; const delay = Math.max(20, Math.round(1000 / Math.max(1, Number(fps) || 4))); return new Promise((resolve, reject) => { const gif = new window.GIF({ workers: 2, quality: 10, repeat: 0, width: first.naturalWidth || first.width, height: first.naturalHeight || first.height, workerScript: GIF_WORKER_URL, }); for (const img of images) { gif.addFrame(img, { delay }); } gif.on("finished", (blob) => resolve(blob)); gif.on("abort", () => reject(new Error("GIF rendering aborted."))); try { gif.render(); } catch (err) { reject(err); } }); }