@editframe/elements 0.31.0-beta.0 → 0.31.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","_xmlSerializer: XMLSerializer | null","_textEncoder: TextEncoder | null","_workerPool: WorkerPool | null","workerUrl: string","dataUrl: string","results: Array<{ canvas: HTMLCanvasElement; dataUrl: string; preserveAlpha: boolean }>","scaleColors: Record<number, string>","canvasRestoreInfo: CanvasRestoreInfo[]","base64: string","captureCanvas: HTMLCanvasElement","dpr","image: HTMLCanvasElement | HTMLImageElement","options: CanvasPreviewOptions","pendingResolutionScale: number | null"],"sources":["../../src/preview/renderTimegroupToCanvas.ts"],"sourcesContent":["import type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport {\n buildCloneStructure,\n syncStyles,\n collectDocumentStyles,\n overrideRootCloneStyles,\n type SyncState,\n} from \"./renderTimegroupPreview.js\";\nimport { getEffectiveRenderMode } from \"./renderers.js\";\n\n// Re-export renderer types for external use\nexport type { RenderOptions, RenderResult, Renderer } from \"./renderers.js\";\nexport { getEffectiveRenderMode, isCanvas, isImage } from \"./renderers.js\";\nimport { WorkerPool, encodeCanvasInWorker } from \"./workers/WorkerPool.js\";\nimport {\n type TemporalElement,\n isVisibleAtTime,\n DEFAULT_WIDTH,\n DEFAULT_HEIGHT,\n DEFAULT_THUMBNAIL_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n JPEG_QUALITY_HIGH,\n JPEG_QUALITY_MEDIUM,\n createPreviewContainer,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\n\n// ============================================================================\n// Constants (module-specific, not shared)\n// ============================================================================\n\n/** Number of rows to sample when checking canvas content */\nconst CANVAS_SAMPLE_STRIP_HEIGHT = 4;\n\n/** Interval between profiling log outputs (ms) */\nconst PROFILING_LOG_INTERVAL_MS = 2000;\n\n/** Maximum number of cached inline images before eviction */\nconst MAX_INLINE_IMAGE_CACHE_SIZE = 100;\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Content readiness strategy for capture operations.\n * - \"immediate\": Capture NOW, skip all waits. May have blank video frames.\n * - \"blocking\": Wait for video content to be ready. Throws on timeout.\n */\nexport type ContentReadyMode = \"immediate\" | \"blocking\";\n\n/**\n * Extended CanvasRenderingContext2D with HTML-in-Canvas API support.\n * @see https://github.com/WICG/html-in-canvas\n */\ninterface HtmlInCanvasContext extends CanvasRenderingContext2D {\n drawElementImage(element: HTMLElement, x: number, y: number): void;\n}\n\n/**\n * Extended HTMLCanvasElement with layoutSubtree property for HTML-in-Canvas.\n */\ninterface HtmlInCanvasElement extends HTMLCanvasElement {\n layoutSubtree?: boolean;\n}\n\n/**\n * Options for capturing a timegroup frame.\n */\nexport interface CaptureOptions {\n /** Time to capture at in milliseconds (required) */\n timeMs: number;\n /** Scale factor (default: 0.25 for captureTimegroupAtTime) */\n scale?: number;\n /** Skip restoring original time after capture (for batch operations) */\n skipRestore?: boolean;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n}\n\n/**\n * Options for batch capture operations, excluding timeMs which is provided per-timestamp.\n */\nexport interface CaptureBatchOptions {\n /** Scale factor for thumbnails (default: 0.25) */\n scale?: number;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n}\n\n/**\n * Error thrown when video content is not ready within the blocking timeout.\n */\nexport class ContentNotReadyError extends Error {\n constructor(\n public readonly timeMs: number,\n public readonly timeoutMs: number,\n public readonly blankVideos: string[],\n ) {\n super(`Video content not ready at ${timeMs}ms after ${timeoutMs}ms timeout. Blank videos: ${blankVideos.join(', ')}`);\n this.name = 'ContentNotReadyError';\n }\n}\n\n// ============================================================================\n// Module State (reset via resetRenderState)\n// ============================================================================\n\n/** Image cache for inlining external images as data URIs (foreignObject path) */\nconst _inlineImageCache = new Map<string, string>();\n\n/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */\nconst _layoutInitializedCanvases = new WeakSet<HTMLCanvasElement>();\n\n// Reusable instances for better performance (avoid creating new instances every frame)\nlet _xmlSerializer: XMLSerializer | null = null;\nlet _textEncoder: TextEncoder | null = null;\n\n// Worker pool for parallel canvas encoding (lazy initialization)\nlet _workerPool: WorkerPool | null = null;\nlet _workerPoolWarningLogged = false;\n\n/**\n * Get or create the worker pool for canvas encoding.\n * Returns null if workers are not available.\n */\nfunction getWorkerPool(): WorkerPool | null {\n if (_workerPool) {\n return _workerPool;\n }\n\n // Check if workers are available\n if (\n typeof Worker === \"undefined\" ||\n typeof OffscreenCanvas === \"undefined\" ||\n typeof createImageBitmap === \"undefined\"\n ) {\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n console.warn(\n \"[renderTimegroupToCanvas] Web Workers or OffscreenCanvas not available, using main thread fallback\",\n );\n }\n return null;\n }\n\n try {\n // Create worker URL - Vite processes worker files when using ?worker_file suffix\n // In browser environments (Vite), import.meta.url is available at runtime\n let workerUrl: string;\n try {\n // TypeScript may not recognize import.meta in CommonJS mode, but it works at runtime in Vite\n // Access it dynamically to avoid TypeScript compilation errors\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-expect-error - import.meta.url works at runtime in Vite even if TS doesn't recognize it\n const metaUrl = import.meta?.url;\n if (metaUrl) {\n // Use ?worker_file&type=module - this is what Vite uses internally when processing workers\n // The ?worker suffix creates a wrapper, but ?worker_file gives us the actual worker\n workerUrl = new URL(\"./workers/encoderWorker.ts?worker_file&type=module\", metaUrl).href;\n } else {\n workerUrl = \"./workers/encoderWorker.ts?worker_file&type=module\";\n }\n } catch {\n // Fallback: use relative path\n workerUrl = \"./workers/encoderWorker.ts?worker_file&type=module\";\n }\n \n _workerPool = new WorkerPool(workerUrl);\n \n // Check if workers were actually created\n if (!_workerPool.isAvailable()) {\n const reason = _workerPool.workerCount === 0 \n ? \"no workers created (check console for errors)\" \n : \"workers not available\";\n _workerPool = null;\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n console.warn(\n `[renderTimegroupToCanvas] Worker pool initialization failed (${reason}), using main thread fallback`,\n );\n }\n }\n } catch (error) {\n _workerPool = null;\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.warn(\n `[renderTimegroupToCanvas] Failed to create worker pool: ${errorMessage} - using main thread fallback`,\n );\n }\n }\n\n return _workerPool;\n}\n\n/**\n * Encode a single canvas to a data URL (fallback implementation for main thread).\n */\nfunction encodeCanvasOnMainThread(\n canvas: HTMLCanvasElement,\n canvasScale: number,\n): { dataUrl: string; preserveAlpha: boolean } | null {\n try {\n if (canvas.width === 0 || canvas.height === 0) {\n return null;\n }\n\n const preserveAlpha = canvas.dataset.preserveAlpha === \"true\";\n let dataUrl: string;\n\n if (canvasScale < 1) {\n // Scale down canvas before encoding\n const scaledWidth = Math.floor(canvas.width * canvasScale);\n const scaledHeight = Math.floor(canvas.height * canvasScale);\n const scaledCanvas = document.createElement(\"canvas\");\n scaledCanvas.width = scaledWidth;\n scaledCanvas.height = scaledHeight;\n const scaledCtx = scaledCanvas.getContext(\"2d\");\n if (scaledCtx) {\n scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);\n const quality = canvasScale < 0.5 ? JPEG_QUALITY_MEDIUM : JPEG_QUALITY_HIGH;\n dataUrl = preserveAlpha\n ? scaledCanvas.toDataURL(\"image/png\")\n : scaledCanvas.toDataURL(\"image/jpeg\", quality);\n } else {\n dataUrl = preserveAlpha\n ? canvas.toDataURL(\"image/png\")\n : canvas.toDataURL(\"image/jpeg\", JPEG_QUALITY_HIGH);\n }\n } else {\n dataUrl = preserveAlpha\n ? canvas.toDataURL(\"image/png\")\n : canvas.toDataURL(\"image/jpeg\", JPEG_QUALITY_HIGH);\n }\n\n return { dataUrl, preserveAlpha };\n } catch (e) {\n // Cross-origin canvas or other error - skip\n return null;\n }\n}\n\n/**\n * Encode canvases to data URLs in parallel using worker pool.\n * Falls back to main thread encoding if workers are unavailable.\n */\nasync function encodeCanvasesInParallel(\n canvases: HTMLCanvasElement[],\n canvasScale: number = 1,\n): Promise<Array<{ canvas: HTMLCanvasElement; dataUrl: string; preserveAlpha: boolean }>> {\n const workerPool = getWorkerPool();\n\n // If no worker pool available, fall back to main thread\n if (!workerPool) {\n const results: Array<{ canvas: HTMLCanvasElement; dataUrl: string; preserveAlpha: boolean }> = [];\n for (const canvas of canvases) {\n const encoded = encodeCanvasOnMainThread(canvas, canvasScale);\n if (encoded) {\n results.push({ canvas, ...encoded });\n }\n }\n return results;\n }\n\n // Use worker pool for parallel encoding\n const encodingTasks = canvases.map(async (canvas) => {\n try {\n if (canvas.width === 0 || canvas.height === 0) {\n return null;\n }\n\n const preserveAlpha = canvas.dataset.preserveAlpha === \"true\";\n let sourceCanvas = canvas;\n\n // Handle canvas scaling on main thread before encoding\n if (canvasScale < 1) {\n const scaledWidth = Math.floor(canvas.width * canvasScale);\n const scaledHeight = Math.floor(canvas.height * canvasScale);\n const scaledCanvas = document.createElement(\"canvas\");\n scaledCanvas.width = scaledWidth;\n scaledCanvas.height = scaledHeight;\n const scaledCtx = scaledCanvas.getContext(\"2d\");\n if (scaledCtx) {\n scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);\n sourceCanvas = scaledCanvas;\n }\n }\n \n // Encode in worker\n const dataUrl = await workerPool.execute((worker) =>\n encodeCanvasInWorker(worker, sourceCanvas, preserveAlpha),\n );\n\n return { canvas, dataUrl, preserveAlpha };\n } catch (error) {\n // Fallback to main thread if worker encoding fails\n const encoded = encodeCanvasOnMainThread(canvas, canvasScale);\n if (encoded) {\n return { canvas, ...encoded };\n }\n \n // Cross-origin canvas or other error - skip\n return null;\n }\n });\n\n const encodedResults = await Promise.all(encodingTasks);\n const validResults = encodedResults.filter(\n (r): r is { canvas: HTMLCanvasElement; dataUrl: string; preserveAlpha: boolean } => r !== null,\n );\n return validResults;\n}\n\n/**\n * Fast base64 encoding directly from Uint8Array.\n * Avoids the overhead of converting to binary string first.\n * Uses lookup table for optimal performance.\n */\nfunction encodeBase64Fast(bytes: Uint8Array): string {\n const base64Chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n let result = \"\";\n let i = 0;\n const len = bytes.length;\n \n // Process 3 bytes at a time (produces 4 base64 chars)\n while (i < len - 2) {\n const byte1 = bytes[i++]!;\n const byte2 = bytes[i++]!;\n const byte3 = bytes[i++]!;\n \n const bitmap = (byte1 << 16) | (byte2 << 8) | byte3;\n \n result += base64Chars.charAt((bitmap >> 18) & 63);\n result += base64Chars.charAt((bitmap >> 12) & 63);\n result += base64Chars.charAt((bitmap >> 6) & 63);\n result += base64Chars.charAt(bitmap & 63);\n }\n \n // Handle remaining bytes (1 or 2)\n if (i < len) {\n const byte1 = bytes[i++]!;\n const bitmap = byte1 << 16;\n \n result += base64Chars.charAt((bitmap >> 18) & 63);\n result += base64Chars.charAt((bitmap >> 12) & 63);\n \n if (i < len) {\n const byte2 = bytes[i++]!;\n const bitmap2 = (byte1 << 16) | (byte2 << 8);\n result += base64Chars.charAt((bitmap2 >> 6) & 63);\n result += \"=\";\n } else {\n result += \"==\";\n }\n }\n \n return result;\n}\n\n/**\n * Reset all module state including profiling counters, caches, and logging flags.\n * Call at the start of export sessions to ensure clean state.\n */\nexport function resetRenderState(): void {\n defaultProfiler.reset();\n _inlineImageCache.clear();\n}\n\n/**\n * Clear the inline image cache. Useful for memory management in long-running sessions.\n */\nexport function clearInlineImageCache(): void {\n _inlineImageCache.clear();\n}\n\n/**\n * Get current inline image cache size for diagnostics.\n */\nexport function getInlineImageCacheSize(): number {\n return _inlineImageCache.size;\n}\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\n/**\n * Options for creating a DPR-aware canvas.\n */\ninterface CanvasOptions {\n /** Render width (internal resolution) */\n renderWidth: number;\n /** Render height (internal resolution) */\n renderHeight: number;\n /** Display scale factor */\n scale: number;\n /** Device pixel ratio (defaults to window.devicePixelRatio) */\n dpr?: number;\n /** Full logical width (for CSS sizing) */\n fullWidth: number;\n /** Full logical height (for CSS sizing) */\n fullHeight: number;\n}\n\n/**\n * Create a canvas element with proper DPR handling.\n * Buffer size is based on renderWidth/renderHeight (internal resolution).\n * CSS size is based on fullWidth/fullHeight (logical display size).\n */\nfunction createDprCanvas(options: CanvasOptions): HTMLCanvasElement {\n const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;\n const dpr = options.dpr ?? window.devicePixelRatio ?? 1;\n \n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(renderWidth * scale * dpr);\n canvas.height = Math.floor(renderHeight * scale * dpr);\n canvas.style.width = `${Math.floor(fullWidth * scale)}px`;\n canvas.style.height = `${Math.floor(fullHeight * scale)}px`;\n \n return canvas;\n}\n\n/**\n * Create a debug label for showing render info.\n */\nfunction createDebugLabel(): HTMLDivElement {\n const debugLabel = document.createElement(\"div\");\n debugLabel.style.cssText = `\n position: absolute;\n top: -24px;\n left: 0;\n padding: 2px 8px;\n font: bold 12px monospace;\n background: rgba(0, 0, 0, 0.8);\n border-radius: 3px;\n white-space: nowrap;\n z-index: 1000;\n pointer-events: none;\n `;\n return debugLabel;\n}\n\n/**\n * Update debug label with resolution info.\n */\nfunction updateDebugLabel(\n label: HTMLDivElement,\n renderWidth: number,\n renderHeight: number,\n resolutionScale: number,\n): void {\n const scaleColors: Record<number, string> = {\n 1: \"#00ff00\",\n 0.75: \"#ffff00\",\n 0.5: \"#ff8800\",\n 0.25: \"#ff0000\",\n };\n label.style.color = scaleColors[resolutionScale] || \"#ffffff\";\n label.textContent = `Render: ${renderWidth}x${renderHeight} (${Math.round(resolutionScale * 100)}%)`;\n}\n\n/**\n * Information needed to restore canvases after serialization.\n */\ninterface CanvasRestoreInfo {\n canvas: HTMLCanvasElement;\n parent: Node;\n nextSibling: Node | null;\n img: HTMLImageElement;\n}\n\n/**\n * Options for SVG serialization.\n */\ninterface SerializeToSvgOptions {\n /** Scale factor for encoding canvases (default: 1) */\n canvasScale?: number;\n /** Whether to inline external images (default: false for cloned containers) */\n inlineImages?: boolean;\n /** Whether to log early render info (default: false) */\n logEarlyRenders?: boolean;\n}\n\n/**\n * Result of SVG serialization.\n */\ninterface SerializationResult {\n dataUri: string;\n /** Call this to restore canvases if they were modified in-place */\n restore: () => void;\n}\n\n/**\n * Common SVG foreignObject serialization pipeline.\n * Handles canvas encoding, serialization, and base64 encoding.\n * \n * @param container - The HTML element to serialize\n * @param width - Output width\n * @param height - Output height\n * @param options - Serialization options\n * @returns Serialization result with data URI and restore function\n */\nasync function serializeToSvgDataUri(\n container: HTMLElement,\n width: number,\n height: number,\n options: SerializeToSvgOptions = {},\n): Promise<SerializationResult> {\n const { canvasScale = 1, inlineImages: shouldInlineImages = false, logEarlyRenders = false } = options;\n \n // Store info for restoration (only used if modifying in-place)\n const canvasRestoreInfo: CanvasRestoreInfo[] = [];\n \n // Phase 1: Encode canvases to data URLs (parallel)\n const canvasStart = performance.now();\n const canvases = Array.from(container.querySelectorAll(\"canvas\"));\n const encodedResults = await encodeCanvasesInParallel(canvases, canvasScale);\n \n // Replace canvases with images\n for (const { canvas, dataUrl } of encodedResults) {\n try {\n const img = document.createElement(\"img\");\n img.src = dataUrl;\n img.width = canvas.width;\n img.height = canvas.height;\n const style = canvas.getAttribute(\"style\");\n if (style) img.setAttribute(\"style\", style);\n \n const parent = canvas.parentNode;\n if (parent) {\n const nextSibling = canvas.nextSibling;\n parent.replaceChild(img, canvas);\n canvasRestoreInfo.push({ canvas, parent, nextSibling, img });\n }\n } catch {\n // Cross-origin canvas - leave as-is\n }\n }\n defaultProfiler.addTime(\"canvasEncode\", performance.now() - canvasStart);\n \n // Phase 2: Inline external images (if requested)\n if (shouldInlineImages) {\n const inlineStart = performance.now();\n await inlineImages(container);\n defaultProfiler.addTime(\"inline\", performance.now() - inlineStart);\n }\n \n // Phase 3: Serialize to XHTML\n const serializeStart = performance.now();\n const wrapper = document.createElement(\"div\");\n wrapper.setAttribute(\"xmlns\", \"http://www.w3.org/1999/xhtml\");\n wrapper.setAttribute(\"style\", `width:${width}px;height:${height}px;overflow:hidden;position:relative;`);\n wrapper.appendChild(container);\n \n if (!_xmlSerializer) {\n _xmlSerializer = new XMLSerializer();\n }\n const serialized = _xmlSerializer.serializeToString(wrapper);\n defaultProfiler.addTime(\"serialize\", performance.now() - serializeStart);\n \n // Prepare restore function (removes container from wrapper, restores canvases)\n const restore = (): void => {\n const restoreStart = performance.now();\n wrapper.removeChild(container);\n \n for (const { canvas, parent, nextSibling, img } of canvasRestoreInfo) {\n if (img.parentNode === parent) {\n if (nextSibling) {\n parent.insertBefore(canvas, nextSibling);\n parent.removeChild(img);\n } else {\n parent.replaceChild(canvas, img);\n }\n }\n }\n defaultProfiler.addTime(\"restore\", performance.now() - restoreStart);\n };\n \n // DEBUG: Log serialized HTML size for early renders\n if (logEarlyRenders && defaultProfiler.isEarlyRender(2)) {\n console.log(`[serializeToSvgDataUri] FO serialized: ${serialized.length} chars`);\n }\n \n // Phase 4: Create SVG and encode to base64\n const base64Start = performance.now();\n const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\"><foreignObject width=\"100%\" height=\"100%\">${serialized}</foreignObject></svg>`;\n \n if (!_textEncoder) {\n _textEncoder = new TextEncoder();\n }\n const utf8Bytes = _textEncoder.encode(svg);\n \n let base64: string;\n if (typeof (Uint8Array.prototype as any).toBase64 === \"function\") {\n base64 = (utf8Bytes as any).toBase64();\n } else {\n base64 = encodeBase64Fast(utf8Bytes);\n }\n const dataUri = `data:image/svg+xml;base64,${base64}`;\n defaultProfiler.addTime(\"base64\", performance.now() - base64Start);\n \n return { dataUri, restore };\n}\n\n/**\n * Inline all images in a container as base64 data URIs.\n * SVG foreignObject can't load external images due to security restrictions.\n * Uses an LRU-style cache with size limits to prevent memory leaks.\n */\nasync function inlineImages(container: HTMLElement): Promise<void> {\n const images = container.querySelectorAll(\"img\");\n for (const image of images) {\n const src = image.getAttribute(\"src\");\n if (!src || src.startsWith(\"data:\")) continue;\n\n const cached = _inlineImageCache.get(src);\n if (cached) {\n image.setAttribute(\"src\", cached);\n continue;\n }\n\n try {\n const response = await fetch(src);\n const blob = await response.blob();\n const dataUrl = await blobToDataURL(blob);\n image.setAttribute(\"src\", dataUrl);\n \n // Evict oldest entries if cache is full (simple FIFO eviction)\n if (_inlineImageCache.size >= MAX_INLINE_IMAGE_CACHE_SIZE) {\n const firstKey = _inlineImageCache.keys().next().value;\n if (firstKey) _inlineImageCache.delete(firstKey);\n }\n _inlineImageCache.set(src, dataUrl);\n } catch (e) {\n console.warn(\"Failed to inline image:\", src, e);\n }\n }\n}\n\n\n/**\n * Convert a Blob to a data URL.\n */\nfunction blobToDataURL(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise(resolve => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Wait for multiple animation frames to ensure all paints are flushed.\n * This is necessary because video frame decoding and canvas painting may\n * happen asynchronously even after seek() returns.\n */\nfunction waitForPaintFlush(): Promise<void> {\n return new Promise(resolve => {\n // Double RAF ensures we wait for:\n // 1. First RAF: Any pending paints are scheduled\n // 2. Second RAF: Those paints have completed\n requestAnimationFrame(() => {\n requestAnimationFrame(() => resolve());\n });\n });\n}\n\n/**\n * Check if a canvas has any rendered content (not all transparent/uninitialized).\n * Returns true if there's ANY non-transparent pixel.\n */\nfunction canvasHasContent(canvas: HTMLCanvasElement): boolean {\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return false;\n \n try {\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return false;\n \n // Sample a horizontal strip across the middle of the canvas\n // This catches most video content even if edges are black\n const stripY = Math.floor(height / 2);\n const imageData = ctx.getImageData(0, stripY, width, CANVAS_SAMPLE_STRIP_HEIGHT);\n const data = imageData.data;\n \n // Check if ANY pixel has non-zero alpha (is not transparent)\n // A truly blank/uninitialized canvas has all pixels at [0,0,0,0]\n // A black video frame would have pixels at [0,0,0,255] (opaque black)\n for (let i = 3; i < data.length; i += 4) {\n if (data[i] !== 0) {\n return true;\n }\n }\n \n return false;\n } catch {\n // Canvas might be tainted, assume it has content\n return true;\n }\n}\n\ninterface WaitForVideoContentResult {\n ready: boolean;\n blankVideos: string[];\n}\n\n/**\n * Wait for video canvases within a timegroup to have content.\n * Only checks videos that should be visible at the current time.\n * Returns result with ready status and list of blank video names.\n */\nasync function waitForVideoContent(\n timegroup: EFTimegroup,\n timeMs: number,\n maxWaitMs: number,\n): Promise<WaitForVideoContentResult> {\n const startTime = performance.now();\n \n // Find all video elements in the timegroup (including nested)\n const allVideos = timegroup.querySelectorAll(\"ef-video\");\n if (allVideos.length === 0) return { ready: true, blankVideos: [] };\n \n // Filter to only videos that should be visible at this time\n const visibleVideos = Array.from(allVideos).filter(video => {\n // Check if video itself is in time range\n if (!isVisibleAtTime(video, timeMs)) return false;\n \n // Check if all ancestor timegroups are in time range\n let parent = video.parentElement;\n while (parent && parent !== timegroup) {\n if (parent.tagName === 'EF-TIMEGROUP' && !isVisibleAtTime(parent, timeMs)) {\n return false;\n }\n parent = parent.parentElement;\n }\n return true;\n });\n \n if (visibleVideos.length === 0) return { ready: true, blankVideos: [] };\n \n const getBlankVideoNames = () => visibleVideos\n .filter(video => {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n return shadowCanvas && !canvasHasContent(shadowCanvas);\n })\n .map(v => (v as TemporalElement).src || v.id || 'unnamed');\n \n while (performance.now() - startTime < maxWaitMs) {\n let allHaveContent = true;\n \n for (const video of visibleVideos) {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n if (shadowCanvas && shadowCanvas.width > 0 && shadowCanvas.height > 0) {\n if (!canvasHasContent(shadowCanvas)) {\n allHaveContent = false;\n break;\n }\n }\n }\n \n if (allHaveContent) return { ready: true, blankVideos: [] };\n \n // Wait a bit and check again\n await waitForFrame();\n }\n \n return { ready: false, blankVideos: getBlankVideoNames() };\n}\n\n/**\n * Options for native rendering.\n */\nexport interface NativeRenderOptions {\n /**\n * Wait for RAF before capturing. Only needed if content hasn't been laid out yet.\n * Default: false (capture immediately - frame tasks should already be complete)\n * \n * Set to true only for edge cases where you're rendering content that was just\n * added to the DOM and hasn't had a chance to layout yet.\n */\n waitForPaint?: boolean;\n \n /**\n * Reuse an existing canvas instead of creating a new one.\n * The canvas must have layoutsubtree enabled and be in the DOM.\n */\n reuseCanvas?: HTMLCanvasElement;\n \n /**\n * Skip device pixel ratio scaling. When true, renders at 1x regardless of display DPR.\n * Default: false (respects display DPR for crisp rendering)\n * \n * Set to true for video export where retina resolution isn't needed.\n * This can provide a 4x speedup on 2x DPR displays!\n */\n skipDprScaling?: boolean;\n}\n\n/**\n * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).\n * This is much faster than the foreignObject approach and avoids canvas tainting.\n * \n * Note: The native API renders at device pixel ratio, so we capture at DPR scale\n * and then downsample to logical pixels to match the foreignObject path's output.\n * \n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options (skipWait for batch mode)\n * \n * @see https://github.com/WICG/html-in-canvas\n */\nexport async function renderToImageNative(\n container: HTMLElement,\n width: number,\n height: number,\n options: NativeRenderOptions = {},\n): Promise<HTMLCanvasElement> {\n const t0 = performance.now();\n const { waitForPaint = false, reuseCanvas, skipDprScaling = false } = options;\n // Use 1x DPR when skipDprScaling is true (for video export) - 4x fewer pixels!\n const dpr = skipDprScaling ? 1 : (window.devicePixelRatio || 1);\n \n // Use provided canvas or create new one\n let captureCanvas: HTMLCanvasElement;\n let shouldCleanup = false;\n \n if (reuseCanvas) {\n captureCanvas = reuseCanvas;\n \n // Ensure canvas dimensions match (both attribute and CSS)\n const dpr = skipDprScaling ? 1 : (window.devicePixelRatio || 1);\n const targetWidth = Math.floor(width * dpr);\n const targetHeight = Math.floor(height * dpr);\n \n // Set attribute dimensions (pixel buffer size)\n if (captureCanvas.width !== targetWidth) {\n captureCanvas.width = targetWidth;\n }\n if (captureCanvas.height !== targetHeight) {\n captureCanvas.height = targetHeight;\n }\n \n // Ensure CSS dimensions match logical size (required for layoutsubtree)\n captureCanvas.style.width = `${width}px`;\n captureCanvas.style.height = `${height}px`;\n \n // Ensure layoutsubtree is set (required for drawElementImage)\n if (!captureCanvas.hasAttribute(\"layoutsubtree\")) {\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n }\n \n // Ensure canvas is in DOM (required for drawElementImage layout)\n if (!captureCanvas.parentNode) {\n document.body.appendChild(captureCanvas);\n }\n \n // Ensure container is child of canvas\n if (container.parentElement !== captureCanvas) {\n captureCanvas.appendChild(container);\n }\n \n // Ensure container is visible (not display: none) for layout\n // drawElementImage requires the element to be laid out\n const containerStyle = getComputedStyle(container);\n if (containerStyle.display === 'none') {\n container.style.display = 'block';\n }\n \n // Force synchronous layout by reading layout properties\n // This ensures both canvas and container are laid out (required for drawElementImage)\n // Reading offsetHeight forces a synchronous layout recalculation\n void captureCanvas.offsetHeight;\n void container.offsetHeight;\n getComputedStyle(captureCanvas).opacity;\n getComputedStyle(container).opacity;\n } else {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.width = Math.floor(width * dpr);\n captureCanvas.height = Math.floor(height * dpr);\n \n // Enable HTML-in-Canvas mode via layoutsubtree attribute/property\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n \n captureCanvas.appendChild(container);\n \n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n document.body.appendChild(captureCanvas);\n shouldCleanup = true;\n }\n \n const t1 = performance.now();\n defaultProfiler.addTime(\"setup\", t1 - t0);\n \n try {\n // Force style calculation to ensure CSS is computed before capture\n // This ensures both canvas and container are laid out (required for drawElementImage)\n getComputedStyle(container).opacity;\n \n // When reusing canvas with layoutsubtree, wait for initial layout (first use only)\n // Use a WeakSet to track canvases that have been initialized\n if (reuseCanvas && (captureCanvas as any).layoutSubtree && !_layoutInitializedCanvases.has(captureCanvas)) {\n await waitForFrame();\n _layoutInitializedCanvases.add(captureCanvas);\n \n // Canvas may have been detached during async wait (e.g., test cleanup)\n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n \n // Only wait for paint in rare edge cases where content was just added to DOM\n if (waitForPaint) {\n await waitForPaintFlush();\n \n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n \n const ctx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n ctx.drawElementImage(container, 0, 0);\n } finally {\n // Only clean up if we created the canvas\n if (shouldCleanup && captureCanvas.parentNode) {\n captureCanvas.parentNode.removeChild(captureCanvas);\n }\n }\n \n const t2 = performance.now();\n defaultProfiler.addTime(\"draw\", t2 - t1);\n \n // If DPR is 1, no downsampling needed - return as-is\n if (dpr === 1) {\n defaultProfiler.incrementRenderCount();\n return captureCanvas;\n }\n \n // Downsample to logical pixel dimensions to match foreignObject path output\n // This ensures consistent behavior regardless of which rendering path is used\n const outputCanvas = document.createElement(\"canvas\");\n outputCanvas.width = width;\n outputCanvas.height = height;\n \n const outputCtx = outputCanvas.getContext(\"2d\")!;\n // Draw the DPR-scaled capture onto the 1x output canvas\n outputCtx.drawImage(\n captureCanvas,\n 0, 0, captureCanvas.width, captureCanvas.height, // source (full DPR capture)\n 0, 0, width, height // destination (logical pixels)\n );\n \n const t3 = performance.now();\n defaultProfiler.addTime(\"downsample\", t3 - t2);\n defaultProfiler.incrementRenderCount();\n \n // Log timing periodically\n defaultProfiler.shouldLogByTime(PROFILING_LOG_INTERVAL_MS);\n \n return outputCanvas;\n}\n\n/**\n * Options for foreignObject rendering path.\n */\nexport interface ForeignObjectRenderOptions extends NativeRenderOptions {\n /**\n * Scale factor for encoding internal canvases.\n * When set, canvases are scaled down before encoding to data URLs,\n * dramatically reducing encoding time for thumbnails.\n * Default: 1 (no scaling - encode at full resolution)\n */\n canvasScale?: number;\n}\n\n/**\n * Render HTML content to an image (or canvas) for drawing.\n * \n * Supports two rendering modes (configurable via previewSettings):\n * - \"native\": Chrome's experimental drawElementImage API (fastest when available)\n * - \"foreignObject\": SVG foreignObject serialization (fallback, works everywhere)\n * \n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options\n * @returns HTMLCanvasElement when using native, HTMLImageElement when using foreignObject\n */\nexport async function renderToImage(\n container: HTMLElement,\n width: number,\n height: number,\n options?: ForeignObjectRenderOptions,\n): Promise<HTMLImageElement | HTMLCanvasElement> {\n const renderMode = getEffectiveRenderMode();\n \n // Native HTML-in-Canvas API path (fastest, requires Chrome flag)\n if (renderMode === \"native\") {\n return renderToImageNative(container, width, height, options);\n }\n \n // Fallback: SVG foreignObject serialization\n // Clone the container first (don't modify original)\n // Note: cloneNode doesn't copy canvas pixels, so we encode from original canvases\n const originalCanvases = Array.from(container.querySelectorAll(\"canvas\"));\n const clone = container.cloneNode(true) as HTMLElement;\n const clonedCanvases = clone.querySelectorAll(\"canvas\");\n \n // Encode original canvases and map to cloned elements\n const canvasScale = options?.canvasScale ?? 1;\n const canvasStart = performance.now();\n const encodedResults = await encodeCanvasesInParallel(originalCanvases, canvasScale);\n \n for (let i = 0; i < originalCanvases.length; i++) {\n const srcCanvas = originalCanvases[i];\n const dstCanvas = clonedCanvases[i];\n const encoded = encodedResults.find((r) => r.canvas === srcCanvas);\n \n if (!srcCanvas || !dstCanvas || !encoded) continue;\n \n try {\n const img = document.createElement(\"img\");\n img.src = encoded.dataUrl;\n img.width = srcCanvas.width;\n img.height = srcCanvas.height;\n const style = dstCanvas.getAttribute(\"style\");\n if (style) img.setAttribute(\"style\", style);\n dstCanvas.parentNode?.replaceChild(img, dstCanvas);\n } catch {\n // Cross-origin or other error - skip\n }\n }\n defaultProfiler.addTime(\"canvasEncode\", performance.now() - canvasStart);\n\n // Inline external images in the clone\n const inlineStart = performance.now();\n await inlineImages(clone);\n defaultProfiler.addTime(\"inline\", performance.now() - inlineStart);\n\n // Use common serialization pipeline (no restore needed since we're working on a clone)\n const { dataUri } = await serializeToSvgDataUri(clone, width, height);\n \n // Load as image\n return loadImageFromDataUri(dataUri);\n}\n\n\n/**\n * Render a pre-built clone container to an image WITHOUT cloning it again.\n * This is the fast path for reusing clone structures across frames.\n * \n * Key difference from renderToImage:\n * - Does NOT call cloneNode (avoids expensive DOM duplication)\n * - Converts canvases to images in-place, then restores them after serialization\n * - Assumes the container already has refreshed canvas content\n * \n * @param container - Pre-built clone container with refreshed canvas content\n * @param width - Output width\n * @param height - Output height\n * @returns Promise resolving to an HTMLImageElement\n */\nexport async function renderToImageDirect(\n container: HTMLElement,\n width: number,\n height: number,\n): Promise<HTMLImageElement> {\n defaultProfiler.incrementRenderCount();\n \n // Use common serialization pipeline (modifies in-place, restores after)\n const { dataUri, restore } = await serializeToSvgDataUri(container, width, height, {\n inlineImages: true,\n logEarlyRenders: true,\n });\n restore();\n \n // Load as image\n const image = await loadImageFromDataUri(dataUri);\n \n // Log timing breakdown periodically\n defaultProfiler.shouldLogByFrameCount(100);\n \n return image;\n}\n\n/**\n * Prepare a frame's data URI without waiting for image load.\n * Returns the data URI asynchronously (after parallel canvas encoding and serialization) for pipelined loading.\n * The DOM is restored before this function returns.\n */\nexport async function prepareFrameDataUri(\n container: HTMLElement,\n width: number,\n height: number,\n): Promise<string> {\n defaultProfiler.incrementRenderCount();\n \n // Use common serialization pipeline (modifies in-place, restores after)\n const { dataUri, restore } = await serializeToSvgDataUri(container, width, height);\n restore();\n \n return dataUri;\n}\n\n/**\n * Load an image from a data URI. Returns a Promise that resolves when loaded.\n */\nexport function loadImageFromDataUri(dataUri: string): Promise<HTMLImageElement> {\n const img = new Image();\n const imageLoadStart = performance.now();\n \n return new Promise<HTMLImageElement>((resolve, reject) => {\n img.onload = () => {\n defaultProfiler.addTime(\"imageLoad\", performance.now() - imageLoadStart);\n resolve(img);\n };\n img.onerror = reject;\n img.src = dataUri;\n });\n}\n\n/**\n * Options for capturing from an existing render clone.\n */\nexport interface CaptureFromCloneOptions {\n /** Scale factor for the output canvas (default: 0.25) */\n scale?: number;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n /** Original timegroup (for dimension and background reference) */\n originalTimegroup?: EFTimegroup;\n}\n\n/**\n * Captures a frame from an already-seeked render clone.\n * Used internally by captureBatch for efficiency (reuses one clone across all captures).\n * \n * @param renderClone - A render clone that has already been seeked to the target time\n * @param renderContainer - The container holding the render clone (from createRenderClone)\n * @param options - Capture options\n * @returns Canvas with the rendered frame\n */\nexport async function captureFromClone(\n renderClone: EFTimegroup,\n renderContainer: HTMLElement,\n options: CaptureFromCloneOptions = {},\n): Promise<HTMLCanvasElement> {\n const {\n scale = DEFAULT_THUMBNAIL_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n originalTimegroup,\n } = options;\n\n // Use original timegroup dimensions if available, otherwise clone dimensions\n const sourceForDimensions = originalTimegroup ?? renderClone;\n const width = sourceForDimensions.offsetWidth || DEFAULT_WIDTH;\n const height = sourceForDimensions.offsetHeight || DEFAULT_HEIGHT;\n\n // Create canvas at scaled size\n const dpr = window.devicePixelRatio || 1;\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(width * scale * dpr);\n canvas.height = Math.floor(height * scale * dpr);\n canvas.style.width = `${Math.floor(width * scale)}px`;\n canvas.style.height = `${Math.floor(height * scale)}px`;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Handle content readiness based on mode\n const timeMs = renderClone.currentTimeMs;\n if (contentReadyMode === \"blocking\") {\n const result = await waitForVideoContent(renderClone, timeMs, blockingTimeoutMs);\n if (!result.ready) {\n throw new ContentNotReadyError(timeMs, blockingTimeoutMs, result.blankVideos);\n }\n }\n\n let image: HTMLCanvasElement | HTMLImageElement;\n const renderMode = getEffectiveRenderMode();\n \n if (renderMode === \"native\") {\n // NATIVE PATH: Render the seeked renderClone directly from live DOM\n // The clone is already at the correct time, so drawElementImage captures its current\n // visual state including video frames at the correct position.\n // \n // Position render container properly for capture\n renderContainer.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n pointer-events: none;\n overflow: hidden;\n `;\n \n // OPTIMIZATION: Skip DPR scaling for thumbnails - retina quality isn't needed\n // and DPR=2 means 4x more pixels to render.\n const skipDpr = scale < 1;\n image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: skipDpr });\n } else {\n // FOREIGNOBJECT PATH: Build passive structure from the SEEKED render clone\n // The clone is already at the correct time, so getComputedStyle captures the right values.\n // Styles are synced during clone building in a single pass.\n const t0 = performance.now();\n const { container, syncState } = buildCloneStructure(renderClone, timeMs);\n const buildTime = performance.now() - t0;\n\n // Create wrapper using shared helper\n const bgSource = originalTimegroup ?? renderClone;\n const previewContainer = createPreviewContainer({\n width,\n height,\n background: getComputedStyle(bgSource).background || \"#000\",\n });\n \n const t1 = performance.now();\n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectDocumentStyles();\n const stylesTime = performance.now() - t1;\n previewContainer.appendChild(styleEl);\n previewContainer.appendChild(container);\n \n // Ensure clone root is visible\n overrideRootCloneStyles(syncState, true);\n\n // Render using foreignObject serialization\n // Pass scale so canvases are encoded at thumbnail size (MUCH faster)\n const t2 = performance.now();\n image = await renderToImage(previewContainer, width, height, { canvasScale: scale });\n const renderTime = performance.now() - t2;\n \n console.log(`[captureFromClone] build=${buildTime.toFixed(0)}ms, styles=${stylesTime.toFixed(0)}ms, render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`);\n }\n\n // Draw to canvas (may need scaling for native path which is at DPR)\n const srcWidth = image.width;\n const srcHeight = image.height;\n ctx.drawImage(\n image,\n 0, 0, srcWidth, srcHeight,\n 0, 0, canvas.width, canvas.height\n );\n\n return canvas;\n}\n\n/**\n * Captures a single frame from a timegroup at a specific time.\n * \n * CLONE-TIMELINE ARCHITECTURE:\n * Creates an independent render clone, seeks it to the target time, and captures.\n * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.\n * \n * @param timegroup - The source timegroup\n * @param options - Capture options including timeMs, scale, contentReadyMode\n * @returns Canvas with the rendered frame\n * @throws ContentNotReadyError if blocking mode times out waiting for video content\n */\nexport async function captureTimegroupAtTime(\n timegroup: EFTimegroup,\n options: CaptureOptions,\n): Promise<HTMLCanvasElement> {\n const {\n timeMs,\n scale = DEFAULT_THUMBNAIL_SCALE,\n // skipRestore is deprecated with Clone-timeline (Prime is never seeked)\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n } = options;\n\n // CLONE-TIMELINE: Create a short-lived render clone for this capture\n // Prime-timeline is NEVER seeked - clone is fully independent\n const { clone: renderClone, container: renderContainer, cleanup: cleanupRenderClone } = \n await timegroup.createRenderClone();\n \n try {\n // Seek the clone to target time (Prime stays at user position)\n // Use seekForRender which bypasses duration clamping - render clones may have\n // zero duration initially until media durations are computed, but we still\n // want to seek to the requested time for capture purposes.\n await renderClone.seekForRender(timeMs);\n \n // Use the shared capture helper\n return await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n });\n } finally {\n // Clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/** Epsilon for comparing time values (ms) - times within this are considered equal */\nconst TIME_EPSILON_MS = 1;\n\n/** Default scale for preview rendering */\nconst DEFAULT_PREVIEW_SCALE = 1;\n\n/** Default resolution scale (full resolution) */\nconst DEFAULT_RESOLUTION_SCALE = 1;\n\n/**\n * Convert relative time to absolute time for a timegroup.\n * Nested timegroup children have ABSOLUTE startTimeMs values,\n * so relative capture times must be converted for temporal culling.\n */\nfunction toAbsoluteTime(timegroup: EFTimegroup, relativeTimeMs: number): number {\n return relativeTimeMs + (timegroup.startTimeMs ?? 0);\n}\n\nexport interface CanvasPreviewResult {\n /**\n * Wrapper container holding the canvas and debug label.\n * Append this to your DOM - the canvas inside will receive transforms.\n */\n container: HTMLDivElement;\n canvas: HTMLCanvasElement;\n /**\n * Call this to re-render the timegroup to canvas at current visual state.\n * Returns a promise that resolves when rendering is complete.\n */\n refresh: () => Promise<void>;\n syncState: SyncState;\n /**\n * Dynamically change the resolution scale without rebuilding the clone structure.\n * This is nearly instant - just updates CSS and internal variables.\n * The next refresh() call will render at the new resolution.\n */\n setResolutionScale: (scale: number) => void;\n /**\n * Get the current resolution scale.\n */\n getResolutionScale: () => number;\n}\n\n/**\n * Options for canvas preview rendering.\n */\nexport interface CanvasPreviewOptions {\n /**\n * Output scale factor (default: 1).\n * Scales the final canvas size.\n */\n scale?: number;\n \n /**\n * Resolution scale for internal rendering (default: 1).\n * Reduces the internal render resolution for better performance.\n * The canvas CSS size remains the same (browser upscales).\n * - 1: Full resolution\n * - 0.75: 3/4 resolution\n * - 0.5: Half resolution\n * - 0.25: Quarter resolution\n */\n resolutionScale?: number;\n}\n\n/**\n * Renders a timegroup preview to a canvas using SVG foreignObject.\n * \n * Optimized with:\n * - Persistent clone structure (built once)\n * - Temporal bucketing for time-based culling\n * - Property split (static vs animated)\n * - Parent index for O(1) visibility checks\n * - Resolution scaling for performance (renders at lower resolution, CSS upscales)\n *\n * @param timegroup - The source timegroup to preview\n * @param scaleOrOptions - Scale factor (default 1) or options object\n * @returns Object with canvas and refresh function\n */\nexport function renderTimegroupToCanvas(\n timegroup: EFTimegroup,\n scaleOrOptions: number | CanvasPreviewOptions = DEFAULT_PREVIEW_SCALE,\n): CanvasPreviewResult {\n // Normalize options\n const options: CanvasPreviewOptions = typeof scaleOrOptions === \"number\"\n ? { scale: scaleOrOptions }\n : scaleOrOptions;\n \n const scale = options.scale ?? DEFAULT_PREVIEW_SCALE;\n // These are mutable to support dynamic resolution changes\n let currentResolutionScale = options.resolutionScale ?? DEFAULT_RESOLUTION_SCALE;\n \n const width = timegroup.offsetWidth || DEFAULT_WIDTH;\n const height = timegroup.offsetHeight || DEFAULT_HEIGHT;\n const dpr = window.devicePixelRatio || 1;\n \n // Calculate effective render dimensions (internal resolution) - mutable\n let renderWidth = Math.floor(width * currentResolutionScale);\n let renderHeight = Math.floor(height * currentResolutionScale);\n\n // Create canvas with proper DPR handling\n const canvas = createDprCanvas({\n renderWidth,\n renderHeight,\n scale,\n fullWidth: width,\n fullHeight: height,\n dpr,\n });\n \n // Create wrapper container with debug label\n const wrapperContainer = document.createElement(\"div\");\n wrapperContainer.style.cssText = \"position: relative; display: inline-block;\";\n const debugLabel = createDebugLabel();\n wrapperContainer.appendChild(debugLabel);\n wrapperContainer.appendChild(canvas);\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Build clone structure ONCE with optimized sync state\n // Initial sync happens during clone building in a single pass\n const initialTimeMs = toAbsoluteTime(timegroup, timegroup.currentTimeMs ?? 0);\n const { container, syncState } = buildCloneStructure(timegroup, initialTimeMs);\n\n // Create a wrapper div with scaled dimensions\n // When resolutionScale < 1, we render at a smaller size and CSS transform scales the content\n const previewContainer = createPreviewContainer({\n width: renderWidth,\n height: renderHeight,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\n \n // Apply CSS transform to scale down the content within the container\n // This makes the clone render at reduced complexity\n if (currentResolutionScale < 1) {\n container.style.transform = `scale(${currentResolutionScale})`;\n container.style.transformOrigin = \"top left\";\n }\n \n // Inject document styles so CSS rules work in SVG foreignObject\n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectDocumentStyles();\n previewContainer.appendChild(styleEl);\n \n previewContainer.appendChild(container);\n overrideRootCloneStyles(syncState);\n\n // Track render state\n let rendering = false;\n let lastTimeMs = -1;\n\n // Log resolution scale on first render for debugging\n let hasLoggedScale = false;\n \n // Pending resolution change - applied at start of next refresh to avoid blanking\n let pendingResolutionScale: number | null = null;\n \n /**\n * Apply pending resolution scale changes.\n * Called at the start of refresh() before rendering, so the old content\n * stays visible until new content is ready to be drawn.\n */\n const applyPendingResolutionChange = (): void => {\n if (pendingResolutionScale === null) return;\n \n const newScale = pendingResolutionScale;\n pendingResolutionScale = null;\n \n currentResolutionScale = newScale;\n renderWidth = Math.floor(width * currentResolutionScale);\n renderHeight = Math.floor(height * currentResolutionScale);\n \n // Update previewContainer dimensions (affects what renderToImage produces)\n previewContainer.style.width = `${renderWidth}px`;\n previewContainer.style.height = `${renderHeight}px`;\n \n // Update clone transform\n if (currentResolutionScale < 1) {\n container.style.transform = `scale(${currentResolutionScale})`;\n container.style.transformOrigin = \"top left\";\n } else {\n container.style.transform = \"\";\n }\n \n // Canvas dimensions will be updated right before drawing (in refresh)\n // to avoid clearing the canvas until new content is ready\n };\n \n /**\n * Dynamically change resolution scale without rebuilding clone structure.\n * The actual change is deferred until next refresh() to avoid blanking -\n * old content stays visible until new content is ready.\n */\n const setResolutionScale = (newScale: number): void => {\n // Clamp to valid range\n newScale = Math.max(0.1, Math.min(1, newScale));\n \n if (newScale === currentResolutionScale && pendingResolutionScale === null) return;\n \n // Queue the change - will be applied at start of next refresh\n pendingResolutionScale = newScale;\n \n // Force re-render on next refresh by invalidating lastTimeMs\n lastTimeMs = -1;\n };\n \n const getResolutionScale = (): number => pendingResolutionScale ?? currentResolutionScale;\n \n const refresh = async (): Promise<void> => {\n if (rendering) return;\n // Clone-timeline: captures use separate clones, Prime-timeline is never locked\n \n const sourceTimeMs = timegroup.currentTimeMs ?? 0;\n const userTimeMs = timegroup.userTimeMs ?? 0;\n if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;\n \n if (userTimeMs === lastTimeMs) return;\n lastTimeMs = userTimeMs;\n \n rendering = true;\n \n // Apply any pending resolution changes before rendering\n // This updates previewContainer and clone transform, but NOT canvas dimensions yet\n applyPendingResolutionChange();\n \n // Log scale info once per initialization\n if (!hasLoggedScale) {\n hasLoggedScale = true;\n const mode = getEffectiveRenderMode();\n console.log(`[renderTimegroupToCanvas] Resolution scale: ${currentResolutionScale} (${width}x${height} → ${renderWidth}x${renderHeight}), canvas buffer: ${canvas.width}x${canvas.height}, CSS size: ${canvas.style.width}x${canvas.style.height}, renderMode: ${mode}`);\n }\n\n try {\n syncStyles(syncState, toAbsoluteTime(timegroup, userTimeMs));\n overrideRootCloneStyles(syncState);\n\n // Render at scaled dimensions with canvas scaling for internal video frames\n const t0 = performance.now();\n const image = await renderToImage(previewContainer, renderWidth, renderHeight, {\n canvasScale: currentResolutionScale,\n });\n const renderTime = performance.now() - t0;\n\n // Update canvas buffer dimensions NOW, right before drawing\n // This clears the canvas, but we immediately draw new content\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n \n ctx.save();\n ctx.scale(dpr * scale, dpr * scale);\n ctx.drawImage(image, 0, 0);\n ctx.restore();\n \n // Log render time periodically (every 60 frames)\n defaultProfiler.incrementRenderCount();\n if (defaultProfiler.shouldLogByFrameCount(60)) {\n console.log(`[renderTimegroupToCanvas] Frame render: ${renderTime.toFixed(1)}ms (resolutionScale=${currentResolutionScale}, image=${image.width}x${image.height})`);\n }\n \n // Update debug label\n updateDebugLabel(debugLabel, renderWidth, renderHeight, currentResolutionScale);\n } catch (e) {\n console.error(\"Canvas preview render failed:\", e);\n } finally {\n rendering = false;\n }\n };\n\n // Do initial render\n refresh();\n\n return { container: wrapperContainer, canvas, refresh, syncState, setResolutionScale, getResolutionScale };\n}\n\n"],"mappings":"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"}
1
+ {"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","_xmlSerializer: XMLSerializer | null","_textEncoder: TextEncoder | null","_workerPool: WorkerPool | null","dataUrl: string","results: Array<{ canvas: HTMLCanvasElement; dataUrl: string; preserveAlpha: boolean }>","scaleColors: Record<number, string>","canvasRestoreInfo: CanvasRestoreInfo[]","base64: string","captureCanvas: HTMLCanvasElement","dpr","image: HTMLCanvasElement | HTMLImageElement","options: CanvasPreviewOptions","pendingResolutionScale: number | null"],"sources":["../../src/preview/renderTimegroupToCanvas.ts"],"sourcesContent":["import type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport {\n buildCloneStructure,\n syncStyles,\n collectDocumentStyles,\n overrideRootCloneStyles,\n type SyncState,\n} from \"./renderTimegroupPreview.js\";\nimport { getEffectiveRenderMode } from \"./renderers.js\";\n\n// Re-export renderer types for external use\nexport type { RenderOptions, RenderResult, Renderer } from \"./renderers.js\";\nexport { getEffectiveRenderMode, isCanvas, isImage } from \"./renderers.js\";\nimport { WorkerPool, encodeCanvasInWorker } from \"./workers/WorkerPool.js\";\nimport { getEncoderWorkerUrl } from \"./workers/encoderWorkerInline.js\";\nimport {\n type TemporalElement,\n isVisibleAtTime,\n DEFAULT_WIDTH,\n DEFAULT_HEIGHT,\n DEFAULT_THUMBNAIL_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n JPEG_QUALITY_HIGH,\n JPEG_QUALITY_MEDIUM,\n createPreviewContainer,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\n\n// ============================================================================\n// Constants (module-specific, not shared)\n// ============================================================================\n\n/** Number of rows to sample when checking canvas content */\nconst CANVAS_SAMPLE_STRIP_HEIGHT = 4;\n\n/** Interval between profiling log outputs (ms) */\nconst PROFILING_LOG_INTERVAL_MS = 2000;\n\n/** Maximum number of cached inline images before eviction */\nconst MAX_INLINE_IMAGE_CACHE_SIZE = 100;\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Content readiness strategy for capture operations.\n * - \"immediate\": Capture NOW, skip all waits. May have blank video frames.\n * - \"blocking\": Wait for video content to be ready. Throws on timeout.\n */\nexport type ContentReadyMode = \"immediate\" | \"blocking\";\n\n/**\n * Extended CanvasRenderingContext2D with HTML-in-Canvas API support.\n * @see https://github.com/WICG/html-in-canvas\n */\ninterface HtmlInCanvasContext extends CanvasRenderingContext2D {\n drawElementImage(element: HTMLElement, x: number, y: number): void;\n}\n\n/**\n * Extended HTMLCanvasElement with layoutSubtree property for HTML-in-Canvas.\n */\ninterface HtmlInCanvasElement extends HTMLCanvasElement {\n layoutSubtree?: boolean;\n}\n\n/**\n * Options for capturing a timegroup frame.\n */\nexport interface CaptureOptions {\n /** Time to capture at in milliseconds (required) */\n timeMs: number;\n /** Scale factor (default: 0.25 for captureTimegroupAtTime) */\n scale?: number;\n /** Skip restoring original time after capture (for batch operations) */\n skipRestore?: boolean;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n}\n\n/**\n * Options for batch capture operations, excluding timeMs which is provided per-timestamp.\n */\nexport interface CaptureBatchOptions {\n /** Scale factor for thumbnails (default: 0.25) */\n scale?: number;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n}\n\n/**\n * Error thrown when video content is not ready within the blocking timeout.\n */\nexport class ContentNotReadyError extends Error {\n constructor(\n public readonly timeMs: number,\n public readonly timeoutMs: number,\n public readonly blankVideos: string[],\n ) {\n super(`Video content not ready at ${timeMs}ms after ${timeoutMs}ms timeout. Blank videos: ${blankVideos.join(', ')}`);\n this.name = 'ContentNotReadyError';\n }\n}\n\n// ============================================================================\n// Module State (reset via resetRenderState)\n// ============================================================================\n\n/** Image cache for inlining external images as data URIs (foreignObject path) */\nconst _inlineImageCache = new Map<string, string>();\n\n/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */\nconst _layoutInitializedCanvases = new WeakSet<HTMLCanvasElement>();\n\n// Reusable instances for better performance (avoid creating new instances every frame)\nlet _xmlSerializer: XMLSerializer | null = null;\nlet _textEncoder: TextEncoder | null = null;\n\n// Worker pool for parallel canvas encoding (lazy initialization)\nlet _workerPool: WorkerPool | null = null;\nlet _workerPoolWarningLogged = false;\n\n/**\n * Get or create the worker pool for canvas encoding.\n * Returns null if workers are not available.\n */\nfunction getWorkerPool(): WorkerPool | null {\n if (_workerPool) {\n return _workerPool;\n }\n\n // Check if workers are available\n if (\n typeof Worker === \"undefined\" ||\n typeof OffscreenCanvas === \"undefined\" ||\n typeof createImageBitmap === \"undefined\"\n ) {\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n console.warn(\n \"[renderTimegroupToCanvas] Web Workers or OffscreenCanvas not available, using main thread fallback\",\n );\n }\n return null;\n }\n\n try {\n // Use inline worker URL - this works in any bundler environment\n // because the worker code is embedded in the bundle as a blob URL\n const workerUrl = getEncoderWorkerUrl();\n \n _workerPool = new WorkerPool(workerUrl);\n \n // Check if workers were actually created\n if (!_workerPool.isAvailable()) {\n const reason = _workerPool.workerCount === 0 \n ? \"no workers created (check console for errors)\" \n : \"workers not available\";\n _workerPool = null;\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n console.warn(\n `[renderTimegroupToCanvas] Worker pool initialization failed (${reason}), using main thread fallback`,\n );\n }\n }\n } catch (error) {\n _workerPool = null;\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.warn(\n `[renderTimegroupToCanvas] Failed to create worker pool: ${errorMessage} - using main thread fallback`,\n );\n }\n }\n\n return _workerPool;\n}\n\n/**\n * Encode a single canvas to a data URL (fallback implementation for main thread).\n */\nfunction encodeCanvasOnMainThread(\n canvas: HTMLCanvasElement,\n canvasScale: number,\n): { dataUrl: string; preserveAlpha: boolean } | null {\n try {\n if (canvas.width === 0 || canvas.height === 0) {\n return null;\n }\n\n const preserveAlpha = canvas.dataset.preserveAlpha === \"true\";\n let dataUrl: string;\n\n if (canvasScale < 1) {\n // Scale down canvas before encoding\n const scaledWidth = Math.floor(canvas.width * canvasScale);\n const scaledHeight = Math.floor(canvas.height * canvasScale);\n const scaledCanvas = document.createElement(\"canvas\");\n scaledCanvas.width = scaledWidth;\n scaledCanvas.height = scaledHeight;\n const scaledCtx = scaledCanvas.getContext(\"2d\");\n if (scaledCtx) {\n scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);\n const quality = canvasScale < 0.5 ? JPEG_QUALITY_MEDIUM : JPEG_QUALITY_HIGH;\n dataUrl = preserveAlpha\n ? scaledCanvas.toDataURL(\"image/png\")\n : scaledCanvas.toDataURL(\"image/jpeg\", quality);\n } else {\n dataUrl = preserveAlpha\n ? canvas.toDataURL(\"image/png\")\n : canvas.toDataURL(\"image/jpeg\", JPEG_QUALITY_HIGH);\n }\n } else {\n dataUrl = preserveAlpha\n ? canvas.toDataURL(\"image/png\")\n : canvas.toDataURL(\"image/jpeg\", JPEG_QUALITY_HIGH);\n }\n\n return { dataUrl, preserveAlpha };\n } catch (e) {\n // Cross-origin canvas or other error - skip\n return null;\n }\n}\n\n/**\n * Encode canvases to data URLs in parallel using worker pool.\n * Falls back to main thread encoding if workers are unavailable.\n */\nasync function encodeCanvasesInParallel(\n canvases: HTMLCanvasElement[],\n canvasScale: number = 1,\n): Promise<Array<{ canvas: HTMLCanvasElement; dataUrl: string; preserveAlpha: boolean }>> {\n const workerPool = getWorkerPool();\n\n // If no worker pool available, fall back to main thread\n if (!workerPool) {\n const results: Array<{ canvas: HTMLCanvasElement; dataUrl: string; preserveAlpha: boolean }> = [];\n for (const canvas of canvases) {\n const encoded = encodeCanvasOnMainThread(canvas, canvasScale);\n if (encoded) {\n results.push({ canvas, ...encoded });\n }\n }\n return results;\n }\n\n // Use worker pool for parallel encoding\n const encodingTasks = canvases.map(async (canvas) => {\n try {\n if (canvas.width === 0 || canvas.height === 0) {\n return null;\n }\n\n const preserveAlpha = canvas.dataset.preserveAlpha === \"true\";\n let sourceCanvas = canvas;\n\n // Handle canvas scaling on main thread before encoding\n if (canvasScale < 1) {\n const scaledWidth = Math.floor(canvas.width * canvasScale);\n const scaledHeight = Math.floor(canvas.height * canvasScale);\n const scaledCanvas = document.createElement(\"canvas\");\n scaledCanvas.width = scaledWidth;\n scaledCanvas.height = scaledHeight;\n const scaledCtx = scaledCanvas.getContext(\"2d\");\n if (scaledCtx) {\n scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);\n sourceCanvas = scaledCanvas;\n }\n }\n \n // Encode in worker\n const dataUrl = await workerPool.execute((worker) =>\n encodeCanvasInWorker(worker, sourceCanvas, preserveAlpha),\n );\n\n return { canvas, dataUrl, preserveAlpha };\n } catch (error) {\n // Fallback to main thread if worker encoding fails\n const encoded = encodeCanvasOnMainThread(canvas, canvasScale);\n if (encoded) {\n return { canvas, ...encoded };\n }\n \n // Cross-origin canvas or other error - skip\n return null;\n }\n });\n\n const encodedResults = await Promise.all(encodingTasks);\n const validResults = encodedResults.filter(\n (r): r is { canvas: HTMLCanvasElement; dataUrl: string; preserveAlpha: boolean } => r !== null,\n );\n return validResults;\n}\n\n/**\n * Fast base64 encoding directly from Uint8Array.\n * Avoids the overhead of converting to binary string first.\n * Uses lookup table for optimal performance.\n */\nfunction encodeBase64Fast(bytes: Uint8Array): string {\n const base64Chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n let result = \"\";\n let i = 0;\n const len = bytes.length;\n \n // Process 3 bytes at a time (produces 4 base64 chars)\n while (i < len - 2) {\n const byte1 = bytes[i++]!;\n const byte2 = bytes[i++]!;\n const byte3 = bytes[i++]!;\n \n const bitmap = (byte1 << 16) | (byte2 << 8) | byte3;\n \n result += base64Chars.charAt((bitmap >> 18) & 63);\n result += base64Chars.charAt((bitmap >> 12) & 63);\n result += base64Chars.charAt((bitmap >> 6) & 63);\n result += base64Chars.charAt(bitmap & 63);\n }\n \n // Handle remaining bytes (1 or 2)\n if (i < len) {\n const byte1 = bytes[i++]!;\n const bitmap = byte1 << 16;\n \n result += base64Chars.charAt((bitmap >> 18) & 63);\n result += base64Chars.charAt((bitmap >> 12) & 63);\n \n if (i < len) {\n const byte2 = bytes[i++]!;\n const bitmap2 = (byte1 << 16) | (byte2 << 8);\n result += base64Chars.charAt((bitmap2 >> 6) & 63);\n result += \"=\";\n } else {\n result += \"==\";\n }\n }\n \n return result;\n}\n\n/**\n * Reset all module state including profiling counters, caches, and logging flags.\n * Call at the start of export sessions to ensure clean state.\n */\nexport function resetRenderState(): void {\n defaultProfiler.reset();\n _inlineImageCache.clear();\n}\n\n/**\n * Clear the inline image cache. Useful for memory management in long-running sessions.\n */\nexport function clearInlineImageCache(): void {\n _inlineImageCache.clear();\n}\n\n/**\n * Get current inline image cache size for diagnostics.\n */\nexport function getInlineImageCacheSize(): number {\n return _inlineImageCache.size;\n}\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\n/**\n * Options for creating a DPR-aware canvas.\n */\ninterface CanvasOptions {\n /** Render width (internal resolution) */\n renderWidth: number;\n /** Render height (internal resolution) */\n renderHeight: number;\n /** Display scale factor */\n scale: number;\n /** Device pixel ratio (defaults to window.devicePixelRatio) */\n dpr?: number;\n /** Full logical width (for CSS sizing) */\n fullWidth: number;\n /** Full logical height (for CSS sizing) */\n fullHeight: number;\n}\n\n/**\n * Create a canvas element with proper DPR handling.\n * Buffer size is based on renderWidth/renderHeight (internal resolution).\n * CSS size is based on fullWidth/fullHeight (logical display size).\n */\nfunction createDprCanvas(options: CanvasOptions): HTMLCanvasElement {\n const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;\n const dpr = options.dpr ?? window.devicePixelRatio ?? 1;\n \n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(renderWidth * scale * dpr);\n canvas.height = Math.floor(renderHeight * scale * dpr);\n canvas.style.width = `${Math.floor(fullWidth * scale)}px`;\n canvas.style.height = `${Math.floor(fullHeight * scale)}px`;\n \n return canvas;\n}\n\n/**\n * Create a debug label for showing render info.\n */\nfunction createDebugLabel(): HTMLDivElement {\n const debugLabel = document.createElement(\"div\");\n debugLabel.style.cssText = `\n position: absolute;\n top: -24px;\n left: 0;\n padding: 2px 8px;\n font: bold 12px monospace;\n background: rgba(0, 0, 0, 0.8);\n border-radius: 3px;\n white-space: nowrap;\n z-index: 1000;\n pointer-events: none;\n `;\n return debugLabel;\n}\n\n/**\n * Update debug label with resolution info.\n */\nfunction updateDebugLabel(\n label: HTMLDivElement,\n renderWidth: number,\n renderHeight: number,\n resolutionScale: number,\n): void {\n const scaleColors: Record<number, string> = {\n 1: \"#00ff00\",\n 0.75: \"#ffff00\",\n 0.5: \"#ff8800\",\n 0.25: \"#ff0000\",\n };\n label.style.color = scaleColors[resolutionScale] || \"#ffffff\";\n label.textContent = `Render: ${renderWidth}x${renderHeight} (${Math.round(resolutionScale * 100)}%)`;\n}\n\n/**\n * Information needed to restore canvases after serialization.\n */\ninterface CanvasRestoreInfo {\n canvas: HTMLCanvasElement;\n parent: Node;\n nextSibling: Node | null;\n img: HTMLImageElement;\n}\n\n/**\n * Options for SVG serialization.\n */\ninterface SerializeToSvgOptions {\n /** Scale factor for encoding canvases (default: 1) */\n canvasScale?: number;\n /** Whether to inline external images (default: false for cloned containers) */\n inlineImages?: boolean;\n /** Whether to log early render info (default: false) */\n logEarlyRenders?: boolean;\n}\n\n/**\n * Result of SVG serialization.\n */\ninterface SerializationResult {\n dataUri: string;\n /** Call this to restore canvases if they were modified in-place */\n restore: () => void;\n}\n\n/**\n * Common SVG foreignObject serialization pipeline.\n * Handles canvas encoding, serialization, and base64 encoding.\n * \n * @param container - The HTML element to serialize\n * @param width - Output width\n * @param height - Output height\n * @param options - Serialization options\n * @returns Serialization result with data URI and restore function\n */\nasync function serializeToSvgDataUri(\n container: HTMLElement,\n width: number,\n height: number,\n options: SerializeToSvgOptions = {},\n): Promise<SerializationResult> {\n const { canvasScale = 1, inlineImages: shouldInlineImages = false, logEarlyRenders = false } = options;\n \n // Store info for restoration (only used if modifying in-place)\n const canvasRestoreInfo: CanvasRestoreInfo[] = [];\n \n // Phase 1: Encode canvases to data URLs (parallel)\n const canvasStart = performance.now();\n const canvases = Array.from(container.querySelectorAll(\"canvas\"));\n const encodedResults = await encodeCanvasesInParallel(canvases, canvasScale);\n \n // Replace canvases with images\n for (const { canvas, dataUrl } of encodedResults) {\n try {\n const img = document.createElement(\"img\");\n img.src = dataUrl;\n img.width = canvas.width;\n img.height = canvas.height;\n const style = canvas.getAttribute(\"style\");\n if (style) img.setAttribute(\"style\", style);\n \n const parent = canvas.parentNode;\n if (parent) {\n const nextSibling = canvas.nextSibling;\n parent.replaceChild(img, canvas);\n canvasRestoreInfo.push({ canvas, parent, nextSibling, img });\n }\n } catch {\n // Cross-origin canvas - leave as-is\n }\n }\n defaultProfiler.addTime(\"canvasEncode\", performance.now() - canvasStart);\n \n // Phase 2: Inline external images (if requested)\n if (shouldInlineImages) {\n const inlineStart = performance.now();\n await inlineImages(container);\n defaultProfiler.addTime(\"inline\", performance.now() - inlineStart);\n }\n \n // Phase 3: Serialize to XHTML\n const serializeStart = performance.now();\n const wrapper = document.createElement(\"div\");\n wrapper.setAttribute(\"xmlns\", \"http://www.w3.org/1999/xhtml\");\n wrapper.setAttribute(\"style\", `width:${width}px;height:${height}px;overflow:hidden;position:relative;`);\n wrapper.appendChild(container);\n \n if (!_xmlSerializer) {\n _xmlSerializer = new XMLSerializer();\n }\n const serialized = _xmlSerializer.serializeToString(wrapper);\n defaultProfiler.addTime(\"serialize\", performance.now() - serializeStart);\n \n // Prepare restore function (removes container from wrapper, restores canvases)\n const restore = (): void => {\n const restoreStart = performance.now();\n wrapper.removeChild(container);\n \n for (const { canvas, parent, nextSibling, img } of canvasRestoreInfo) {\n if (img.parentNode === parent) {\n if (nextSibling) {\n parent.insertBefore(canvas, nextSibling);\n parent.removeChild(img);\n } else {\n parent.replaceChild(canvas, img);\n }\n }\n }\n defaultProfiler.addTime(\"restore\", performance.now() - restoreStart);\n };\n \n // DEBUG: Log serialized HTML size for early renders\n if (logEarlyRenders && defaultProfiler.isEarlyRender(2)) {\n console.log(`[serializeToSvgDataUri] FO serialized: ${serialized.length} chars`);\n }\n \n // Phase 4: Create SVG and encode to base64\n const base64Start = performance.now();\n const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\"><foreignObject width=\"100%\" height=\"100%\">${serialized}</foreignObject></svg>`;\n \n if (!_textEncoder) {\n _textEncoder = new TextEncoder();\n }\n const utf8Bytes = _textEncoder.encode(svg);\n \n let base64: string;\n if (typeof (Uint8Array.prototype as any).toBase64 === \"function\") {\n base64 = (utf8Bytes as any).toBase64();\n } else {\n base64 = encodeBase64Fast(utf8Bytes);\n }\n const dataUri = `data:image/svg+xml;base64,${base64}`;\n defaultProfiler.addTime(\"base64\", performance.now() - base64Start);\n \n return { dataUri, restore };\n}\n\n/**\n * Inline all images in a container as base64 data URIs.\n * SVG foreignObject can't load external images due to security restrictions.\n * Uses an LRU-style cache with size limits to prevent memory leaks.\n */\nasync function inlineImages(container: HTMLElement): Promise<void> {\n const images = container.querySelectorAll(\"img\");\n for (const image of images) {\n const src = image.getAttribute(\"src\");\n if (!src || src.startsWith(\"data:\")) continue;\n\n const cached = _inlineImageCache.get(src);\n if (cached) {\n image.setAttribute(\"src\", cached);\n continue;\n }\n\n try {\n const response = await fetch(src);\n const blob = await response.blob();\n const dataUrl = await blobToDataURL(blob);\n image.setAttribute(\"src\", dataUrl);\n \n // Evict oldest entries if cache is full (simple FIFO eviction)\n if (_inlineImageCache.size >= MAX_INLINE_IMAGE_CACHE_SIZE) {\n const firstKey = _inlineImageCache.keys().next().value;\n if (firstKey) _inlineImageCache.delete(firstKey);\n }\n _inlineImageCache.set(src, dataUrl);\n } catch (e) {\n console.warn(\"Failed to inline image:\", src, e);\n }\n }\n}\n\n\n/**\n * Convert a Blob to a data URL.\n */\nfunction blobToDataURL(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise(resolve => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Wait for multiple animation frames to ensure all paints are flushed.\n * This is necessary because video frame decoding and canvas painting may\n * happen asynchronously even after seek() returns.\n */\nfunction waitForPaintFlush(): Promise<void> {\n return new Promise(resolve => {\n // Double RAF ensures we wait for:\n // 1. First RAF: Any pending paints are scheduled\n // 2. Second RAF: Those paints have completed\n requestAnimationFrame(() => {\n requestAnimationFrame(() => resolve());\n });\n });\n}\n\n/**\n * Check if a canvas has any rendered content (not all transparent/uninitialized).\n * Returns true if there's ANY non-transparent pixel.\n */\nfunction canvasHasContent(canvas: HTMLCanvasElement): boolean {\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return false;\n \n try {\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return false;\n \n // Sample a horizontal strip across the middle of the canvas\n // This catches most video content even if edges are black\n const stripY = Math.floor(height / 2);\n const imageData = ctx.getImageData(0, stripY, width, CANVAS_SAMPLE_STRIP_HEIGHT);\n const data = imageData.data;\n \n // Check if ANY pixel has non-zero alpha (is not transparent)\n // A truly blank/uninitialized canvas has all pixels at [0,0,0,0]\n // A black video frame would have pixels at [0,0,0,255] (opaque black)\n for (let i = 3; i < data.length; i += 4) {\n if (data[i] !== 0) {\n return true;\n }\n }\n \n return false;\n } catch {\n // Canvas might be tainted, assume it has content\n return true;\n }\n}\n\ninterface WaitForVideoContentResult {\n ready: boolean;\n blankVideos: string[];\n}\n\n/**\n * Wait for video canvases within a timegroup to have content.\n * Only checks videos that should be visible at the current time.\n * Returns result with ready status and list of blank video names.\n */\nasync function waitForVideoContent(\n timegroup: EFTimegroup,\n timeMs: number,\n maxWaitMs: number,\n): Promise<WaitForVideoContentResult> {\n const startTime = performance.now();\n \n // Find all video elements in the timegroup (including nested)\n const allVideos = timegroup.querySelectorAll(\"ef-video\");\n if (allVideos.length === 0) return { ready: true, blankVideos: [] };\n \n // Filter to only videos that should be visible at this time\n const visibleVideos = Array.from(allVideos).filter(video => {\n // Check if video itself is in time range\n if (!isVisibleAtTime(video, timeMs)) return false;\n \n // Check if all ancestor timegroups are in time range\n let parent = video.parentElement;\n while (parent && parent !== timegroup) {\n if (parent.tagName === 'EF-TIMEGROUP' && !isVisibleAtTime(parent, timeMs)) {\n return false;\n }\n parent = parent.parentElement;\n }\n return true;\n });\n \n if (visibleVideos.length === 0) return { ready: true, blankVideos: [] };\n \n const getBlankVideoNames = () => visibleVideos\n .filter(video => {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n return shadowCanvas && !canvasHasContent(shadowCanvas);\n })\n .map(v => (v as TemporalElement).src || v.id || 'unnamed');\n \n while (performance.now() - startTime < maxWaitMs) {\n let allHaveContent = true;\n \n for (const video of visibleVideos) {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n if (shadowCanvas && shadowCanvas.width > 0 && shadowCanvas.height > 0) {\n if (!canvasHasContent(shadowCanvas)) {\n allHaveContent = false;\n break;\n }\n }\n }\n \n if (allHaveContent) return { ready: true, blankVideos: [] };\n \n // Wait a bit and check again\n await waitForFrame();\n }\n \n return { ready: false, blankVideos: getBlankVideoNames() };\n}\n\n/**\n * Options for native rendering.\n */\nexport interface NativeRenderOptions {\n /**\n * Wait for RAF before capturing. Only needed if content hasn't been laid out yet.\n * Default: false (capture immediately - frame tasks should already be complete)\n * \n * Set to true only for edge cases where you're rendering content that was just\n * added to the DOM and hasn't had a chance to layout yet.\n */\n waitForPaint?: boolean;\n \n /**\n * Reuse an existing canvas instead of creating a new one.\n * The canvas must have layoutsubtree enabled and be in the DOM.\n */\n reuseCanvas?: HTMLCanvasElement;\n \n /**\n * Skip device pixel ratio scaling. When true, renders at 1x regardless of display DPR.\n * Default: false (respects display DPR for crisp rendering)\n * \n * Set to true for video export where retina resolution isn't needed.\n * This can provide a 4x speedup on 2x DPR displays!\n */\n skipDprScaling?: boolean;\n}\n\n/**\n * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).\n * This is much faster than the foreignObject approach and avoids canvas tainting.\n * \n * Note: The native API renders at device pixel ratio, so we capture at DPR scale\n * and then downsample to logical pixels to match the foreignObject path's output.\n * \n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options (skipWait for batch mode)\n * \n * @see https://github.com/WICG/html-in-canvas\n */\nexport async function renderToImageNative(\n container: HTMLElement,\n width: number,\n height: number,\n options: NativeRenderOptions = {},\n): Promise<HTMLCanvasElement> {\n const t0 = performance.now();\n const { waitForPaint = false, reuseCanvas, skipDprScaling = false } = options;\n // Use 1x DPR when skipDprScaling is true (for video export) - 4x fewer pixels!\n const dpr = skipDprScaling ? 1 : (window.devicePixelRatio || 1);\n \n // Use provided canvas or create new one\n let captureCanvas: HTMLCanvasElement;\n let shouldCleanup = false;\n \n if (reuseCanvas) {\n captureCanvas = reuseCanvas;\n \n // Ensure canvas dimensions match (both attribute and CSS)\n const dpr = skipDprScaling ? 1 : (window.devicePixelRatio || 1);\n const targetWidth = Math.floor(width * dpr);\n const targetHeight = Math.floor(height * dpr);\n \n // Set attribute dimensions (pixel buffer size)\n if (captureCanvas.width !== targetWidth) {\n captureCanvas.width = targetWidth;\n }\n if (captureCanvas.height !== targetHeight) {\n captureCanvas.height = targetHeight;\n }\n \n // Ensure CSS dimensions match logical size (required for layoutsubtree)\n captureCanvas.style.width = `${width}px`;\n captureCanvas.style.height = `${height}px`;\n \n // Ensure layoutsubtree is set (required for drawElementImage)\n if (!captureCanvas.hasAttribute(\"layoutsubtree\")) {\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n }\n \n // Ensure canvas is in DOM (required for drawElementImage layout)\n if (!captureCanvas.parentNode) {\n document.body.appendChild(captureCanvas);\n }\n \n // Ensure container is child of canvas\n if (container.parentElement !== captureCanvas) {\n captureCanvas.appendChild(container);\n }\n \n // Ensure container is visible (not display: none) for layout\n // drawElementImage requires the element to be laid out\n const containerStyle = getComputedStyle(container);\n if (containerStyle.display === 'none') {\n container.style.display = 'block';\n }\n \n // Force synchronous layout by reading layout properties\n // This ensures both canvas and container are laid out (required for drawElementImage)\n // Reading offsetHeight forces a synchronous layout recalculation\n void captureCanvas.offsetHeight;\n void container.offsetHeight;\n getComputedStyle(captureCanvas).opacity;\n getComputedStyle(container).opacity;\n } else {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.width = Math.floor(width * dpr);\n captureCanvas.height = Math.floor(height * dpr);\n \n // Enable HTML-in-Canvas mode via layoutsubtree attribute/property\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n \n captureCanvas.appendChild(container);\n \n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n document.body.appendChild(captureCanvas);\n shouldCleanup = true;\n }\n \n const t1 = performance.now();\n defaultProfiler.addTime(\"setup\", t1 - t0);\n \n try {\n // Force style calculation to ensure CSS is computed before capture\n // This ensures both canvas and container are laid out (required for drawElementImage)\n getComputedStyle(container).opacity;\n \n // When reusing canvas with layoutsubtree, wait for initial layout (first use only)\n // Use a WeakSet to track canvases that have been initialized\n if (reuseCanvas && (captureCanvas as any).layoutSubtree && !_layoutInitializedCanvases.has(captureCanvas)) {\n await waitForFrame();\n _layoutInitializedCanvases.add(captureCanvas);\n \n // Canvas may have been detached during async wait (e.g., test cleanup)\n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n \n // Only wait for paint in rare edge cases where content was just added to DOM\n if (waitForPaint) {\n await waitForPaintFlush();\n \n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n \n const ctx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n ctx.drawElementImage(container, 0, 0);\n } finally {\n // Only clean up if we created the canvas\n if (shouldCleanup && captureCanvas.parentNode) {\n captureCanvas.parentNode.removeChild(captureCanvas);\n }\n }\n \n const t2 = performance.now();\n defaultProfiler.addTime(\"draw\", t2 - t1);\n \n // If DPR is 1, no downsampling needed - return as-is\n if (dpr === 1) {\n defaultProfiler.incrementRenderCount();\n return captureCanvas;\n }\n \n // Downsample to logical pixel dimensions to match foreignObject path output\n // This ensures consistent behavior regardless of which rendering path is used\n const outputCanvas = document.createElement(\"canvas\");\n outputCanvas.width = width;\n outputCanvas.height = height;\n \n const outputCtx = outputCanvas.getContext(\"2d\")!;\n // Draw the DPR-scaled capture onto the 1x output canvas\n outputCtx.drawImage(\n captureCanvas,\n 0, 0, captureCanvas.width, captureCanvas.height, // source (full DPR capture)\n 0, 0, width, height // destination (logical pixels)\n );\n \n const t3 = performance.now();\n defaultProfiler.addTime(\"downsample\", t3 - t2);\n defaultProfiler.incrementRenderCount();\n \n // Log timing periodically\n defaultProfiler.shouldLogByTime(PROFILING_LOG_INTERVAL_MS);\n \n return outputCanvas;\n}\n\n/**\n * Options for foreignObject rendering path.\n */\nexport interface ForeignObjectRenderOptions extends NativeRenderOptions {\n /**\n * Scale factor for encoding internal canvases.\n * When set, canvases are scaled down before encoding to data URLs,\n * dramatically reducing encoding time for thumbnails.\n * Default: 1 (no scaling - encode at full resolution)\n */\n canvasScale?: number;\n}\n\n/**\n * Render HTML content to an image (or canvas) for drawing.\n * \n * Supports two rendering modes (configurable via previewSettings):\n * - \"native\": Chrome's experimental drawElementImage API (fastest when available)\n * - \"foreignObject\": SVG foreignObject serialization (fallback, works everywhere)\n * \n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options\n * @returns HTMLCanvasElement when using native, HTMLImageElement when using foreignObject\n */\nexport async function renderToImage(\n container: HTMLElement,\n width: number,\n height: number,\n options?: ForeignObjectRenderOptions,\n): Promise<HTMLImageElement | HTMLCanvasElement> {\n const renderMode = getEffectiveRenderMode();\n \n // Native HTML-in-Canvas API path (fastest, requires Chrome flag)\n if (renderMode === \"native\") {\n return renderToImageNative(container, width, height, options);\n }\n \n // Fallback: SVG foreignObject serialization\n // Clone the container first (don't modify original)\n // Note: cloneNode doesn't copy canvas pixels, so we encode from original canvases\n const originalCanvases = Array.from(container.querySelectorAll(\"canvas\"));\n const clone = container.cloneNode(true) as HTMLElement;\n const clonedCanvases = clone.querySelectorAll(\"canvas\");\n \n // Encode original canvases and map to cloned elements\n const canvasScale = options?.canvasScale ?? 1;\n const canvasStart = performance.now();\n const encodedResults = await encodeCanvasesInParallel(originalCanvases, canvasScale);\n \n for (let i = 0; i < originalCanvases.length; i++) {\n const srcCanvas = originalCanvases[i];\n const dstCanvas = clonedCanvases[i];\n const encoded = encodedResults.find((r) => r.canvas === srcCanvas);\n \n if (!srcCanvas || !dstCanvas || !encoded) continue;\n \n try {\n const img = document.createElement(\"img\");\n img.src = encoded.dataUrl;\n img.width = srcCanvas.width;\n img.height = srcCanvas.height;\n const style = dstCanvas.getAttribute(\"style\");\n if (style) img.setAttribute(\"style\", style);\n dstCanvas.parentNode?.replaceChild(img, dstCanvas);\n } catch {\n // Cross-origin or other error - skip\n }\n }\n defaultProfiler.addTime(\"canvasEncode\", performance.now() - canvasStart);\n\n // Inline external images in the clone\n const inlineStart = performance.now();\n await inlineImages(clone);\n defaultProfiler.addTime(\"inline\", performance.now() - inlineStart);\n\n // Use common serialization pipeline (no restore needed since we're working on a clone)\n const { dataUri } = await serializeToSvgDataUri(clone, width, height);\n \n // Load as image\n return loadImageFromDataUri(dataUri);\n}\n\n\n/**\n * Render a pre-built clone container to an image WITHOUT cloning it again.\n * This is the fast path for reusing clone structures across frames.\n * \n * Key difference from renderToImage:\n * - Does NOT call cloneNode (avoids expensive DOM duplication)\n * - Converts canvases to images in-place, then restores them after serialization\n * - Assumes the container already has refreshed canvas content\n * \n * @param container - Pre-built clone container with refreshed canvas content\n * @param width - Output width\n * @param height - Output height\n * @returns Promise resolving to an HTMLImageElement\n */\nexport async function renderToImageDirect(\n container: HTMLElement,\n width: number,\n height: number,\n): Promise<HTMLImageElement> {\n defaultProfiler.incrementRenderCount();\n \n // Use common serialization pipeline (modifies in-place, restores after)\n const { dataUri, restore } = await serializeToSvgDataUri(container, width, height, {\n inlineImages: true,\n logEarlyRenders: true,\n });\n restore();\n \n // Load as image\n const image = await loadImageFromDataUri(dataUri);\n \n // Log timing breakdown periodically\n defaultProfiler.shouldLogByFrameCount(100);\n \n return image;\n}\n\n/**\n * Prepare a frame's data URI without waiting for image load.\n * Returns the data URI asynchronously (after parallel canvas encoding and serialization) for pipelined loading.\n * The DOM is restored before this function returns.\n */\nexport async function prepareFrameDataUri(\n container: HTMLElement,\n width: number,\n height: number,\n): Promise<string> {\n defaultProfiler.incrementRenderCount();\n \n // Use common serialization pipeline (modifies in-place, restores after)\n const { dataUri, restore } = await serializeToSvgDataUri(container, width, height);\n restore();\n \n return dataUri;\n}\n\n/**\n * Load an image from a data URI. Returns a Promise that resolves when loaded.\n */\nexport function loadImageFromDataUri(dataUri: string): Promise<HTMLImageElement> {\n const img = new Image();\n const imageLoadStart = performance.now();\n \n return new Promise<HTMLImageElement>((resolve, reject) => {\n img.onload = () => {\n defaultProfiler.addTime(\"imageLoad\", performance.now() - imageLoadStart);\n resolve(img);\n };\n img.onerror = reject;\n img.src = dataUri;\n });\n}\n\n/**\n * Options for capturing from an existing render clone.\n */\nexport interface CaptureFromCloneOptions {\n /** Scale factor for the output canvas (default: 0.25) */\n scale?: number;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n /** Original timegroup (for dimension and background reference) */\n originalTimegroup?: EFTimegroup;\n}\n\n/**\n * Captures a frame from an already-seeked render clone.\n * Used internally by captureBatch for efficiency (reuses one clone across all captures).\n * \n * @param renderClone - A render clone that has already been seeked to the target time\n * @param renderContainer - The container holding the render clone (from createRenderClone)\n * @param options - Capture options\n * @returns Canvas with the rendered frame\n */\nexport async function captureFromClone(\n renderClone: EFTimegroup,\n renderContainer: HTMLElement,\n options: CaptureFromCloneOptions = {},\n): Promise<HTMLCanvasElement> {\n const {\n scale = DEFAULT_THUMBNAIL_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n originalTimegroup,\n } = options;\n\n // Use original timegroup dimensions if available, otherwise clone dimensions\n const sourceForDimensions = originalTimegroup ?? renderClone;\n const width = sourceForDimensions.offsetWidth || DEFAULT_WIDTH;\n const height = sourceForDimensions.offsetHeight || DEFAULT_HEIGHT;\n\n // Create canvas at scaled size\n const dpr = window.devicePixelRatio || 1;\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(width * scale * dpr);\n canvas.height = Math.floor(height * scale * dpr);\n canvas.style.width = `${Math.floor(width * scale)}px`;\n canvas.style.height = `${Math.floor(height * scale)}px`;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Handle content readiness based on mode\n const timeMs = renderClone.currentTimeMs;\n if (contentReadyMode === \"blocking\") {\n const result = await waitForVideoContent(renderClone, timeMs, blockingTimeoutMs);\n if (!result.ready) {\n throw new ContentNotReadyError(timeMs, blockingTimeoutMs, result.blankVideos);\n }\n }\n\n let image: HTMLCanvasElement | HTMLImageElement;\n const renderMode = getEffectiveRenderMode();\n \n if (renderMode === \"native\") {\n // NATIVE PATH: Render the seeked renderClone directly from live DOM\n // The clone is already at the correct time, so drawElementImage captures its current\n // visual state including video frames at the correct position.\n // \n // Position render container properly for capture\n renderContainer.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n pointer-events: none;\n overflow: hidden;\n `;\n \n // OPTIMIZATION: Skip DPR scaling for thumbnails - retina quality isn't needed\n // and DPR=2 means 4x more pixels to render.\n const skipDpr = scale < 1;\n image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: skipDpr });\n } else {\n // FOREIGNOBJECT PATH: Build passive structure from the SEEKED render clone\n // The clone is already at the correct time, so getComputedStyle captures the right values.\n // Styles are synced during clone building in a single pass.\n const t0 = performance.now();\n const { container, syncState } = buildCloneStructure(renderClone, timeMs);\n const buildTime = performance.now() - t0;\n\n // Create wrapper using shared helper\n const bgSource = originalTimegroup ?? renderClone;\n const previewContainer = createPreviewContainer({\n width,\n height,\n background: getComputedStyle(bgSource).background || \"#000\",\n });\n \n const t1 = performance.now();\n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectDocumentStyles();\n const stylesTime = performance.now() - t1;\n previewContainer.appendChild(styleEl);\n previewContainer.appendChild(container);\n \n // Ensure clone root is visible\n overrideRootCloneStyles(syncState, true);\n\n // Render using foreignObject serialization\n // Pass scale so canvases are encoded at thumbnail size (MUCH faster)\n const t2 = performance.now();\n image = await renderToImage(previewContainer, width, height, { canvasScale: scale });\n const renderTime = performance.now() - t2;\n \n console.log(`[captureFromClone] build=${buildTime.toFixed(0)}ms, styles=${stylesTime.toFixed(0)}ms, render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`);\n }\n\n // Draw to canvas (may need scaling for native path which is at DPR)\n const srcWidth = image.width;\n const srcHeight = image.height;\n ctx.drawImage(\n image,\n 0, 0, srcWidth, srcHeight,\n 0, 0, canvas.width, canvas.height\n );\n\n return canvas;\n}\n\n/**\n * Captures a single frame from a timegroup at a specific time.\n * \n * CLONE-TIMELINE ARCHITECTURE:\n * Creates an independent render clone, seeks it to the target time, and captures.\n * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.\n * \n * @param timegroup - The source timegroup\n * @param options - Capture options including timeMs, scale, contentReadyMode\n * @returns Canvas with the rendered frame\n * @throws ContentNotReadyError if blocking mode times out waiting for video content\n */\nexport async function captureTimegroupAtTime(\n timegroup: EFTimegroup,\n options: CaptureOptions,\n): Promise<HTMLCanvasElement> {\n const {\n timeMs,\n scale = DEFAULT_THUMBNAIL_SCALE,\n // skipRestore is deprecated with Clone-timeline (Prime is never seeked)\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n } = options;\n\n // CLONE-TIMELINE: Create a short-lived render clone for this capture\n // Prime-timeline is NEVER seeked - clone is fully independent\n const { clone: renderClone, container: renderContainer, cleanup: cleanupRenderClone } = \n await timegroup.createRenderClone();\n \n try {\n // Seek the clone to target time (Prime stays at user position)\n // Use seekForRender which bypasses duration clamping - render clones may have\n // zero duration initially until media durations are computed, but we still\n // want to seek to the requested time for capture purposes.\n await renderClone.seekForRender(timeMs);\n \n // Use the shared capture helper\n return await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n });\n } finally {\n // Clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/** Epsilon for comparing time values (ms) - times within this are considered equal */\nconst TIME_EPSILON_MS = 1;\n\n/** Default scale for preview rendering */\nconst DEFAULT_PREVIEW_SCALE = 1;\n\n/** Default resolution scale (full resolution) */\nconst DEFAULT_RESOLUTION_SCALE = 1;\n\n/**\n * Convert relative time to absolute time for a timegroup.\n * Nested timegroup children have ABSOLUTE startTimeMs values,\n * so relative capture times must be converted for temporal culling.\n */\nfunction toAbsoluteTime(timegroup: EFTimegroup, relativeTimeMs: number): number {\n return relativeTimeMs + (timegroup.startTimeMs ?? 0);\n}\n\nexport interface CanvasPreviewResult {\n /**\n * Wrapper container holding the canvas and debug label.\n * Append this to your DOM - the canvas inside will receive transforms.\n */\n container: HTMLDivElement;\n canvas: HTMLCanvasElement;\n /**\n * Call this to re-render the timegroup to canvas at current visual state.\n * Returns a promise that resolves when rendering is complete.\n */\n refresh: () => Promise<void>;\n syncState: SyncState;\n /**\n * Dynamically change the resolution scale without rebuilding the clone structure.\n * This is nearly instant - just updates CSS and internal variables.\n * The next refresh() call will render at the new resolution.\n */\n setResolutionScale: (scale: number) => void;\n /**\n * Get the current resolution scale.\n */\n getResolutionScale: () => number;\n}\n\n/**\n * Options for canvas preview rendering.\n */\nexport interface CanvasPreviewOptions {\n /**\n * Output scale factor (default: 1).\n * Scales the final canvas size.\n */\n scale?: number;\n \n /**\n * Resolution scale for internal rendering (default: 1).\n * Reduces the internal render resolution for better performance.\n * The canvas CSS size remains the same (browser upscales).\n * - 1: Full resolution\n * - 0.75: 3/4 resolution\n * - 0.5: Half resolution\n * - 0.25: Quarter resolution\n */\n resolutionScale?: number;\n}\n\n/**\n * Renders a timegroup preview to a canvas using SVG foreignObject.\n * \n * Optimized with:\n * - Persistent clone structure (built once)\n * - Temporal bucketing for time-based culling\n * - Property split (static vs animated)\n * - Parent index for O(1) visibility checks\n * - Resolution scaling for performance (renders at lower resolution, CSS upscales)\n *\n * @param timegroup - The source timegroup to preview\n * @param scaleOrOptions - Scale factor (default 1) or options object\n * @returns Object with canvas and refresh function\n */\nexport function renderTimegroupToCanvas(\n timegroup: EFTimegroup,\n scaleOrOptions: number | CanvasPreviewOptions = DEFAULT_PREVIEW_SCALE,\n): CanvasPreviewResult {\n // Normalize options\n const options: CanvasPreviewOptions = typeof scaleOrOptions === \"number\"\n ? { scale: scaleOrOptions }\n : scaleOrOptions;\n \n const scale = options.scale ?? DEFAULT_PREVIEW_SCALE;\n // These are mutable to support dynamic resolution changes\n let currentResolutionScale = options.resolutionScale ?? DEFAULT_RESOLUTION_SCALE;\n \n const width = timegroup.offsetWidth || DEFAULT_WIDTH;\n const height = timegroup.offsetHeight || DEFAULT_HEIGHT;\n const dpr = window.devicePixelRatio || 1;\n \n // Calculate effective render dimensions (internal resolution) - mutable\n let renderWidth = Math.floor(width * currentResolutionScale);\n let renderHeight = Math.floor(height * currentResolutionScale);\n\n // Create canvas with proper DPR handling\n const canvas = createDprCanvas({\n renderWidth,\n renderHeight,\n scale,\n fullWidth: width,\n fullHeight: height,\n dpr,\n });\n \n // Create wrapper container with debug label\n const wrapperContainer = document.createElement(\"div\");\n wrapperContainer.style.cssText = \"position: relative; display: inline-block;\";\n const debugLabel = createDebugLabel();\n wrapperContainer.appendChild(debugLabel);\n wrapperContainer.appendChild(canvas);\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Build clone structure ONCE with optimized sync state\n // Initial sync happens during clone building in a single pass\n const initialTimeMs = toAbsoluteTime(timegroup, timegroup.currentTimeMs ?? 0);\n const { container, syncState } = buildCloneStructure(timegroup, initialTimeMs);\n\n // Create a wrapper div with scaled dimensions\n // When resolutionScale < 1, we render at a smaller size and CSS transform scales the content\n const previewContainer = createPreviewContainer({\n width: renderWidth,\n height: renderHeight,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\n \n // Apply CSS transform to scale down the content within the container\n // This makes the clone render at reduced complexity\n if (currentResolutionScale < 1) {\n container.style.transform = `scale(${currentResolutionScale})`;\n container.style.transformOrigin = \"top left\";\n }\n \n // Inject document styles so CSS rules work in SVG foreignObject\n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectDocumentStyles();\n previewContainer.appendChild(styleEl);\n \n previewContainer.appendChild(container);\n overrideRootCloneStyles(syncState);\n\n // Track render state\n let rendering = false;\n let lastTimeMs = -1;\n\n // Log resolution scale on first render for debugging\n let hasLoggedScale = false;\n \n // Pending resolution change - applied at start of next refresh to avoid blanking\n let pendingResolutionScale: number | null = null;\n \n /**\n * Apply pending resolution scale changes.\n * Called at the start of refresh() before rendering, so the old content\n * stays visible until new content is ready to be drawn.\n */\n const applyPendingResolutionChange = (): void => {\n if (pendingResolutionScale === null) return;\n \n const newScale = pendingResolutionScale;\n pendingResolutionScale = null;\n \n currentResolutionScale = newScale;\n renderWidth = Math.floor(width * currentResolutionScale);\n renderHeight = Math.floor(height * currentResolutionScale);\n \n // Update previewContainer dimensions (affects what renderToImage produces)\n previewContainer.style.width = `${renderWidth}px`;\n previewContainer.style.height = `${renderHeight}px`;\n \n // Update clone transform\n if (currentResolutionScale < 1) {\n container.style.transform = `scale(${currentResolutionScale})`;\n container.style.transformOrigin = \"top left\";\n } else {\n container.style.transform = \"\";\n }\n \n // Canvas dimensions will be updated right before drawing (in refresh)\n // to avoid clearing the canvas until new content is ready\n };\n \n /**\n * Dynamically change resolution scale without rebuilding clone structure.\n * The actual change is deferred until next refresh() to avoid blanking -\n * old content stays visible until new content is ready.\n */\n const setResolutionScale = (newScale: number): void => {\n // Clamp to valid range\n newScale = Math.max(0.1, Math.min(1, newScale));\n \n if (newScale === currentResolutionScale && pendingResolutionScale === null) return;\n \n // Queue the change - will be applied at start of next refresh\n pendingResolutionScale = newScale;\n \n // Force re-render on next refresh by invalidating lastTimeMs\n lastTimeMs = -1;\n };\n \n const getResolutionScale = (): number => pendingResolutionScale ?? currentResolutionScale;\n \n const refresh = async (): Promise<void> => {\n if (rendering) return;\n // Clone-timeline: captures use separate clones, Prime-timeline is never locked\n \n const sourceTimeMs = timegroup.currentTimeMs ?? 0;\n const userTimeMs = timegroup.userTimeMs ?? 0;\n if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;\n \n if (userTimeMs === lastTimeMs) return;\n lastTimeMs = userTimeMs;\n \n rendering = true;\n \n // Apply any pending resolution changes before rendering\n // This updates previewContainer and clone transform, but NOT canvas dimensions yet\n applyPendingResolutionChange();\n \n // Log scale info once per initialization\n if (!hasLoggedScale) {\n hasLoggedScale = true;\n const mode = getEffectiveRenderMode();\n console.log(`[renderTimegroupToCanvas] Resolution scale: ${currentResolutionScale} (${width}x${height} → ${renderWidth}x${renderHeight}), canvas buffer: ${canvas.width}x${canvas.height}, CSS size: ${canvas.style.width}x${canvas.style.height}, renderMode: ${mode}`);\n }\n\n try {\n syncStyles(syncState, toAbsoluteTime(timegroup, userTimeMs));\n overrideRootCloneStyles(syncState);\n\n // Render at scaled dimensions with canvas scaling for internal video frames\n const t0 = performance.now();\n const image = await renderToImage(previewContainer, renderWidth, renderHeight, {\n canvasScale: currentResolutionScale,\n });\n const renderTime = performance.now() - t0;\n\n // Update canvas buffer dimensions NOW, right before drawing\n // This clears the canvas, but we immediately draw new content\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n \n ctx.save();\n ctx.scale(dpr * scale, dpr * scale);\n ctx.drawImage(image, 0, 0);\n ctx.restore();\n \n // Log render time periodically (every 60 frames)\n defaultProfiler.incrementRenderCount();\n if (defaultProfiler.shouldLogByFrameCount(60)) {\n console.log(`[renderTimegroupToCanvas] Frame render: ${renderTime.toFixed(1)}ms (resolutionScale=${currentResolutionScale}, image=${image.width}x${image.height})`);\n }\n \n // Update debug label\n updateDebugLabel(debugLabel, renderWidth, renderHeight, currentResolutionScale);\n } catch (e) {\n console.error(\"Canvas preview render failed:\", e);\n } finally {\n rendering = false;\n }\n };\n\n // Do initial render\n refresh();\n\n return { container: wrapperContainer, canvas, refresh, syncState, setResolutionScale, getResolutionScale };\n}\n\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"WorkerPool.js","names":["poolSize: number","testTimeout: number | null","testHandler: ((event: MessageEvent) => void) | null","timeoutId: number | null"],"sources":["../../../src/preview/workers/WorkerPool.ts"],"sourcesContent":["/**\n * Worker pool for parallel task execution.\n * Manages a pool of workers and distributes tasks across them.\n */\n\n// Constants\nconst WORKER_TASK_TIMEOUT_MS = 30000;\nconst WORKER_INIT_TEST_TIMEOUT_MS = 2000;\n\ninterface QueuedTask<T> {\n resolve: (value: T) => void;\n reject: (error: Error) => void;\n task: (worker: Worker) => Promise<T>;\n}\n\nexport class WorkerPool {\n private workers: Worker[] = [];\n private availableWorkers: Worker[] = [];\n private taskQueue: QueuedTask<unknown>[] = [];\n private isTerminated = false;\n private workerUrl: string;\n private taskIdCounter = 0;\n\n constructor(\n workerScriptUrl: string,\n private poolSize: number = navigator.hardwareConcurrency || 4,\n ) {\n this.workerUrl = workerScriptUrl;\n\n // Check browser support first, then initialize workers\n if (this.hasBrowserSupport()) {\n this.initializeWorkers();\n }\n }\n\n /**\n * Check if browser supports workers (before initialization).\n */\n private hasBrowserSupport(): boolean {\n return (\n typeof Worker !== \"undefined\" &&\n typeof OffscreenCanvas !== \"undefined\" &&\n typeof createImageBitmap !== \"undefined\"\n );\n }\n\n private initializeWorkers(): void {\n for (let i = 0; i < this.poolSize; i++) {\n try {\n // Vite processes worker files - the URL should point to the actual worker file\n // When using ?worker_file&type=module, Vite will serve the processed worker\n const worker = new Worker(this.workerUrl, { type: \"module\" });\n \n // Test if worker is responding - cleanup handler after confirmation\n let testTimeout: number | null = null;\n let testHandler: ((event: MessageEvent) => void) | null = null;\n \n const cleanupTest = () => {\n if (testTimeout !== null) {\n clearTimeout(testTimeout);\n testTimeout = null;\n }\n if (testHandler !== null) {\n worker.removeEventListener(\"message\", testHandler);\n testHandler = null;\n }\n };\n \n testTimeout = window.setTimeout(() => {\n cleanupTest();\n }, WORKER_INIT_TEST_TIMEOUT_MS);\n \n testHandler = (event: MessageEvent) => {\n // Check if this is a test response (worker startup message)\n if (event.data && typeof event.data === \"string\" && event.data.includes(\"encoderWorker\")) {\n cleanupTest();\n }\n };\n worker.addEventListener(\"message\", testHandler);\n \n worker.onerror = (error) => {\n cleanupTest();\n console.error(`[WorkerPool] Worker ${i} error:`, {\n message: error.message,\n filename: error.filename,\n lineno: error.lineno,\n colno: error.colno,\n });\n };\n worker.onmessageerror = (error) => {\n console.error(`[WorkerPool] Worker ${i} message error:`, error);\n };\n this.workers.push(worker);\n this.availableWorkers.push(worker);\n } catch (error) {\n console.error(`[WorkerPool] Failed to create worker ${i}:`, error instanceof Error ? error.message : String(error));\n }\n }\n if (this.workers.length === 0) {\n console.error(`[WorkerPool] Failed to create any workers. URL: ${this.workerUrl}`);\n console.error(`[WorkerPool] Browser support check:`, {\n Worker: typeof Worker !== \"undefined\",\n OffscreenCanvas: typeof OffscreenCanvas !== \"undefined\",\n createImageBitmap: typeof createImageBitmap !== \"undefined\",\n });\n }\n }\n\n /**\n * Get the number of workers in the pool.\n */\n get workerCount(): number {\n return this.workers.length;\n }\n\n /**\n * Check if workers are available and initialized.\n */\n isAvailable(): boolean {\n return (\n this.hasBrowserSupport() &&\n this.workers.length > 0 &&\n !this.isTerminated\n );\n }\n\n /**\n * Execute a task using an available worker from the pool.\n */\n async execute<T>(task: (worker: Worker) => Promise<T>): Promise<T> {\n if (this.isTerminated) {\n throw new Error(\"WorkerPool has been terminated\");\n }\n\n // If workers aren't available, this will be handled by the caller's fallback\n if (!this.isAvailable()) {\n throw new Error(\"Workers not available\");\n }\n\n return new Promise<T>((resolve, reject) => {\n this.taskQueue.push({ \n resolve: resolve as (value: unknown) => void, \n reject, \n task \n });\n this.processQueue();\n });\n }\n\n private processQueue(): void {\n // Process tasks while we have available workers and queued tasks\n while (this.availableWorkers.length > 0 && this.taskQueue.length > 0) {\n const worker = this.availableWorkers.shift();\n const queuedTask = this.taskQueue.shift();\n \n if (!worker || !queuedTask) {\n // Safety check - should not happen but prevents crashes\n break;\n }\n\n const { resolve, reject, task } = queuedTask;\n\n // Execute the task\n task(worker)\n .then((result) => {\n resolve(result);\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n })\n .catch((error) => {\n reject(error instanceof Error ? error : new Error(String(error)));\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n });\n }\n }\n\n /**\n * Terminate all workers and clear the task queue.\n */\n terminate(): void {\n this.isTerminated = true;\n\n // Reject all pending tasks\n for (const { reject } of this.taskQueue) {\n reject(new Error(\"WorkerPool terminated\"));\n }\n this.taskQueue = [];\n\n // Terminate all workers\n for (const worker of this.workers) {\n worker.terminate();\n }\n this.workers = [];\n this.availableWorkers = [];\n }\n}\n\n/**\n * Helper function to encode a canvas using a worker.\n */\nexport async function encodeCanvasInWorker(\n worker: Worker,\n canvas: HTMLCanvasElement,\n preserveAlpha: boolean,\n): Promise<string> {\n return new Promise<string>((resolve, reject) => {\n const taskId = `task-${Date.now()}-${Math.random()}-${performance.now()}`;\n const startTime = performance.now();\n let timeoutId: number | null = null;\n\n const cleanup = () => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId);\n timeoutId = null;\n }\n worker.removeEventListener(\"message\", messageHandler);\n worker.removeEventListener(\"messageerror\", messageErrorHandler);\n };\n\n const messageHandler = (event: MessageEvent) => {\n const result = event.data as { taskId: string; dataUrl: string; error?: string };\n if (result.taskId === taskId) {\n cleanup();\n if (result.error) {\n reject(new Error(`Worker encoding failed: ${result.error}`));\n } else {\n resolve(result.dataUrl);\n }\n }\n };\n\n const messageErrorHandler = () => {\n cleanup();\n reject(new Error(\"Worker message error\"));\n };\n\n worker.addEventListener(\"message\", messageHandler);\n worker.addEventListener(\"messageerror\", messageErrorHandler);\n\n // Set timeout to detect if worker never responds\n timeoutId = window.setTimeout(() => {\n cleanup();\n reject(new Error(\"Worker task timed out\"));\n }, WORKER_TASK_TIMEOUT_MS);\n\n // Create ImageBitmap from canvas\n createImageBitmap(canvas)\n .then((bitmap) => {\n // Transfer bitmap to worker (zero-copy)\n worker.postMessage(\n {\n taskId,\n bitmap,\n preserveAlpha,\n },\n [bitmap],\n );\n })\n .catch((error) => {\n cleanup();\n reject(error instanceof Error ? error : new Error(String(error)));\n });\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"WorkerPool.js","names":["poolSize: number","testTimeout: number | null","testHandler: ((event: MessageEvent) => void) | null","timeoutId: number | null"],"sources":["../../../src/preview/workers/WorkerPool.ts"],"sourcesContent":["/**\n * Worker pool for parallel task execution.\n * Manages a pool of workers and distributes tasks across them.\n */\n\n// Constants\nconst WORKER_TASK_TIMEOUT_MS = 30000;\nconst WORKER_INIT_TEST_TIMEOUT_MS = 2000;\n\ninterface QueuedTask<T> {\n resolve: (value: T) => void;\n reject: (error: Error) => void;\n task: (worker: Worker) => Promise<T>;\n}\n\nexport class WorkerPool {\n private workers: Worker[] = [];\n private availableWorkers: Worker[] = [];\n private taskQueue: QueuedTask<unknown>[] = [];\n private isTerminated = false;\n private workerUrl: string;\n private taskIdCounter = 0;\n\n constructor(\n workerScriptUrl: string,\n private poolSize: number = navigator.hardwareConcurrency || 4,\n ) {\n this.workerUrl = workerScriptUrl;\n\n // Check browser support first, then initialize workers\n if (this.hasBrowserSupport()) {\n this.initializeWorkers();\n }\n }\n\n /**\n * Check if browser supports workers (before initialization).\n */\n private hasBrowserSupport(): boolean {\n return (\n typeof Worker !== \"undefined\" &&\n typeof OffscreenCanvas !== \"undefined\" &&\n typeof createImageBitmap !== \"undefined\"\n );\n }\n\n private initializeWorkers(): void {\n for (let i = 0; i < this.poolSize; i++) {\n try {\n // Create worker from URL (typically a blob URL from inlined worker code)\n const worker = new Worker(this.workerUrl, { type: \"module\" });\n \n // Test if worker is responding - cleanup handler after confirmation\n let testTimeout: number | null = null;\n let testHandler: ((event: MessageEvent) => void) | null = null;\n \n const cleanupTest = () => {\n if (testTimeout !== null) {\n clearTimeout(testTimeout);\n testTimeout = null;\n }\n if (testHandler !== null) {\n worker.removeEventListener(\"message\", testHandler);\n testHandler = null;\n }\n };\n \n testTimeout = window.setTimeout(() => {\n cleanupTest();\n }, WORKER_INIT_TEST_TIMEOUT_MS);\n \n testHandler = (event: MessageEvent) => {\n // Check if this is a test response (worker startup message)\n if (event.data && typeof event.data === \"string\" && event.data.includes(\"encoderWorker\")) {\n cleanupTest();\n }\n };\n worker.addEventListener(\"message\", testHandler);\n \n worker.onerror = (error) => {\n cleanupTest();\n console.error(`[WorkerPool] Worker ${i} error:`, {\n message: error.message,\n filename: error.filename,\n lineno: error.lineno,\n colno: error.colno,\n });\n };\n worker.onmessageerror = (error) => {\n console.error(`[WorkerPool] Worker ${i} message error:`, error);\n };\n this.workers.push(worker);\n this.availableWorkers.push(worker);\n } catch (error) {\n console.error(`[WorkerPool] Failed to create worker ${i}:`, error instanceof Error ? error.message : String(error));\n }\n }\n if (this.workers.length === 0) {\n console.error(`[WorkerPool] Failed to create any workers. URL: ${this.workerUrl}`);\n console.error(`[WorkerPool] Browser support check:`, {\n Worker: typeof Worker !== \"undefined\",\n OffscreenCanvas: typeof OffscreenCanvas !== \"undefined\",\n createImageBitmap: typeof createImageBitmap !== \"undefined\",\n });\n }\n }\n\n /**\n * Get the number of workers in the pool.\n */\n get workerCount(): number {\n return this.workers.length;\n }\n\n /**\n * Check if workers are available and initialized.\n */\n isAvailable(): boolean {\n return (\n this.hasBrowserSupport() &&\n this.workers.length > 0 &&\n !this.isTerminated\n );\n }\n\n /**\n * Execute a task using an available worker from the pool.\n */\n async execute<T>(task: (worker: Worker) => Promise<T>): Promise<T> {\n if (this.isTerminated) {\n throw new Error(\"WorkerPool has been terminated\");\n }\n\n // If workers aren't available, this will be handled by the caller's fallback\n if (!this.isAvailable()) {\n throw new Error(\"Workers not available\");\n }\n\n return new Promise<T>((resolve, reject) => {\n this.taskQueue.push({ \n resolve: resolve as (value: unknown) => void, \n reject, \n task \n });\n this.processQueue();\n });\n }\n\n private processQueue(): void {\n // Process tasks while we have available workers and queued tasks\n while (this.availableWorkers.length > 0 && this.taskQueue.length > 0) {\n const worker = this.availableWorkers.shift();\n const queuedTask = this.taskQueue.shift();\n \n if (!worker || !queuedTask) {\n // Safety check - should not happen but prevents crashes\n break;\n }\n\n const { resolve, reject, task } = queuedTask;\n\n // Execute the task\n task(worker)\n .then((result) => {\n resolve(result);\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n })\n .catch((error) => {\n reject(error instanceof Error ? error : new Error(String(error)));\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n });\n }\n }\n\n /**\n * Terminate all workers and clear the task queue.\n */\n terminate(): void {\n this.isTerminated = true;\n\n // Reject all pending tasks\n for (const { reject } of this.taskQueue) {\n reject(new Error(\"WorkerPool terminated\"));\n }\n this.taskQueue = [];\n\n // Terminate all workers\n for (const worker of this.workers) {\n worker.terminate();\n }\n this.workers = [];\n this.availableWorkers = [];\n }\n}\n\n/**\n * Helper function to encode a canvas using a worker.\n */\nexport async function encodeCanvasInWorker(\n worker: Worker,\n canvas: HTMLCanvasElement,\n preserveAlpha: boolean,\n): Promise<string> {\n return new Promise<string>((resolve, reject) => {\n const taskId = `task-${Date.now()}-${Math.random()}-${performance.now()}`;\n const startTime = performance.now();\n let timeoutId: number | null = null;\n\n const cleanup = () => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId);\n timeoutId = null;\n }\n worker.removeEventListener(\"message\", messageHandler);\n worker.removeEventListener(\"messageerror\", messageErrorHandler);\n };\n\n const messageHandler = (event: MessageEvent) => {\n const result = event.data as { taskId: string; dataUrl: string; error?: string };\n if (result.taskId === taskId) {\n cleanup();\n if (result.error) {\n reject(new Error(`Worker encoding failed: ${result.error}`));\n } else {\n resolve(result.dataUrl);\n }\n }\n };\n\n const messageErrorHandler = () => {\n cleanup();\n reject(new Error(\"Worker message error\"));\n };\n\n worker.addEventListener(\"message\", messageHandler);\n worker.addEventListener(\"messageerror\", messageErrorHandler);\n\n // Set timeout to detect if worker never responds\n timeoutId = window.setTimeout(() => {\n cleanup();\n reject(new Error(\"Worker task timed out\"));\n }, WORKER_TASK_TIMEOUT_MS);\n\n // Create ImageBitmap from canvas\n createImageBitmap(canvas)\n .then((bitmap) => {\n // Transfer bitmap to worker (zero-copy)\n worker.postMessage(\n {\n taskId,\n bitmap,\n preserveAlpha,\n },\n [bitmap],\n );\n })\n .catch((error) => {\n cleanup();\n reject(error instanceof Error ? error : new Error(String(error)));\n });\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ //#region src/preview/workers/encoderWorkerInline.ts
2
+ /**
3
+ * Inline encoder worker - creates a blob URL from inlined worker code.
4
+ * This approach works in any bundler environment (Vite, webpack, etc.)
5
+ * because the worker code is embedded in the bundle rather than loaded
6
+ * from a separate file.
7
+ */
8
+ const workerCode = `
9
+ /**
10
+ * Fast base64 encoding directly from Uint8Array.
11
+ */
12
+ function encodeBase64Fast(bytes) {
13
+ const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
14
+ let result = "";
15
+ let i = 0;
16
+ const len = bytes.length;
17
+
18
+ while (i < len - 2) {
19
+ const byte1 = bytes[i++];
20
+ const byte2 = bytes[i++];
21
+ const byte3 = bytes[i++];
22
+
23
+ const bitmap = (byte1 << 16) | (byte2 << 8) | byte3;
24
+
25
+ result += base64Chars.charAt((bitmap >> 18) & 63);
26
+ result += base64Chars.charAt((bitmap >> 12) & 63);
27
+ result += base64Chars.charAt((bitmap >> 6) & 63);
28
+ result += base64Chars.charAt(bitmap & 63);
29
+ }
30
+
31
+ if (i < len) {
32
+ const byte1 = bytes[i++];
33
+ const bitmap = byte1 << 16;
34
+
35
+ result += base64Chars.charAt((bitmap >> 18) & 63);
36
+ result += base64Chars.charAt((bitmap >> 12) & 63);
37
+
38
+ if (i < len) {
39
+ const byte2 = bytes[i++];
40
+ const bitmap2 = (byte1 << 16) | (byte2 << 8);
41
+ result += base64Chars.charAt((bitmap2 >> 6) & 63);
42
+ result += "=";
43
+ } else {
44
+ result += "==";
45
+ }
46
+ }
47
+
48
+ return result;
49
+ }
50
+
51
+ // Send a startup message to confirm worker is loaded
52
+ postMessage("encoderWorker-loaded");
53
+
54
+ // Use addEventListener for better compatibility
55
+ addEventListener("message", async (event) => {
56
+ const { taskId, bitmap, preserveAlpha } = event.data;
57
+
58
+ try {
59
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
60
+ const ctx = canvas.getContext("2d");
61
+
62
+ if (!ctx) {
63
+ throw new Error("Failed to get 2d context from OffscreenCanvas");
64
+ }
65
+
66
+ ctx.drawImage(bitmap, 0, 0);
67
+ bitmap.close();
68
+
69
+ const blob = await canvas.convertToBlob({
70
+ type: preserveAlpha ? "image/png" : "image/jpeg",
71
+ quality: preserveAlpha ? undefined : 0.95,
72
+ });
73
+
74
+ const arrayBuffer = await blob.arrayBuffer();
75
+ const bytes = new Uint8Array(arrayBuffer);
76
+ const base64 = encodeBase64Fast(bytes);
77
+ const mimeType = preserveAlpha ? "image/png" : "image/jpeg";
78
+ const dataUrl = \`data:\${mimeType};base64,\${base64}\`;
79
+
80
+ postMessage({ taskId, dataUrl });
81
+ } catch (error) {
82
+ const errorMessage = error instanceof Error ? error.message : String(error);
83
+ postMessage({ taskId, dataUrl: "", error: errorMessage });
84
+ }
85
+ });
86
+ `;
87
+ let cachedBlobUrl = null;
88
+ /**
89
+ * Creates a blob URL for the encoder worker.
90
+ * The blob URL is cached so multiple calls return the same URL.
91
+ *
92
+ * @returns The blob URL that can be passed to `new Worker(url, { type: "module" })`
93
+ */
94
+ function getEncoderWorkerUrl() {
95
+ if (cachedBlobUrl) return cachedBlobUrl;
96
+ const blob = new Blob([workerCode], { type: "application/javascript" });
97
+ cachedBlobUrl = URL.createObjectURL(blob);
98
+ return cachedBlobUrl;
99
+ }
100
+
101
+ //#endregion
102
+ export { getEncoderWorkerUrl };
103
+ //# sourceMappingURL=encoderWorkerInline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encoderWorkerInline.js","names":["cachedBlobUrl: string | null"],"sources":["../../../src/preview/workers/encoderWorkerInline.ts"],"sourcesContent":["/**\n * Inline encoder worker - creates a blob URL from inlined worker code.\n * This approach works in any bundler environment (Vite, webpack, etc.)\n * because the worker code is embedded in the bundle rather than loaded\n * from a separate file.\n */\n\n// The worker code as a string. This is the same logic as encoderWorker.ts\n// but inlined so it can be converted to a blob URL at runtime.\nconst workerCode = `\n/**\n * Fast base64 encoding directly from Uint8Array.\n */\nfunction encodeBase64Fast(bytes) {\n const base64Chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n let result = \"\";\n let i = 0;\n const len = bytes.length;\n \n while (i < len - 2) {\n const byte1 = bytes[i++];\n const byte2 = bytes[i++];\n const byte3 = bytes[i++];\n \n const bitmap = (byte1 << 16) | (byte2 << 8) | byte3;\n \n result += base64Chars.charAt((bitmap >> 18) & 63);\n result += base64Chars.charAt((bitmap >> 12) & 63);\n result += base64Chars.charAt((bitmap >> 6) & 63);\n result += base64Chars.charAt(bitmap & 63);\n }\n \n if (i < len) {\n const byte1 = bytes[i++];\n const bitmap = byte1 << 16;\n \n result += base64Chars.charAt((bitmap >> 18) & 63);\n result += base64Chars.charAt((bitmap >> 12) & 63);\n \n if (i < len) {\n const byte2 = bytes[i++];\n const bitmap2 = (byte1 << 16) | (byte2 << 8);\n result += base64Chars.charAt((bitmap2 >> 6) & 63);\n result += \"=\";\n } else {\n result += \"==\";\n }\n }\n \n return result;\n}\n\n// Send a startup message to confirm worker is loaded\npostMessage(\"encoderWorker-loaded\");\n\n// Use addEventListener for better compatibility\naddEventListener(\"message\", async (event) => {\n const { taskId, bitmap, preserveAlpha } = event.data;\n \n try {\n const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);\n const ctx = canvas.getContext(\"2d\");\n \n if (!ctx) {\n throw new Error(\"Failed to get 2d context from OffscreenCanvas\");\n }\n \n ctx.drawImage(bitmap, 0, 0);\n bitmap.close();\n \n const blob = await canvas.convertToBlob({\n type: preserveAlpha ? \"image/png\" : \"image/jpeg\",\n quality: preserveAlpha ? undefined : 0.95,\n });\n \n const arrayBuffer = await blob.arrayBuffer();\n const bytes = new Uint8Array(arrayBuffer);\n const base64 = encodeBase64Fast(bytes);\n const mimeType = preserveAlpha ? \"image/png\" : \"image/jpeg\";\n const dataUrl = \\`data:\\${mimeType};base64,\\${base64}\\`;\n \n postMessage({ taskId, dataUrl });\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n postMessage({ taskId, dataUrl: \"\", error: errorMessage });\n }\n});\n`;\n\n// Cache the blob URL so we only create it once\nlet cachedBlobUrl: string | null = null;\n\n/**\n * Creates a blob URL for the encoder worker.\n * The blob URL is cached so multiple calls return the same URL.\n * \n * @returns The blob URL that can be passed to `new Worker(url, { type: \"module\" })`\n */\nexport function getEncoderWorkerUrl(): string {\n if (cachedBlobUrl) {\n return cachedBlobUrl;\n }\n\n const blob = new Blob([workerCode], { type: \"application/javascript\" });\n cachedBlobUrl = URL.createObjectURL(blob);\n return cachedBlobUrl;\n}\n\n/**\n * Revokes the cached blob URL to free memory.\n * Call this when you're done with all workers (e.g., during cleanup).\n */\nexport function revokeEncoderWorkerUrl(): void {\n if (cachedBlobUrl) {\n URL.revokeObjectURL(cachedBlobUrl);\n cachedBlobUrl = null;\n }\n}\n"],"mappings":";;;;;;;AASA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiFnB,IAAIA,gBAA+B;;;;;;;AAQnC,SAAgB,sBAA8B;AAC5C,KAAI,cACF,QAAO;CAGT,MAAM,OAAO,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,MAAM,0BAA0B,CAAC;AACvE,iBAAgB,IAAI,gBAAgB,KAAK;AACzC,QAAO"}
package/dist/style.css CHANGED
@@ -577,6 +577,9 @@ video {
577
577
  .h-\[360px\] {
578
578
  height: 360px;
579
579
  }
580
+ .h-\[400px\] {
581
+ height: 400px;
582
+ }
580
583
  .h-\[500px\] {
581
584
  height: 500px;
582
585
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.31.0-beta.0",
3
+ "version": "0.31.1-beta.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -13,7 +13,7 @@
13
13
  "license": "UNLICENSED",
14
14
  "dependencies": {
15
15
  "@bramus/style-observer": "^1.3.0",
16
- "@editframe/assets": "0.31.0-beta.0",
16
+ "@editframe/assets": "0.31.1-beta.0",
17
17
  "@lit/context": "^1.1.6",
18
18
  "@lit/task": "^1.0.3",
19
19
  "@opentelemetry/api": "^1.9.0",