From 47fcdb2edcd849ea52d14eec03247f102c603ef8 Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Fri, 20 Mar 2026 22:07:49 -0700 Subject: [PATCH] Feat: GIF assembly from base64 frames via gif.js --- gifmaker.js | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 gifmaker.js diff --git a/gifmaker.js b/gifmaker.js new file mode 100644 index 0000000..7db4a1f --- /dev/null +++ b/gifmaker.js @@ -0,0 +1,67 @@ +const WORKER_URL = 'https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.worker.js'; + +let workerBlobUrl = null; + +async function getWorkerUrl() { + if (workerBlobUrl) return workerBlobUrl; + const res = await fetch(WORKER_URL); + if (!res.ok) throw new Error('Failed to fetch gif worker'); + const blob = await res.blob(); + workerBlobUrl = URL.createObjectURL(blob); + return workerBlobUrl; +} + +/** + * Load a base64 data URL into an HTMLImageElement. + */ +function loadImage(src) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + }); +} + +/** + * Assembles frames into an animated GIF blob. + * @param {string[]} base64Frames - array of data:image/png;base64,... strings + * @param {number} fps - frames per second + * @returns {Promise} + */ +export async function assembleGif(base64Frames, fps) { + const workerScript = await getWorkerUrl(); + const images = await Promise.all(base64Frames.map(loadImage)); + + const w = images[0].naturalWidth; + const h = images[0].naturalHeight; + const delay = Math.round(1000 / fps); + + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + + return new Promise((resolve, reject) => { + const gif = new GIF({ + workers: 2, + quality: 10, + width: w, + height: h, + workerScript, + repeat: 0, + }); + + for (const img of images) { + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, w, h); + ctx.drawImage(img, 0, 0, w, h); + gif.addFrame(ctx, { copy: true, delay }); + } + + gif.on('finished', resolve); + gif.on('error', reject); + gif.render(); + }); +}