export function downscaleRgbaToDataUrl(pixels, srcW, srcH, dstSize) { const dst = new Uint8ClampedArray(dstSize * dstSize * 4); for (let y = 0; y < dstSize; y++) { const y0 = Math.floor((y * srcH) / dstSize); const y1 = Math.max(y0 + 1, Math.floor(((y + 1) * srcH) / dstSize)); for (let x = 0; x < dstSize; x++) { const x0 = Math.floor((x * srcW) / dstSize); const x1 = Math.max(x0 + 1, Math.floor(((x + 1) * srcW) / dstSize)); let r = 0, g = 0, b = 0, c = 0; for (let sy = y0; sy < y1; sy++) for (let sx = x0; sx < x1; sx++) { const i = (sy * srcW + sx) * 4; r += pixels[i]; g += pixels[i + 1]; b += pixels[i + 2]; c++; } const o = (y * dstSize + x) * 4; dst[o] = c ? Math.round(r / c) : 0; dst[o + 1] = c ? Math.round(g / c) : 0; dst[o + 2] = c ? Math.round(b / c) : 0; dst[o + 3] = 255; } } if (typeof document !== 'undefined' && document.createElement) { try { const canvas = document.createElement('canvas'); canvas.width = dstSize; canvas.height = dstSize; const ctx = canvas.getContext('2d'); if (ctx) { ctx.putImageData(new ImageData(dst, dstSize, dstSize), 0, 0); return canvas.toDataURL('image/jpeg', 0.6); } } catch { /* fallback */ } } return 'data:image/jpeg;base64,'; } function uniformFrames(count, frameCount) { const seen = new Set(); const out = []; for (let i = 0; i < count; i++) { const f = Math.max(0, Math.min(frameCount - 1, Math.round(((i + 0.5) / count) * frameCount))); if (!seen.has(f)) { seen.add(f); out.push(f); } } return out; } function pickFrames(show, count, frameCount, fps) { const sections = show.track && show.track.sections; if (sections && sections.length) { const mids = []; const seen = new Set(); for (const s of sections) { let mid; if (Number.isFinite(s.startFrame) && Number.isFinite(s.endFrame)) mid = Math.round((s.startFrame + s.endFrame) / 2); else if (Number.isFinite(s.start) && Number.isFinite(s.end)) mid = Math.round(((s.start + s.end) / 2) * fps); else continue; mid = Math.max(0, Math.min(frameCount - 1, mid)); if (!seen.has(mid)) { seen.add(mid); mids.push(mid); } } if (!mids.length) return uniformFrames(count, frameCount); if (mids.length >= count) { if (mids.length === count) return mids; mids.sort((a, b) => a - b); const out = []; for (let i = 0; i < count; i++) { const idx = Math.min(mids.length - 1, Math.floor(((i + 0.5) / count) * mids.length)); const v = mids[idx]; if (!out.includes(v)) out.push(v); } return out; } const need = count - mids.length; for (const f of uniformFrames(need, frameCount)) if (!seen.has(f)) { seen.add(f); mids.push(f); } if (mids.length < count) for (const f of uniformFrames(count, frameCount)) if (!seen.has(f) && mids.length < count) { seen.add(f); mids.push(f); } const sorted = [...seen].sort((a, b) => a - b); if (sorted.length <= count) return sorted; const out = []; for (let i = 0; i < count; i++) { const idx = Math.min(sorted.length - 1, Math.floor(((i + 0.5) / count) * sorted.length)); const v = sorted[idx]; if (!out.includes(v)) out.push(v); } return out; } return uniformFrames(count, frameCount); } export async function captureThumbnails(show, { count = 8, size = 32 } = {}) { if (!show || !show.ready || !show.track || !show.engine) return []; const fps = show.fps || 60; const frameCount = show.frameCount || (show.track && show.track.frameCount) || Math.round((show.track.duration || 0) * fps) || 0; if (!frameCount) return []; const originalFrame = show.timeline ? show.timeline.frame : 0; const origW = show.engine.width, origH = show.engine.height; const frames = pickFrames(show, count, frameCount, fps).sort((a, b) => a - b); const out = []; try { for (const frame of frames) { try { if (typeof show.warmUp === 'function') { const wf = typeof show.warmupFrames === 'function' ? show.warmupFrames() : 120; show.warmUp(frame, Math.min(120, wf)); } else if (show.engine.compositor) { if (typeof show.engine.warmUp === 'function') show.engine.warmUp(frame, 120); else show.engine.compositor.reset(); } } catch { /* ignore */ } let target; try { target = show.renderFrame(frame); } catch { continue; } let pixels; try { pixels = show.readPixels(target); } catch { continue; } if (!pixels || !pixels.length) continue; let dataUrl = ''; try { dataUrl = downscaleRgbaToDataUrl(pixels, origW, origH, size); } catch { dataUrl = 'data:image/jpeg;base64,'; } let kind = ''; try { const sec = show.track.sectionAt ? show.track.sectionAt(frame) : null; if (sec && sec.kind) kind = sec.kind; else if (show.track.sections) for (const s of show.track.sections) if (Number.isFinite(s.startFrame) && frame >= s.startFrame && frame < s.endFrame) { kind = s.kind || ''; break; } } catch { /* ignore */ } out.push({ kind: kind || '', time: frame / fps, frame, dataUrl }); } } finally { try { if (typeof show.seek === 'function') show.seek(originalFrame); else if (show.timeline && typeof show.timeline.seek === 'function') { show.timeline.seek(originalFrame); try { const t = show.renderFrame(originalFrame); if (typeof show.present === 'function') show.present(t); } catch { /* ignore */ } } } catch { /* ignore */ } try { if (show.engine && (show.engine.width !== origW || show.engine.height !== origH)) { if (typeof show.engine.setSize === 'function') show.engine.setSize(origW, origH); else if (typeof show.setSize === 'function') show.setSize(origW, origH); } } catch { /* ignore */ } } return out; }