diff --git a/src/services/gif.js b/src/services/gif.js new file mode 100644 index 0000000..eda4b11 --- /dev/null +++ b/src/services/gif.js @@ -0,0 +1,60 @@ +const WORKER_CDN = '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_CDN); + const blob = await res.blob(); + workerBlobUrl = URL.createObjectURL(blob); + return workerBlobUrl; +} + +function loadImage(src) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = () => reject(new Error('Failed to load frame image')); + img.src = src; + }); +} + +export async function assembleGif(frames, fps) { + if (!frames?.length) throw new Error('No frames to assemble.'); + if (!window.GIF) throw new Error('GIF.js not loaded.'); + + const workerScript = await getWorkerUrl(); + const images = await Promise.all(frames.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'); + if (!ctx) throw new Error('Could not create 2D canvas context.'); + + return new Promise((resolve, reject) => { + const gif = new window.GIF({ + workers: 2, + quality: 10, + width: w, + height: h, + workerScript, + repeat: 0 + }); + + for (const image of images) { + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, w, h); + ctx.drawImage(image, 0, 0, w, h); + gif.addFrame(ctx, { copy: true, delay }); + } + + gif.on('finished', (blob) => resolve(blob)); + gif.on('error', (err) => reject(err)); + gif.render(); + }); +}