Feat: GIF assembly from base64 frames via gif.js

This commit is contained in:
2026-03-20 22:07:49 -07:00
parent 390a1c9693
commit 47fcdb2edc

67
gifmaker.js Normal file
View File

@@ -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<Blob>}
*/
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();
});
}