diff --git a/assets/js/gif-builder.js b/assets/js/gif-builder.js new file mode 100644 index 0000000..399b889 --- /dev/null +++ b/assets/js/gif-builder.js @@ -0,0 +1,48 @@ +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); + } + }); +}