@editframe/elements 0.34.5-beta → 0.34.6-beta

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,6 +1,6 @@
1
1
  import { SelectionContext } from "../selection/selectionContext.js";
2
2
  import { PanZoomTransform } from "../../elements/EFPanZoom.js";
3
- import * as lit36 from "lit";
3
+ import * as lit37 from "lit";
4
4
  import { LitElement } from "lit";
5
5
  import * as lit_html34 from "lit-html";
6
6
 
@@ -10,7 +10,7 @@ import * as lit_html34 from "lit-html";
10
10
  * Uses fixed positioning to ensure 1:1 pixel ratio regardless of zoom level.
11
11
  */
12
12
  declare class SelectionOverlay extends LitElement {
13
- static styles: lit36.CSSResult[];
13
+ static styles: lit37.CSSResult[];
14
14
  createRenderRoot(): this;
15
15
  firstUpdated(changedProperties: Map<string | number | symbol, unknown>): void;
16
16
  selectionFromContext?: SelectionContext;
@@ -1,5 +1,5 @@
1
1
  import { TemporalMixinInterface } from "../../elements/EFTemporal.js";
2
- import * as lit37 from "lit";
2
+ import * as lit36 from "lit";
3
3
  import { LitElement, PropertyValues, TemplateResult, nothing } from "lit";
4
4
 
5
5
  //#region src/gui/timeline/EFTimelineRow.d.ts
@@ -14,7 +14,7 @@ declare const EFTimelineRow_base: typeof LitElement;
14
14
  * Heights are determined by content, not hardcoded.
15
15
  */
16
16
  declare class EFTimelineRow extends EFTimelineRow_base {
17
- static styles: lit37.CSSResult[];
17
+ static styles: lit36.CSSResult[];
18
18
  element: TemporalMixinInterface & Element;
19
19
  depth: number;
20
20
  pixelsPerMs: number;
@@ -26,11 +26,12 @@ var ContentNotReadyError = class extends Error {
26
26
  };
27
27
  /**
28
28
  * Module-level state for render operations.
29
+ * Note: xmlSerializer is lazy-initialized for Node.js compatibility
29
30
  */
30
31
  const renderState = {
31
32
  inlineImageCache: /* @__PURE__ */ new Map(),
32
33
  layoutInitializedCanvases: /* @__PURE__ */ new WeakSet(),
33
- xmlSerializer: new XMLSerializer(),
34
+ xmlSerializer: null,
34
35
  textEncoder: new TextEncoder(),
35
36
  metrics: {
36
37
  inlineImageCacheHits: 0,
@@ -1 +1 @@
1
- {"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","renderState: RenderState","scaleColors: Record<number, string>","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 removeHiddenNodesForSerialization,\n restoreHiddenNodes,\n type SyncState,\n} from \"./renderTimegroupPreview.js\";\nimport { getEffectiveRenderMode } from \"./renderers.js\";\nimport { RenderContext } from \"./RenderContext.js\";\nimport { FrameController } from \"./FrameController.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 {\n type TemporalElement,\n isVisibleAtTime,\n DEFAULT_WIDTH,\n DEFAULT_HEIGHT,\n DEFAULT_THUMBNAIL_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n createPreviewContainer,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\nimport { logger } from \"./logger.js\";\n\n// Import rendering modules\nimport {\n renderToImage,\n renderToImageDirect,\n prepareFrameDataUri,\n loadImageFromDataUri,\n} from \"./rendering/renderToImage.js\";\nimport { renderToImageNative, createDprCanvas } from \"./rendering/renderToImageNative.js\";\nimport { clearInlineImageCache, getInlineImageCacheSize } from \"./rendering/inlineImages.js\";\n\n// Re-export rendering types and functions for external use\nexport type {\n NativeRenderOptions,\n ForeignObjectRenderOptions,\n} from \"./rendering/types.js\";\nexport {\n renderToImage,\n renderToImageNative,\n renderToImageDirect,\n prepareFrameDataUri,\n loadImageFromDataUri,\n};\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// ============================================================================\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 * 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/**\n * Module-level render state including caches and reusable objects.\n */\ninterface RenderState {\n inlineImageCache: Map<string, string>;\n layoutInitializedCanvases: WeakSet<HTMLCanvasElement>;\n xmlSerializer: XMLSerializer;\n textEncoder: TextEncoder;\n metrics: {\n inlineImageCacheHits: number;\n inlineImageCacheMisses: number;\n inlineImageCacheEvictions: number;\n };\n}\n\n/**\n * Module-level state for render operations.\n */\nconst renderState: RenderState = {\n inlineImageCache: new Map(),\n layoutInitializedCanvases: new WeakSet(),\n xmlSerializer: new XMLSerializer(),\n textEncoder: new TextEncoder(),\n metrics: {\n inlineImageCacheHits: 0,\n inlineImageCacheMisses: 0,\n inlineImageCacheEvictions: 0,\n },\n};\n\n/**\n * Get the current render state for testing and debugging.\n * @returns The module-level render state object\n */\nexport function getRenderState(): RenderState {\n return renderState;\n}\n\n/**\n * Get cache metrics for monitoring performance.\n * @returns Object with cache hit/miss/eviction counts\n */\nexport function getCacheMetrics(): RenderState['metrics'] {\n return { ...renderState.metrics };\n}\n\n/**\n * Reset cache metrics to zero.\n */\nexport function resetCacheMetrics(): void {\n renderState.metrics.inlineImageCacheHits = 0;\n renderState.metrics.inlineImageCacheMisses = 0;\n renderState.metrics.inlineImageCacheEvictions = 0;\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 clearInlineImageCache();\n resetCacheMetrics();\n}\n\n// Re-export cache management functions\nexport { clearInlineImageCache, getInlineImageCacheSize };\n\n// ============================================================================\n// Internal Helpers\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 * 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 * 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 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 \n // Use FrameController to ensure all FrameRenderable elements are ready\n // This coordinates prepare → render phases for video, audio, captions, etc.\n const frameController = new FrameController(renderClone);\n await frameController.renderFrame(timeMs, { waitForLitUpdate: false });\n \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 // Create RenderContext for caching during this capture operation\n const renderContext = new RenderContext();\n \n try {\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: Always skip DPR scaling for captures (thumbnails and video export).\n // Retina quality isn't needed for captured frames, and DPR=2 means 4x more pixels.\n // Live preview uses a different code path (renderTimegroupToCanvas) which handles DPR properly.\n image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: true });\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, renderContext, and sourceMap for caching optimization\n const t2 = performance.now();\n image = await renderToImage(previewContainer, width, height, { \n canvasScale: scale,\n renderContext,\n sourceMap: syncState.canvasSourceMap,\n });\n const renderTime = performance.now() - t2;\n \n logger.debug(`[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 } finally {\n // Ensure RenderContext is disposed even if an error occurs\n renderContext.dispose();\n }\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 * Dispose the preview and release resources.\n * Call this when the preview is no longer needed.\n */\n dispose: () => void;\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 let disposed = false;\n\n // Create RenderContext for caching across refresh calls\n const renderContext = new RenderContext();\n \n // Create FrameController for coordinating element rendering\n // Cached for the lifetime of this preview instance\n const frameController = new FrameController(timegroup);\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 || disposed) 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 logger.debug(`[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 // Use FrameController to ensure all FrameRenderable elements are ready\n // This coordinates prepare → render phases before we capture their state\n await frameController.renderFrame(userTimeMs);\n \n syncStyles(syncState, toAbsoluteTime(timegroup, userTimeMs));\n overrideRootCloneStyles(syncState);\n\n // Remove hidden nodes from DOM for serialization - they won't be serialized\n // or have their canvases encoded. This is a significant optimization.\n const removedNodes = removeHiddenNodesForSerialization(syncState);\n\n // Render at scaled dimensions with canvas scaling for internal video frames\n // Pass renderContext and sourceMap for caching optimization\n const t0 = performance.now();\n const image = await renderToImage(previewContainer, renderWidth, renderHeight, {\n canvasScale: currentResolutionScale,\n renderContext,\n sourceMap: syncState.canvasSourceMap,\n });\n const renderTime = performance.now() - t0;\n \n // Restore hidden nodes for next frame's delta tracking\n restoreHiddenNodes(removedNodes);\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 logger.debug(`[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 logger.error(\"Canvas preview render failed:\", e);\n } finally {\n rendering = false;\n }\n };\n\n /**\n * Dispose the preview and release resources.\n */\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n frameController.abort();\n renderContext.dispose();\n };\n\n // Do initial render\n refresh();\n\n return { container: wrapperContainer, canvas, refresh, syncState, setResolutionScale, getResolutionScale, dispose };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","renderState: RenderState","scaleColors: Record<number, string>","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 removeHiddenNodesForSerialization,\n restoreHiddenNodes,\n type SyncState,\n} from \"./renderTimegroupPreview.js\";\nimport { getEffectiveRenderMode } from \"./renderers.js\";\nimport { RenderContext } from \"./RenderContext.js\";\nimport { FrameController } from \"./FrameController.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 {\n type TemporalElement,\n isVisibleAtTime,\n DEFAULT_WIDTH,\n DEFAULT_HEIGHT,\n DEFAULT_THUMBNAIL_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n createPreviewContainer,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\nimport { logger } from \"./logger.js\";\n\n// Import rendering modules\nimport {\n renderToImage,\n renderToImageDirect,\n prepareFrameDataUri,\n loadImageFromDataUri,\n} from \"./rendering/renderToImage.js\";\nimport { renderToImageNative, createDprCanvas } from \"./rendering/renderToImageNative.js\";\nimport { clearInlineImageCache, getInlineImageCacheSize } from \"./rendering/inlineImages.js\";\n\n// Re-export rendering types and functions for external use\nexport type {\n NativeRenderOptions,\n ForeignObjectRenderOptions,\n} from \"./rendering/types.js\";\nexport {\n renderToImage,\n renderToImageNative,\n renderToImageDirect,\n prepareFrameDataUri,\n loadImageFromDataUri,\n};\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// ============================================================================\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 * 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/**\n * Module-level render state including caches and reusable objects.\n */\ninterface RenderState {\n inlineImageCache: Map<string, string>;\n layoutInitializedCanvases: WeakSet<HTMLCanvasElement>;\n xmlSerializer: XMLSerializer | null;\n textEncoder: TextEncoder;\n metrics: {\n inlineImageCacheHits: number;\n inlineImageCacheMisses: number;\n inlineImageCacheEvictions: number;\n };\n}\n\n/**\n * Module-level state for render operations.\n * Note: xmlSerializer is lazy-initialized for Node.js compatibility\n */\nconst renderState: RenderState = {\n inlineImageCache: new Map(),\n layoutInitializedCanvases: new WeakSet(),\n xmlSerializer: null, // Lazy-initialized in browser context\n textEncoder: new TextEncoder(),\n metrics: {\n inlineImageCacheHits: 0,\n inlineImageCacheMisses: 0,\n inlineImageCacheEvictions: 0,\n },\n};\n\n/**\n * Get the current render state for testing and debugging.\n * @returns The module-level render state object\n */\nexport function getRenderState(): RenderState {\n return renderState;\n}\n\n/**\n * Get cache metrics for monitoring performance.\n * @returns Object with cache hit/miss/eviction counts\n */\nexport function getCacheMetrics(): RenderState['metrics'] {\n return { ...renderState.metrics };\n}\n\n/**\n * Reset cache metrics to zero.\n */\nexport function resetCacheMetrics(): void {\n renderState.metrics.inlineImageCacheHits = 0;\n renderState.metrics.inlineImageCacheMisses = 0;\n renderState.metrics.inlineImageCacheEvictions = 0;\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 clearInlineImageCache();\n resetCacheMetrics();\n}\n\n// Re-export cache management functions\nexport { clearInlineImageCache, getInlineImageCacheSize };\n\n// ============================================================================\n// Internal Helpers\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 * 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 * 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 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 \n // Use FrameController to ensure all FrameRenderable elements are ready\n // This coordinates prepare → render phases for video, audio, captions, etc.\n const frameController = new FrameController(renderClone);\n await frameController.renderFrame(timeMs, { waitForLitUpdate: false });\n \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 // Create RenderContext for caching during this capture operation\n const renderContext = new RenderContext();\n \n try {\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: Always skip DPR scaling for captures (thumbnails and video export).\n // Retina quality isn't needed for captured frames, and DPR=2 means 4x more pixels.\n // Live preview uses a different code path (renderTimegroupToCanvas) which handles DPR properly.\n image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: true });\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, renderContext, and sourceMap for caching optimization\n const t2 = performance.now();\n image = await renderToImage(previewContainer, width, height, { \n canvasScale: scale,\n renderContext,\n sourceMap: syncState.canvasSourceMap,\n });\n const renderTime = performance.now() - t2;\n \n logger.debug(`[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 } finally {\n // Ensure RenderContext is disposed even if an error occurs\n renderContext.dispose();\n }\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 * Dispose the preview and release resources.\n * Call this when the preview is no longer needed.\n */\n dispose: () => void;\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 let disposed = false;\n\n // Create RenderContext for caching across refresh calls\n const renderContext = new RenderContext();\n \n // Create FrameController for coordinating element rendering\n // Cached for the lifetime of this preview instance\n const frameController = new FrameController(timegroup);\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 || disposed) 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 logger.debug(`[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 // Use FrameController to ensure all FrameRenderable elements are ready\n // This coordinates prepare → render phases before we capture their state\n await frameController.renderFrame(userTimeMs);\n \n syncStyles(syncState, toAbsoluteTime(timegroup, userTimeMs));\n overrideRootCloneStyles(syncState);\n\n // Remove hidden nodes from DOM for serialization - they won't be serialized\n // or have their canvases encoded. This is a significant optimization.\n const removedNodes = removeHiddenNodesForSerialization(syncState);\n\n // Render at scaled dimensions with canvas scaling for internal video frames\n // Pass renderContext and sourceMap for caching optimization\n const t0 = performance.now();\n const image = await renderToImage(previewContainer, renderWidth, renderHeight, {\n canvasScale: currentResolutionScale,\n renderContext,\n sourceMap: syncState.canvasSourceMap,\n });\n const renderTime = performance.now() - t0;\n \n // Restore hidden nodes for next frame's delta tracking\n restoreHiddenNodes(removedNodes);\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 logger.debug(`[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 logger.error(\"Canvas preview render failed:\", e);\n } finally {\n rendering = false;\n }\n };\n\n /**\n * Dispose the preview and release resources.\n */\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n frameController.abort();\n renderContext.dispose();\n };\n\n // Do initial render\n refresh();\n\n return { container: wrapperContainer, canvas, refresh, syncState, setResolutionScale, getResolutionScale, dispose };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.34.5-beta",
3
+ "version": "0.34.6-beta",
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.34.5-beta",
16
+ "@editframe/assets": "0.34.6-beta",
17
17
  "@lit/context": "^1.1.6",
18
18
  "@opentelemetry/api": "^1.9.0",
19
19
  "@opentelemetry/context-zone": "^1.26.0",