@editframe/elements 0.35.0-beta → 0.36.1-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.
Files changed (72) hide show
  1. package/dist/canvas/EFCanvas.d.ts +4 -4
  2. package/dist/elements/EFAudio.d.ts +4 -4
  3. package/dist/elements/EFCaptions.d.ts +0 -4
  4. package/dist/elements/EFCaptions.js +12 -32
  5. package/dist/elements/EFCaptions.js.map +1 -1
  6. package/dist/elements/EFImage.js +11 -2
  7. package/dist/elements/EFImage.js.map +1 -1
  8. package/dist/elements/EFPanZoom.d.ts +4 -4
  9. package/dist/elements/EFSurface.d.ts +4 -4
  10. package/dist/elements/EFTemporal.js +1 -0
  11. package/dist/elements/EFTemporal.js.map +1 -1
  12. package/dist/elements/EFText.d.ts +4 -4
  13. package/dist/elements/EFTextSegment.d.ts +4 -4
  14. package/dist/elements/EFThumbnailStrip.d.ts +4 -4
  15. package/dist/elements/EFTimegroup.d.ts +40 -6
  16. package/dist/elements/EFTimegroup.js +127 -8
  17. package/dist/elements/EFTimegroup.js.map +1 -1
  18. package/dist/elements/EFVideo.d.ts +6 -6
  19. package/dist/elements/EFWaveform.d.ts +4 -4
  20. package/dist/elements/updateAnimations.js +113 -15
  21. package/dist/elements/updateAnimations.js.map +1 -1
  22. package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
  23. package/dist/gui/EFConfiguration.d.ts +4 -4
  24. package/dist/gui/EFControls.d.ts +2 -2
  25. package/dist/gui/EFDial.d.ts +4 -4
  26. package/dist/gui/EFFilmstrip.d.ts +2 -2
  27. package/dist/gui/EFFitScale.d.ts +3 -3
  28. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  29. package/dist/gui/EFPause.d.ts +4 -4
  30. package/dist/gui/EFPlay.d.ts +4 -4
  31. package/dist/gui/EFPreview.d.ts +4 -4
  32. package/dist/gui/EFResizableBox.d.ts +4 -4
  33. package/dist/gui/EFScrubber.d.ts +4 -4
  34. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  35. package/dist/gui/EFToggleLoop.d.ts +4 -4
  36. package/dist/gui/EFTogglePlay.d.ts +4 -4
  37. package/dist/gui/EFTransformHandles.d.ts +4 -4
  38. package/dist/gui/EFWorkbench.d.ts +6 -6
  39. package/dist/gui/EFWorkbench.js +38 -12
  40. package/dist/gui/EFWorkbench.js.map +1 -1
  41. package/dist/gui/TWMixin.js +1 -1
  42. package/dist/gui/TWMixin.js.map +1 -1
  43. package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
  44. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +2 -2
  45. package/dist/gui/timeline/tracks/ImageTrack.d.ts +2 -2
  46. package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +5 -5
  47. package/dist/gui/timeline/tracks/VideoTrack.d.ts +4 -4
  48. package/dist/gui/tree/EFTree.d.ts +4 -4
  49. package/dist/gui/tree/EFTreeItem.d.ts +4 -4
  50. package/dist/preview/FrameController.js +6 -1
  51. package/dist/preview/FrameController.js.map +1 -1
  52. package/dist/preview/encoding/canvasEncoder.js.map +1 -1
  53. package/dist/preview/encoding/mainThreadEncoder.js +3 -0
  54. package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
  55. package/dist/preview/renderTimegroupPreview.js +57 -55
  56. package/dist/preview/renderTimegroupPreview.js.map +1 -1
  57. package/dist/preview/renderTimegroupToCanvas.js +22 -23
  58. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  59. package/dist/preview/renderTimegroupToVideo.d.ts +2 -1
  60. package/dist/preview/renderTimegroupToVideo.js +77 -40
  61. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  62. package/dist/preview/rendering/renderToImage.d.ts +1 -0
  63. package/dist/preview/rendering/renderToImage.js +1 -26
  64. package/dist/preview/rendering/renderToImage.js.map +1 -1
  65. package/dist/preview/rendering/renderToImageForeignObject.js +34 -6
  66. package/dist/preview/rendering/renderToImageForeignObject.js.map +1 -1
  67. package/dist/preview/rendering/serializeTimelineDirect.js +379 -0
  68. package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -0
  69. package/dist/render/EFRenderAPI.js +45 -0
  70. package/dist/render/EFRenderAPI.js.map +1 -1
  71. package/dist/style.css +45 -0
  72. package/package.json +2 -2
@@ -89,32 +89,7 @@ async function renderToImage(container, width, height, options) {
89
89
  const { dataUri } = await serializeToSvgDataUri(clone, width, height);
90
90
  return loadImageFromDataUri(dataUri);
91
91
  }
92
- /**
93
- * Render a pre-built clone container to an image WITHOUT cloning it again.
94
- * This is the fast path for reusing clone structures across frames.
95
- *
96
- * Key difference from renderToImage:
97
- * - Does NOT call cloneNode (avoids expensive DOM duplication)
98
- * - Converts canvases to images in-place, then restores them after serialization
99
- * - Assumes the container already has refreshed canvas content
100
- *
101
- * @param container - Pre-built clone container with refreshed canvas content
102
- * @param width - Output width
103
- * @param height - Output height
104
- * @returns Promise resolving to an HTMLImageElement
105
- */
106
- async function renderToImageDirect(container, width, height) {
107
- defaultProfiler.incrementRenderCount();
108
- const { dataUri, restore } = await serializeToSvgDataUri(container, width, height, {
109
- inlineImages: true,
110
- logEarlyRenders: true
111
- });
112
- restore();
113
- const image = await loadImageFromDataUri(dataUri);
114
- defaultProfiler.shouldLogByFrameCount(100);
115
- return image;
116
- }
117
92
 
118
93
  //#endregion
119
- export { loadImageFromDataUri, renderToImage, renderToImageDirect };
94
+ export { loadImageFromDataUri, renderToImage };
120
95
  //# sourceMappingURL=renderToImage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"renderToImage.js","names":["current: Element | null","visibleIndices: number[]","visibleCanvases: HTMLCanvasElement[]"],"sources":["../../../src/preview/rendering/renderToImage.ts"],"sourcesContent":["/**\n * Public rendering API facade.\n * Dispatches to native or foreignObject rendering paths based on settings.\n */\n\nimport type { ForeignObjectRenderOptions } from \"./types.js\";\nimport { renderToImageNative } from \"./renderToImageNative.js\";\nimport { serializeToSvgDataUri } from \"./renderToImageForeignObject.js\";\nimport { inlineImages } from \"./inlineImages.js\";\nimport { getEffectiveRenderMode } from \"../renderers.js\";\nimport { encodeCanvasesInParallel } from \"../encoding/canvasEncoder.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\n\n/**\n * Check if an element or any of its ancestors has display:none.\n * Used to skip encoding hidden canvases.\n */\nfunction isElementHidden(element: Element): boolean {\n let current: Element | null = element;\n while (current) {\n if (current instanceof HTMLElement && current.style.display === \"none\") {\n return true;\n }\n current = current.parentElement;\n }\n return false;\n}\n\n/**\n * Load an image from a data URI. Returns a Promise that resolves when loaded.\n */\nexport function loadImageFromDataUri(dataUri: string): Promise<HTMLImageElement> {\n const img = new Image();\n const imageLoadStart = performance.now();\n \n return new Promise<HTMLImageElement>((resolve, reject) => {\n img.onload = () => {\n defaultProfiler.addTime(\"imageLoad\", performance.now() - imageLoadStart);\n resolve(img);\n };\n img.onerror = reject;\n img.src = dataUri;\n });\n}\n\n/**\n * Render HTML content to an image (or canvas) for drawing.\n * \n * Supports two rendering modes (configurable via previewSettings):\n * - \"native\": Chrome's experimental drawElementImage API (fastest when available)\n * - \"foreignObject\": SVG foreignObject serialization (fallback, works everywhere)\n * \n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options\n * @returns HTMLCanvasElement when using native, HTMLImageElement when using foreignObject\n */\nexport async function renderToImage(\n container: HTMLElement,\n width: number,\n height: number,\n options?: ForeignObjectRenderOptions,\n): Promise<HTMLImageElement | HTMLCanvasElement> {\n const renderMode = getEffectiveRenderMode();\n \n // Native HTML-in-Canvas API path (fastest, requires Chrome flag)\n if (renderMode === \"native\") {\n return renderToImageNative(container, width, height, options);\n }\n \n // Fallback: SVG foreignObject serialization\n // Clone the container first (don't modify original)\n // Note: cloneNode doesn't copy canvas pixels, so we encode from original canvases\n const allOriginalCanvases = Array.from(container.querySelectorAll(\"canvas\"));\n const clone = container.cloneNode(true) as HTMLElement;\n const allClonedCanvases = Array.from(clone.querySelectorAll(\"canvas\"));\n \n // Filter out hidden canvases - they have display:none and won't render anyway\n // Keep track of indices to match with cloned canvases\n const visibleIndices: number[] = [];\n const visibleCanvases: HTMLCanvasElement[] = [];\n for (let i = 0; i < allOriginalCanvases.length; i++) {\n const canvas = allOriginalCanvases[i]!;\n if (!isElementHidden(canvas)) {\n visibleIndices.push(i);\n visibleCanvases.push(canvas);\n }\n }\n \n // Encode visible original canvases\n // Pass through renderContext and sourceMap for caching\n const canvasScale = options?.canvasScale ?? 1;\n const canvasStart = performance.now();\n const encodedResults = await encodeCanvasesInParallel(visibleCanvases, { \n scale: canvasScale,\n renderContext: options?.renderContext,\n sourceMap: options?.sourceMap,\n });\n \n // Map encoded results to corresponding cloned canvases using tracked indices\n for (let j = 0; j < visibleCanvases.length; j++) {\n const srcCanvas = visibleCanvases[j]!;\n const originalIndex = visibleIndices[j]!;\n const dstCanvas = allClonedCanvases[originalIndex];\n const encoded = encodedResults.find((r) => r.canvas === srcCanvas);\n \n if (!dstCanvas || !encoded) continue;\n \n try {\n const img = document.createElement(\"img\");\n img.src = encoded.dataUrl;\n img.width = srcCanvas.width;\n img.height = srcCanvas.height;\n const style = dstCanvas.getAttribute(\"style\");\n if (style) img.setAttribute(\"style\", style);\n dstCanvas.parentNode?.replaceChild(img, dstCanvas);\n } catch {\n // Cross-origin or other error - skip\n }\n }\n defaultProfiler.addTime(\"canvasEncode\", performance.now() - canvasStart);\n\n // Inline external images in the clone\n const inlineStart = performance.now();\n await inlineImages(clone);\n defaultProfiler.addTime(\"inline\", performance.now() - inlineStart);\n\n // Use common serialization pipeline (no restore needed since we're working on a clone)\n const { dataUri } = await serializeToSvgDataUri(clone, width, height);\n \n // Load as image\n return loadImageFromDataUri(dataUri);\n}\n\n/**\n * Render a pre-built clone container to an image WITHOUT cloning it again.\n * This is the fast path for reusing clone structures across frames.\n * \n * Key difference from renderToImage:\n * - Does NOT call cloneNode (avoids expensive DOM duplication)\n * - Converts canvases to images in-place, then restores them after serialization\n * - Assumes the container already has refreshed canvas content\n * \n * @param container - Pre-built clone container with refreshed canvas content\n * @param width - Output width\n * @param height - Output height\n * @returns Promise resolving to an HTMLImageElement\n */\nexport async function renderToImageDirect(\n container: HTMLElement,\n width: number,\n height: number,\n): Promise<HTMLImageElement> {\n defaultProfiler.incrementRenderCount();\n \n // Use common serialization pipeline (modifies in-place, restores after)\n const { dataUri, restore } = await serializeToSvgDataUri(container, width, height, {\n inlineImages: true,\n logEarlyRenders: true,\n });\n restore();\n \n // Load as image\n const image = await loadImageFromDataUri(dataUri);\n \n // Log timing breakdown periodically\n defaultProfiler.shouldLogByFrameCount(100);\n \n return image;\n}\n\n/**\n * Prepare a frame's data URI without waiting for image load.\n * Returns the data URI asynchronously (after parallel canvas encoding and serialization) for pipelined loading.\n * The DOM is restored before this function returns.\n */\nexport async function prepareFrameDataUri(\n container: HTMLElement,\n width: number,\n height: number,\n): Promise<string> {\n defaultProfiler.incrementRenderCount();\n \n // Use common serialization pipeline (modifies in-place, restores after)\n const { dataUri, restore } = await serializeToSvgDataUri(container, width, height);\n restore();\n \n return dataUri;\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,SAAS,gBAAgB,SAA2B;CAClD,IAAIA,UAA0B;AAC9B,QAAO,SAAS;AACd,MAAI,mBAAmB,eAAe,QAAQ,MAAM,YAAY,OAC9D,QAAO;AAET,YAAU,QAAQ;;AAEpB,QAAO;;;;;AAMT,SAAgB,qBAAqB,SAA4C;CAC/E,MAAM,MAAM,IAAI,OAAO;CACvB,MAAM,iBAAiB,YAAY,KAAK;AAExC,QAAO,IAAI,SAA2B,SAAS,WAAW;AACxD,MAAI,eAAe;AACjB,mBAAgB,QAAQ,aAAa,YAAY,KAAK,GAAG,eAAe;AACxE,WAAQ,IAAI;;AAEd,MAAI,UAAU;AACd,MAAI,MAAM;GACV;;;;;;;;;;;;;;;AAgBJ,eAAsB,cACpB,WACA,OACA,QACA,SAC+C;AAI/C,KAHmB,wBAAwB,KAGxB,SACjB,QAAO,oBAAoB,WAAW,OAAO,QAAQ,QAAQ;CAM/D,MAAM,sBAAsB,MAAM,KAAK,UAAU,iBAAiB,SAAS,CAAC;CAC5E,MAAM,QAAQ,UAAU,UAAU,KAAK;CACvC,MAAM,oBAAoB,MAAM,KAAK,MAAM,iBAAiB,SAAS,CAAC;CAItE,MAAMC,iBAA2B,EAAE;CACnC,MAAMC,kBAAuC,EAAE;AAC/C,MAAK,IAAI,IAAI,GAAG,IAAI,oBAAoB,QAAQ,KAAK;EACnD,MAAM,SAAS,oBAAoB;AACnC,MAAI,CAAC,gBAAgB,OAAO,EAAE;AAC5B,kBAAe,KAAK,EAAE;AACtB,mBAAgB,KAAK,OAAO;;;CAMhC,MAAM,cAAc,SAAS,eAAe;CAC5C,MAAM,cAAc,YAAY,KAAK;CACrC,MAAM,iBAAiB,MAAM,yBAAyB,iBAAiB;EACrE,OAAO;EACP,eAAe,SAAS;EACxB,WAAW,SAAS;EACrB,CAAC;AAGF,MAAK,IAAI,IAAI,GAAG,IAAI,gBAAgB,QAAQ,KAAK;EAC/C,MAAM,YAAY,gBAAgB;EAElC,MAAM,YAAY,kBADI,eAAe;EAErC,MAAM,UAAU,eAAe,MAAM,MAAM,EAAE,WAAW,UAAU;AAElE,MAAI,CAAC,aAAa,CAAC,QAAS;AAE5B,MAAI;GACF,MAAM,MAAM,SAAS,cAAc,MAAM;AACzC,OAAI,MAAM,QAAQ;AAClB,OAAI,QAAQ,UAAU;AACtB,OAAI,SAAS,UAAU;GACvB,MAAM,QAAQ,UAAU,aAAa,QAAQ;AAC7C,OAAI,MAAO,KAAI,aAAa,SAAS,MAAM;AAC3C,aAAU,YAAY,aAAa,KAAK,UAAU;UAC5C;;AAIV,iBAAgB,QAAQ,gBAAgB,YAAY,KAAK,GAAG,YAAY;CAGxE,MAAM,cAAc,YAAY,KAAK;AACrC,OAAM,aAAa,MAAM;AACzB,iBAAgB,QAAQ,UAAU,YAAY,KAAK,GAAG,YAAY;CAGlE,MAAM,EAAE,YAAY,MAAM,sBAAsB,OAAO,OAAO,OAAO;AAGrE,QAAO,qBAAqB,QAAQ;;;;;;;;;;;;;;;;AAiBtC,eAAsB,oBACpB,WACA,OACA,QAC2B;AAC3B,iBAAgB,sBAAsB;CAGtC,MAAM,EAAE,SAAS,YAAY,MAAM,sBAAsB,WAAW,OAAO,QAAQ;EACjF,cAAc;EACd,iBAAiB;EAClB,CAAC;AACF,UAAS;CAGT,MAAM,QAAQ,MAAM,qBAAqB,QAAQ;AAGjD,iBAAgB,sBAAsB,IAAI;AAE1C,QAAO"}
1
+ {"version":3,"file":"renderToImage.js","names":["current: Element | null","visibleIndices: number[]","visibleCanvases: HTMLCanvasElement[]"],"sources":["../../../src/preview/rendering/renderToImage.ts"],"sourcesContent":["/**\n * Public rendering API facade.\n * Dispatches to native or foreignObject rendering paths based on settings.\n */\n\nimport type { ForeignObjectRenderOptions } from \"./types.js\";\nimport type { RenderContext } from \"../RenderContext.js\";\nimport { renderToImageNative } from \"./renderToImageNative.js\";\nimport { serializeToSvgDataUri } from \"./renderToImageForeignObject.js\";\nimport { inlineImages } from \"./inlineImages.js\";\nimport { getEffectiveRenderMode } from \"../renderers.js\";\nimport { encodeCanvasesInParallel } from \"../encoding/canvasEncoder.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\n\n/**\n * Check if an element or any of its ancestors has display:none.\n * Used to skip encoding hidden canvases.\n */\nfunction isElementHidden(element: Element): boolean {\n let current: Element | null = element;\n while (current) {\n if (current instanceof HTMLElement && current.style.display === \"none\") {\n return true;\n }\n current = current.parentElement;\n }\n return false;\n}\n\n/**\n * Load an image from a data URI. Returns a Promise that resolves when loaded.\n */\nexport function loadImageFromDataUri(dataUri: string): Promise<HTMLImageElement> {\n const img = new Image();\n const imageLoadStart = performance.now();\n \n return new Promise<HTMLImageElement>((resolve, reject) => {\n img.onload = () => {\n defaultProfiler.addTime(\"imageLoad\", performance.now() - imageLoadStart);\n resolve(img);\n };\n img.onerror = reject;\n img.src = dataUri;\n });\n}\n\n/**\n * Render HTML content to an image (or canvas) for drawing.\n * \n * Supports two rendering modes (configurable via previewSettings):\n * - \"native\": Chrome's experimental drawElementImage API (fastest when available)\n * - \"foreignObject\": SVG foreignObject serialization (fallback, works everywhere)\n * \n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options\n * @returns HTMLCanvasElement when using native, HTMLImageElement when using foreignObject\n */\nexport async function renderToImage(\n container: HTMLElement,\n width: number,\n height: number,\n options?: ForeignObjectRenderOptions,\n): Promise<HTMLImageElement | HTMLCanvasElement> {\n const renderMode = getEffectiveRenderMode();\n \n // Native HTML-in-Canvas API path (fastest, requires Chrome flag)\n if (renderMode === \"native\") {\n return renderToImageNative(container, width, height, options);\n }\n \n // Fallback: SVG foreignObject serialization\n // Clone the container first (don't modify original)\n // Note: cloneNode doesn't copy canvas pixels, so we encode from original canvases\n const allOriginalCanvases = Array.from(container.querySelectorAll(\"canvas\"));\n const clone = container.cloneNode(true) as HTMLElement;\n const allClonedCanvases = Array.from(clone.querySelectorAll(\"canvas\"));\n \n // Filter out hidden canvases - they have display:none and won't render anyway\n // Keep track of indices to match with cloned canvases\n const visibleIndices: number[] = [];\n const visibleCanvases: HTMLCanvasElement[] = [];\n for (let i = 0; i < allOriginalCanvases.length; i++) {\n const canvas = allOriginalCanvases[i]!;\n if (!isElementHidden(canvas)) {\n visibleIndices.push(i);\n visibleCanvases.push(canvas);\n }\n }\n \n // Encode visible original canvases\n // Pass through renderContext and sourceMap for caching\n const canvasScale = options?.canvasScale ?? 1;\n const canvasStart = performance.now();\n const encodedResults = await encodeCanvasesInParallel(visibleCanvases, { \n scale: canvasScale,\n renderContext: options?.renderContext,\n sourceMap: options?.sourceMap,\n });\n \n // Map encoded results to corresponding cloned canvases using tracked indices\n for (let j = 0; j < visibleCanvases.length; j++) {\n const srcCanvas = visibleCanvases[j]!;\n const originalIndex = visibleIndices[j]!;\n const dstCanvas = allClonedCanvases[originalIndex];\n const encoded = encodedResults.find((r) => r.canvas === srcCanvas);\n \n if (!dstCanvas || !encoded) continue;\n \n try {\n const img = document.createElement(\"img\");\n img.src = encoded.dataUrl;\n img.width = srcCanvas.width;\n img.height = srcCanvas.height;\n const style = dstCanvas.getAttribute(\"style\");\n if (style) img.setAttribute(\"style\", style);\n dstCanvas.parentNode?.replaceChild(img, dstCanvas);\n } catch {\n // Cross-origin or other error - skip\n }\n }\n defaultProfiler.addTime(\"canvasEncode\", performance.now() - canvasStart);\n\n // Inline external images in the clone\n const inlineStart = performance.now();\n await inlineImages(clone);\n defaultProfiler.addTime(\"inline\", performance.now() - inlineStart);\n\n // Use common serialization pipeline (no restore needed since we're working on a clone)\n const { dataUri } = await serializeToSvgDataUri(clone, width, height);\n \n // Load as image\n return loadImageFromDataUri(dataUri);\n}\n\n/**\n * Render a pre-built clone container to an image WITHOUT cloning it again.\n * This is the fast path for reusing clone structures across frames.\n * \n * Key difference from renderToImage:\n * - Does NOT call cloneNode (avoids expensive DOM duplication)\n * - Converts canvases to images in-place, then restores them after serialization\n * - Assumes the container already has refreshed canvas content\n * \n * @param container - Pre-built clone container with refreshed canvas content\n * @param width - Output width\n * @param height - Output height\n * @returns Promise resolving to an HTMLImageElement\n */\nexport async function renderToImageDirect(\n container: HTMLElement,\n width: number,\n height: number,\n options?: {\n renderContext?: RenderContext;\n sourceMap?: WeakMap<HTMLCanvasElement, Element>;\n canvasScale?: number;\n },\n): Promise<HTMLImageElement> {\n defaultProfiler.incrementRenderCount();\n \n // Use common serialization pipeline (modifies in-place, restores after)\n const { dataUri, restore } = await serializeToSvgDataUri(container, width, height, {\n inlineImages: true,\n logEarlyRenders: true,\n renderContext: options?.renderContext,\n sourceMap: options?.sourceMap,\n canvasScale: options?.canvasScale ?? 1,\n });\n restore();\n \n // Load as image\n const image = await loadImageFromDataUri(dataUri);\n \n // Log timing breakdown periodically\n defaultProfiler.shouldLogByFrameCount(100);\n \n return image;\n}\n\n/**\n * Prepare a frame's data URI without waiting for image load.\n * Returns the data URI asynchronously (after parallel canvas encoding and serialization) for pipelined loading.\n * The DOM is restored before this function returns.\n */\nexport async function prepareFrameDataUri(\n container: HTMLElement,\n width: number,\n height: number,\n): Promise<string> {\n defaultProfiler.incrementRenderCount();\n \n // Use common serialization pipeline (modifies in-place, restores after)\n const { dataUri, restore } = await serializeToSvgDataUri(container, width, height);\n restore();\n \n return dataUri;\n}\n"],"mappings":";;;;;;;;;;;;AAkBA,SAAS,gBAAgB,SAA2B;CAClD,IAAIA,UAA0B;AAC9B,QAAO,SAAS;AACd,MAAI,mBAAmB,eAAe,QAAQ,MAAM,YAAY,OAC9D,QAAO;AAET,YAAU,QAAQ;;AAEpB,QAAO;;;;;AAMT,SAAgB,qBAAqB,SAA4C;CAC/E,MAAM,MAAM,IAAI,OAAO;CACvB,MAAM,iBAAiB,YAAY,KAAK;AAExC,QAAO,IAAI,SAA2B,SAAS,WAAW;AACxD,MAAI,eAAe;AACjB,mBAAgB,QAAQ,aAAa,YAAY,KAAK,GAAG,eAAe;AACxE,WAAQ,IAAI;;AAEd,MAAI,UAAU;AACd,MAAI,MAAM;GACV;;;;;;;;;;;;;;;AAgBJ,eAAsB,cACpB,WACA,OACA,QACA,SAC+C;AAI/C,KAHmB,wBAAwB,KAGxB,SACjB,QAAO,oBAAoB,WAAW,OAAO,QAAQ,QAAQ;CAM/D,MAAM,sBAAsB,MAAM,KAAK,UAAU,iBAAiB,SAAS,CAAC;CAC5E,MAAM,QAAQ,UAAU,UAAU,KAAK;CACvC,MAAM,oBAAoB,MAAM,KAAK,MAAM,iBAAiB,SAAS,CAAC;CAItE,MAAMC,iBAA2B,EAAE;CACnC,MAAMC,kBAAuC,EAAE;AAC/C,MAAK,IAAI,IAAI,GAAG,IAAI,oBAAoB,QAAQ,KAAK;EACnD,MAAM,SAAS,oBAAoB;AACnC,MAAI,CAAC,gBAAgB,OAAO,EAAE;AAC5B,kBAAe,KAAK,EAAE;AACtB,mBAAgB,KAAK,OAAO;;;CAMhC,MAAM,cAAc,SAAS,eAAe;CAC5C,MAAM,cAAc,YAAY,KAAK;CACrC,MAAM,iBAAiB,MAAM,yBAAyB,iBAAiB;EACrE,OAAO;EACP,eAAe,SAAS;EACxB,WAAW,SAAS;EACrB,CAAC;AAGF,MAAK,IAAI,IAAI,GAAG,IAAI,gBAAgB,QAAQ,KAAK;EAC/C,MAAM,YAAY,gBAAgB;EAElC,MAAM,YAAY,kBADI,eAAe;EAErC,MAAM,UAAU,eAAe,MAAM,MAAM,EAAE,WAAW,UAAU;AAElE,MAAI,CAAC,aAAa,CAAC,QAAS;AAE5B,MAAI;GACF,MAAM,MAAM,SAAS,cAAc,MAAM;AACzC,OAAI,MAAM,QAAQ;AAClB,OAAI,QAAQ,UAAU;AACtB,OAAI,SAAS,UAAU;GACvB,MAAM,QAAQ,UAAU,aAAa,QAAQ;AAC7C,OAAI,MAAO,KAAI,aAAa,SAAS,MAAM;AAC3C,aAAU,YAAY,aAAa,KAAK,UAAU;UAC5C;;AAIV,iBAAgB,QAAQ,gBAAgB,YAAY,KAAK,GAAG,YAAY;CAGxE,MAAM,cAAc,YAAY,KAAK;AACrC,OAAM,aAAa,MAAM;AACzB,iBAAgB,QAAQ,UAAU,YAAY,KAAK,GAAG,YAAY;CAGlE,MAAM,EAAE,YAAY,MAAM,sBAAsB,OAAO,OAAO,OAAO;AAGrE,QAAO,qBAAqB,QAAQ"}
@@ -41,25 +41,53 @@ async function serializeToSvgDataUri(container, width, height, options = {}) {
41
41
  const canvasStart = performance.now();
42
42
  const visibleCanvases = Array.from(container.querySelectorAll("canvas")).filter((canvas) => !isElementHidden(canvas));
43
43
  const canvasSnapshots = [];
44
+ const qualityMultiplier = 1.5;
44
45
  for (let i = 0; i < visibleCanvases.length; i++) {
45
46
  const canvas = visibleCanvases[i];
46
47
  if (canvas.width > 0 && canvas.height > 0) {
48
+ let optimalScale = canvasScale;
49
+ if (sourceMap) {
50
+ const sourceElement = sourceMap.get(canvas);
51
+ if (sourceElement) try {
52
+ const computedStyle = getComputedStyle(sourceElement);
53
+ const cssWidth = parseFloat(computedStyle.width) || canvas.width;
54
+ const cssHeight = parseFloat(computedStyle.height) || canvas.height;
55
+ const displayScaleX = cssWidth / canvas.width;
56
+ const displayScaleY = cssHeight / canvas.height;
57
+ const displayScale = Math.min(displayScaleX, displayScaleY);
58
+ optimalScale = Math.min(1, displayScale * canvasScale * qualityMultiplier);
59
+ console.log(`[serializeToSvg] Canvas ${canvas.width}x${canvas.height} -> CSS ${cssWidth.toFixed(0)}x${cssHeight.toFixed(0)}, displayScale=${displayScale.toFixed(3)}, videoScale=${canvasScale}, optimalScale=${optimalScale.toFixed(3)}`);
60
+ } catch (e) {
61
+ console.warn(`[serializeToSvg] Failed to get computed style for ${sourceElement.tagName}:`, e);
62
+ }
63
+ }
64
+ const targetWidth = Math.max(1, Math.floor(canvas.width * optimalScale));
65
+ const targetHeight = Math.max(1, Math.floor(canvas.height * optimalScale));
47
66
  const copy = document.createElement("canvas");
48
- copy.width = canvas.width;
49
- copy.height = canvas.height;
67
+ copy.width = targetWidth;
68
+ copy.height = targetHeight;
50
69
  if (canvas.dataset.preserveAlpha) copy.dataset.preserveAlpha = canvas.dataset.preserveAlpha;
51
70
  const ctx = copy.getContext("2d");
52
- if (ctx) ctx.drawImage(canvas, 0, 0);
71
+ if (ctx) ctx.drawImage(canvas, 0, 0, targetWidth, targetHeight);
53
72
  canvasSnapshots.push({
54
73
  original: canvas,
55
74
  copy
56
75
  });
57
76
  }
58
77
  }
59
- const encodedWithOriginals = (await encodeCanvasesInParallel(canvasSnapshots.map((s) => s.copy), {
60
- scale: canvasScale,
78
+ const snapshotCanvases = canvasSnapshots.map((s) => s.copy);
79
+ let snapshotSourceMap;
80
+ if (sourceMap) {
81
+ snapshotSourceMap = /* @__PURE__ */ new WeakMap();
82
+ for (const { original, copy } of canvasSnapshots) {
83
+ const sourceElement = sourceMap.get(original);
84
+ if (sourceElement) snapshotSourceMap.set(copy, sourceElement);
85
+ }
86
+ }
87
+ const encodedWithOriginals = (await encodeCanvasesInParallel(snapshotCanvases, {
88
+ scale: 1,
61
89
  renderContext,
62
- sourceMap
90
+ sourceMap: snapshotSourceMap
63
91
  })).map((result) => {
64
92
  const snapshot = canvasSnapshots.find((s) => s.copy === result.canvas);
65
93
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"renderToImageForeignObject.js","names":["_xmlSerializer: XMLSerializer | null","_textEncoder: TextEncoder | null","current: Element | null","canvasRestoreInfo: CanvasRestoreInfo[]","canvasSnapshots: { original: HTMLCanvasElement; copy: HTMLCanvasElement }[]","base64: string"],"sources":["../../../src/preview/rendering/renderToImageForeignObject.ts"],"sourcesContent":["/**\n * SVG foreignObject rendering path with serialization.\n */\n\nimport type { SerializeToSvgOptions, SerializationResult, CanvasRestoreInfo } from \"./types.js\";\nimport { encodeBase64Fast } from \"./svgSerializer.js\";\nimport { inlineImages } from \"./inlineImages.js\";\nimport { encodeCanvasesInParallel } from \"../encoding/canvasEncoder.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\nimport { logger } from \"../logger.js\";\n\n// Reusable instances for better performance (avoid creating new instances every frame)\n// Note: wrapper element is NOT reused - each concurrent frame needs its own wrapper\nlet _xmlSerializer: XMLSerializer | null = null;\nlet _textEncoder: TextEncoder | null = null;\n\n/**\n * Check if an element or any of its ancestors has display:none.\n * Used to skip encoding hidden canvases.\n */\nfunction isElementHidden(element: Element): boolean {\n let current: Element | null = element;\n while (current) {\n if (current instanceof HTMLElement && current.style.display === \"none\") {\n return true;\n }\n current = current.parentElement;\n }\n return false;\n}\n\n// Pre-computed SVG constants\nconst SVG_PREFIX = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"';\nconst SVG_HEIGHT_PREFIX = '\" height=\"';\nconst SVG_MIDDLE = '\"><foreignObject width=\"100%\" height=\"100%\">';\nconst SVG_SUFFIX = '</foreignObject></svg>';\nconst DATA_URI_PREFIX = 'data:image/svg+xml;base64,';\n\n// Shared style string to reduce allocations\nconst WRAPPER_STYLE_BASE = \"overflow:hidden;position:relative;\";\n\n/**\n * Common SVG foreignObject serialization pipeline.\n * Handles canvas encoding, serialization, and base64 encoding.\n * \n * @param container - The HTML element to serialize\n * @param width - Output width\n * @param height - Output height\n * @param options - Serialization options\n * @returns Serialization result with data URI and restore function\n */\nexport async function serializeToSvgDataUri(\n container: HTMLElement,\n width: number,\n height: number,\n options: SerializeToSvgOptions = {},\n): Promise<SerializationResult> {\n const { \n canvasScale = 1, \n inlineImages: shouldInlineImages = false, \n logEarlyRenders = false,\n renderContext,\n sourceMap,\n } = options;\n \n // Store info for restoration (only used if modifying in-place)\n const canvasRestoreInfo: CanvasRestoreInfo[] = [];\n \n // Phase 1: Encode canvases to data URLs (parallel)\n // Filter out hidden canvases - they have display:none and won't render anyway\n const canvasStart = performance.now();\n const allCanvases = Array.from(container.querySelectorAll(\"canvas\"));\n const visibleCanvases = allCanvases.filter(canvas => !isElementHidden(canvas));\n \n // CRITICAL FIX: Synchronously copy canvas pixels BEFORE any async work.\n // This prevents race conditions where concurrent render tasks overwrite\n // the shared clone canvases while encoding is in progress.\n // See: Hypothesis 1 - Clone Canvas Overwritten During Serialization\n const canvasSnapshots: { original: HTMLCanvasElement; copy: HTMLCanvasElement }[] = [];\n for (let i = 0; i < visibleCanvases.length; i++) {\n const canvas = visibleCanvases[i]!;\n if (canvas.width > 0 && canvas.height > 0) {\n const copy = document.createElement(\"canvas\");\n copy.width = canvas.width;\n copy.height = canvas.height;\n // Copy dataset attributes (e.g., preserveAlpha)\n if (canvas.dataset.preserveAlpha) {\n copy.dataset.preserveAlpha = canvas.dataset.preserveAlpha;\n }\n const ctx = copy.getContext(\"2d\");\n if (ctx) {\n // drawImage is SYNCHRONOUS - pixels are copied immediately\n ctx.drawImage(canvas, 0, 0);\n }\n canvasSnapshots.push({ original: canvas, copy });\n }\n }\n \n // Encode from the snapshot copies (safe from concurrent overwrites)\n const snapshotCanvases = canvasSnapshots.map(s => s.copy);\n const encodedResults = await encodeCanvasesInParallel(snapshotCanvases, { \n scale: canvasScale,\n renderContext,\n sourceMap,\n });\n \n // Map encoded results back to original canvases for DOM replacement\n const encodedWithOriginals = encodedResults.map(result => {\n const snapshot = canvasSnapshots.find(s => s.copy === result.canvas);\n return {\n ...result,\n canvas: snapshot?.original ?? result.canvas,\n };\n });\n \n // Replace canvases with images\n for (const { canvas, dataUrl } of encodedWithOriginals) {\n try {\n const img = document.createElement(\"img\");\n img.src = dataUrl;\n img.width = canvas.width;\n img.height = canvas.height;\n const style = canvas.getAttribute(\"style\");\n if (style) img.setAttribute(\"style\", style);\n \n const parent = canvas.parentNode;\n if (parent) {\n const nextSibling = canvas.nextSibling;\n parent.replaceChild(img, canvas);\n canvasRestoreInfo.push({ canvas, parent, nextSibling, img });\n }\n } catch {\n // Cross-origin canvas - leave as-is\n }\n }\n defaultProfiler.addTime(\"canvasEncode\", performance.now() - canvasStart);\n \n // Phase 2: Inline external images (if requested)\n if (shouldInlineImages) {\n const inlineStart = performance.now();\n await inlineImages(container);\n defaultProfiler.addTime(\"inline\", performance.now() - inlineStart);\n }\n \n // Phase 3: Serialize to XHTML\n const serializeStart = performance.now();\n \n // Create fresh wrapper element for THIS frame (local variable for closure safety)\n // Multiple concurrent frames in video export each get their own wrapper\n const wrapperElement = document.createElement(\"div\");\n wrapperElement.setAttribute(\"xmlns\", \"http://www.w3.org/1999/xhtml\");\n wrapperElement.setAttribute(\"style\", `width:${width}px;height:${height}px;${WRAPPER_STYLE_BASE}`);\n wrapperElement.appendChild(container);\n \n if (!_xmlSerializer) {\n _xmlSerializer = new XMLSerializer();\n }\n \n // NOTE: Hidden element handling is now done by the caller via removeHiddenNodesForSerialization().\n // The caller physically removes hidden nodes from the clone tree BEFORE calling this function,\n // so hidden elements are never serialized at all - not just hidden with display:none.\n //\n // Benefits of removing before serialization:\n // - Hidden canvases are not encoded (saves encoding time and memory)\n // - Hidden elements are not serialized (smaller SVG, faster serialization)\n // - Hidden images are not inlined (saves fetch and encoding)\n // - The serialized output is smaller and faster to base64 encode\n \n // Serialize to XHTML string\n const perfStart = performance.now();\n const serialized = _xmlSerializer.serializeToString(wrapperElement);\n const serializeTime = performance.now() - perfStart;\n \n // Sample 1% of frames to avoid spam\n if (Math.random() < 0.01) {\n const elementCount = wrapperElement.querySelectorAll('*').length;\n console.log(`[serialize] elements=${elementCount}, time=${serializeTime.toFixed(1)}ms, size=${(serialized.length / 1024).toFixed(1)}KB`);\n }\n\n defaultProfiler.addTime(\"serialize\", performance.now() - serializeStart);\n \n // Prepare restore function (removes container from wrapper, restores canvases)\n // Must be robust against concurrent frame rendering where DOM state may change\n const restore = (): void => {\n const restoreStart = performance.now();\n \n // Guard: only remove if container is still a child of wrapper\n if (container.parentNode === wrapperElement) {\n wrapperElement.removeChild(container);\n }\n \n for (const { canvas, parent, nextSibling, img } of canvasRestoreInfo) {\n // Guard: only restore if img is still in expected position\n if (img.parentNode === parent) {\n // Use replaceChild which is atomic and safer than insertBefore + removeChild\n parent.replaceChild(canvas, img);\n } else if (canvas.parentNode !== parent) {\n // Canvas was never restored and img was moved/removed - try to restore canvas\n if (nextSibling && nextSibling.parentNode === parent) {\n parent.insertBefore(canvas, nextSibling);\n } else {\n parent.appendChild(canvas);\n }\n }\n }\n defaultProfiler.addTime(\"restore\", performance.now() - restoreStart);\n };\n \n // DEBUG: Log serialized HTML size for early renders\n if (logEarlyRenders && defaultProfiler.isEarlyRender(2)) {\n logger.debug(`[serializeToSvgDataUri] FO serialized: ${serialized.length} chars`);\n }\n \n // Phase 4: Create SVG and encode to base64\n const base64Start = performance.now();\n \n // Build SVG string with minimal allocations (concatenation is faster for small strings)\n const svg = SVG_PREFIX + width + SVG_HEIGHT_PREFIX + height + SVG_MIDDLE + serialized + SVG_SUFFIX;\n \n if (!_textEncoder) {\n _textEncoder = new TextEncoder();\n }\n const utf8Bytes = _textEncoder.encode(svg);\n \n let base64: string;\n if (typeof (Uint8Array.prototype as any).toBase64 === \"function\") {\n base64 = (utf8Bytes as any).toBase64();\n } else {\n base64 = encodeBase64Fast(utf8Bytes);\n }\n const dataUri = DATA_URI_PREFIX + base64;\n defaultProfiler.addTime(\"base64\", performance.now() - base64Start);\n \n return { dataUri, restore };\n}\n"],"mappings":";;;;;;;AAaA,IAAIA,iBAAuC;AAC3C,IAAIC,eAAmC;;;;;AAMvC,SAAS,gBAAgB,SAA2B;CAClD,IAAIC,UAA0B;AAC9B,QAAO,SAAS;AACd,MAAI,mBAAmB,eAAe,QAAQ,MAAM,YAAY,OAC9D,QAAO;AAET,YAAU,QAAQ;;AAEpB,QAAO;;AAIT,MAAM,aAAa;AACnB,MAAM,oBAAoB;AAC1B,MAAM,aAAa;AACnB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AAGxB,MAAM,qBAAqB;;;;;;;;;;;AAY3B,eAAsB,sBACpB,WACA,OACA,QACA,UAAiC,EAAE,EACL;CAC9B,MAAM,EACJ,cAAc,GACd,cAAc,qBAAqB,OACnC,kBAAkB,OAClB,eACA,cACE;CAGJ,MAAMC,oBAAyC,EAAE;CAIjD,MAAM,cAAc,YAAY,KAAK;CAErC,MAAM,kBADc,MAAM,KAAK,UAAU,iBAAiB,SAAS,CAAC,CAChC,QAAO,WAAU,CAAC,gBAAgB,OAAO,CAAC;CAM9E,MAAMC,kBAA8E,EAAE;AACtF,MAAK,IAAI,IAAI,GAAG,IAAI,gBAAgB,QAAQ,KAAK;EAC/C,MAAM,SAAS,gBAAgB;AAC/B,MAAI,OAAO,QAAQ,KAAK,OAAO,SAAS,GAAG;GACzC,MAAM,OAAO,SAAS,cAAc,SAAS;AAC7C,QAAK,QAAQ,OAAO;AACpB,QAAK,SAAS,OAAO;AAErB,OAAI,OAAO,QAAQ,cACjB,MAAK,QAAQ,gBAAgB,OAAO,QAAQ;GAE9C,MAAM,MAAM,KAAK,WAAW,KAAK;AACjC,OAAI,IAEF,KAAI,UAAU,QAAQ,GAAG,EAAE;AAE7B,mBAAgB,KAAK;IAAE,UAAU;IAAQ;IAAM,CAAC;;;CAapD,MAAM,wBAPiB,MAAM,yBADJ,gBAAgB,KAAI,MAAK,EAAE,KAAK,EACe;EACtE,OAAO;EACP;EACA;EACD,CAAC,EAG0C,KAAI,WAAU;EACxD,MAAM,WAAW,gBAAgB,MAAK,MAAK,EAAE,SAAS,OAAO,OAAO;AACpE,SAAO;GACL,GAAG;GACH,QAAQ,UAAU,YAAY,OAAO;GACtC;GACD;AAGF,MAAK,MAAM,EAAE,QAAQ,aAAa,qBAChC,KAAI;EACF,MAAM,MAAM,SAAS,cAAc,MAAM;AACzC,MAAI,MAAM;AACV,MAAI,QAAQ,OAAO;AACnB,MAAI,SAAS,OAAO;EACpB,MAAM,QAAQ,OAAO,aAAa,QAAQ;AAC1C,MAAI,MAAO,KAAI,aAAa,SAAS,MAAM;EAE3C,MAAM,SAAS,OAAO;AACtB,MAAI,QAAQ;GACV,MAAM,cAAc,OAAO;AAC3B,UAAO,aAAa,KAAK,OAAO;AAChC,qBAAkB,KAAK;IAAE;IAAQ;IAAQ;IAAa;IAAK,CAAC;;SAExD;AAIV,iBAAgB,QAAQ,gBAAgB,YAAY,KAAK,GAAG,YAAY;AAGxE,KAAI,oBAAoB;EACtB,MAAM,cAAc,YAAY,KAAK;AACrC,QAAM,aAAa,UAAU;AAC7B,kBAAgB,QAAQ,UAAU,YAAY,KAAK,GAAG,YAAY;;CAIpE,MAAM,iBAAiB,YAAY,KAAK;CAIxC,MAAM,iBAAiB,SAAS,cAAc,MAAM;AACpD,gBAAe,aAAa,SAAS,+BAA+B;AACpE,gBAAe,aAAa,SAAS,SAAS,MAAM,YAAY,OAAO,KAAK,qBAAqB;AACjG,gBAAe,YAAY,UAAU;AAErC,KAAI,CAAC,eACH,kBAAiB,IAAI,eAAe;CActC,MAAM,YAAY,YAAY,KAAK;CACnC,MAAM,aAAa,eAAe,kBAAkB,eAAe;CACnE,MAAM,gBAAgB,YAAY,KAAK,GAAG;AAG1C,KAAI,KAAK,QAAQ,GAAG,KAAM;EACxB,MAAM,eAAe,eAAe,iBAAiB,IAAI,CAAC;AAC1D,UAAQ,IAAI,wBAAwB,aAAa,SAAS,cAAc,QAAQ,EAAE,CAAC,YAAY,WAAW,SAAS,MAAM,QAAQ,EAAE,CAAC,IAAI;;AAG1I,iBAAgB,QAAQ,aAAa,YAAY,KAAK,GAAG,eAAe;CAIxE,MAAM,gBAAsB;EAC1B,MAAM,eAAe,YAAY,KAAK;AAGtC,MAAI,UAAU,eAAe,eAC3B,gBAAe,YAAY,UAAU;AAGvC,OAAK,MAAM,EAAE,QAAQ,QAAQ,aAAa,SAAS,kBAEjD,KAAI,IAAI,eAAe,OAErB,QAAO,aAAa,QAAQ,IAAI;WACvB,OAAO,eAAe,OAE/B,KAAI,eAAe,YAAY,eAAe,OAC5C,QAAO,aAAa,QAAQ,YAAY;MAExC,QAAO,YAAY,OAAO;AAIhC,kBAAgB,QAAQ,WAAW,YAAY,KAAK,GAAG,aAAa;;AAItE,KAAI,mBAAmB,gBAAgB,cAAc,EAAE,CACrD,QAAO,MAAM,0CAA0C,WAAW,OAAO,QAAQ;CAInF,MAAM,cAAc,YAAY,KAAK;CAGrC,MAAM,MAAM,aAAa,QAAQ,oBAAoB,SAAS,aAAa,aAAa;AAExF,KAAI,CAAC,aACH,gBAAe,IAAI,aAAa;CAElC,MAAM,YAAY,aAAa,OAAO,IAAI;CAE1C,IAAIC;AACJ,KAAI,OAAQ,WAAW,UAAkB,aAAa,WACpD,UAAU,UAAkB,UAAU;KAEtC,UAAS,iBAAiB,UAAU;CAEtC,MAAM,UAAU,kBAAkB;AAClC,iBAAgB,QAAQ,UAAU,YAAY,KAAK,GAAG,YAAY;AAElE,QAAO;EAAE;EAAS;EAAS"}
1
+ {"version":3,"file":"renderToImageForeignObject.js","names":["_xmlSerializer: XMLSerializer | null","_textEncoder: TextEncoder | null","current: Element | null","canvasRestoreInfo: CanvasRestoreInfo[]","canvasSnapshots: { original: HTMLCanvasElement; copy: HTMLCanvasElement }[]","snapshotSourceMap: WeakMap<HTMLCanvasElement, Element> | undefined","base64: string"],"sources":["../../../src/preview/rendering/renderToImageForeignObject.ts"],"sourcesContent":["/**\n * SVG foreignObject rendering path with serialization.\n */\n\nimport type { SerializeToSvgOptions, SerializationResult, CanvasRestoreInfo } from \"./types.js\";\nimport { encodeBase64Fast } from \"./svgSerializer.js\";\nimport { inlineImages } from \"./inlineImages.js\";\nimport { encodeCanvasesInParallel } from \"../encoding/canvasEncoder.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\nimport { logger } from \"../logger.js\";\n\n// Reusable instances for better performance (avoid creating new instances every frame)\n// Note: wrapper element is NOT reused - each concurrent frame needs its own wrapper\nlet _xmlSerializer: XMLSerializer | null = null;\nlet _textEncoder: TextEncoder | null = null;\n\n/**\n * Check if an element or any of its ancestors has display:none.\n * Used to skip encoding hidden canvases.\n */\nfunction isElementHidden(element: Element): boolean {\n let current: Element | null = element;\n while (current) {\n if (current instanceof HTMLElement && current.style.display === \"none\") {\n return true;\n }\n current = current.parentElement;\n }\n return false;\n}\n\n// Pre-computed SVG constants\nconst SVG_PREFIX = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"';\nconst SVG_HEIGHT_PREFIX = '\" height=\"';\nconst SVG_MIDDLE = '\"><foreignObject width=\"100%\" height=\"100%\">';\nconst SVG_SUFFIX = '</foreignObject></svg>';\nconst DATA_URI_PREFIX = 'data:image/svg+xml;base64,';\n\n// Shared style string to reduce allocations\nconst WRAPPER_STYLE_BASE = \"overflow:hidden;position:relative;\";\n\n/**\n * Common SVG foreignObject serialization pipeline.\n * Handles canvas encoding, serialization, and base64 encoding.\n * \n * @param container - The HTML element to serialize\n * @param width - Output width\n * @param height - Output height\n * @param options - Serialization options\n * @returns Serialization result with data URI and restore function\n */\nexport async function serializeToSvgDataUri(\n container: HTMLElement,\n width: number,\n height: number,\n options: SerializeToSvgOptions = {},\n): Promise<SerializationResult> {\n const { \n canvasScale = 1, \n inlineImages: shouldInlineImages = false, \n logEarlyRenders = false,\n renderContext,\n sourceMap,\n } = options;\n \n // Store info for restoration (only used if modifying in-place)\n const canvasRestoreInfo: CanvasRestoreInfo[] = [];\n \n // Phase 1: Encode canvases to data URLs (parallel)\n // Filter out hidden canvases - they have display:none and won't render anyway\n const canvasStart = performance.now();\n const allCanvases = Array.from(container.querySelectorAll(\"canvas\"));\n const visibleCanvases = allCanvases.filter(canvas => !isElementHidden(canvas));\n \n // CRITICAL FIX: Synchronously copy canvas pixels BEFORE any async work.\n // This prevents race conditions where concurrent render tasks overwrite\n // the shared clone canvases while encoding is in progress.\n // \n // OPTIMIZATION: Calculate optimal encoding resolution based on:\n // 1. CSS display size (how big it actually appears)\n // 2. Video export scale (output resolution multiplier)\n // 3. Quality multiplier (for sharpness, default 1.5x)\n const canvasSnapshots: { original: HTMLCanvasElement; copy: HTMLCanvasElement }[] = [];\n const qualityMultiplier = 1.5; // Encode at 1.5x display size for quality\n \n for (let i = 0; i < visibleCanvases.length; i++) {\n const canvas = visibleCanvases[i]!;\n if (canvas.width > 0 && canvas.height > 0) {\n // Calculate optimal encoding scale\n let optimalScale = canvasScale; // Start with video export scale\n \n // If we have sourceMap, calculate based on CSS display size\n if (sourceMap) {\n const sourceElement = sourceMap.get(canvas);\n if (sourceElement) {\n try {\n const computedStyle = getComputedStyle(sourceElement);\n const cssWidth = parseFloat(computedStyle.width) || canvas.width;\n const cssHeight = parseFloat(computedStyle.height) || canvas.height;\n \n // Calculate how much smaller the display is vs natural size\n const displayScaleX = cssWidth / canvas.width;\n const displayScaleY = cssHeight / canvas.height;\n const displayScale = Math.min(displayScaleX, displayScaleY);\n \n // Combine display scale, video scale, and quality multiplier\n // Clamp to 1.0 max (never upscale beyond natural resolution)\n optimalScale = Math.min(1.0, displayScale * canvasScale * qualityMultiplier);\n \n console.log(`[serializeToSvg] Canvas ${canvas.width}x${canvas.height} -> CSS ${cssWidth.toFixed(0)}x${cssHeight.toFixed(0)}, displayScale=${displayScale.toFixed(3)}, videoScale=${canvasScale}, optimalScale=${optimalScale.toFixed(3)}`);\n } catch (e) {\n // Fallback to just video scale if we can't get computed style\n console.warn(`[serializeToSvg] Failed to get computed style for ${sourceElement.tagName}:`, e);\n }\n }\n }\n \n // Create snapshot at optimal resolution\n const targetWidth = Math.max(1, Math.floor(canvas.width * optimalScale));\n const targetHeight = Math.max(1, Math.floor(canvas.height * optimalScale));\n \n const copy = document.createElement(\"canvas\");\n copy.width = targetWidth;\n copy.height = targetHeight;\n \n // Copy dataset attributes (e.g., preserveAlpha)\n if (canvas.dataset.preserveAlpha) {\n copy.dataset.preserveAlpha = canvas.dataset.preserveAlpha;\n }\n \n const ctx = copy.getContext(\"2d\");\n if (ctx) {\n // drawImage with scaling is SYNCHRONOUS - pixels are copied and scaled immediately\n ctx.drawImage(canvas, 0, 0, targetWidth, targetHeight);\n }\n canvasSnapshots.push({ original: canvas, copy });\n }\n }\n \n // Encode from the snapshot copies (safe from concurrent overwrites)\n const snapshotCanvases = canvasSnapshots.map(s => s.copy);\n \n // Create a new sourceMap that maps snapshot canvases to their source elements\n // The original sourceMap maps original canvases -> source elements\n // We need snapshot canvases -> source elements for caching to work\n let snapshotSourceMap: WeakMap<HTMLCanvasElement, Element> | undefined;\n if (sourceMap) {\n snapshotSourceMap = new WeakMap();\n for (const { original, copy } of canvasSnapshots) {\n const sourceElement = sourceMap.get(original);\n if (sourceElement) {\n snapshotSourceMap.set(copy, sourceElement);\n }\n }\n }\n \n // Snapshots are already scaled to optimal resolution, so encode at 1.0 scale\n const encodedResults = await encodeCanvasesInParallel(snapshotCanvases, { \n scale: 1.0, // Already scaled during snapshot creation\n renderContext,\n sourceMap: snapshotSourceMap,\n });\n \n // Map encoded results back to original canvases for DOM replacement\n const encodedWithOriginals = encodedResults.map(result => {\n const snapshot = canvasSnapshots.find(s => s.copy === result.canvas);\n return {\n ...result,\n canvas: snapshot?.original ?? result.canvas,\n };\n });\n \n // Replace canvases with images\n for (const { canvas, dataUrl } of encodedWithOriginals) {\n try {\n const img = document.createElement(\"img\");\n img.src = dataUrl;\n img.width = canvas.width;\n img.height = canvas.height;\n const style = canvas.getAttribute(\"style\");\n if (style) img.setAttribute(\"style\", style);\n \n const parent = canvas.parentNode;\n if (parent) {\n const nextSibling = canvas.nextSibling;\n parent.replaceChild(img, canvas);\n canvasRestoreInfo.push({ canvas, parent, nextSibling, img });\n }\n } catch {\n // Cross-origin canvas - leave as-is\n }\n }\n defaultProfiler.addTime(\"canvasEncode\", performance.now() - canvasStart);\n \n // Phase 2: Inline external images (if requested)\n if (shouldInlineImages) {\n const inlineStart = performance.now();\n await inlineImages(container);\n defaultProfiler.addTime(\"inline\", performance.now() - inlineStart);\n }\n \n // Phase 3: Serialize to XHTML\n const serializeStart = performance.now();\n \n // Create fresh wrapper element for THIS frame (local variable for closure safety)\n // Multiple concurrent frames in video export each get their own wrapper\n const wrapperElement = document.createElement(\"div\");\n wrapperElement.setAttribute(\"xmlns\", \"http://www.w3.org/1999/xhtml\");\n wrapperElement.setAttribute(\"style\", `width:${width}px;height:${height}px;${WRAPPER_STYLE_BASE}`);\n wrapperElement.appendChild(container);\n \n if (!_xmlSerializer) {\n _xmlSerializer = new XMLSerializer();\n }\n \n // NOTE: Hidden element handling is now done by the caller via removeHiddenNodesForSerialization().\n // The caller physically removes hidden nodes from the clone tree BEFORE calling this function,\n // so hidden elements are never serialized at all - not just hidden with display:none.\n //\n // Benefits of removing before serialization:\n // - Hidden canvases are not encoded (saves encoding time and memory)\n // - Hidden elements are not serialized (smaller SVG, faster serialization)\n // - Hidden images are not inlined (saves fetch and encoding)\n // - The serialized output is smaller and faster to base64 encode\n \n // Serialize to XHTML string\n const perfStart = performance.now();\n const serialized = _xmlSerializer.serializeToString(wrapperElement);\n const serializeTime = performance.now() - perfStart;\n \n // Sample 1% of frames to avoid spam\n if (Math.random() < 0.01) {\n const elementCount = wrapperElement.querySelectorAll('*').length;\n console.log(`[serialize] elements=${elementCount}, time=${serializeTime.toFixed(1)}ms, size=${(serialized.length / 1024).toFixed(1)}KB`);\n }\n\n defaultProfiler.addTime(\"serialize\", performance.now() - serializeStart);\n \n // Prepare restore function (removes container from wrapper, restores canvases)\n // Must be robust against concurrent frame rendering where DOM state may change\n const restore = (): void => {\n const restoreStart = performance.now();\n \n // Guard: only remove if container is still a child of wrapper\n if (container.parentNode === wrapperElement) {\n wrapperElement.removeChild(container);\n }\n \n for (const { canvas, parent, nextSibling, img } of canvasRestoreInfo) {\n // Guard: only restore if img is still in expected position\n if (img.parentNode === parent) {\n // Use replaceChild which is atomic and safer than insertBefore + removeChild\n parent.replaceChild(canvas, img);\n } else if (canvas.parentNode !== parent) {\n // Canvas was never restored and img was moved/removed - try to restore canvas\n if (nextSibling && nextSibling.parentNode === parent) {\n parent.insertBefore(canvas, nextSibling);\n } else {\n parent.appendChild(canvas);\n }\n }\n }\n defaultProfiler.addTime(\"restore\", performance.now() - restoreStart);\n };\n \n // DEBUG: Log serialized HTML size for early renders\n if (logEarlyRenders && defaultProfiler.isEarlyRender(2)) {\n logger.debug(`[serializeToSvgDataUri] FO serialized: ${serialized.length} chars`);\n }\n \n // Phase 4: Create SVG and encode to base64\n const base64Start = performance.now();\n \n // Build SVG string with minimal allocations (concatenation is faster for small strings)\n const svg = SVG_PREFIX + width + SVG_HEIGHT_PREFIX + height + SVG_MIDDLE + serialized + SVG_SUFFIX;\n \n if (!_textEncoder) {\n _textEncoder = new TextEncoder();\n }\n const utf8Bytes = _textEncoder.encode(svg);\n \n let base64: string;\n if (typeof (Uint8Array.prototype as any).toBase64 === \"function\") {\n base64 = (utf8Bytes as any).toBase64();\n } else {\n base64 = encodeBase64Fast(utf8Bytes);\n }\n const dataUri = DATA_URI_PREFIX + base64;\n defaultProfiler.addTime(\"base64\", performance.now() - base64Start);\n \n return { dataUri, restore };\n}\n"],"mappings":"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"}
@@ -0,0 +1,379 @@
1
+ import { isVisibleAtTime } from "../previewTypes.js";
2
+ import { collectDocumentStyles } from "../renderTimegroupPreview.js";
3
+ import { encodeCanvasesInParallel } from "../encoding/canvasEncoder.js";
4
+
5
+ //#region src/preview/rendering/serializeTimelineDirect.ts
6
+ /**
7
+ * Elements to skip entirely when serializing.
8
+ * NOTE: SLOT is NOT skipped - it's handled specially to serialize light DOM children.
9
+ */
10
+ const SKIP_TAGS = new Set([
11
+ "EF-AUDIO",
12
+ "EF-THUMBNAIL-STRIP",
13
+ "EF-FILMSTRIP",
14
+ "EF-TIMELINE",
15
+ "EF-WORKBENCH",
16
+ "SCRIPT",
17
+ "STYLE",
18
+ "TEMPLATE"
19
+ ]);
20
+ /**
21
+ * HTML void elements - these cannot have children and must be self-closing in XHTML.
22
+ * Using `<br />` instead of `<br></br>`.
23
+ */
24
+ const VOID_ELEMENTS = new Set([
25
+ "area",
26
+ "base",
27
+ "br",
28
+ "col",
29
+ "embed",
30
+ "hr",
31
+ "img",
32
+ "input",
33
+ "link",
34
+ "meta",
35
+ "param",
36
+ "source",
37
+ "track",
38
+ "wbr"
39
+ ]);
40
+ /**
41
+ * CSS properties to serialize as inline styles.
42
+ */
43
+ const SERIALIZED_STYLE_PROPERTIES = [
44
+ "display",
45
+ "visibility",
46
+ "opacity",
47
+ "position",
48
+ "top",
49
+ "right",
50
+ "bottom",
51
+ "left",
52
+ "zIndex",
53
+ "width",
54
+ "height",
55
+ "minWidth",
56
+ "minHeight",
57
+ "maxWidth",
58
+ "maxHeight",
59
+ "flexGrow",
60
+ "flexShrink",
61
+ "flexBasis",
62
+ "flexDirection",
63
+ "flexWrap",
64
+ "justifyContent",
65
+ "alignItems",
66
+ "alignContent",
67
+ "alignSelf",
68
+ "gap",
69
+ "gridTemplate",
70
+ "gridColumn",
71
+ "gridRow",
72
+ "gridArea",
73
+ "margin",
74
+ "padding",
75
+ "boxSizing",
76
+ "border",
77
+ "borderTop",
78
+ "borderRight",
79
+ "borderBottom",
80
+ "borderLeft",
81
+ "borderRadius",
82
+ "background",
83
+ "color",
84
+ "boxShadow",
85
+ "filter",
86
+ "backdropFilter",
87
+ "clipPath",
88
+ "fontFamily",
89
+ "fontSize",
90
+ "fontWeight",
91
+ "fontStyle",
92
+ "fontVariant",
93
+ "textAlign",
94
+ "textDecoration",
95
+ "textTransform",
96
+ "letterSpacing",
97
+ "wordSpacing",
98
+ "whiteSpace",
99
+ "textOverflow",
100
+ "lineHeight",
101
+ "verticalAlign",
102
+ "transform",
103
+ "transformOrigin",
104
+ "transformStyle",
105
+ "perspective",
106
+ "perspectiveOrigin",
107
+ "backfaceVisibility",
108
+ "cursor",
109
+ "pointerEvents",
110
+ "userSelect",
111
+ "overflow",
112
+ "objectFit",
113
+ "objectPosition"
114
+ ];
115
+ /**
116
+ * Caption child elements that should preserve display:none.
117
+ * These use display:none for content visibility, not temporal visibility.
118
+ */
119
+ const CAPTION_CHILD_TAGS = new Set([
120
+ "EF-CAPTIONS-ACTIVE-WORD",
121
+ "EF-CAPTIONS-BEFORE-ACTIVE-WORD",
122
+ "EF-CAPTIONS-AFTER-ACTIVE-WORD",
123
+ "EF-CAPTIONS-SEGMENT"
124
+ ]);
125
+ /**
126
+ * Escape special XML characters.
127
+ */
128
+ function escapeXML(str) {
129
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
130
+ }
131
+ /**
132
+ * Serialize computed styles as inline style string.
133
+ * Handles display:none → block conversion for non-caption elements
134
+ * (temporal visibility is handled separately).
135
+ */
136
+ function serializeComputedStyles(element) {
137
+ const styles = getComputedStyle(element);
138
+ const styleParts = [];
139
+ const tagName = element.tagName;
140
+ const isCaptionChild = CAPTION_CHILD_TAGS.has(tagName);
141
+ for (const prop of SERIALIZED_STYLE_PROPERTIES) {
142
+ const value = styles[prop];
143
+ if (!value || value === "") continue;
144
+ let finalValue = value;
145
+ if (prop === "display") {
146
+ if (tagName === "EF-TEXT") finalValue = element.getAttribute("split") === "line" ? "flex" : "inline-flex";
147
+ else if (tagName === "EF-TEXT-SEGMENT") finalValue = element.hasAttribute("data-line-segment") ? "block" : "inline-block";
148
+ else if (value === "none" && !isCaptionChild) finalValue = "block";
149
+ }
150
+ if (prop === "visibility") finalValue = "visible";
151
+ if (prop === "clipPath") continue;
152
+ const kebab = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
153
+ styleParts.push(`${kebab}:${finalValue}`);
154
+ }
155
+ styleParts.push("animation:none", "transition:none");
156
+ return styleParts.join(";");
157
+ }
158
+ /**
159
+ * Serialize element attributes (excluding style, id, xmlns, event handlers).
160
+ */
161
+ function serializeAttributes(element, parts) {
162
+ for (const attr of element.attributes) {
163
+ const name = attr.name.toLowerCase();
164
+ if (name === "id" || name === "style" || name === "xmlns" || name.startsWith("on")) continue;
165
+ parts.push(` ${attr.name}="${escapeXML(attr.value)}"`);
166
+ }
167
+ }
168
+ /**
169
+ * Check if a canvas element should preserve alpha channel.
170
+ * EF-WAVEFORM always needs alpha, EF-IMAGE checks hasAlpha property.
171
+ */
172
+ function shouldPreserveAlpha(sourceElement) {
173
+ const tagName = sourceElement.tagName;
174
+ if (tagName === "EF-WAVEFORM") return true;
175
+ if (tagName === "EF-IMAGE") return "hasAlpha" in sourceElement && sourceElement.hasAlpha === true;
176
+ return false;
177
+ }
178
+ /**
179
+ * Create a snapshot copy of a canvas's current pixels.
180
+ * This captures the pixels synchronously before any async encoding,
181
+ * preventing race conditions where the source canvas is modified.
182
+ */
183
+ function snapshotCanvas(canvas, scale, preserveAlpha) {
184
+ const targetWidth = Math.max(1, Math.floor(canvas.width * scale));
185
+ const targetHeight = Math.max(1, Math.floor(canvas.height * scale));
186
+ const copy = document.createElement("canvas");
187
+ copy.width = targetWidth;
188
+ copy.height = targetHeight;
189
+ if (preserveAlpha) copy.dataset.preserveAlpha = "true";
190
+ const ctx = copy.getContext("2d");
191
+ if (ctx && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0, targetWidth, targetHeight);
192
+ return copy;
193
+ }
194
+ /**
195
+ * Serialize a canvas element as an <img> with base64 data URL.
196
+ * Creates a snapshot of current pixels before async encoding to prevent race conditions.
197
+ *
198
+ * OPTIMIZATION: Calculate optimal encoding resolution based on:
199
+ * 1. CSS display size (how big it actually appears)
200
+ * 2. Video export scale (output resolution multiplier)
201
+ * 3. Quality multiplier (for sharpness, default 1.5x)
202
+ */
203
+ function serializeCanvas(sourceElement, canvas, parts, canvasJobs, options) {
204
+ const width = canvas.width;
205
+ const height = canvas.height;
206
+ if (width === 0 || height === 0) return;
207
+ const styleStr = serializeComputedStyles(sourceElement);
208
+ const computedStyle = getComputedStyle(sourceElement);
209
+ const computedWidth = computedStyle.width;
210
+ const computedHeight = computedStyle.height;
211
+ const styleParts = styleStr ? styleStr.split(";").filter((s) => s.trim()) : [];
212
+ const hasWidth = styleParts.some((s) => s.trim().startsWith("width:"));
213
+ const hasHeight = styleParts.some((s) => s.trim().startsWith("height:"));
214
+ if (!hasWidth) styleParts.push(`width:${computedWidth || `${width}px`}`);
215
+ if (!hasHeight) styleParts.push(`height:${computedHeight || `${height}px`}`);
216
+ styleParts.push(`display:block`);
217
+ const finalStyle = styleParts.join(";");
218
+ const preserveAlpha = shouldPreserveAlpha(sourceElement);
219
+ let optimalScale = options.canvasScale;
220
+ const qualityMultiplier = 1.5;
221
+ try {
222
+ const cssWidth = parseFloat(computedWidth) || canvas.width;
223
+ const cssHeight = parseFloat(computedHeight) || canvas.height;
224
+ const displayScaleX = cssWidth / canvas.width;
225
+ const displayScaleY = cssHeight / canvas.height;
226
+ const displayScale = Math.min(displayScaleX, displayScaleY);
227
+ optimalScale = Math.min(1, displayScale * options.canvasScale * qualityMultiplier);
228
+ } catch (e) {
229
+ console.warn(`[serializeCanvas] Failed to get computed style for ${sourceElement.tagName}:`, e);
230
+ }
231
+ const snapshot = snapshotCanvas(canvas, optimalScale, preserveAlpha);
232
+ parts.push(`<img style="${escapeXML(finalStyle)}" src="`);
233
+ const promiseIndex = parts.length;
234
+ const sourceMap = /* @__PURE__ */ new WeakMap();
235
+ sourceMap.set(snapshot, sourceElement);
236
+ const encodePromise = encodeCanvasesInParallel([snapshot], {
237
+ scale: 1,
238
+ renderContext: options.renderContext,
239
+ sourceMap
240
+ }).then((results) => results[0]?.dataUrl || "");
241
+ parts.push(encodePromise);
242
+ canvasJobs.push({
243
+ canvas: snapshot,
244
+ sourceElement,
245
+ promiseIndex
246
+ });
247
+ parts.push("\" />");
248
+ }
249
+ /**
250
+ * Serialize an image element as a canvas (for shadow DOM img elements).
251
+ */
252
+ function serializeImageAsCanvas(sourceElement, img, parts, canvasJobs, options) {
253
+ const canvas = document.createElement("canvas");
254
+ canvas.width = img.naturalWidth;
255
+ canvas.height = img.naturalHeight;
256
+ const ctx = canvas.getContext("2d");
257
+ if (ctx) try {
258
+ ctx.drawImage(img, 0, 0);
259
+ } catch (e) {
260
+ return;
261
+ }
262
+ serializeCanvas(sourceElement, canvas, parts, canvasJobs, options);
263
+ }
264
+ /**
265
+ * Serialize slotted light DOM children of a host element.
266
+ */
267
+ function serializeSlottedContent(slotHost, parts, canvasJobs, options, parentIsSVG) {
268
+ for (const slottedChild of slotHost.childNodes) if (slottedChild.nodeType === Node.TEXT_NODE) {
269
+ const text = slottedChild.textContent;
270
+ if (text && text.length > 0) parts.push(escapeXML(text));
271
+ } else if (slottedChild.nodeType === Node.ELEMENT_NODE) serializeElement(slottedChild, parts, canvasJobs, options, parentIsSVG, null);
272
+ }
273
+ /**
274
+ * Recursively serialize an element and its children to XML parts.
275
+ * @param slotHost - When serializing inside shadow DOM, the custom element whose light DOM children should be serialized for slots
276
+ */
277
+ function serializeElement(element, parts, canvasJobs, options, parentIsSVG = false, slotHost = null) {
278
+ if (SKIP_TAGS.has(element.tagName)) return;
279
+ if (element.tagName === "SLOT" && slotHost) {
280
+ serializeSlottedContent(slotHost, parts, canvasJobs, options, parentIsSVG);
281
+ return;
282
+ }
283
+ if (!isTemporallyVisible(element, options.timeMs)) return;
284
+ if (element.tagName.includes("-") && element.shadowRoot) {
285
+ const shadowCanvas = element.shadowRoot.querySelector("canvas");
286
+ if (shadowCanvas) {
287
+ serializeCanvas(element, shadowCanvas, parts, canvasJobs, options);
288
+ return;
289
+ }
290
+ const shadowImg = element.shadowRoot.querySelector("img");
291
+ if (shadowImg?.complete && shadowImg.naturalWidth > 0) {
292
+ serializeImageAsCanvas(element, shadowImg, parts, canvasJobs, options);
293
+ return;
294
+ }
295
+ const computedDisplay = getComputedStyle(element).display;
296
+ const containerTag = computedDisplay === "inline" || computedDisplay === "inline-block" || computedDisplay === "inline-flex" ? "span" : "div";
297
+ let styleStr$1 = serializeComputedStyles(element);
298
+ parts.push(`<${containerTag}`);
299
+ for (const attr of element.attributes) {
300
+ const name = attr.name.toLowerCase();
301
+ if (name === "class" || name.startsWith("data-")) parts.push(` ${attr.name}="${escapeXML(attr.value)}"`);
302
+ }
303
+ if (styleStr$1) parts.push(` style="${escapeXML(styleStr$1)}"`);
304
+ parts.push(">");
305
+ for (const child of element.shadowRoot.childNodes) if (child.nodeType === Node.TEXT_NODE) {
306
+ const text = child.textContent;
307
+ if (text && text.length > 0) parts.push(escapeXML(text));
308
+ } else if (child.nodeType === Node.ELEMENT_NODE) serializeElement(child, parts, canvasJobs, options, parentIsSVG, element);
309
+ parts.push(`</${containerTag}>`);
310
+ return;
311
+ }
312
+ if (element instanceof HTMLCanvasElement) {
313
+ serializeCanvas(element, element, parts, canvasJobs, options);
314
+ return;
315
+ }
316
+ const tagName = element.tagName.toLowerCase();
317
+ const isSVG = element instanceof SVGElement;
318
+ const isVoid = VOID_ELEMENTS.has(tagName);
319
+ if (isSVG && !parentIsSVG) parts.push(`<${tagName} xmlns="http://www.w3.org/2000/svg"`);
320
+ else parts.push(`<${tagName}`);
321
+ serializeAttributes(element, parts);
322
+ const styleStr = serializeComputedStyles(element);
323
+ if (styleStr) parts.push(` style="${escapeXML(styleStr)}"`);
324
+ if (isVoid) {
325
+ parts.push(" />");
326
+ return;
327
+ }
328
+ parts.push(">");
329
+ const children = element.shadowRoot?.childNodes || element.childNodes;
330
+ for (const child of children) if (child.nodeType === Node.TEXT_NODE) {
331
+ const text = child.textContent;
332
+ if (text && text.length > 0) parts.push(escapeXML(text));
333
+ } else if (child.nodeType === Node.ELEMENT_NODE) serializeElement(child, parts, canvasJobs, options, isSVG, slotHost);
334
+ parts.push(`</${tagName}>`);
335
+ }
336
+ /**
337
+ * Check if an element is temporally visible at the given time.
338
+ * Returns false if the element or any ancestor is outside its temporal bounds.
339
+ */
340
+ function isTemporallyVisible(element, timeMs) {
341
+ if (!isVisibleAtTime(element, timeMs)) return false;
342
+ return true;
343
+ }
344
+ /**
345
+ * Serialize a timeline element directly to XHTML string.
346
+ *
347
+ * @param timeline - The timeline element to serialize (e.g., EFTimegroup)
348
+ * @param width - Output width
349
+ * @param height - Output height
350
+ * @param options - Serialization options (renderContext, canvasScale, timeMs)
351
+ * @returns XHTML string with all canvases encoded as base64 data URLs
352
+ */
353
+ async function serializeTimelineToXHTML(timeline, width, height, options) {
354
+ const parts = [];
355
+ const canvasJobs = [];
356
+ const documentStyles = collectDocumentStyles();
357
+ parts.push(`<div xmlns="http://www.w3.org/1999/xhtml" style="width:${width}px;height:${height}px;overflow:hidden;position:relative;">`);
358
+ if (documentStyles) parts.push(`<style type="text/css"><![CDATA[${documentStyles}]]></style>`);
359
+ serializeElement(timeline, parts, canvasJobs, options);
360
+ parts.push("</div>");
361
+ return (await Promise.all(parts)).join("");
362
+ }
363
+ /**
364
+ * Serialize timeline to SVG foreignObject data URI (ready for rendering).
365
+ *
366
+ * @param timeline - The timeline element to serialize
367
+ * @param width - Output width
368
+ * @param height - Output height
369
+ * @param options - Serialization options
370
+ * @returns SVG data URI
371
+ */
372
+ async function serializeTimelineToDataUri(timeline, width, height, options) {
373
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><foreignObject x="0" y="0" width="${width}" height="${height}">${await serializeTimelineToXHTML(timeline, width, height, options)}</foreignObject></svg>`;
374
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
375
+ }
376
+
377
+ //#endregion
378
+ export { serializeTimelineToDataUri };
379
+ //# sourceMappingURL=serializeTimelineDirect.js.map