import { GIFEncoder, quantize, applyPalette } from "gifenc";
/**
* Builds an animated GIF from event frames captured during a recording.
* Frames are scaled to max 800px wide; every frame carries the delay of the
* gap that followed its event (clamped), so the GIF paces like the track.
*/
export interface GifFrame {
dataUrl: string;
delayMs: number;
}
export async function buildGif(frames: GifFrame[], maxWidth = 800): Promise<Blob | null> {
if (!frames.length) return null;
const encoder = GIFEncoder();
let size: { w: number; h: number } | null = null;
for (const frame of frames) {
const blob = await (await fetch(frame.dataUrl)).blob();
const image = await createImageBitmap(blob);
try {
if (!size) {
const scale = Math.min(1, maxWidth / image.width);
size = { w: Math.max(1, Math.round(image.width * scale)), h: Math.max(1, Math.round(image.height * scale)) };
}
const canvas = new OffscreenCanvas(size.w, size.h);
const context = canvas.getContext("2d");
if (!context) throw new Error("no 2d context");
context.drawImage(image, 0, 0, size.w, size.h);
const { data } = context.getImageData(0, 0, size.w, size.h);
const palette = quantize(data, 256, { format: "rgb444" });
const index = applyPalette(data, palette, "rgb444");
encoder.writeFrame(index, size.w, size.h, { palette, delay: Math.round(frame.delayMs) });
} finally {
image.close();
}
}
encoder.finish();
return new Blob([encoder.bytes().buffer as ArrayBuffer], { type: "image/gif" });
}