/**
 * Decode an animated image (GIF / APNG / animated WebP / animated AVIF) into
 * a list of frames with cumulative timing using the browser's ImageDecoder
 * API. Used by the server-side export pipeline so that animated <img> elements
 * can be rendered frame-accurately under our virtual clock — native <img>
 * playback ignores virtual time, so without this every captured frame would
 * show the GIF stuck at frame 0.
 *
 * Live `/present` keeps using <img> directly; only the export pipeline opts
 * into the canvas-from-decoded-frames path.
 */
/** True if the URL/data-URL looks like an animated container we can decode.
 * We don't try to detect "animated PNG" vs "static PNG" upfront — the decoder
 * call below will return one frame for static images and that's fine. */
export function isLikelyAnimated(src) {
    if (!src)
        return false;
    // Data URL — sniff the MIME.
    if (src.startsWith('data:')) {
        const mime = src.slice(5).split(';')[0].toLowerCase();
        return mime === 'image/gif' || mime === 'image/apng' || mime === 'image/webp' || mime === 'image/avif' || mime === 'image/png';
    }
    // Otherwise look at the path extension.
    const lower = src.toLowerCase().split('?')[0];
    return lower.endsWith('.gif') || lower.endsWith('.apng') || lower.endsWith('.webp') || lower.endsWith('.avif');
}
async function srcToBlob(src) {
    const res = await fetch(src);
    return await res.blob();
}
/** Decode every frame of an animated image. Falls back to a single-frame
 * result for static images. Returns null when ImageDecoder is unavailable
 * (e.g. older browsers) so callers can degrade gracefully. */
export async function decodeAnimatedImage(src) {
    if (typeof globalThis.ImageDecoder === 'undefined')
        return null;
    try {
        const blob = await srcToBlob(src);
        const decoder = new globalThis.ImageDecoder({ data: blob.stream(), type: blob.type || 'image/gif' });
        await decoder.tracks.ready;
        await decoder.completed;
        const frameCount = decoder.tracks.selectedTrack?.frameCount ?? 1;
        const frames = [];
        let totalMs = 0;
        let width = 0;
        let height = 0;
        for (let i = 0; i < frameCount; i++) {
            const result = await decoder.decode({ frameIndex: i });
            const vf = result.image;
            width = vf.displayWidth || vf.codedWidth || width;
            height = vf.displayHeight || vf.codedHeight || height;
            // duration is in microseconds when present; some decoders return null
            // for the last frame — fall back to a sensible 100ms (~10fps) so we
            // don't divide by zero when seeking.
            const durMs = vf.duration != null ? vf.duration / 1000 : 100;
            const bitmap = await createImageBitmap(vf);
            vf.close?.();
            frames.push({ bitmap, durationMs: durMs });
            totalMs += durMs;
        }
        decoder.close?.();
        return { frames, totalMs, width, height };
    }
    catch (err) {
        console.warn('Failed to decode animated image:', err);
        return null;
    }
}
/** Pick the frame that should be visible at `elapsedMs` for a looping animation
 * of total `totalMs`. Returns the index. */
export function frameIndexAt(decoded, elapsedMs) {
    if (decoded.totalMs <= 0 || decoded.frames.length === 0)
        return 0;
    const t = ((elapsedMs % decoded.totalMs) + decoded.totalMs) % decoded.totalMs;
    let acc = 0;
    for (let i = 0; i < decoded.frames.length; i++) {
        acc += decoded.frames[i].durationMs;
        if (t < acc)
            return i;
    }
    return decoded.frames.length - 1;
}
/** Draw the frame visible at `elapsedMs` into the given canvas. */
export function drawFrameAt(canvas, decoded, elapsedMs) {
    const idx = frameIndexAt(decoded, elapsedMs);
    const frame = decoded.frames[idx];
    if (!frame)
        return;
    // Resize the canvas backing-store to the natural frame size only once;
    // the wrapping CSS scales it to the element box.
    if (canvas.width !== decoded.width)
        canvas.width = decoded.width;
    if (canvas.height !== decoded.height)
        canvas.height = decoded.height;
    const ctx = canvas.getContext('2d');
    if (!ctx)
        return;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(frame.bitmap, 0, 0);
}
