@editframe/elements 0.35.0-beta → 0.36.0-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.
- package/dist/elements/EFImage.js +11 -2
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFTemporal.js +1 -0
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +40 -6
- package/dist/elements/EFTimegroup.js +127 -8
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/updateAnimations.js +38 -15
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/gui/EFWorkbench.js +10 -12
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/preview/FrameController.js +6 -1
- package/dist/preview/FrameController.js.map +1 -1
- package/dist/preview/encoding/canvasEncoder.js.map +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js +3 -0
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
- package/dist/preview/renderTimegroupPreview.js +57 -55
- package/dist/preview/renderTimegroupPreview.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.js +22 -23
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.d.ts +2 -1
- package/dist/preview/renderTimegroupToVideo.js +77 -40
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/rendering/renderToImage.d.ts +1 -0
- package/dist/preview/rendering/renderToImage.js +1 -26
- package/dist/preview/rendering/renderToImage.js.map +1 -1
- package/dist/preview/rendering/renderToImageForeignObject.js +34 -6
- package/dist/preview/rendering/renderToImageForeignObject.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +379 -0
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -0
- package/dist/render/EFRenderAPI.js +45 -0
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/style.css +12 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderTimegroupPreview.js","names":["CSS_SAFE_DEFAULT_VALUES: Record<string, string | string[]>","removed: RemovedNodeInfo[]","cs: CSSStyleDeclaration","contentCs: CSSStyleDeclaration | undefined","srcMap: StylePropertyMapReadOnly","node: CloneNode","node","clone","syncState: SyncState","syncStats: SyncStats","rules: string[]"],"sources":["../../src/preview/renderTimegroupPreview.ts"],"sourcesContent":["/**\n * Canvas refresh fix:\n * Canvas pixels must be explicitly cleared and redrawn in syncNodeStyles\n * to ensure video frames are captured correctly during foreignObject rendering.\n * Without clearRect, stale frames from previous seeks may be serialized.\n * \n * See FOREIGNOBJECT_BUG_FIX.md for detailed explanation.\n */\n\nimport { logger } from \"./logger.js\";\n\n/**\n * Elements to skip entirely when building the preview.\n */\nconst SKIP_TAGS = new Set([\n \"EF-AUDIO\",\n \"EF-THUMBNAIL-STRIP\",\n \"EF-FILMSTRIP\",\n \"EF-TIMELINE\",\n \"EF-WORKBENCH\",\n \"SCRIPT\",\n \"STYLE\",\n]);\n\n/**\n * All CSS properties to sync (camelCase for style[] access).\n */\nconst SYNC_PROPERTIES = [\n \"display\", \"visibility\", \"opacity\",\n \"position\", \"top\", \"right\", \"bottom\", \"left\", \"zIndex\",\n \"width\", \"height\", \"minWidth\", \"minHeight\", \"maxWidth\", \"maxHeight\",\n \"flex\", \"flexFlow\", \"justifyContent\", \"alignItems\", \"alignContent\", \"alignSelf\", \"gap\",\n \"gridTemplate\", \"gridColumn\", \"gridRow\", \"gridArea\",\n \"margin\", \"padding\", \"boxSizing\",\n \"border\", \"borderTop\", \"borderRight\", \"borderBottom\", \"borderLeft\", \"borderRadius\",\n \"background\", \"color\", \"boxShadow\", \"filter\", \"backdropFilter\", \"clipPath\",\n \"font\", \"textAlign\", \"textDecoration\", \"textTransform\",\n \"letterSpacing\", \"whiteSpace\", \"textOverflow\", \"lineHeight\",\n \"transform\", \"transformOrigin\", \"transformStyle\",\n \"perspective\", \"perspectiveOrigin\", \"backfaceVisibility\",\n \"cursor\", \"pointerEvents\", \"userSelect\", \"overflow\",\n] as const;\n\n/**\n * Kebab-case versions for computedStyleMap.get() - pre-computed for speed.\n */\nconst SYNC_PROPERTIES_KEBAB = SYNC_PROPERTIES.map(prop =>\n prop.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)\n);\n\n/**\n * Feature detection: computedStyleMap is ~15% faster for style syncing.\n */\nconst HAS_COMPUTED_STYLE_MAP = typeof Element !== \"undefined\" && typeof Element.prototype.computedStyleMap === \"function\";\n\n/**\n * CSS initial/default values for SAFE-TO-SKIP properties.\n * Only includes NON-INHERITED properties where skipping the default\n * won't affect visual output.\n * \n * EXCLUDED (must always serialize):\n * - Inherited properties (color, font, text-*, visibility, cursor)\n * - Display (affects layout significantly)\n * - Properties where \"auto\" computes to a specific value\n * \n * INCLUDED (safe to skip):\n * - Transform/filter effects (none = no effect)\n * - Box shadows (none = no shadow)\n * - Borders when none/0 (no visual impact)\n * - backdrop-filter (none = no effect)\n */\nconst CSS_SAFE_DEFAULT_VALUES: Record<string, string | string[]> = {\n // Transforms & effects - safe to skip \"none\" (no visual impact)\n transform: \"none\",\n filter: \"none\",\n backdropFilter: \"none\",\n boxShadow: \"none\",\n \n // Borders - safe to skip when none/0\n border: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderTop: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderRight: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderBottom: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderLeft: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderRadius: [\"0px\", \"0\"],\n \n // Positioning - safe to skip \"static\"\n position: \"static\",\n \n // Z-index - \"auto\" is safe when position is static\n zIndex: \"auto\",\n \n // 3D transforms - safe to skip defaults\n transformStyle: \"flat\",\n perspective: \"none\",\n backfaceVisibility: \"visible\",\n};\n\n/**\n * Check if a value matches a safe-to-skip default.\n */\nfunction isDefaultValue(prop: string, value: string): boolean {\n const defaults = CSS_SAFE_DEFAULT_VALUES[prop];\n if (!defaults) return false;\n if (Array.isArray(defaults)) {\n return defaults.includes(value);\n }\n return defaults === value;\n}\n\n// Re-export temporal types from shared module\nexport {\n type TemporalElement,\n isTemporal,\n getTemporalBounds,\n isVisibleAtTime,\n} from \"./previewTypes.js\";\n\n// Import for internal use\nimport {\n getTemporalBounds,\n} from \"./previewTypes.js\";\n\n/**\n * Tree node representing a source/clone pair with children.\n * This replaces the flat array approach for cleaner recursive traversal.\n */\nexport interface CloneNode {\n source: Element;\n clone: HTMLElement;\n children: CloneNode[];\n isCanvasClone: boolean;\n /** Cached temporal bounds for this node */\n bounds: { startMs: number; endMs: number };\n /** Parent node reference for ancestor visibility checks */\n parent: CloneNode | null;\n}\n\n/** Tree-based sync state */\nexport interface CloneTree {\n root: CloneNode | null;\n}\n\n/** Sync state with tree structure and delta tracking */\nexport interface SyncState {\n tree: CloneTree;\n nodeCount: number; // Total number of nodes (for debugging/logging)\n /** Maps clone canvases to their original source elements (ef-video, ef-image, etc.) */\n canvasSourceMap: WeakMap<HTMLCanvasElement, Element>;\n /** Previous frame's visible set for delta tracking */\n previousVisibleSet: Set<CloneNode>;\n /** Current frame's visible set (updated by syncStyles) */\n currentVisibleSet: Set<CloneNode>;\n}\n\n/** Info needed to restore a removed node */\ninterface RemovedNodeInfo {\n node: CloneNode;\n parent: Node;\n nextSibling: Node | null;\n}\n\n/**\n * Remove hidden nodes from the clone DOM for serialization.\n * Returns info needed to restore them afterward.\n * \n * This physically removes non-visible nodes so they won't be serialized,\n * avoiding the cost of serializing hidden elements and their resources.\n */\nexport function removeHiddenNodesForSerialization(state: SyncState): RemovedNodeInfo[] {\n const removed: RemovedNodeInfo[] = [];\n const visibleSet = state.currentVisibleSet;\n \n // Traverse all nodes and remove those not in visible set\n function visit(node: CloneNode): void {\n // First recurse to children (before potentially removing this node)\n for (const child of node.children) {\n visit(child);\n }\n \n // If this node isn't visible, remove it from DOM\n if (!visibleSet.has(node)) {\n const parent = node.clone.parentNode;\n if (parent) {\n const nextSibling = node.clone.nextSibling;\n parent.removeChild(node.clone);\n removed.push({ node, parent, nextSibling });\n }\n }\n }\n \n if (state.tree.root) {\n visit(state.tree.root);\n }\n \n return removed;\n}\n\n/**\n * Restore previously removed hidden nodes to the clone DOM.\n * Must be called after serialization to maintain tree integrity for next frame.\n */\nexport function restoreHiddenNodes(removed: RemovedNodeInfo[]): void {\n // Restore in reverse order to maintain correct DOM positions\n for (let i = removed.length - 1; i >= 0; i--) {\n const { node, parent, nextSibling } = removed[i]!;\n if (nextSibling) {\n parent.insertBefore(node.clone, nextSibling);\n } else {\n parent.appendChild(node.clone);\n }\n }\n}\n\n/**\n * Get visible canvases from the current visible set.\n * Use this to skip encoding hidden canvases during serialization.\n */\nexport function getVisibleCanvases(state: SyncState): Set<HTMLCanvasElement> {\n const visibleCanvases = new Set<HTMLCanvasElement>();\n for (const node of state.currentVisibleSet) {\n if (node.clone instanceof HTMLCanvasElement) {\n visibleCanvases.add(node.clone);\n }\n }\n return visibleCanvases;\n}\n\n/**\n * Traverse all nodes in the clone tree, calling the callback for each.\n */\nexport function traverseCloneTree(state: SyncState, callback: (node: CloneNode) => void): void {\n function visit(node: CloneNode): void {\n callback(node);\n for (const child of node.children) {\n visit(child);\n }\n }\n if (state.tree.root) {\n visit(state.tree.root);\n }\n}\n\n/**\n * Unified CSS property sync for all elements (canvas clones and regular elements).\n * \n * Canvas clones use a limited property set matching the original implementation\n * to avoid dimension/layout issues. Regular elements use the full SYNC_PROPERTIES array.\n * \n * @param source - Source element to read styles from\n * @param clone - Clone element to write styles to\n * @param contentSource - Optional content element for width/height (canvas clones only)\n */\nfunction syncElementStyles(\n source: Element,\n clone: HTMLElement,\n contentSource?: Element,\n): void {\n const cloneStyle = clone.style as any;\n const tagName = (source as HTMLElement).tagName;\n const isCanvasClone = !!contentSource;\n \n // Canvas clones: Use exact property list from original implementation\n if (isCanvasClone) {\n let cs: CSSStyleDeclaration;\n let contentCs: CSSStyleDeclaration | undefined;\n \n try {\n cs = getComputedStyle(source);\n if (contentSource) {\n contentCs = getComputedStyle(contentSource);\n }\n } catch { return; }\n \n // Exact properties from original copyCanvasCloneStyles + syncNodeStyles\n cloneStyle.position = cs.position;\n cloneStyle.top = cs.top;\n cloneStyle.right = cs.right;\n cloneStyle.bottom = cs.bottom;\n cloneStyle.left = cs.left;\n cloneStyle.margin = cs.margin;\n cloneStyle.zIndex = cs.zIndex;\n cloneStyle.transform = cs.transform;\n cloneStyle.transformOrigin = cs.transformOrigin;\n cloneStyle.opacity = cs.opacity;\n cloneStyle.visibility = cs.visibility;\n cloneStyle.backfaceVisibility = cs.backfaceVisibility;\n cloneStyle.transformStyle = cs.transformStyle;\n \n // Width/height from content source (shadow canvas/img)\n if (contentCs) {\n cloneStyle.width = contentCs.width;\n cloneStyle.height = contentCs.height;\n }\n \n cloneStyle.display = \"block\";\n cloneStyle.animation = \"none\";\n cloneStyle.transition = \"none\";\n \n return;\n }\n \n // Regular elements: full property sync from SYNC_PROPERTIES\n const propLen = SYNC_PROPERTIES.length;\n \n if (HAS_COMPUTED_STYLE_MAP) {\n let srcMap: StylePropertyMapReadOnly;\n \n try {\n srcMap = source.computedStyleMap();\n } catch { return; }\n \n for (let j = 0; j < propLen; j++) {\n const kebab = SYNC_PROPERTIES_KEBAB[j]!;\n const camel = SYNC_PROPERTIES[j]!;\n \n const srcVal = srcMap.get(kebab);\n if (!srcVal) continue;\n \n const strVal = srcVal.toString();\n \n if (camel === \"display\") {\n // For caption child elements, preserve display:none when explicitly set\n // (they use it to hide empty content, not for temporal visibility)\n const isCaptionChild = tagName && (\n tagName === 'EF-CAPTIONS-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-BEFORE-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-AFTER-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-SEGMENT'\n );\n const targetDisplay = (strVal === \"none\" && !isCaptionChild) ? \"block\" : strVal;\n cloneStyle.display = targetDisplay;\n continue;\n }\n \n // Skip clipPath - clones always have clipPath: none for rendering\n // (source may have clip-path: inset(100%) from proxy mode)\n if (camel === \"clipPath\") continue;\n \n // OPTIMIZATION: Skip default values to reduce serialized HTML size\n // If the computed value is the CSS default, don't set it as inline style\n if (isDefaultValue(camel, strVal)) {\n // Remove from inline style if it was previously set\n if (cloneStyle[camel]) cloneStyle[camel] = \"\";\n continue;\n }\n \n cloneStyle[camel] = strVal;\n }\n } else {\n let cs: CSSStyleDeclaration;\n \n try {\n cs = getComputedStyle(source);\n } catch { return; }\n \n const srcStyle = cs as any;\n \n for (const prop of SYNC_PROPERTIES) {\n const srcVal = srcStyle[prop];\n if (!srcVal) continue;\n \n if (prop === \"display\") {\n // For caption child elements, preserve display:none when explicitly set\n // (they use it to hide empty content, not for temporal visibility)\n const isCaptionChild = tagName && (\n tagName === 'EF-CAPTIONS-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-BEFORE-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-AFTER-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-SEGMENT'\n );\n const targetDisplay = (srcVal === \"none\" && !isCaptionChild) ? \"block\" : srcVal;\n cloneStyle.display = targetDisplay;\n continue;\n }\n \n // Skip clipPath - clones always have clipPath: none for rendering\n // (source may have clip-path: inset(100%) from proxy mode)\n if (prop === \"clipPath\") continue;\n \n // OPTIMIZATION: Skip default values to reduce serialized HTML size\n if (isDefaultValue(prop, srcVal)) {\n if (cloneStyle[prop]) cloneStyle[prop] = \"\";\n continue;\n }\n \n cloneStyle[prop] = srcVal;\n }\n }\n \n // Disable animations/transitions to prevent re-animation\n cloneStyle.animation = \"none\";\n cloneStyle.transition = \"none\";\n}\n\n/**\n * Refresh canvas pixel content from shadow DOM source.\n * Handles both shadow canvas and shadow img sources.\n */\nfunction refreshCanvasPixels(node: CloneNode): void {\n const { source, clone } = node;\n const canvas = clone as HTMLCanvasElement;\n const shadowCanvas = source.shadowRoot?.querySelector(\"canvas\");\n const shadowImg = source.shadowRoot?.querySelector(\"img\");\n \n if (shadowCanvas) {\n // Update buffer dimensions if needed\n if (canvas.width !== shadowCanvas.width) canvas.width = shadowCanvas.width;\n if (canvas.height !== shadowCanvas.height) canvas.height = shadowCanvas.height;\n \n // Copy pixels with explicit clear\n const ctx = canvas.getContext(\"2d\");\n if (ctx && shadowCanvas.width > 0 && shadowCanvas.height > 0) {\n try {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.drawImage(shadowCanvas, 0, 0);\n } catch (e) {\n logger.warn(\"[refreshCanvasPixels] Canvas draw failed:\", e);\n }\n }\n } else if (shadowImg?.complete && shadowImg.naturalWidth > 0) {\n // Update buffer dimensions if needed\n if (canvas.width !== shadowImg.naturalWidth) canvas.width = shadowImg.naturalWidth;\n if (canvas.height !== shadowImg.naturalHeight) canvas.height = shadowImg.naturalHeight;\n \n // Copy pixels with explicit clear\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n try { ctx.drawImage(shadowImg, 0, 0); } catch {}\n }\n }\n}\n\n/**\n * Sync text content from light DOM to clone.\n */\nfunction syncTextContent(source: Element, clone: HTMLElement): void {\n const srcTextNode = source.childNodes[0];\n if (srcTextNode?.nodeType === Node.TEXT_NODE) {\n const srcText = srcTextNode.textContent || \"\";\n const cloneTextNode = clone.childNodes[0];\n \n if (cloneTextNode?.nodeType === Node.TEXT_NODE) {\n // Update existing text node\n if (cloneTextNode.textContent !== srcText) cloneTextNode.textContent = srcText;\n } else if (!clone.childNodes.length) {\n // Only create text node if clone has NO children (was empty when initially cloned)\n // Don't set textContent as it would delete element children!\n clone.appendChild(document.createTextNode(srcText));\n }\n }\n}\n\n/**\n * Sync input element value.\n */\nfunction syncInputValue(source: Element, clone: HTMLElement): void {\n if (source instanceof HTMLInputElement) {\n const srcVal = source.value;\n const cloneInput = clone as HTMLInputElement;\n if (cloneInput.value !== srcVal) {\n cloneInput.value = srcVal;\n cloneInput.setAttribute(\"value\", srcVal);\n }\n }\n}\n\n/**\n * Build clone tree structure with minimal overhead.\n * Caches temporal bounds on each node for visibility checks.\n * Optionally syncs styles in the same pass if timeMs is provided.\n */\nexport function buildCloneStructure(source: Element, timeMs?: number): {\n container: HTMLDivElement;\n syncState: SyncState;\n} {\n const container = document.createElement(\"div\");\n container.style.cssText = \"position:absolute;top:0;left:0;width:100%;height:100%\";\n \n let nodeCount = 0;\n const canvasSourceMap = new WeakMap<HTMLCanvasElement, Element>();\n \n function cloneElement(srcEl: Element, parentNode: CloneNode | null): CloneNode | null {\n if (SKIP_TAGS.has(srcEl.tagName)) return null;\n \n // Get temporal bounds upfront for indexing\n const bounds = getTemporalBounds(srcEl);\n \n // SVG - clone entire subtree (no children tracking needed)\n if (srcEl instanceof SVGElement) {\n const svgClone = srcEl.cloneNode(true) as SVGElement;\n const node: CloneNode = {\n source: srcEl,\n clone: svgClone as unknown as HTMLElement,\n children: [],\n isCanvasClone: false,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n return node;\n }\n \n // Canvas - copy pixels\n if (srcEl instanceof HTMLCanvasElement) {\n const canvas = document.createElement(\"canvas\");\n canvas.width = srcEl.width;\n canvas.height = srcEl.height;\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n try { ctx.drawImage(srcEl, 0, 0); } catch {}\n }\n // Raw canvas elements don't need style syncing, just return clone\n // return null;\n }\n \n // Custom elements with shadow canvas (e.g., ef-video, ef-image)\n const isCustom = srcEl.tagName.includes(\"-\");\n if (isCustom && srcEl.shadowRoot) {\n const shadowCanvas = srcEl.shadowRoot.querySelector(\"canvas\");\n if (shadowCanvas) {\n const clone = document.createElement(\"canvas\");\n clone.width = shadowCanvas.width || srcEl.clientWidth;\n clone.height = shadowCanvas.height || srcEl.clientHeight;\n // Check if the element actually has alpha channel before preserving it\n // ef-image tracks hasAlpha based on MIME type (JPEG=false, PNG/WebP=true)\n // ef-waveform always needs alpha for proper rendering\n if (srcEl.tagName === \"EF-WAVEFORM\") {\n clone.dataset.preserveAlpha = \"true\";\n } else if (srcEl.tagName === \"EF-IMAGE\" && \"hasAlpha\" in srcEl && (srcEl as any).hasAlpha) {\n clone.dataset.preserveAlpha = \"true\";\n }\n \n const ctx = clone.getContext(\"2d\");\n if (ctx) {\n try { ctx.drawImage(shadowCanvas, 0, 0); } catch {}\n }\n \n // Copy initial CSS styles using unified sync\n // Pass shadowCanvas as contentSource for width/height\n try {\n syncElementStyles(srcEl, clone, shadowCanvas);\n } catch {}\n \n // Map clone canvas to source element for RenderContext caching\n canvasSourceMap.set(clone, srcEl);\n \n const node: CloneNode = {\n source: srcEl,\n clone,\n children: [],\n isCanvasClone: true,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n return node;\n }\n \n const shadowImg = srcEl.shadowRoot.querySelector(\"img\");\n if (shadowImg?.complete && shadowImg.naturalWidth > 0) {\n const clone = document.createElement(\"canvas\");\n clone.width = shadowImg.naturalWidth;\n clone.height = shadowImg.naturalHeight;\n // Check if the element actually has alpha channel before preserving it\n // For direct img elements, check the element's hasAlpha property\n if (srcEl.tagName === \"EF-IMAGE\" && \"hasAlpha\" in srcEl && (srcEl as any).hasAlpha) {\n clone.dataset.preserveAlpha = \"true\";\n }\n const ctx = clone.getContext(\"2d\");\n if (ctx) {\n try { ctx.drawImage(shadowImg, 0, 0); } catch {}\n }\n \n // Copy initial CSS styles using unified sync\n // Pass shadowImg as contentSource for width/height\n try {\n syncElementStyles(srcEl, clone, shadowImg);\n } catch {}\n \n // Map clone canvas to source element for RenderContext caching\n canvasSourceMap.set(clone, srcEl);\n \n const node: CloneNode = {\n source: srcEl,\n clone,\n children: [],\n isCanvasClone: true,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n return node;\n }\n }\n \n // Standard element clone\n const clone = document.createElement(isCustom ? \"div\" : srcEl.tagName.toLowerCase()) as HTMLElement;\n \n // Copy attributes - OPTIMIZATION: Early exit if no attributes\n const attrs = srcEl.attributes;\n const attrLen = attrs.length;\n if (attrLen > 0) {\n for (let i = 0; i < attrLen; i++) {\n const attr = attrs[i]!;\n const name = attr.name.toLowerCase();\n if (name === \"id\" || name.startsWith(\"on\")) continue;\n if (isCustom && name !== \"class\" && !name.startsWith(\"data-\")) continue;\n try { clone.setAttribute(attr.name, attr.value); } catch {}\n }\n }\n \n if (srcEl instanceof HTMLImageElement && srcEl.src) {\n (clone as HTMLImageElement).src = srcEl.src;\n }\n if (srcEl instanceof HTMLInputElement) {\n (clone as HTMLInputElement).value = srcEl.value;\n }\n \n const node: CloneNode = {\n source: srcEl,\n clone,\n children: [],\n isCanvasClone: false,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n \n // Shadow DOM children - OPTIMIZATION: Early exit if no childNodes\n if (srcEl.shadowRoot) {\n const shadowChildren = srcEl.shadowRoot.childNodes;\n const shadowLen = shadowChildren.length;\n if (shadowLen > 0) {\n // For text segments, ALWAYS create a text node placeholder even if empty.\n // Caption elements now use light DOM, so they don't need special handling here.\n const isTextSegment = srcEl.tagName === 'EF-TEXT-SEGMENT';\n let hasTextNode = false;\n \n for (let i = 0; i < shadowLen; i++) {\n const child = shadowChildren[i]!;\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent?.trim();\n // Always include text for text segments (even if whitespace-only, e.g., \" \")\n if (text || isTextSegment) {\n clone.appendChild(document.createTextNode(child.textContent || \"\"));\n hasTextNode = true;\n }\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n const el = child as Element;\n if (el.tagName === \"STYLE\" || el.tagName === \"SLOT\") continue;\n const childNode = cloneElement(el, node);\n if (childNode) {\n node.children.push(childNode);\n clone.appendChild(childNode.clone);\n }\n }\n }\n \n // For text segments, ensure there's always a text node for syncStyles to update\n if (isTextSegment && !hasTextNode) {\n clone.appendChild(document.createTextNode(\"\"));\n }\n }\n }\n \n // Light DOM children - OPTIMIZATION: Use indexed loop for performance\n const lightChildren = srcEl.childNodes;\n const lightLen = lightChildren.length;\n for (let i = 0; i < lightLen; i++) {\n const child = lightChildren[i]!;\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent?.trim();\n if (text) clone.appendChild(document.createTextNode(text));\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n const childNode = cloneElement(child as Element, node);\n if (childNode) {\n node.children.push(childNode);\n clone.appendChild(childNode.clone);\n }\n }\n }\n \n return node;\n }\n \n const root = cloneElement(source, null);\n if (root) container.appendChild(root.clone);\n \n const syncState: SyncState = {\n tree: { root },\n nodeCount,\n canvasSourceMap,\n previousVisibleSet: new Set(),\n currentVisibleSet: new Set(),\n };\n \n // Sync styles in the same pass if timeMs is provided\n if (timeMs !== undefined && root) {\n syncStylesWithIndex(syncState, timeMs);\n }\n \n return {\n container,\n syncState,\n };\n}\n\n/**\n * Sync a single node's styles (extracted for reuse).\n * Now uses unified style syncing with clear separation of concerns:\n * 1. Canvas pixel refresh (if canvas clone)\n * 2. Unified CSS property sync (all elements)\n * 3. Content sync (text, input values)\n */\nfunction syncNodeStyles(node: CloneNode): void {\n const { source, clone, isCanvasClone } = node;\n \n // 1. Canvas-specific: Refresh pixel content from shadow DOM\n if (isCanvasClone) {\n refreshCanvasPixels(node);\n }\n \n // 2. Unified: Sync ALL CSS properties using SYNC_PROPERTIES array\n // For canvas clones, pass content source (shadow canvas/img) for width/height\n const contentSource = isCanvasClone\n ? (source.shadowRoot?.querySelector(\"canvas\") || source.shadowRoot?.querySelector(\"img\") || undefined)\n : undefined;\n syncElementStyles(source, clone, contentSource);\n \n // 3. Element-specific: Sync text content and input values\n syncTextContent(source, clone);\n syncInputValue(source, clone);\n}\n\n// Performance instrumentation counters\ninterface SyncStats {\n nodesVisited: number;\n nodesCulledByParent: number;\n nodesCulledByTemporal: number;\n nodesProcessed: number;\n nodesFullSync: number; // Newly visible nodes (full sync)\n nodesIncrementalSync: number; // Still visible nodes (incremental sync)\n nodesHidden: number; // Newly hidden nodes\n indexQueryTimeMs: number;\n syncTimeMs: number;\n}\n\nlet syncStats: SyncStats = {\n nodesVisited: 0,\n nodesCulledByParent: 0,\n nodesCulledByTemporal: 0,\n nodesProcessed: 0,\n nodesFullSync: 0,\n nodesIncrementalSync: 0,\n nodesHidden: 0,\n indexQueryTimeMs: 0,\n syncTimeMs: 0,\n};\n\n/**\n * Visibility delta between frames.\n * Used for incremental updates - only sync what changed.\n */\ninterface VisibilityDelta {\n nowVisible: Set<CloneNode>; // Need full style sync + show\n stillVisible: Set<CloneNode>; // Only sync animated properties (or skip if same time)\n nowHidden: Set<CloneNode>; // Just set display:none\n}\n\n/**\n * Compute visibility delta between previous and current frame.\n */\nfunction computeVisibilityDelta(\n previousSet: Set<CloneNode>,\n currentSet: Set<CloneNode>,\n): VisibilityDelta {\n const nowVisible = new Set<CloneNode>();\n const stillVisible = new Set<CloneNode>();\n const nowHidden = new Set<CloneNode>();\n \n // Find nodes that became visible or stayed visible\n for (const node of currentSet) {\n if (previousSet.has(node)) {\n stillVisible.add(node);\n } else {\n nowVisible.add(node);\n }\n }\n \n // Find nodes that became hidden\n for (const node of previousSet) {\n if (!currentSet.has(node)) {\n nowHidden.add(node);\n }\n }\n \n return { nowVisible, stillVisible, nowHidden };\n}\n\n/**\n * Build visible set by recursive traversal with bounds checking.\n * Queries fresh bounds from source elements each time - bounds are computed\n * dynamically by timegroups based on composition mode.\n */\nfunction buildVisibleSetRecursive(\n node: CloneNode,\n timeMs: number,\n visibleSet: Set<CloneNode>,\n): void {\n const { children, source } = node;\n \n // Get fresh bounds from source element (not cached - timegroup bounds are dynamic)\n const bounds = getTemporalBounds(source);\n \n // Check if this node is visible at current time\n const isVisible = timeMs >= bounds.startMs && timeMs <= bounds.endMs;\n \n if (isVisible) {\n visibleSet.add(node);\n // Recurse to children\n for (const child of children) {\n buildVisibleSetRecursive(child, timeMs, visibleSet);\n }\n }\n // If not visible, skip entire subtree\n}\n\n/**\n * Sync styles with recursive visibility check and delta tracking.\n * \n * DELTA TRACKING: Tracks visibility changes between frames to minimize work:\n * - nowVisible nodes: Full style sync + show\n * - stillVisible nodes: Incremental sync (source DOM may have changed)\n * - nowHidden nodes: Just hide (display:none)\n */\nfunction syncStylesWithIndex(state: SyncState, timeMs: number): void {\n const queryStart = performance.now();\n \n // Build the set of visible nodes by recursive traversal\n const visibleSet = new Set<CloneNode>();\n if (state.tree.root) {\n buildVisibleSetRecursive(state.tree.root, timeMs, visibleSet);\n }\n \n // Compute delta from previous frame\n const delta = computeVisibilityDelta(state.previousVisibleSet, visibleSet);\n \n syncStats.indexQueryTimeMs = performance.now() - queryStart;\n \n // Now traverse the tree but use the delta for O(1) sync decisions\n const syncStart = performance.now();\n if (state.tree.root) {\n syncNodeWithDelta(state.tree.root, visibleSet, delta);\n }\n syncStats.syncTimeMs = performance.now() - syncStart;\n \n // Update state for next frame and expose current visible set\n state.previousVisibleSet = visibleSet;\n state.currentVisibleSet = visibleSet;\n}\n\n/**\n * Sync a node using visibility delta for incremental updates.\n * \n * DELTA TRACKING optimization:\n * - nowVisible: Full style sync (element just appeared)\n * - stillVisible: Incremental sync (source DOM may have changed)\n * - nowHidden: Just hide the element\n * - Not in any set: Skip entirely (was already hidden)\n */\nfunction syncNodeWithDelta(\n node: CloneNode,\n visibleSet: Set<CloneNode>,\n delta: VisibilityDelta,\n): void {\n syncStats.nodesVisited++;\n \n const isVisible = visibleSet.has(node);\n \n if (!isVisible) {\n // Node is not visible - ALWAYS set display:none\n // This handles both \"just became hidden\" and \"initial build with node outside time range\"\n node.clone.style.display = \"none\";\n if (delta.nowHidden.has(node)) {\n syncStats.nodesHidden++;\n }\n // Already hidden nodes: skip (don't even recurse to children)\n syncStats.nodesCulledByTemporal++;\n return;\n }\n \n // Node is visible - determine sync strategy\n if (delta.nowVisible.has(node)) {\n // Just became visible - need full style sync\n syncNodeStyles(node);\n syncStats.nodesFullSync++;\n } else if (delta.stillVisible.has(node)) {\n // Was visible, still visible - still need to sync\n // Source DOM properties can change independently of time (input values, text, etc.)\n // TODO: Phase 5 could track property changes for smarter incremental sync\n syncNodeStyles(node);\n syncStats.nodesIncrementalSync++;\n }\n \n syncStats.nodesProcessed++;\n \n // Recurse to children\n for (const child of node.children) {\n syncNodeWithDelta(child, visibleSet, delta);\n }\n}\n\n/**\n * Legacy recursive sync (kept for comparison/fallback).\n * Returns early if the node is temporally culled, skipping ALL descendants.\n * @deprecated Use syncStylesWithIndex for better performance\n */\nexport function syncNodeRecursiveLegacy(node: CloneNode, timeMs: number): void {\n const { clone, children, bounds } = node;\n syncStats.nodesVisited++;\n \n // Temporal culling - check if this node is visible at current time\n // NOTE: Canvas clones now participate in temporal culling (lazy canvas copying).\n // Invalid bounds [0,0] are treated as [-Infinity, Infinity] by getTemporalBounds.\n {\n // OPTIMIZATION: Check if parent is already hidden to skip bounds computation\n const parent = clone.parentElement;\n if (parent instanceof HTMLElement) {\n // If parent has display:none, this element is already hidden - skip bounds check\n if (parent.style.display === \"none\") {\n clone.style.display = \"none\";\n syncStats.nodesCulledByParent++;\n return;\n }\n }\n \n // Use cached bounds from node instead of calling getTemporalBounds\n const { startMs, endMs } = bounds;\n if (timeMs < startMs || timeMs > endMs) {\n // Hide this element and BAIL OUT - skip all descendants automatically!\n clone.style.display = \"none\";\n syncStats.nodesCulledByTemporal++;\n return;\n }\n }\n \n // Sync this node's styles\n syncNodeStyles(node);\n syncStats.nodesProcessed++;\n \n // Recursively sync children\n for (const child of children) {\n syncNodeRecursiveLegacy(child, timeMs);\n }\n}\n\n/**\n * Sync all CSS properties from source elements to their clones.\n * Uses interval index for O(log n + k) visibility queries instead of O(n) traversal.\n * Uses delta tracking for incremental updates between frames.\n */\nexport function syncStyles(state: SyncState, timeMs: number): void {\n // Reset stats\n syncStats = {\n nodesVisited: 0,\n nodesCulledByParent: 0,\n nodesCulledByTemporal: 0,\n nodesProcessed: 0,\n nodesFullSync: 0,\n nodesIncrementalSync: 0,\n nodesHidden: 0,\n indexQueryTimeMs: 0,\n syncTimeMs: 0,\n };\n \n // Use interval-index-based sync with delta tracking\n syncStylesWithIndex(state, timeMs);\n}\n\n/**\n * Collect document styles for shadow DOM injection.\n */\nexport function collectDocumentStyles(): string {\n const rules: string[] = [];\n try {\n for (const sheet of document.styleSheets) {\n try {\n if (sheet.cssRules) {\n for (const rule of sheet.cssRules) {\n rules.push(rule.cssText);\n }\n }\n } catch {}\n }\n } catch {}\n return rules.join(\"\\n\");\n}\n\n\n// Backward-compatible aliases\nexport const syncStaticStyles = syncStyles;\nexport const syncAnimatedStyles = syncStyles;\n\n/**\n * Override clip-path, opacity, and optionally transform on the root clone element.\n * The source may have these properties set for proxy mode or workbench scaling.\n * \n * @param syncState - The sync state containing the clone tree\n * @param fullReset - If true, also resets opacity and transform (for capture operations)\n */\nexport function overrideRootCloneStyles(syncState: SyncState, fullReset: boolean = false): void {\n const rootClone = syncState.tree.root?.clone;\n if (!rootClone) return;\n \n rootClone.style.clipPath = \"none\";\n if (fullReset) {\n rootClone.style.opacity = \"1\";\n rootClone.style.transform = \"none\";\n }\n}\n\n/**\n * Create a live preview of a timegroup with a refresh function.\n * Used by EFWorkbench for the \"computed\" preview mode.\n * \n * @param source - The source timegroup to preview\n * @returns Object with preview container and refresh function\n */\nexport function renderTimegroupPreview(source: Element): {\n container: HTMLDivElement;\n refresh: (timeMs?: number) => void;\n} {\n const { container, syncState } = buildCloneStructure(source);\n \n // Initial style sync\n syncStyles(syncState, 0);\n \n return {\n container,\n refresh: (timeMs?: number) => {\n syncStyles(syncState, timeMs ?? 0);\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"renderTimegroupPreview.js","names":["CSS_SAFE_DEFAULT_VALUES: Record<string, string | string[]>","removed: RemovedNodeInfo[]","cs: CSSStyleDeclaration","contentCs: CSSStyleDeclaration | undefined","srcMap: StylePropertyMapReadOnly","node: CloneNode","node","clone","clone: HTMLElement","syncState: SyncState","syncStats: SyncStats","rules: string[]"],"sources":["../../src/preview/renderTimegroupPreview.ts"],"sourcesContent":["/**\n * Canvas refresh fix:\n * Canvas pixels must be explicitly cleared and redrawn in syncNodeStyles\n * to ensure video frames are captured correctly during foreignObject rendering.\n * Without clearRect, stale frames from previous seeks may be serialized.\n * \n * See FOREIGNOBJECT_BUG_FIX.md for detailed explanation.\n */\n\nimport { logger } from \"./logger.js\";\n\n/**\n * Elements to skip entirely when building the preview.\n */\nconst SKIP_TAGS = new Set([\n \"EF-AUDIO\",\n \"EF-THUMBNAIL-STRIP\",\n \"EF-FILMSTRIP\",\n \"EF-TIMELINE\",\n \"EF-WORKBENCH\",\n \"SCRIPT\",\n \"STYLE\",\n]);\n\n/**\n * All CSS properties to sync (camelCase for style[] access).\n */\nconst SYNC_PROPERTIES = [\n \"display\", \"visibility\", \"opacity\",\n \"position\", \"top\", \"right\", \"bottom\", \"left\", \"zIndex\",\n \"width\", \"height\", \"minWidth\", \"minHeight\", \"maxWidth\", \"maxHeight\",\n \"flexGrow\", \"flexShrink\", \"flexBasis\", \"flexDirection\", \"flexWrap\",\n \"justifyContent\", \"alignItems\", \"alignContent\", \"alignSelf\", \"gap\",\n \"gridTemplate\", \"gridColumn\", \"gridRow\", \"gridArea\",\n \"margin\", \"padding\", \"boxSizing\",\n \"border\", \"borderTop\", \"borderRight\", \"borderBottom\", \"borderLeft\", \"borderRadius\",\n \"background\", \"color\", \"boxShadow\", \"filter\", \"backdropFilter\", \"clipPath\",\n \"fontFamily\", \"fontSize\", \"fontWeight\", \"fontStyle\", \"fontVariant\",\n \"textAlign\", \"textDecoration\", \"textTransform\",\n \"letterSpacing\", \"wordSpacing\", \"whiteSpace\", \"textOverflow\", \"lineHeight\",\n \"verticalAlign\",\n \"transform\", \"transformOrigin\", \"transformStyle\",\n \"perspective\", \"perspectiveOrigin\", \"backfaceVisibility\",\n \"cursor\", \"pointerEvents\", \"userSelect\", \"overflow\",\n] as const;\n\n/**\n * Kebab-case versions for computedStyleMap.get() - pre-computed for speed.\n */\nconst SYNC_PROPERTIES_KEBAB = SYNC_PROPERTIES.map(prop =>\n prop.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)\n);\n\n/**\n * Feature detection: computedStyleMap is ~15% faster for style syncing.\n */\nconst HAS_COMPUTED_STYLE_MAP = typeof Element !== \"undefined\" && typeof Element.prototype.computedStyleMap === \"function\";\n\n/**\n * CSS initial/default values for SAFE-TO-SKIP properties.\n * Only includes NON-INHERITED properties where skipping the default\n * won't affect visual output.\n * \n * EXCLUDED (must always serialize):\n * - Inherited properties (color, font, text-*, visibility, cursor)\n * - Display (affects layout significantly)\n * - Properties where \"auto\" computes to a specific value\n * \n * INCLUDED (safe to skip):\n * - Transform/filter effects (none = no effect)\n * - Box shadows (none = no shadow)\n * - Borders when none/0 (no visual impact)\n * - backdrop-filter (none = no effect)\n */\nconst CSS_SAFE_DEFAULT_VALUES: Record<string, string | string[]> = {\n // Transforms & effects - safe to skip \"none\" (no visual impact)\n transform: \"none\",\n filter: \"none\",\n backdropFilter: \"none\",\n boxShadow: \"none\",\n \n // Borders - safe to skip when none/0\n border: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderTop: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderRight: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderBottom: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderLeft: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderRadius: [\"0px\", \"0\"],\n \n // Positioning - safe to skip \"static\"\n position: \"static\",\n \n // Z-index - \"auto\" is safe when position is static\n zIndex: \"auto\",\n \n // 3D transforms - safe to skip defaults\n transformStyle: \"flat\",\n perspective: \"none\",\n backfaceVisibility: \"visible\",\n};\n\n/**\n * Check if a value matches a safe-to-skip default.\n */\nfunction isDefaultValue(prop: string, value: string): boolean {\n const defaults = CSS_SAFE_DEFAULT_VALUES[prop];\n if (!defaults) return false;\n if (Array.isArray(defaults)) {\n return defaults.includes(value);\n }\n return defaults === value;\n}\n\n// Re-export temporal types from shared module\nexport {\n type TemporalElement,\n isTemporal,\n getTemporalBounds,\n isVisibleAtTime,\n} from \"./previewTypes.js\";\n\n// Import for internal use\nimport {\n getTemporalBounds,\n} from \"./previewTypes.js\";\n\n/**\n * Tree node representing a source/clone pair with children.\n * This replaces the flat array approach for cleaner recursive traversal.\n */\nexport interface CloneNode {\n source: Element;\n clone: HTMLElement;\n children: CloneNode[];\n isCanvasClone: boolean;\n /** Cached temporal bounds for this node */\n bounds: { startMs: number; endMs: number };\n /** Parent node reference for ancestor visibility checks */\n parent: CloneNode | null;\n}\n\n/** Tree-based sync state */\nexport interface CloneTree {\n root: CloneNode | null;\n}\n\n/** Sync state with tree structure and delta tracking */\nexport interface SyncState {\n tree: CloneTree;\n nodeCount: number; // Total number of nodes (for debugging/logging)\n /** Maps clone canvases to their original source elements (ef-video, ef-image, etc.) */\n canvasSourceMap: WeakMap<HTMLCanvasElement, Element>;\n /** Previous frame's visible set for delta tracking */\n previousVisibleSet: Set<CloneNode>;\n /** Current frame's visible set (updated by syncStyles) */\n currentVisibleSet: Set<CloneNode>;\n}\n\n/** Info needed to restore a removed node */\ninterface RemovedNodeInfo {\n node: CloneNode;\n parent: Node;\n nextSibling: Node | null;\n}\n\n/**\n * Remove hidden nodes from the clone DOM for serialization.\n * Returns info needed to restore them afterward.\n * \n * This physically removes non-visible nodes so they won't be serialized,\n * avoiding the cost of serializing hidden elements and their resources.\n */\nexport function removeHiddenNodesForSerialization(state: SyncState): RemovedNodeInfo[] {\n const removed: RemovedNodeInfo[] = [];\n const visibleSet = state.currentVisibleSet;\n \n // Traverse all nodes and remove those not in visible set\n function visit(node: CloneNode): void {\n // First recurse to children (before potentially removing this node)\n for (const child of node.children) {\n visit(child);\n }\n \n // If this node isn't visible, remove it from DOM\n if (!visibleSet.has(node)) {\n const parent = node.clone.parentNode;\n if (parent) {\n const nextSibling = node.clone.nextSibling;\n parent.removeChild(node.clone);\n removed.push({ node, parent, nextSibling });\n }\n }\n }\n \n if (state.tree.root) {\n visit(state.tree.root);\n }\n \n return removed;\n}\n\n/**\n * Restore previously removed hidden nodes to the clone DOM.\n * Must be called after serialization to maintain tree integrity for next frame.\n */\nexport function restoreHiddenNodes(removed: RemovedNodeInfo[]): void {\n // Restore in reverse order to maintain correct DOM positions\n for (let i = removed.length - 1; i >= 0; i--) {\n const { node, parent, nextSibling } = removed[i]!;\n if (nextSibling) {\n parent.insertBefore(node.clone, nextSibling);\n } else {\n parent.appendChild(node.clone);\n }\n }\n}\n\n/**\n * Get visible canvases from the current visible set.\n * Use this to skip encoding hidden canvases during serialization.\n */\nexport function getVisibleCanvases(state: SyncState): Set<HTMLCanvasElement> {\n const visibleCanvases = new Set<HTMLCanvasElement>();\n for (const node of state.currentVisibleSet) {\n if (node.clone instanceof HTMLCanvasElement) {\n visibleCanvases.add(node.clone);\n }\n }\n return visibleCanvases;\n}\n\n/**\n * Traverse all nodes in the clone tree, calling the callback for each.\n */\nexport function traverseCloneTree(state: SyncState, callback: (node: CloneNode) => void): void {\n function visit(node: CloneNode): void {\n callback(node);\n for (const child of node.children) {\n visit(child);\n }\n }\n if (state.tree.root) {\n visit(state.tree.root);\n }\n}\n\n/**\n * Unified CSS property sync for all elements (canvas clones and regular elements).\n * \n * Canvas clones use a limited property set matching the original implementation\n * to avoid dimension/layout issues. Regular elements use the full SYNC_PROPERTIES array.\n * \n * @param source - Source element to read styles from\n * @param clone - Clone element to write styles to\n * @param contentSource - Optional content element for width/height (canvas clones only)\n */\nfunction syncElementStyles(\n source: Element,\n clone: HTMLElement,\n contentSource?: Element,\n): void {\n const cloneStyle = clone.style as any;\n const tagName = (source as HTMLElement).tagName;\n const isCanvasClone = !!contentSource;\n \n // Canvas clones: Use exact property list from original implementation\n if (isCanvasClone) {\n let cs: CSSStyleDeclaration;\n let contentCs: CSSStyleDeclaration | undefined;\n \n try {\n cs = getComputedStyle(source);\n if (contentSource) {\n contentCs = getComputedStyle(contentSource);\n }\n } catch { return; }\n \n // Exact properties from original copyCanvasCloneStyles + syncNodeStyles\n cloneStyle.position = cs.position;\n cloneStyle.top = cs.top;\n cloneStyle.right = cs.right;\n cloneStyle.bottom = cs.bottom;\n cloneStyle.left = cs.left;\n cloneStyle.margin = cs.margin;\n cloneStyle.zIndex = cs.zIndex;\n cloneStyle.transform = cs.transform;\n cloneStyle.transformOrigin = cs.transformOrigin;\n cloneStyle.opacity = cs.opacity;\n cloneStyle.visibility = cs.visibility;\n cloneStyle.backfaceVisibility = cs.backfaceVisibility;\n cloneStyle.transformStyle = cs.transformStyle;\n \n // Visual properties (safe for canvas clones - don't affect dimensions)\n cloneStyle.background = cs.background;\n cloneStyle.color = cs.color;\n cloneStyle.boxShadow = cs.boxShadow;\n cloneStyle.filter = cs.filter;\n cloneStyle.backdropFilter = cs.backdropFilter;\n \n // Width/height from content source (shadow canvas/img)\n if (contentCs) {\n cloneStyle.width = contentCs.width;\n cloneStyle.height = contentCs.height;\n }\n \n cloneStyle.display = \"block\";\n cloneStyle.animation = \"none\";\n cloneStyle.transition = \"none\";\n \n return;\n }\n \n // Regular elements: full property sync from SYNC_PROPERTIES\n const propLen = SYNC_PROPERTIES.length;\n \n if (HAS_COMPUTED_STYLE_MAP) {\n let srcMap: StylePropertyMapReadOnly;\n \n try {\n srcMap = source.computedStyleMap();\n } catch { return; }\n \n for (let j = 0; j < propLen; j++) {\n const kebab = SYNC_PROPERTIES_KEBAB[j]!;\n const camel = SYNC_PROPERTIES[j]!;\n \n const srcVal = srcMap.get(kebab);\n if (!srcVal) continue;\n \n const strVal = srcVal.toString();\n \n if (camel === \"display\") {\n // For caption child elements, preserve display:none when explicitly set\n // (they use it to hide empty content, not for temporal visibility)\n const isCaptionChild = tagName && (\n tagName === 'EF-CAPTIONS-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-BEFORE-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-AFTER-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-SEGMENT'\n );\n const targetDisplay = (strVal === \"none\" && !isCaptionChild) ? \"block\" : strVal;\n cloneStyle.display = targetDisplay;\n continue;\n }\n \n // Skip clipPath - clones always have clipPath: none for rendering\n // (source may have clip-path: inset(100%) from proxy mode)\n if (camel === \"clipPath\") continue;\n \n // OPTIMIZATION: Skip default values to reduce serialized HTML size\n // If the computed value is the CSS default, don't set it as inline style\n if (isDefaultValue(camel, strVal)) {\n // Remove from inline style if it was previously set\n if (cloneStyle[camel]) cloneStyle[camel] = \"\";\n continue;\n }\n \n cloneStyle[camel] = strVal;\n }\n } else {\n let cs: CSSStyleDeclaration;\n \n try {\n cs = getComputedStyle(source);\n } catch { return; }\n \n const srcStyle = cs as any;\n \n for (const prop of SYNC_PROPERTIES) {\n const srcVal = srcStyle[prop];\n if (!srcVal) continue;\n \n if (prop === \"display\") {\n // For caption child elements, preserve display:none when explicitly set\n // (they use it to hide empty content, not for temporal visibility)\n const isCaptionChild = tagName && (\n tagName === 'EF-CAPTIONS-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-BEFORE-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-AFTER-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-SEGMENT'\n );\n const targetDisplay = (srcVal === \"none\" && !isCaptionChild) ? \"block\" : srcVal;\n cloneStyle.display = targetDisplay;\n continue;\n }\n \n // Skip clipPath - clones always have clipPath: none for rendering\n // (source may have clip-path: inset(100%) from proxy mode)\n if (prop === \"clipPath\") continue;\n \n // OPTIMIZATION: Skip default values to reduce serialized HTML size\n if (isDefaultValue(prop, srcVal)) {\n if (cloneStyle[prop]) cloneStyle[prop] = \"\";\n continue;\n }\n \n cloneStyle[prop] = srcVal;\n }\n }\n \n // Disable animations/transitions to prevent re-animation\n cloneStyle.animation = \"none\";\n cloneStyle.transition = \"none\";\n}\n\n/**\n * Refresh canvas pixel content from shadow DOM source.\n * Handles both shadow canvas and shadow img sources.\n */\nfunction refreshCanvasPixels(node: CloneNode): void {\n const { source, clone } = node;\n const canvas = clone as HTMLCanvasElement;\n const shadowCanvas = source.shadowRoot?.querySelector(\"canvas\");\n const shadowImg = source.shadowRoot?.querySelector(\"img\");\n \n if (shadowCanvas) {\n // Update buffer dimensions if needed\n if (canvas.width !== shadowCanvas.width) canvas.width = shadowCanvas.width;\n if (canvas.height !== shadowCanvas.height) canvas.height = shadowCanvas.height;\n \n // Copy pixels with explicit clear\n const ctx = canvas.getContext(\"2d\");\n if (ctx && shadowCanvas.width > 0 && shadowCanvas.height > 0) {\n try {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.drawImage(shadowCanvas, 0, 0);\n } catch (e) {\n logger.warn(\"[refreshCanvasPixels] Canvas draw failed:\", e);\n }\n }\n } else if (shadowImg?.complete && shadowImg.naturalWidth > 0) {\n // Update buffer dimensions if needed\n if (canvas.width !== shadowImg.naturalWidth) canvas.width = shadowImg.naturalWidth;\n if (canvas.height !== shadowImg.naturalHeight) canvas.height = shadowImg.naturalHeight;\n \n // Copy pixels with explicit clear\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n try { ctx.drawImage(shadowImg, 0, 0); } catch {}\n }\n }\n}\n\n/**\n * Sync text content from light DOM to clone.\n */\nfunction syncTextContent(source: Element, clone: HTMLElement): void {\n const srcTextNode = source.childNodes[0];\n if (srcTextNode?.nodeType === Node.TEXT_NODE) {\n const srcText = srcTextNode.textContent || \"\";\n const cloneTextNode = clone.childNodes[0];\n \n if (cloneTextNode?.nodeType === Node.TEXT_NODE) {\n // Update existing text node\n if (cloneTextNode.textContent !== srcText) cloneTextNode.textContent = srcText;\n } else if (!clone.childNodes.length) {\n // Only create text node if clone has NO children (was empty when initially cloned)\n // Don't set textContent as it would delete element children!\n clone.appendChild(document.createTextNode(srcText));\n }\n }\n}\n\n/**\n * Sync input element value.\n */\nfunction syncInputValue(source: Element, clone: HTMLElement): void {\n if (source instanceof HTMLInputElement) {\n const srcVal = source.value;\n const cloneInput = clone as HTMLInputElement;\n if (cloneInput.value !== srcVal) {\n cloneInput.value = srcVal;\n cloneInput.setAttribute(\"value\", srcVal);\n }\n }\n}\n\n/**\n * Build clone tree structure with minimal overhead.\n * Caches temporal bounds on each node for visibility checks.\n * Optionally syncs styles in the same pass if timeMs is provided.\n */\nexport function buildCloneStructure(source: Element, timeMs?: number): {\n container: HTMLDivElement;\n syncState: SyncState;\n} {\n const container = document.createElement(\"div\");\n container.style.cssText = \"position:absolute;top:0;left:0;width:100%;height:100%\";\n \n let nodeCount = 0;\n const canvasSourceMap = new WeakMap<HTMLCanvasElement, Element>();\n \n function cloneElement(srcEl: Element, parentNode: CloneNode | null): CloneNode | null {\n if (SKIP_TAGS.has(srcEl.tagName)) return null;\n \n // Get temporal bounds upfront for indexing\n const bounds = getTemporalBounds(srcEl);\n \n // Canvas - copy pixels\n // NOTE: Raw canvases are always recopied (no caching) since we can't detect when their content changes.\n // Long-term solution: Create EFCanvas wrapper element to track modifications.\n if (srcEl instanceof HTMLCanvasElement) {\n const canvas = document.createElement(\"canvas\");\n // Use intrinsic buffer dimensions (not affected by zoom/transforms)\n canvas.width = srcEl.width;\n canvas.height = srcEl.height;\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n try { ctx.drawImage(srcEl, 0, 0); } catch {}\n }\n \n // Set explicit CSS dimensions based on buffer size to avoid zoom-affected computed styles\n // This ensures the canvas renders at its natural size regardless of workspace zoom\n canvas.style.width = `${srcEl.width}px`;\n canvas.style.height = `${srcEl.height}px`;\n \n // Sync positioning/transform styles from source, but dimensions are already set above\n try {\n const cs = getComputedStyle(srcEl);\n canvas.style.position = cs.position;\n canvas.style.top = cs.top;\n canvas.style.right = cs.right;\n canvas.style.bottom = cs.bottom;\n canvas.style.left = cs.left;\n canvas.style.margin = cs.margin;\n canvas.style.zIndex = cs.zIndex;\n canvas.style.transform = cs.transform;\n canvas.style.transformOrigin = cs.transformOrigin;\n canvas.style.opacity = cs.opacity;\n canvas.style.visibility = cs.visibility;\n canvas.style.display = \"block\";\n } catch {}\n \n // Map clone canvas to source for RenderContext (though caching won't help here)\n canvasSourceMap.set(canvas, srcEl);\n \n const node: CloneNode = {\n source: srcEl,\n clone: canvas,\n children: [],\n isCanvasClone: true,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n return node;\n }\n \n // Custom elements with shadow canvas (e.g., ef-video, ef-image)\n const isCustom = srcEl.tagName.includes(\"-\");\n if (isCustom && srcEl.shadowRoot) {\n const shadowCanvas = srcEl.shadowRoot.querySelector(\"canvas\");\n if (shadowCanvas) {\n const clone = document.createElement(\"canvas\");\n clone.width = shadowCanvas.width || srcEl.clientWidth;\n clone.height = shadowCanvas.height || srcEl.clientHeight;\n // Check if the element actually has alpha channel before preserving it\n // ef-image tracks hasAlpha based on MIME type (JPEG=false, PNG/WebP=true)\n // ef-waveform always needs alpha for proper rendering\n if (srcEl.tagName === \"EF-WAVEFORM\") {\n clone.dataset.preserveAlpha = \"true\";\n } else if (srcEl.tagName === \"EF-IMAGE\") {\n const hasAlpha = \"hasAlpha\" in srcEl && (srcEl as any).hasAlpha;\n if (hasAlpha) {\n clone.dataset.preserveAlpha = \"true\";\n }\n console.log(`[buildCloneStructure] EF-IMAGE canvas: size=${clone.width}x${clone.height}, hasAlpha=${hasAlpha}, preserveAlpha=${hasAlpha ? 'PNG' : 'JPEG'}`);\n }\n \n const ctx = clone.getContext(\"2d\");\n if (ctx) {\n try { ctx.drawImage(shadowCanvas, 0, 0); } catch {}\n }\n \n // Copy initial CSS styles using unified sync\n // Pass shadowCanvas as contentSource for width/height\n try {\n syncElementStyles(srcEl, clone, shadowCanvas);\n } catch {}\n \n // Map clone canvas to source element for RenderContext caching\n canvasSourceMap.set(clone, srcEl);\n \n const node: CloneNode = {\n source: srcEl,\n clone,\n children: [],\n isCanvasClone: true,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n return node;\n }\n \n const shadowImg = srcEl.shadowRoot.querySelector(\"img\");\n if (shadowImg?.complete && shadowImg.naturalWidth > 0) {\n const clone = document.createElement(\"canvas\");\n clone.width = shadowImg.naturalWidth;\n clone.height = shadowImg.naturalHeight;\n // Check if the element actually has alpha channel before preserving it\n // For direct img elements, check the element's hasAlpha property\n if (srcEl.tagName === \"EF-IMAGE\") {\n const hasAlpha = \"hasAlpha\" in srcEl && (srcEl as any).hasAlpha;\n if (hasAlpha) {\n clone.dataset.preserveAlpha = \"true\";\n }\n }\n const ctx = clone.getContext(\"2d\");\n if (ctx) {\n try { ctx.drawImage(shadowImg, 0, 0); } catch {}\n }\n \n // Copy initial CSS styles using unified sync\n // Pass shadowImg as contentSource for width/height\n try {\n syncElementStyles(srcEl, clone, shadowImg);\n } catch {}\n \n // Map clone canvas to source element for RenderContext caching\n canvasSourceMap.set(clone, srcEl);\n \n const node: CloneNode = {\n source: srcEl,\n clone,\n children: [],\n isCanvasClone: true,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n return node;\n }\n }\n \n // Standard element clone\n // SVG elements need createElementNS, HTML elements use createElement\n let clone: HTMLElement;\n if (srcEl instanceof SVGElement) {\n clone = document.createElementNS(\"http://www.w3.org/2000/svg\", srcEl.tagName) as unknown as HTMLElement;\n } else {\n clone = document.createElement(isCustom ? \"div\" : srcEl.tagName.toLowerCase()) as HTMLElement;\n }\n \n // Copy attributes - OPTIMIZATION: Early exit if no attributes\n const attrs = srcEl.attributes;\n const attrLen = attrs.length;\n if (attrLen > 0) {\n for (let i = 0; i < attrLen; i++) {\n const attr = attrs[i]!;\n const name = attr.name.toLowerCase();\n if (name === \"id\" || name.startsWith(\"on\")) continue;\n if (isCustom && name !== \"class\" && !name.startsWith(\"data-\")) continue;\n try { clone.setAttribute(attr.name, attr.value); } catch {}\n }\n }\n \n if (srcEl instanceof HTMLImageElement && srcEl.src) {\n (clone as HTMLImageElement).src = srcEl.src;\n }\n if (srcEl instanceof HTMLInputElement) {\n (clone as HTMLInputElement).value = srcEl.value;\n }\n \n const node: CloneNode = {\n source: srcEl,\n clone,\n children: [],\n isCanvasClone: false,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n \n // Shadow DOM children - OPTIMIZATION: Early exit if no childNodes\n if (srcEl.shadowRoot) {\n const shadowChildren = srcEl.shadowRoot.childNodes;\n const shadowLen = shadowChildren.length;\n if (shadowLen > 0) {\n // For text segments, ALWAYS create a text node placeholder even if empty.\n // Caption elements now use light DOM, so they don't need special handling here.\n const isTextSegment = srcEl.tagName === 'EF-TEXT-SEGMENT';\n let hasTextNode = false;\n \n for (let i = 0; i < shadowLen; i++) {\n const child = shadowChildren[i]!;\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent?.trim();\n // Always include text for text segments (even if whitespace-only, e.g., \" \")\n if (text || isTextSegment) {\n clone.appendChild(document.createTextNode(child.textContent || \"\"));\n hasTextNode = true;\n }\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n const el = child as Element;\n if (el.tagName === \"STYLE\" || el.tagName === \"SLOT\") continue;\n const childNode = cloneElement(el, node);\n if (childNode) {\n node.children.push(childNode);\n clone.appendChild(childNode.clone);\n }\n }\n }\n \n // For text segments, ensure there's always a text node for syncStyles to update\n if (isTextSegment && !hasTextNode) {\n clone.appendChild(document.createTextNode(\"\"));\n }\n }\n }\n \n // Light DOM children - OPTIMIZATION: Use indexed loop for performance\n const lightChildren = srcEl.childNodes;\n const lightLen = lightChildren.length;\n for (let i = 0; i < lightLen; i++) {\n const child = lightChildren[i]!;\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent?.trim();\n if (text) clone.appendChild(document.createTextNode(text));\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n const childNode = cloneElement(child as Element, node);\n if (childNode) {\n node.children.push(childNode);\n clone.appendChild(childNode.clone);\n }\n }\n }\n \n return node;\n }\n \n const root = cloneElement(source, null);\n if (root) container.appendChild(root.clone);\n \n const syncState: SyncState = {\n tree: { root },\n nodeCount,\n canvasSourceMap,\n previousVisibleSet: new Set(),\n currentVisibleSet: new Set(),\n };\n \n // Sync styles in the same pass if timeMs is provided\n if (timeMs !== undefined && root) {\n syncStylesWithIndex(syncState, timeMs);\n }\n \n return {\n container,\n syncState,\n };\n}\n\n/**\n * Sync a single node's styles (extracted for reuse).\n * Now uses unified style syncing with clear separation of concerns:\n * 1. Canvas pixel refresh (if canvas clone)\n * 2. Unified CSS property sync (all elements)\n * 3. Content sync (text, input values)\n */\nfunction syncNodeStyles(node: CloneNode): void {\n const { source, clone, isCanvasClone } = node;\n \n // 1. Canvas-specific: Refresh pixel content from shadow DOM\n if (isCanvasClone) {\n refreshCanvasPixels(node);\n }\n \n // 2. Unified: Sync ALL CSS properties using SYNC_PROPERTIES array\n // For canvas clones, pass content source (shadow canvas/img) for width/height\n const contentSource = isCanvasClone\n ? (source.shadowRoot?.querySelector(\"canvas\") || source.shadowRoot?.querySelector(\"img\") || undefined)\n : undefined;\n syncElementStyles(source, clone, contentSource);\n \n // 3. Element-specific: Sync text content and input values\n syncTextContent(source, clone);\n syncInputValue(source, clone);\n}\n\n// Performance instrumentation counters\ninterface SyncStats {\n nodesVisited: number;\n nodesCulledByParent: number;\n nodesCulledByTemporal: number;\n nodesProcessed: number;\n nodesFullSync: number; // Newly visible nodes (full sync)\n nodesIncrementalSync: number; // Still visible nodes (incremental sync)\n nodesHidden: number; // Newly hidden nodes\n indexQueryTimeMs: number;\n syncTimeMs: number;\n}\n\nlet syncStats: SyncStats = {\n nodesVisited: 0,\n nodesCulledByParent: 0,\n nodesCulledByTemporal: 0,\n nodesProcessed: 0,\n nodesFullSync: 0,\n nodesIncrementalSync: 0,\n nodesHidden: 0,\n indexQueryTimeMs: 0,\n syncTimeMs: 0,\n};\n\n/**\n * Visibility delta between frames.\n * Used for incremental updates - only sync what changed.\n */\ninterface VisibilityDelta {\n nowVisible: Set<CloneNode>; // Need full style sync + show\n stillVisible: Set<CloneNode>; // Only sync animated properties (or skip if same time)\n nowHidden: Set<CloneNode>; // Just set display:none\n}\n\n/**\n * Compute visibility delta between previous and current frame.\n */\nfunction computeVisibilityDelta(\n previousSet: Set<CloneNode>,\n currentSet: Set<CloneNode>,\n): VisibilityDelta {\n const nowVisible = new Set<CloneNode>();\n const stillVisible = new Set<CloneNode>();\n const nowHidden = new Set<CloneNode>();\n \n // Find nodes that became visible or stayed visible\n for (const node of currentSet) {\n if (previousSet.has(node)) {\n stillVisible.add(node);\n } else {\n nowVisible.add(node);\n }\n }\n \n // Find nodes that became hidden\n for (const node of previousSet) {\n if (!currentSet.has(node)) {\n nowHidden.add(node);\n }\n }\n \n return { nowVisible, stillVisible, nowHidden };\n}\n\n/**\n * Build visible set by recursive traversal with bounds checking.\n * Queries fresh bounds from source elements each time - bounds are computed\n * dynamically by timegroups based on composition mode.\n */\nfunction buildVisibleSetRecursive(\n node: CloneNode,\n timeMs: number,\n visibleSet: Set<CloneNode>,\n): void {\n const { children, source } = node;\n \n // Get fresh bounds from source element (not cached - timegroup bounds are dynamic)\n const bounds = getTemporalBounds(source);\n \n // Check if this node is visible at current time\n const isVisible = timeMs >= bounds.startMs && timeMs <= bounds.endMs;\n \n if (isVisible) {\n visibleSet.add(node);\n // Recurse to children\n for (const child of children) {\n buildVisibleSetRecursive(child, timeMs, visibleSet);\n }\n }\n // If not visible, skip entire subtree\n}\n\n/**\n * Sync styles with recursive visibility check and delta tracking.\n * \n * DELTA TRACKING: Tracks visibility changes between frames to minimize work:\n * - nowVisible nodes: Full style sync + show\n * - stillVisible nodes: Incremental sync (source DOM may have changed)\n * - nowHidden nodes: Just hide (display:none)\n */\nfunction syncStylesWithIndex(state: SyncState, timeMs: number): void {\n const queryStart = performance.now();\n \n // Build the set of visible nodes by recursive traversal\n const visibleSet = new Set<CloneNode>();\n if (state.tree.root) {\n buildVisibleSetRecursive(state.tree.root, timeMs, visibleSet);\n }\n \n // Compute delta from previous frame\n const delta = computeVisibilityDelta(state.previousVisibleSet, visibleSet);\n \n syncStats.indexQueryTimeMs = performance.now() - queryStart;\n \n // Now traverse the tree but use the delta for O(1) sync decisions\n const syncStart = performance.now();\n if (state.tree.root) {\n syncNodeWithDelta(state.tree.root, visibleSet, delta);\n }\n syncStats.syncTimeMs = performance.now() - syncStart;\n \n // Update state for next frame and expose current visible set\n state.previousVisibleSet = visibleSet;\n state.currentVisibleSet = visibleSet;\n}\n\n/**\n * Sync a node using visibility delta for incremental updates.\n * \n * DELTA TRACKING optimization:\n * - nowVisible: Full style sync (element just appeared)\n * - stillVisible: Incremental sync (source DOM may have changed)\n * - nowHidden: Just hide the element\n * - Not in any set: Skip entirely (was already hidden)\n */\nfunction syncNodeWithDelta(\n node: CloneNode,\n visibleSet: Set<CloneNode>,\n delta: VisibilityDelta,\n): void {\n syncStats.nodesVisited++;\n \n const isVisible = visibleSet.has(node);\n \n if (!isVisible) {\n // Node is not visible - ALWAYS set display:none\n // This handles both \"just became hidden\" and \"initial build with node outside time range\"\n node.clone.style.display = \"none\";\n if (delta.nowHidden.has(node)) {\n syncStats.nodesHidden++;\n }\n // Already hidden nodes: skip (don't even recurse to children)\n syncStats.nodesCulledByTemporal++;\n return;\n }\n \n // Node is visible - determine sync strategy\n if (delta.nowVisible.has(node)) {\n // Just became visible - need full style sync\n syncNodeStyles(node);\n syncStats.nodesFullSync++;\n } else if (delta.stillVisible.has(node)) {\n // Was visible, still visible - still need to sync\n // Source DOM properties can change independently of time (input values, text, etc.)\n // TODO: Phase 5 could track property changes for smarter incremental sync\n syncNodeStyles(node);\n syncStats.nodesIncrementalSync++;\n }\n \n syncStats.nodesProcessed++;\n \n // Recurse to children\n for (const child of node.children) {\n syncNodeWithDelta(child, visibleSet, delta);\n }\n}\n\n/**\n * Legacy recursive sync (kept for comparison/fallback).\n * Returns early if the node is temporally culled, skipping ALL descendants.\n * @deprecated Use syncStylesWithIndex for better performance\n */\nexport function syncNodeRecursiveLegacy(node: CloneNode, timeMs: number): void {\n const { clone, children, bounds } = node;\n syncStats.nodesVisited++;\n \n // Temporal culling - check if this node is visible at current time\n // NOTE: Canvas clones now participate in temporal culling (lazy canvas copying).\n // Invalid bounds [0,0] are treated as [-Infinity, Infinity] by getTemporalBounds.\n {\n // OPTIMIZATION: Check if parent is already hidden to skip bounds computation\n const parent = clone.parentElement;\n if (parent instanceof HTMLElement) {\n // If parent has display:none, this element is already hidden - skip bounds check\n if (parent.style.display === \"none\") {\n clone.style.display = \"none\";\n syncStats.nodesCulledByParent++;\n return;\n }\n }\n \n // Use cached bounds from node instead of calling getTemporalBounds\n const { startMs, endMs } = bounds;\n if (timeMs < startMs || timeMs > endMs) {\n // Hide this element and BAIL OUT - skip all descendants automatically!\n clone.style.display = \"none\";\n syncStats.nodesCulledByTemporal++;\n return;\n }\n }\n \n // Sync this node's styles\n syncNodeStyles(node);\n syncStats.nodesProcessed++;\n \n // Recursively sync children\n for (const child of children) {\n syncNodeRecursiveLegacy(child, timeMs);\n }\n}\n\n/**\n * Sync all CSS properties from source elements to their clones.\n * Uses interval index for O(log n + k) visibility queries instead of O(n) traversal.\n * Uses delta tracking for incremental updates between frames.\n */\nexport function syncStyles(state: SyncState, timeMs: number): void {\n // Reset stats\n syncStats = {\n nodesVisited: 0,\n nodesCulledByParent: 0,\n nodesCulledByTemporal: 0,\n nodesProcessed: 0,\n nodesFullSync: 0,\n nodesIncrementalSync: 0,\n nodesHidden: 0,\n indexQueryTimeMs: 0,\n syncTimeMs: 0,\n };\n \n // Use interval-index-based sync with delta tracking\n syncStylesWithIndex(state, timeMs);\n}\n\n/**\n * Collect document styles for shadow DOM injection.\n */\nexport function collectDocumentStyles(): string {\n const rules: string[] = [];\n try {\n for (const sheet of document.styleSheets) {\n try {\n if (sheet.cssRules) {\n for (const rule of sheet.cssRules) {\n rules.push(rule.cssText);\n }\n }\n } catch {}\n }\n } catch {}\n return rules.join(\"\\n\");\n}\n\n\n// Backward-compatible aliases\nexport const syncStaticStyles = syncStyles;\nexport const syncAnimatedStyles = syncStyles;\n\n/**\n * Override clip-path, opacity, and optionally transform on the root clone element.\n * The source may have these properties set for proxy mode or workbench scaling.\n * \n * @param syncState - The sync state containing the clone tree\n * @param fullReset - If true, also resets opacity and transform (for capture operations)\n */\nexport function overrideRootCloneStyles(syncState: SyncState, fullReset: boolean = false): void {\n const rootClone = syncState.tree.root?.clone;\n if (!rootClone) return;\n \n rootClone.style.clipPath = \"none\";\n if (fullReset) {\n rootClone.style.opacity = \"1\";\n rootClone.style.transform = \"none\";\n }\n}\n\n/**\n * Create a live preview of a timegroup with a refresh function.\n * Used by EFWorkbench for the \"computed\" preview mode.\n * \n * @param source - The source timegroup to preview\n * @returns Object with preview container and refresh function\n */\nexport function renderTimegroupPreview(source: Element): {\n container: HTMLDivElement;\n refresh: (timeMs?: number) => void;\n} {\n const { container, syncState } = buildCloneStructure(source);\n \n // Initial style sync\n syncStyles(syncState, 0);\n \n return {\n container,\n refresh: (timeMs?: number) => {\n syncStyles(syncState, timeMs ?? 0);\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { FrameController } from "./FrameController.js";
|
|
2
2
|
import { logger } from "./logger.js";
|
|
3
3
|
import { DEFAULT_BLOCKING_TIMEOUT_MS, DEFAULT_HEIGHT, DEFAULT_THUMBNAIL_SCALE, DEFAULT_WIDTH, createPreviewContainer, isVisibleAtTime } from "./previewTypes.js";
|
|
4
|
-
import { buildCloneStructure, collectDocumentStyles, overrideRootCloneStyles, removeHiddenNodesForSerialization, restoreHiddenNodes
|
|
4
|
+
import { buildCloneStructure, collectDocumentStyles, overrideRootCloneStyles, removeHiddenNodesForSerialization, restoreHiddenNodes } from "./renderTimegroupPreview.js";
|
|
5
5
|
import { getEffectiveRenderMode } from "./renderers.js";
|
|
6
6
|
import { RenderContext } from "./RenderContext.js";
|
|
7
7
|
import { defaultProfiler } from "./RenderProfiler.js";
|
|
8
8
|
import { createDprCanvas, renderToImageNative } from "./rendering/renderToImageNative.js";
|
|
9
9
|
import { clearInlineImageCache } from "./rendering/inlineImages.js";
|
|
10
|
-
import { loadImageFromDataUri, renderToImage
|
|
10
|
+
import { loadImageFromDataUri, renderToImage } from "./rendering/renderToImage.js";
|
|
11
11
|
|
|
12
12
|
//#region src/preview/renderTimegroupToCanvas.ts
|
|
13
13
|
/** Number of rows to sample when checking canvas content */
|
|
@@ -187,7 +187,6 @@ async function captureFromClone(renderClone, renderContainer, options = {}) {
|
|
|
187
187
|
const ctx = canvas.getContext("2d");
|
|
188
188
|
if (!ctx) throw new Error("Failed to get canvas 2d context");
|
|
189
189
|
const timeMs = renderClone.currentTimeMs;
|
|
190
|
-
await new FrameController(renderClone).renderFrame(timeMs, { waitForLitUpdate: false });
|
|
191
190
|
if (contentReadyMode === "blocking") {
|
|
192
191
|
const result = await waitForVideoContent(renderClone, timeMs, blockingTimeoutMs);
|
|
193
192
|
if (!result.ready) throw new ContentNotReadyError(timeMs, blockingTimeoutMs, result.blankVideos);
|
|
@@ -208,7 +207,7 @@ async function captureFromClone(renderClone, renderContainer, options = {}) {
|
|
|
208
207
|
image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: true });
|
|
209
208
|
} else {
|
|
210
209
|
const t0 = performance.now();
|
|
211
|
-
const { container, syncState } = buildCloneStructure(renderClone, timeMs);
|
|
210
|
+
const { container: container$1, syncState } = buildCloneStructure(renderClone, timeMs);
|
|
212
211
|
const buildTime = performance.now() - t0;
|
|
213
212
|
const bgSource = originalTimegroup ?? renderClone;
|
|
214
213
|
const previewContainer = createPreviewContainer({
|
|
@@ -221,7 +220,7 @@ async function captureFromClone(renderClone, renderContainer, options = {}) {
|
|
|
221
220
|
styleEl.textContent = collectDocumentStyles();
|
|
222
221
|
const stylesTime = performance.now() - t1;
|
|
223
222
|
previewContainer.appendChild(styleEl);
|
|
224
|
-
previewContainer.appendChild(container);
|
|
223
|
+
previewContainer.appendChild(container$1);
|
|
225
224
|
overrideRootCloneStyles(syncState, true);
|
|
226
225
|
const t2 = performance.now();
|
|
227
226
|
image = await renderToImage(previewContainer, width, height, {
|
|
@@ -284,14 +283,16 @@ function toAbsoluteTime(timegroup, relativeTimeMs) {
|
|
|
284
283
|
/**
|
|
285
284
|
* Renders a timegroup preview to a canvas using SVG foreignObject.
|
|
286
285
|
*
|
|
286
|
+
* Captures the prime timeline's current visual state including DOM changes
|
|
287
|
+
* from frame tasks (SVG paths, canvas content, text updates, etc.).
|
|
288
|
+
*
|
|
287
289
|
* Optimized with:
|
|
288
|
-
* -
|
|
290
|
+
* - Passive clone structure rebuilt each frame from prime's current state
|
|
289
291
|
* - Temporal bucketing for time-based culling
|
|
290
|
-
* -
|
|
291
|
-
* - Parent index for O(1) visibility checks
|
|
292
|
+
* - RenderContext for canvas pixel caching across frames
|
|
292
293
|
* - Resolution scaling for performance (renders at lower resolution, CSS upscales)
|
|
293
294
|
*
|
|
294
|
-
* @param timegroup - The source timegroup to preview
|
|
295
|
+
* @param timegroup - The source timegroup to preview (prime timeline)
|
|
295
296
|
* @param scaleOrOptions - Scale factor (default 1) or options object
|
|
296
297
|
* @returns Object with canvas and refresh function
|
|
297
298
|
*/
|
|
@@ -319,26 +320,19 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
319
320
|
wrapperContainer.appendChild(canvas);
|
|
320
321
|
const ctx = canvas.getContext("2d");
|
|
321
322
|
if (!ctx) throw new Error("Failed to get canvas 2d context");
|
|
322
|
-
|
|
323
|
+
let rendering = false;
|
|
324
|
+
let lastTimeMs = -1;
|
|
325
|
+
let disposed = false;
|
|
326
|
+
const renderContext = new RenderContext();
|
|
327
|
+
const frameController = new FrameController(timegroup);
|
|
323
328
|
const previewContainer = createPreviewContainer({
|
|
324
329
|
width: renderWidth,
|
|
325
330
|
height: renderHeight,
|
|
326
331
|
background: getComputedStyle(timegroup).background || "#000"
|
|
327
332
|
});
|
|
328
|
-
if (currentResolutionScale < 1) {
|
|
329
|
-
container.style.transform = `scale(${currentResolutionScale})`;
|
|
330
|
-
container.style.transformOrigin = "top left";
|
|
331
|
-
}
|
|
332
333
|
const styleEl = document.createElement("style");
|
|
333
334
|
styleEl.textContent = collectDocumentStyles();
|
|
334
335
|
previewContainer.appendChild(styleEl);
|
|
335
|
-
previewContainer.appendChild(container);
|
|
336
|
-
overrideRootCloneStyles(syncState);
|
|
337
|
-
let rendering = false;
|
|
338
|
-
let lastTimeMs = -1;
|
|
339
|
-
let disposed = false;
|
|
340
|
-
const renderContext = new RenderContext();
|
|
341
|
-
const frameController = new FrameController(timegroup);
|
|
342
336
|
let hasLoggedScale = false;
|
|
343
337
|
let pendingResolutionScale = null;
|
|
344
338
|
/**
|
|
@@ -388,7 +382,13 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
388
382
|
}
|
|
389
383
|
try {
|
|
390
384
|
await frameController.renderFrame(userTimeMs);
|
|
391
|
-
|
|
385
|
+
const { container: container$1, syncState } = buildCloneStructure(timegroup, toAbsoluteTime(timegroup, userTimeMs));
|
|
386
|
+
if (currentResolutionScale < 1) {
|
|
387
|
+
container$1.style.transform = `scale(${currentResolutionScale})`;
|
|
388
|
+
container$1.style.transformOrigin = "top left";
|
|
389
|
+
}
|
|
390
|
+
while (previewContainer.firstChild !== styleEl && previewContainer.firstChild) previewContainer.removeChild(previewContainer.firstChild);
|
|
391
|
+
previewContainer.appendChild(container$1);
|
|
392
392
|
overrideRootCloneStyles(syncState);
|
|
393
393
|
const removedNodes = removeHiddenNodesForSerialization(syncState);
|
|
394
394
|
const t0 = performance.now();
|
|
@@ -432,7 +432,6 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
432
432
|
container: wrapperContainer,
|
|
433
433
|
canvas,
|
|
434
434
|
refresh,
|
|
435
|
-
syncState,
|
|
436
435
|
setResolutionScale,
|
|
437
436
|
getResolutionScale,
|
|
438
437
|
dispose
|
|
@@ -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 | 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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"}
|
|
1
|
+
{"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","renderState: RenderState","scaleColors: Record<number, string>","image: HTMLCanvasElement | HTMLImageElement","container","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 // NOTE: seekForRender() has already:\n // 1. Called frameController.renderFrame() to coordinate FrameRenderable elements\n // 2. Awaited #executeCustomFrameTasks() so frame tasks are complete\n // No need to call frameController.renderFrame() again - it would fire tasks redundantly\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 /**\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 * Captures the prime timeline's current visual state including DOM changes\n * from frame tasks (SVG paths, canvas content, text updates, etc.).\n * \n * Optimized with:\n * - Passive clone structure rebuilt each frame from prime's current state\n * - Temporal bucketing for time-based culling\n * - RenderContext for canvas pixel caching across frames\n * - Resolution scaling for performance (renders at lower resolution, CSS upscales)\n *\n * @param timegroup - The source timegroup to preview (prime timeline)\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 // 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 // Reusable preview container and style element\n // Container gets new content each frame but CSS styles are cached\n const previewContainer = createPreviewContainer({\n width: renderWidth,\n height: renderHeight,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\n \n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectDocumentStyles();\n previewContainer.appendChild(styleEl);\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 // Build passive structure from prime timeline's CURRENT state\n // Frame tasks have already run above, so DOM changes are captured\n const absoluteTimeMs = toAbsoluteTime(timegroup, userTimeMs);\n const { container, syncState } = buildCloneStructure(timegroup, absoluteTimeMs);\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 // Clear previous container content and add new structure\n while (previewContainer.firstChild !== styleEl && previewContainer.firstChild) {\n previewContainer.removeChild(previewContainer.firstChild);\n }\n previewContainer.appendChild(container);\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, setResolutionScale, getResolutionScale, dispose };\n}\n"],"mappings":"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"}
|
|
@@ -13,7 +13,7 @@ interface RenderProgress {
|
|
|
13
13
|
elapsedMs: number;
|
|
14
14
|
estimatedRemainingMs: number;
|
|
15
15
|
speedMultiplier: number;
|
|
16
|
-
|
|
16
|
+
framePreviewCanvas?: HTMLCanvasElement;
|
|
17
17
|
}
|
|
18
18
|
interface RenderToVideoOptions {
|
|
19
19
|
fps?: number;
|
|
@@ -35,6 +35,7 @@ interface RenderToVideoOptions {
|
|
|
35
35
|
preferredAudioCodecs?: AudioCodec[];
|
|
36
36
|
benchmarkMode?: boolean;
|
|
37
37
|
customWritableStream?: WritableStream<Uint8Array>;
|
|
38
|
+
progressPreviewInterval?: number;
|
|
38
39
|
}
|
|
39
40
|
//#endregion
|
|
40
41
|
export { RenderProgress, RenderToVideoOptions };
|