@editframe/elements 0.37.2-beta → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/dist/EF_FRAMEGEN.js +17 -14
  2. package/dist/EF_FRAMEGEN.js.map +1 -1
  3. package/dist/EF_RENDERING.js.map +1 -1
  4. package/dist/canvas/EFCanvas.d.ts +9 -2
  5. package/dist/canvas/EFCanvas.js +14 -4
  6. package/dist/canvas/EFCanvas.js.map +1 -1
  7. package/dist/canvas/EFCanvasItem.d.ts +4 -4
  8. package/dist/canvas/overlays/SelectionOverlay.d.ts +10 -2
  9. package/dist/canvas/overlays/SelectionOverlay.js +5 -12
  10. package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
  11. package/dist/canvas/overlays/overlayState.js.map +1 -1
  12. package/dist/canvas/selection/SelectionController.js.map +1 -1
  13. package/dist/elements/EFAudio.d.ts +1 -11
  14. package/dist/elements/EFAudio.js +2 -10
  15. package/dist/elements/EFAudio.js.map +1 -1
  16. package/dist/elements/EFCaptions.d.ts +5 -9
  17. package/dist/elements/EFCaptions.js +34 -11
  18. package/dist/elements/EFCaptions.js.map +1 -1
  19. package/dist/elements/EFImage.d.ts +10 -8
  20. package/dist/elements/EFImage.js +117 -32
  21. package/dist/elements/EFImage.js.map +1 -1
  22. package/dist/elements/EFMedia/AssetMediaEngine.js +2 -2
  23. package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
  24. package/dist/elements/EFMedia/BaseMediaEngine.js +15 -92
  25. package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
  26. package/dist/elements/EFMedia/BufferedSeekingInput.js +10 -11
  27. package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
  28. package/dist/elements/EFMedia/{AssetIdMediaEngine.js → FileMediaEngine.js} +44 -24
  29. package/dist/elements/EFMedia/FileMediaEngine.js.map +1 -0
  30. package/dist/elements/EFMedia/JitMediaEngine.js +14 -13
  31. package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
  32. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -3
  33. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  34. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +12 -7
  35. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  36. package/dist/elements/EFMedia/shared/timeoutUtils.js +44 -0
  37. package/dist/elements/EFMedia/shared/timeoutUtils.js.map +1 -0
  38. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +1 -1
  39. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
  40. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +4 -4
  41. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
  42. package/dist/elements/EFMedia.d.ts +14 -8
  43. package/dist/elements/EFMedia.js +52 -19
  44. package/dist/elements/EFMedia.js.map +1 -1
  45. package/dist/elements/EFPanZoom.d.ts +2 -2
  46. package/dist/elements/EFPanZoom.js +1 -1
  47. package/dist/elements/EFPanZoom.js.map +1 -1
  48. package/dist/elements/EFSourceMixin.js +16 -8
  49. package/dist/elements/EFSourceMixin.js.map +1 -1
  50. package/dist/elements/EFSurface.d.ts +7 -10
  51. package/dist/elements/EFSurface.js +4 -43
  52. package/dist/elements/EFSurface.js.map +1 -1
  53. package/dist/elements/EFTemporal.d.ts +33 -8
  54. package/dist/elements/EFTemporal.js +92 -40
  55. package/dist/elements/EFTemporal.js.map +1 -1
  56. package/dist/elements/EFText.d.ts +3 -0
  57. package/dist/elements/EFText.js +54 -21
  58. package/dist/elements/EFText.js.map +1 -1
  59. package/dist/elements/EFTextSegment.js +8 -4
  60. package/dist/elements/EFTextSegment.js.map +1 -1
  61. package/dist/elements/EFTimegroup.d.ts +26 -43
  62. package/dist/elements/EFTimegroup.js +295 -314
  63. package/dist/elements/EFTimegroup.js.map +1 -1
  64. package/dist/elements/EFVideo.d.ts +44 -42
  65. package/dist/elements/EFVideo.js +259 -172
  66. package/dist/elements/EFVideo.js.map +1 -1
  67. package/dist/elements/EFWaveform.d.ts +3 -8
  68. package/dist/elements/EFWaveform.js +18 -13
  69. package/dist/elements/EFWaveform.js.map +1 -1
  70. package/dist/elements/ElementPositionInfo.js.map +1 -1
  71. package/dist/elements/FetchMixin.js.map +1 -1
  72. package/dist/elements/TargetController.d.ts +0 -3
  73. package/dist/elements/TargetController.js +12 -35
  74. package/dist/elements/TargetController.js.map +1 -1
  75. package/dist/elements/TimegroupController.js.map +1 -1
  76. package/dist/elements/cloneFactoryRegistry.d.ts +14 -0
  77. package/dist/elements/cloneFactoryRegistry.js +15 -0
  78. package/dist/elements/cloneFactoryRegistry.js.map +1 -0
  79. package/dist/elements/renderTemporalAudio.js +8 -6
  80. package/dist/elements/renderTemporalAudio.js.map +1 -1
  81. package/dist/elements/setupTemporalHierarchy.js +62 -0
  82. package/dist/elements/setupTemporalHierarchy.js.map +1 -0
  83. package/dist/elements/updateAnimations.js +62 -87
  84. package/dist/elements/updateAnimations.js.map +1 -1
  85. package/dist/getRenderInfo.d.ts +3 -2
  86. package/dist/getRenderInfo.js +20 -4
  87. package/dist/getRenderInfo.js.map +1 -1
  88. package/dist/gui/ContextMixin.js +68 -12
  89. package/dist/gui/ContextMixin.js.map +1 -1
  90. package/dist/gui/Controllable.js +1 -1
  91. package/dist/gui/Controllable.js.map +1 -1
  92. package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
  93. package/dist/gui/EFActiveRootTemporal.js.map +1 -1
  94. package/dist/gui/EFControls.d.ts +2 -2
  95. package/dist/gui/EFControls.js +2 -2
  96. package/dist/gui/EFControls.js.map +1 -1
  97. package/dist/gui/EFDial.d.ts +4 -4
  98. package/dist/gui/EFDial.js +12 -9
  99. package/dist/gui/EFDial.js.map +1 -1
  100. package/dist/gui/EFFilmstrip.d.ts +2 -0
  101. package/dist/gui/EFFilmstrip.js +18 -10
  102. package/dist/gui/EFFilmstrip.js.map +1 -1
  103. package/dist/gui/EFFitScale.d.ts +28 -4
  104. package/dist/gui/EFFitScale.js +88 -26
  105. package/dist/gui/EFFitScale.js.map +1 -1
  106. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  107. package/dist/gui/EFFocusOverlay.js +3 -3
  108. package/dist/gui/EFFocusOverlay.js.map +1 -1
  109. package/dist/gui/EFOverlayItem.d.ts +4 -4
  110. package/dist/gui/EFOverlayLayer.d.ts +4 -4
  111. package/dist/gui/EFPause.d.ts +4 -4
  112. package/dist/gui/EFPause.js +1 -1
  113. package/dist/gui/EFPlay.d.ts +4 -4
  114. package/dist/gui/EFPlay.js +1 -1
  115. package/dist/gui/EFPreview.js +1 -1
  116. package/dist/gui/EFResizableBox.d.ts +4 -4
  117. package/dist/gui/EFResizableBox.js +5 -5
  118. package/dist/gui/EFResizableBox.js.map +1 -1
  119. package/dist/gui/EFScrubber.d.ts +4 -4
  120. package/dist/gui/EFScrubber.js +8 -13
  121. package/dist/gui/EFScrubber.js.map +1 -1
  122. package/dist/gui/EFTimeDisplay.d.ts +8 -4
  123. package/dist/gui/EFTimeDisplay.js +25 -7
  124. package/dist/gui/EFTimeDisplay.js.map +1 -1
  125. package/dist/gui/EFTimelineRuler.d.ts +4 -4
  126. package/dist/gui/EFTimelineRuler.js +3 -3
  127. package/dist/gui/EFTimelineRuler.js.map +1 -1
  128. package/dist/gui/EFToggleLoop.d.ts +4 -4
  129. package/dist/gui/EFToggleLoop.js +1 -1
  130. package/dist/gui/EFTogglePlay.d.ts +4 -4
  131. package/dist/gui/EFTogglePlay.js +1 -1
  132. package/dist/gui/EFTransformHandles.d.ts +4 -4
  133. package/dist/gui/EFTransformHandles.js +6 -6
  134. package/dist/gui/EFTransformHandles.js.map +1 -1
  135. package/dist/gui/EFWorkbench.d.ts +40 -36
  136. package/dist/gui/EFWorkbench.js +436 -822
  137. package/dist/gui/EFWorkbench.js.map +1 -1
  138. package/dist/gui/FitScaleHelpers.js.map +1 -1
  139. package/dist/gui/PlaybackController.d.ts +3 -8
  140. package/dist/gui/PlaybackController.js +59 -56
  141. package/dist/gui/PlaybackController.js.map +1 -1
  142. package/dist/gui/TWMixin.js +1 -1
  143. package/dist/gui/TWMixin.js.map +1 -1
  144. package/dist/gui/TargetOrContextMixin.js +43 -6
  145. package/dist/gui/TargetOrContextMixin.js.map +1 -1
  146. package/dist/gui/ef-theme.css +136 -0
  147. package/dist/gui/hierarchy/EFHierarchy.d.ts +2 -2
  148. package/dist/gui/hierarchy/EFHierarchy.js +14 -24
  149. package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
  150. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +3 -3
  151. package/dist/gui/hierarchy/EFHierarchyItem.js +22 -10
  152. package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
  153. package/dist/gui/icons.js.map +1 -1
  154. package/dist/gui/previewSettingsContext.d.ts +18 -0
  155. package/dist/gui/previewSettingsContext.js.map +1 -1
  156. package/dist/gui/theme.js +34 -0
  157. package/dist/gui/theme.js.map +1 -0
  158. package/dist/gui/timeline/EFTimeline.d.ts +2 -2
  159. package/dist/gui/timeline/EFTimeline.js +70 -52
  160. package/dist/gui/timeline/EFTimeline.js.map +1 -1
  161. package/dist/gui/timeline/EFTimelineRow.d.ts +3 -1
  162. package/dist/gui/timeline/EFTimelineRow.js +55 -32
  163. package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
  164. package/dist/gui/timeline/TrimHandles.d.ts +23 -9
  165. package/dist/gui/timeline/TrimHandles.js +224 -51
  166. package/dist/gui/timeline/TrimHandles.js.map +1 -1
  167. package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
  168. package/dist/gui/timeline/timelineEditingContext.d.ts +34 -0
  169. package/dist/gui/timeline/timelineEditingContext.js +24 -0
  170. package/dist/gui/timeline/timelineEditingContext.js.map +1 -0
  171. package/dist/gui/timeline/timelineStateContext.js.map +1 -1
  172. package/dist/gui/timeline/tracks/AudioTrack.js +1 -1
  173. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
  174. package/dist/gui/timeline/tracks/CaptionsTrack.d.ts +2 -3
  175. package/dist/gui/timeline/tracks/CaptionsTrack.js +17 -75
  176. package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
  177. package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +52 -0
  178. package/dist/gui/timeline/tracks/EFThumbnailStrip.js +596 -0
  179. package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -0
  180. package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -1
  181. package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
  182. package/dist/gui/timeline/tracks/TextTrack.d.ts +3 -2
  183. package/dist/gui/timeline/tracks/TextTrack.js +17 -43
  184. package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
  185. package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +5 -6
  186. package/dist/gui/timeline/tracks/TimegroupTrack.js +33 -23
  187. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
  188. package/dist/gui/timeline/tracks/TrackItem.d.ts +7 -9
  189. package/dist/gui/timeline/tracks/TrackItem.js +18 -17
  190. package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
  191. package/dist/gui/timeline/tracks/VideoTrack.d.ts +3 -3
  192. package/dist/gui/timeline/tracks/VideoTrack.js +11 -14
  193. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
  194. package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -1
  195. package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
  196. package/dist/gui/timeline/tracks/waveformUtils.js +1 -1
  197. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
  198. package/dist/gui/tree/EFTree.d.ts +4 -4
  199. package/dist/gui/tree/EFTree.js +8 -14
  200. package/dist/gui/tree/EFTree.js.map +1 -1
  201. package/dist/gui/tree/EFTreeItem.d.ts +4 -4
  202. package/dist/gui/tree/EFTreeItem.js +3 -3
  203. package/dist/gui/tree/EFTreeItem.js.map +1 -1
  204. package/dist/gui/tree/treeContext.js.map +1 -1
  205. package/dist/index.d.ts +10 -8
  206. package/dist/index.js +6 -5
  207. package/dist/index.js.map +1 -1
  208. package/dist/node.d.ts +2 -2
  209. package/dist/node.js +2 -2
  210. package/dist/preview/AdaptiveResolutionTracker.js +3 -3
  211. package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
  212. package/dist/preview/FrameController.d.ts +2 -17
  213. package/dist/preview/FrameController.js +40 -63
  214. package/dist/preview/FrameController.js.map +1 -1
  215. package/dist/preview/QualityUpgradeScheduler.d.ts +76 -0
  216. package/dist/preview/QualityUpgradeScheduler.js +158 -0
  217. package/dist/preview/QualityUpgradeScheduler.js.map +1 -0
  218. package/dist/preview/RenderContext.d.ts +119 -1
  219. package/dist/preview/RenderContext.js +21 -3
  220. package/dist/preview/RenderContext.js.map +1 -1
  221. package/dist/preview/RenderProfiler.js.map +1 -1
  222. package/dist/preview/RenderStats.js +85 -0
  223. package/dist/preview/RenderStats.js.map +1 -0
  224. package/dist/preview/encoding/canvasEncoder.js +2 -52
  225. package/dist/preview/encoding/canvasEncoder.js.map +1 -1
  226. package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
  227. package/dist/preview/encoding/workerEncoder.js.map +1 -1
  228. package/dist/preview/logger.js.map +1 -1
  229. package/dist/preview/previewSettings.d.ts +34 -0
  230. package/dist/preview/previewSettings.js +29 -17
  231. package/dist/preview/previewSettings.js.map +1 -1
  232. package/dist/preview/previewTypes.js +4 -4
  233. package/dist/preview/previewTypes.js.map +1 -1
  234. package/dist/preview/renderElementToCanvas.d.ts +44 -0
  235. package/dist/preview/renderElementToCanvas.js +72 -0
  236. package/dist/preview/renderElementToCanvas.js.map +1 -0
  237. package/dist/preview/renderTimegroupToCanvas.js +267 -145
  238. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  239. package/dist/preview/renderTimegroupToCanvas.types.d.ts +30 -0
  240. package/dist/preview/renderTimegroupToVideo.js +85 -105
  241. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  242. package/dist/preview/{renderTimegroupToVideo.d.ts → renderTimegroupToVideo.types.d.ts} +9 -9
  243. package/dist/preview/renderVideoToVideo.js +286 -0
  244. package/dist/preview/renderVideoToVideo.js.map +1 -0
  245. package/dist/preview/renderers.js.map +1 -1
  246. package/dist/preview/rendering/ScaleConfig.js +74 -0
  247. package/dist/preview/rendering/ScaleConfig.js.map +1 -0
  248. package/dist/preview/rendering/inlineImages.js +1 -44
  249. package/dist/preview/rendering/inlineImages.js.map +1 -1
  250. package/dist/preview/rendering/loadImage.js +22 -0
  251. package/dist/preview/rendering/loadImage.js.map +1 -0
  252. package/dist/preview/rendering/renderToImageNative.js +3 -3
  253. package/dist/preview/rendering/renderToImageNative.js.map +1 -1
  254. package/dist/preview/rendering/serializeTimelineDirect.js +224 -68
  255. package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
  256. package/dist/preview/statsTrackingStrategy.js +1 -101
  257. package/dist/preview/statsTrackingStrategy.js.map +1 -1
  258. package/dist/preview/workers/WorkerPool.js +0 -1
  259. package/dist/preview/workers/WorkerPool.js.map +1 -1
  260. package/dist/preview/workers/encoderWorkerInline.js +21 -54
  261. package/dist/preview/workers/encoderWorkerInline.js.map +1 -1
  262. package/dist/render/EFRenderAPI.d.ts +2 -1
  263. package/dist/render/EFRenderAPI.js +12 -36
  264. package/dist/render/EFRenderAPI.js.map +1 -1
  265. package/dist/render/getRenderData.js +4 -4
  266. package/dist/render/getRenderData.js.map +1 -1
  267. package/dist/style.css +114 -163
  268. package/dist/transcoding/cache/RequestDeduplicator.js +1 -0
  269. package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
  270. package/dist/transcoding/types/index.d.ts +1 -1
  271. package/dist/transcoding/utils/UrlGenerator.js +10 -3
  272. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  273. package/dist/utils/LRUCache.js +1 -0
  274. package/dist/utils/LRUCache.js.map +1 -1
  275. package/dist/utils/frameTime.js +23 -1
  276. package/dist/utils/frameTime.js.map +1 -1
  277. package/package.json +21 -8
  278. package/scripts/build-css.js +8 -1
  279. package/test/setup.ts +0 -1
  280. package/test/useAssetMSW.ts +50 -0
  281. package/test/visualRegressionUtils.ts +23 -9
  282. package/dist/_virtual/rolldown_runtime.js +0 -27
  283. package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +0 -1
  284. package/dist/elements/EFThumbnailStrip.d.ts +0 -167
  285. package/dist/elements/EFThumbnailStrip.js +0 -731
  286. package/dist/elements/EFThumbnailStrip.js.map +0 -1
  287. package/dist/elements/SessionThumbnailCache.js +0 -154
  288. package/dist/elements/SessionThumbnailCache.js.map +0 -1
  289. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +0 -688
  290. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +0 -1
  291. package/dist/node_modules/react/cjs/react.development.js +0 -1521
  292. package/dist/node_modules/react/cjs/react.development.js.map +0 -1
  293. package/dist/node_modules/react/index.js +0 -13
  294. package/dist/node_modules/react/index.js.map +0 -1
  295. package/dist/node_modules/react/jsx-runtime.js +0 -13
  296. package/dist/node_modules/react/jsx-runtime.js.map +0 -1
  297. package/dist/preview/encoding/types.d.ts +0 -1
  298. package/dist/preview/renderTimegroupPreview.js +0 -686
  299. package/dist/preview/renderTimegroupPreview.js.map +0 -1
  300. package/dist/preview/renderTimegroupToCanvas.d.ts +0 -42
  301. package/dist/preview/rendering/renderToImage.d.ts +0 -2
  302. package/dist/preview/rendering/renderToImage.js +0 -95
  303. package/dist/preview/rendering/renderToImage.js.map +0 -1
  304. package/dist/preview/rendering/renderToImageForeignObject.js +0 -163
  305. package/dist/preview/rendering/renderToImageForeignObject.js.map +0 -1
  306. package/dist/preview/rendering/renderToImageNative.d.ts +0 -1
  307. package/dist/preview/rendering/svgSerializer.js +0 -43
  308. package/dist/preview/rendering/svgSerializer.js.map +0 -1
  309. package/dist/preview/rendering/types.d.ts +0 -2
  310. package/dist/preview/thumbnailCacheSettings.js +0 -52
  311. package/dist/preview/thumbnailCacheSettings.js.map +0 -1
  312. package/dist/sandbox/PlaybackControls.d.ts +0 -1
  313. package/dist/sandbox/PlaybackControls.js +0 -10
  314. package/dist/sandbox/PlaybackControls.js.map +0 -1
  315. package/dist/sandbox/ScenarioRunner.d.ts +0 -1
  316. package/dist/sandbox/ScenarioRunner.js +0 -1
  317. package/dist/sandbox/defineSandbox.d.ts +0 -1
  318. package/dist/sandbox/index.d.ts +0 -3
  319. package/dist/sandbox/index.js +0 -2
  320. package/test/EFVideo.framegen.browsertest.ts +0 -80
  321. package/test/thumbnail-performance-test.html +0 -116
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScaleConfig.js","names":[],"sources":["../../../src/preview/rendering/ScaleConfig.ts"],"sourcesContent":["/**\n * ScaleConfig - Unified scaling configuration for timeline serialization.\n *\n * Consolidates the multi-stage scaling architecture into a single,\n * well-defined abstraction with clear contracts.\n *\n * Previously, scaling was applied in 4 separate stages with implicit contracts:\n * 1. captureTimelineToDataUri: scaled output dimensions\n * 2. captureElementParts: CSS transform wrapper for DOM content\n * 3. serializeCanvas: independent optimalScale calculation per canvas\n * 4. encodeCanvasesInParallel: received pre-scaled snapshots\n *\n * Now, ScaleConfig centralizes all scaling logic and makes the contracts explicit.\n */\n\nexport interface CanvasScaleParams {\n /** Natural canvas pixel dimensions */\n naturalWidth: number;\n naturalHeight: number;\n /** CSS display dimensions (how big it appears) */\n displayWidth: number;\n displayHeight: number;\n}\n\n/**\n * Immutable scaling configuration for a serialization operation.\n *\n * All scaling decisions are computed once at construction and cached.\n * This ensures consistency across all stages of serialization.\n */\nexport class ScaleConfig {\n /** User-specified export scale (e.g., 0.25 for thumbnails, 1.0 for full resolution) */\n readonly exportScale: number;\n\n /** Input dimensions (before scaling) */\n readonly inputWidth: number;\n readonly inputHeight: number;\n\n /** Output SVG dimensions (after scaling) */\n readonly outputWidth: number;\n readonly outputHeight: number;\n\n /** Whether DOM content needs CSS transform:scale() wrapper */\n readonly needsDOMScaling: boolean;\n\n /** Quality multiplier for canvas encoding (1.5x for sharpness) */\n readonly qualityMultiplier: number = 1.5;\n\n constructor(width: number, height: number, exportScale: number) {\n this.inputWidth = width;\n this.inputHeight = height;\n this.exportScale = exportScale;\n\n // Compute output dimensions (Stage 1)\n this.outputWidth = Math.floor(width * exportScale);\n this.outputHeight = Math.floor(height * exportScale);\n\n // Determine if DOM needs CSS scaling (Stage 2)\n this.needsDOMScaling = exportScale < 1;\n\n // Freeze to ensure immutability\n Object.freeze(this);\n }\n\n /**\n * Compute optimal encoding scale for a canvas element.\n *\n * This is Stage 3 of the scaling architecture. Canvas pixels are scaled\n * independently from DOM content because they have intrinsic resolution.\n *\n * Algorithm:\n * 1. Calculate display scale (CSS size vs natural size)\n * 2. Multiply by export scale\n * 3. Multiply by quality multiplier (1.5x for sharpness)\n * 4. Cap at 1.0 (never upscale beyond natural resolution)\n *\n * @param params - Canvas dimensions (natural and display)\n * @returns Optimal scale for encoding (0.0 to 1.0)\n */\n computeCanvasScale(params: CanvasScaleParams): number {\n const { naturalWidth, naturalHeight, displayWidth, displayHeight } = params;\n\n // Calculate how much smaller the display is vs natural size\n const displayScaleX = displayWidth / naturalWidth;\n const displayScaleY = displayHeight / naturalHeight;\n const displayScale = Math.min(displayScaleX, displayScaleY);\n\n // Combine display scale, export scale, and quality multiplier\n // Cap at 1.0 to never upscale beyond natural resolution\n const optimalScale = Math.min(\n 1.0,\n displayScale * this.exportScale * this.qualityMultiplier,\n );\n\n return optimalScale;\n }\n\n /**\n * Get the CSS transform value for DOM scaling.\n * Returns null if no scaling is needed.\n */\n getDOMTransform(): string | null {\n return this.needsDOMScaling ? `scale(${this.exportScale})` : null;\n }\n\n /**\n * Get the wrapper dimensions for the CSS transform.\n * When DOM is scaled, the wrapper must be larger to accommodate\n * the scaled-down content.\n */\n getDOMWrapperDimensions(): { width: number; height: number } {\n if (!this.needsDOMScaling) {\n return { width: this.outputWidth, height: this.outputHeight };\n }\n\n return {\n width: Math.floor(this.outputWidth / this.exportScale),\n height: Math.floor(this.outputHeight / this.exportScale),\n };\n }\n\n /**\n * Create a ScaleConfig from legacy options.\n * Maintains backward compatibility with existing callsites.\n */\n static fromOptions(\n width: number,\n height: number,\n canvasScale: number,\n ): ScaleConfig {\n return new ScaleConfig(width, height, canvasScale);\n }\n}\n"],"mappings":";;;;;;;AA8BA,IAAa,cAAb,MAAa,YAAY;CAkBvB,YAAY,OAAe,QAAgB,aAAqB;2BAF3B;AAGnC,OAAK,aAAa;AAClB,OAAK,cAAc;AACnB,OAAK,cAAc;AAGnB,OAAK,cAAc,KAAK,MAAM,QAAQ,YAAY;AAClD,OAAK,eAAe,KAAK,MAAM,SAAS,YAAY;AAGpD,OAAK,kBAAkB,cAAc;AAGrC,SAAO,OAAO,KAAK;;;;;;;;;;;;;;;;;CAkBrB,mBAAmB,QAAmC;EACpD,MAAM,EAAE,cAAc,eAAe,cAAc,kBAAkB;EAGrE,MAAM,gBAAgB,eAAe;EACrC,MAAM,gBAAgB,gBAAgB;EACtC,MAAM,eAAe,KAAK,IAAI,eAAe,cAAc;AAS3D,SALqB,KAAK,IACxB,GACA,eAAe,KAAK,cAAc,KAAK,kBACxC;;;;;;CASH,kBAAiC;AAC/B,SAAO,KAAK,kBAAkB,SAAS,KAAK,YAAY,KAAK;;;;;;;CAQ/D,0BAA6D;AAC3D,MAAI,CAAC,KAAK,gBACR,QAAO;GAAE,OAAO,KAAK;GAAa,QAAQ,KAAK;GAAc;AAG/D,SAAO;GACL,OAAO,KAAK,MAAM,KAAK,cAAc,KAAK,YAAY;GACtD,QAAQ,KAAK,MAAM,KAAK,eAAe,KAAK,YAAY;GACzD;;;;;;CAOH,OAAO,YACL,OACA,QACA,aACa;AACb,SAAO,IAAI,YAAY,OAAO,QAAQ,YAAY"}
@@ -1,50 +1,7 @@
1
- import { logger } from "../logger.js";
2
-
3
1
  //#region src/preview/rendering/inlineImages.ts
4
- /** Maximum number of cached inline images before eviction */
5
- const MAX_INLINE_IMAGE_CACHE_SIZE = 100;
6
2
  /** Image cache for inlining external images as data URIs (foreignObject path) */
7
3
  const _inlineImageCache = /* @__PURE__ */ new Map();
8
4
  /**
9
- * Convert a Blob to a data URL.
10
- */
11
- function blobToDataURL(blob) {
12
- return new Promise((resolve, reject) => {
13
- const reader = new FileReader();
14
- reader.onload = () => resolve(reader.result);
15
- reader.onerror = reject;
16
- reader.readAsDataURL(blob);
17
- });
18
- }
19
- /**
20
- * Inline all images in a container as base64 data URIs.
21
- * SVG foreignObject can't load external images due to security restrictions.
22
- * Uses an LRU-style cache with size limits to prevent memory leaks.
23
- */
24
- async function inlineImages(container) {
25
- const images = container.querySelectorAll("img");
26
- for (const image of images) {
27
- const src = image.getAttribute("src");
28
- if (!src || src.startsWith("data:")) continue;
29
- const cached = _inlineImageCache.get(src);
30
- if (cached) {
31
- image.setAttribute("src", cached);
32
- continue;
33
- }
34
- try {
35
- const dataUrl = await blobToDataURL(await (await fetch(src)).blob());
36
- image.setAttribute("src", dataUrl);
37
- if (_inlineImageCache.size >= MAX_INLINE_IMAGE_CACHE_SIZE) {
38
- const firstKey = _inlineImageCache.keys().next().value;
39
- if (firstKey) _inlineImageCache.delete(firstKey);
40
- }
41
- _inlineImageCache.set(src, dataUrl);
42
- } catch (e) {
43
- logger.warn("Failed to inline image:", src, e);
44
- }
45
- }
46
- }
47
- /**
48
5
  * Clear the inline image cache. Useful for memory management in long-running sessions.
49
6
  */
50
7
  function clearInlineImageCache() {
@@ -52,5 +9,5 @@ function clearInlineImageCache() {
52
9
  }
53
10
 
54
11
  //#endregion
55
- export { clearInlineImageCache, inlineImages };
12
+ export { clearInlineImageCache };
56
13
  //# sourceMappingURL=inlineImages.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"inlineImages.js","names":[],"sources":["../../../src/preview/rendering/inlineImages.ts"],"sourcesContent":["/**\n * Image inlining utilities for SVG foreignObject rendering.\n * SVG foreignObject can't load external images due to security restrictions,\n * so we convert them to base64 data URIs.\n */\n\nimport { logger } from \"../logger.js\";\n\n/** Maximum number of cached inline images before eviction */\nconst MAX_INLINE_IMAGE_CACHE_SIZE = 100;\n\n/** Image cache for inlining external images as data URIs (foreignObject path) */\nconst _inlineImageCache = new Map<string, string>();\n\n/**\n * Convert a Blob to a data URL.\n */\nfunction blobToDataURL(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\n/**\n * Inline all images in a container as base64 data URIs.\n * SVG foreignObject can't load external images due to security restrictions.\n * Uses an LRU-style cache with size limits to prevent memory leaks.\n */\nexport async function inlineImages(container: HTMLElement): Promise<void> {\n const images = container.querySelectorAll(\"img\");\n for (const image of images) {\n const src = image.getAttribute(\"src\");\n if (!src || src.startsWith(\"data:\")) continue;\n\n const cached = _inlineImageCache.get(src);\n if (cached) {\n image.setAttribute(\"src\", cached);\n continue;\n }\n\n try {\n const response = await fetch(src);\n const blob = await response.blob();\n const dataUrl = await blobToDataURL(blob);\n image.setAttribute(\"src\", dataUrl);\n \n // Evict oldest entries if cache is full (simple FIFO eviction)\n if (_inlineImageCache.size >= MAX_INLINE_IMAGE_CACHE_SIZE) {\n const firstKey = _inlineImageCache.keys().next().value;\n if (firstKey) _inlineImageCache.delete(firstKey);\n }\n _inlineImageCache.set(src, dataUrl);\n } catch (e) {\n logger.warn(\"Failed to inline image:\", src, e);\n }\n }\n}\n\n/**\n * Clear the inline image cache. Useful for memory management in long-running sessions.\n */\nexport function clearInlineImageCache(): void {\n _inlineImageCache.clear();\n}\n\n/**\n * Get current inline image cache size for diagnostics.\n */\nexport function getInlineImageCacheSize(): number {\n return _inlineImageCache.size;\n}\n"],"mappings":";;;;AASA,MAAM,8BAA8B;;AAGpC,MAAM,oCAAoB,IAAI,KAAqB;;;;AAKnD,SAAS,cAAc,MAA6B;AAClD,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAS,IAAI,YAAY;AAC/B,SAAO,eAAe,QAAQ,OAAO,OAAiB;AACtD,SAAO,UAAU;AACjB,SAAO,cAAc,KAAK;GAC1B;;;;;;;AAQJ,eAAsB,aAAa,WAAuC;CACxE,MAAM,SAAS,UAAU,iBAAiB,MAAM;AAChD,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,MAAM,aAAa,MAAM;AACrC,MAAI,CAAC,OAAO,IAAI,WAAW,QAAQ,CAAE;EAErC,MAAM,SAAS,kBAAkB,IAAI,IAAI;AACzC,MAAI,QAAQ;AACV,SAAM,aAAa,OAAO,OAAO;AACjC;;AAGF,MAAI;GAGF,MAAM,UAAU,MAAM,cADT,OADI,MAAM,MAAM,IAAI,EACL,MAAM,CACO;AACzC,SAAM,aAAa,OAAO,QAAQ;AAGlC,OAAI,kBAAkB,QAAQ,6BAA6B;IACzD,MAAM,WAAW,kBAAkB,MAAM,CAAC,MAAM,CAAC;AACjD,QAAI,SAAU,mBAAkB,OAAO,SAAS;;AAElD,qBAAkB,IAAI,KAAK,QAAQ;WAC5B,GAAG;AACV,UAAO,KAAK,2BAA2B,KAAK,EAAE;;;;;;;AAQpD,SAAgB,wBAA8B;AAC5C,mBAAkB,OAAO"}
1
+ {"version":3,"file":"inlineImages.js","names":[],"sources":["../../../src/preview/rendering/inlineImages.ts"],"sourcesContent":["/**\n * Image inlining utilities for SVG foreignObject rendering.\n * SVG foreignObject can't load external images due to security restrictions,\n * so we convert them to base64 data URIs.\n */\n\nimport { logger } from \"../logger.js\";\n\n/** Maximum number of cached inline images before eviction */\nconst MAX_INLINE_IMAGE_CACHE_SIZE = 100;\n\n/** Image cache for inlining external images as data URIs (foreignObject path) */\nconst _inlineImageCache = new Map<string, string>();\n\n/**\n * Convert a Blob to a data URL.\n */\nfunction blobToDataURL(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\n/**\n * Inline all images in a container as base64 data URIs.\n * SVG foreignObject can't load external images due to security restrictions.\n * Uses an LRU-style cache with size limits to prevent memory leaks.\n */\nexport async function inlineImages(container: HTMLElement): Promise<void> {\n const images = container.querySelectorAll(\"img\");\n for (const image of images) {\n const src = image.getAttribute(\"src\");\n if (!src || src.startsWith(\"data:\")) continue;\n\n const cached = _inlineImageCache.get(src);\n if (cached) {\n image.setAttribute(\"src\", cached);\n continue;\n }\n\n try {\n const response = await fetch(src);\n const blob = await response.blob();\n const dataUrl = await blobToDataURL(blob);\n image.setAttribute(\"src\", dataUrl);\n\n // Evict oldest entries if cache is full (simple FIFO eviction)\n if (_inlineImageCache.size >= MAX_INLINE_IMAGE_CACHE_SIZE) {\n const firstKey = _inlineImageCache.keys().next().value;\n if (firstKey) _inlineImageCache.delete(firstKey);\n }\n _inlineImageCache.set(src, dataUrl);\n } catch (e) {\n logger.warn(\"Failed to inline image:\", src, e);\n }\n }\n}\n\n/**\n * Clear the inline image cache. Useful for memory management in long-running sessions.\n */\nexport function clearInlineImageCache(): void {\n _inlineImageCache.clear();\n}\n\n/**\n * Get current inline image cache size for diagnostics.\n */\nexport function getInlineImageCacheSize(): number {\n return _inlineImageCache.size;\n}\n"],"mappings":";;AAYA,MAAM,oCAAoB,IAAI,KAAqB;;;;AAoDnD,SAAgB,wBAA8B;AAC5C,mBAAkB,OAAO"}
@@ -0,0 +1,22 @@
1
+ import { defaultProfiler } from "../RenderProfiler.js";
2
+
3
+ //#region src/preview/rendering/loadImage.ts
4
+ /**
5
+ * Load an image from a data URI. Returns a Promise that resolves when loaded.
6
+ */
7
+ function loadImageFromDataUri(dataUri) {
8
+ const img = new Image();
9
+ const imageLoadStart = performance.now();
10
+ return new Promise((resolve, reject) => {
11
+ img.onload = () => {
12
+ defaultProfiler.addTime("imageLoad", performance.now() - imageLoadStart);
13
+ resolve(img);
14
+ };
15
+ img.onerror = reject;
16
+ img.src = dataUri;
17
+ });
18
+ }
19
+
20
+ //#endregion
21
+ export { loadImageFromDataUri };
22
+ //# sourceMappingURL=loadImage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadImage.js","names":[],"sources":["../../../src/preview/rendering/loadImage.ts"],"sourcesContent":["import { defaultProfiler } from \"../RenderProfiler.js\";\n\n/**\n * Load an image from a data URI. Returns a Promise that resolves when loaded.\n */\nexport function loadImageFromDataUri(\n dataUri: string,\n): 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"],"mappings":";;;;;;AAKA,SAAgB,qBACd,SAC2B;CAC3B,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"}
@@ -27,15 +27,15 @@ function createDprCanvas(options) {
27
27
  /**
28
28
  * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).
29
29
  * This is much faster than the foreignObject approach and avoids canvas tainting.
30
- *
30
+ *
31
31
  * Note: The native API renders at device pixel ratio, so we capture at DPR scale
32
32
  * and then downsample to logical pixels to match the foreignObject path's output.
33
- *
33
+ *
34
34
  * @param container - The HTML element to render
35
35
  * @param width - Target width in logical pixels
36
36
  * @param height - Target height in logical pixels
37
37
  * @param options - Rendering options (skipWait for batch mode)
38
- *
38
+ *
39
39
  * @see https://github.com/WICG/html-in-canvas
40
40
  */
41
41
  async function renderToImageNative(container, width, height, options = {}) {
@@ -1 +1 @@
1
- {"version":3,"file":"renderToImageNative.js","names":["captureCanvas: HTMLCanvasElement","dpr"],"sources":["../../../src/preview/rendering/renderToImageNative.ts"],"sourcesContent":["/**\n * Native HTML-in-Canvas rendering using drawElementImage API.\n */\n\nimport type { HtmlInCanvasContext, HtmlInCanvasElement, NativeRenderOptions } from \"./types.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\n\n/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */\nconst _layoutInitializedCanvases = new WeakSet<HTMLCanvasElement>();\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 * Create a canvas element with proper DPR handling.\n * Buffer size is based on renderWidth/renderHeight (internal resolution).\n * CSS size is based on fullWidth/fullHeight (logical display size).\n */\nexport function createDprCanvas(options: {\n renderWidth: number;\n renderHeight: number;\n scale: number;\n dpr?: number;\n fullWidth: number;\n fullHeight: number;\n}): HTMLCanvasElement {\n const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;\n const dpr = options.dpr ?? window.devicePixelRatio ?? 1;\n \n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(renderWidth * scale * dpr);\n canvas.height = Math.floor(renderHeight * scale * dpr);\n canvas.style.width = `${Math.floor(fullWidth * scale)}px`;\n canvas.style.height = `${Math.floor(fullHeight * scale)}px`;\n \n return canvas;\n}\n\n/**\n * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).\n * This is much faster than the foreignObject approach and avoids canvas tainting.\n * \n * Note: The native API renders at device pixel ratio, so we capture at DPR scale\n * and then downsample to logical pixels to match the foreignObject path's output.\n * \n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options (skipWait for batch mode)\n * \n * @see https://github.com/WICG/html-in-canvas\n */\nexport async function renderToImageNative(\n container: HTMLElement,\n width: number,\n height: number,\n options: NativeRenderOptions = {},\n): Promise<HTMLCanvasElement> {\n const t0 = performance.now();\n const { reuseCanvas, skipDprScaling = false } = options;\n // Use 1x DPR when skipDprScaling is true (for video export) - 4x fewer pixels!\n const dpr = skipDprScaling ? 1 : (window.devicePixelRatio || 1);\n \n // Use provided canvas or create new one\n let captureCanvas: HTMLCanvasElement;\n let shouldCleanup = false;\n \n if (reuseCanvas) {\n captureCanvas = reuseCanvas;\n \n // Ensure canvas dimensions match (both attribute and CSS)\n const dpr = skipDprScaling ? 1 : (window.devicePixelRatio || 1);\n const targetWidth = Math.floor(width * dpr);\n const targetHeight = Math.floor(height * dpr);\n \n // Set attribute dimensions (pixel buffer size)\n if (captureCanvas.width !== targetWidth) {\n captureCanvas.width = targetWidth;\n }\n if (captureCanvas.height !== targetHeight) {\n captureCanvas.height = targetHeight;\n }\n \n // Ensure CSS dimensions and positioning (same as non-reuse path)\n // This ensures consistent behavior and avoids layout issues\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n \n // Ensure layoutsubtree is set (required for drawElementImage)\n if (!captureCanvas.hasAttribute(\"layoutsubtree\")) {\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n }\n \n // Ensure canvas is in DOM (required for drawElementImage layout)\n if (!captureCanvas.parentNode) {\n document.body.appendChild(captureCanvas);\n }\n \n // Ensure container is child of canvas\n if (container.parentElement !== captureCanvas) {\n captureCanvas.appendChild(container);\n }\n \n // Ensure container is visible (not display: none) for layout\n // drawElementImage requires the element to be laid out\n const containerStyle = getComputedStyle(container);\n if (containerStyle.display === 'none') {\n container.style.display = 'block';\n }\n \n // Force synchronous layout ONLY on first use with this canvas\n // For batch rendering (video export), repeated layout forces are expensive\n // We only need to force layout once to ensure everything is ready\n if (!_layoutInitializedCanvases.has(captureCanvas)) {\n void captureCanvas.offsetHeight;\n void container.offsetHeight;\n getComputedStyle(captureCanvas).opacity;\n getComputedStyle(container).opacity;\n _layoutInitializedCanvases.add(captureCanvas);\n }\n } else {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.width = Math.floor(width * dpr);\n captureCanvas.height = Math.floor(height * dpr);\n \n // Enable HTML-in-Canvas mode via layoutsubtree attribute/property\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n \n captureCanvas.appendChild(container);\n \n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n document.body.appendChild(captureCanvas);\n shouldCleanup = true;\n }\n \n const t1 = performance.now();\n defaultProfiler.addTime(\"setup\", t1 - t0);\n \n try {\n // Force style calculation to ensure CSS is computed before capture\n // This ensures both canvas and container are laid out (required for drawElementImage)\n getComputedStyle(container).opacity;\n \n // When reusing canvas with layoutsubtree, wait for initial layout (first use only)\n // Use a WeakSet to track canvases that have been initialized\n if (reuseCanvas && (captureCanvas as any).layoutSubtree && !_layoutInitializedCanvases.has(captureCanvas)) {\n await waitForFrame();\n _layoutInitializedCanvases.add(captureCanvas);\n \n // Canvas may have been detached during async wait (e.g., test cleanup)\n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n \n const ctx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n ctx.drawElementImage(container, 0, 0);\n } finally {\n // Only clean up if we created the canvas\n if (shouldCleanup && captureCanvas.parentNode) {\n captureCanvas.parentNode.removeChild(captureCanvas);\n }\n }\n \n const t2 = performance.now();\n defaultProfiler.addTime(\"draw\", t2 - t1);\n \n // If DPR is 1, no downsampling needed - return as-is\n if (dpr === 1) {\n defaultProfiler.incrementRenderCount();\n return captureCanvas;\n }\n \n // Downsample to logical pixel dimensions to match foreignObject path output\n // This ensures consistent behavior regardless of which rendering path is used\n const outputCanvas = document.createElement(\"canvas\");\n outputCanvas.width = width;\n outputCanvas.height = height;\n \n const outputCtx = outputCanvas.getContext(\"2d\")!;\n // Draw the DPR-scaled capture onto the 1x output canvas\n outputCtx.drawImage(\n captureCanvas,\n 0, 0, captureCanvas.width, captureCanvas.height, // source (full DPR capture)\n 0, 0, width, height // destination (logical pixels)\n );\n \n const t3 = performance.now();\n defaultProfiler.addTime(\"downsample\", t3 - t2);\n defaultProfiler.incrementRenderCount();\n \n return outputCanvas;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"renderToImageNative.js","names":["captureCanvas: HTMLCanvasElement","dpr"],"sources":["../../../src/preview/rendering/renderToImageNative.ts"],"sourcesContent":["/**\n * Native HTML-in-Canvas rendering using drawElementImage API.\n */\n\nimport type {\n HtmlInCanvasContext,\n HtmlInCanvasElement,\n NativeRenderOptions,\n} from \"./types.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\n\n/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */\nconst _layoutInitializedCanvases = new WeakSet<HTMLCanvasElement>();\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 * Create a canvas element with proper DPR handling.\n * Buffer size is based on renderWidth/renderHeight (internal resolution).\n * CSS size is based on fullWidth/fullHeight (logical display size).\n */\nexport function createDprCanvas(options: {\n renderWidth: number;\n renderHeight: number;\n scale: number;\n dpr?: number;\n fullWidth: number;\n fullHeight: number;\n}): HTMLCanvasElement {\n const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;\n const dpr = options.dpr ?? window.devicePixelRatio ?? 1;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(renderWidth * scale * dpr);\n canvas.height = Math.floor(renderHeight * scale * dpr);\n canvas.style.width = `${Math.floor(fullWidth * scale)}px`;\n canvas.style.height = `${Math.floor(fullHeight * scale)}px`;\n\n return canvas;\n}\n\n/**\n * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).\n * This is much faster than the foreignObject approach and avoids canvas tainting.\n *\n * Note: The native API renders at device pixel ratio, so we capture at DPR scale\n * and then downsample to logical pixels to match the foreignObject path's output.\n *\n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options (skipWait for batch mode)\n *\n * @see https://github.com/WICG/html-in-canvas\n */\nexport async function renderToImageNative(\n container: HTMLElement,\n width: number,\n height: number,\n options: NativeRenderOptions = {},\n): Promise<HTMLCanvasElement> {\n const t0 = performance.now();\n const { reuseCanvas, skipDprScaling = false } = options;\n // Use 1x DPR when skipDprScaling is true (for video export) - 4x fewer pixels!\n const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;\n\n // Use provided canvas or create new one\n let captureCanvas: HTMLCanvasElement;\n let shouldCleanup = false;\n\n if (reuseCanvas) {\n captureCanvas = reuseCanvas;\n\n // Ensure canvas dimensions match (both attribute and CSS)\n const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;\n const targetWidth = Math.floor(width * dpr);\n const targetHeight = Math.floor(height * dpr);\n\n // Set attribute dimensions (pixel buffer size)\n if (captureCanvas.width !== targetWidth) {\n captureCanvas.width = targetWidth;\n }\n if (captureCanvas.height !== targetHeight) {\n captureCanvas.height = targetHeight;\n }\n\n // Ensure CSS dimensions and positioning (same as non-reuse path)\n // This ensures consistent behavior and avoids layout issues\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n\n // Ensure layoutsubtree is set (required for drawElementImage)\n if (!captureCanvas.hasAttribute(\"layoutsubtree\")) {\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n }\n\n // Ensure canvas is in DOM (required for drawElementImage layout)\n if (!captureCanvas.parentNode) {\n document.body.appendChild(captureCanvas);\n }\n\n // Ensure container is child of canvas\n if (container.parentElement !== captureCanvas) {\n captureCanvas.appendChild(container);\n }\n\n // Ensure container is visible (not display: none) for layout\n // drawElementImage requires the element to be laid out\n const containerStyle = getComputedStyle(container);\n if (containerStyle.display === \"none\") {\n container.style.display = \"block\";\n }\n\n // Force synchronous layout ONLY on first use with this canvas\n // For batch rendering (video export), repeated layout forces are expensive\n // We only need to force layout once to ensure everything is ready\n if (!_layoutInitializedCanvases.has(captureCanvas)) {\n void captureCanvas.offsetHeight;\n void container.offsetHeight;\n getComputedStyle(captureCanvas).opacity;\n getComputedStyle(container).opacity;\n _layoutInitializedCanvases.add(captureCanvas);\n }\n } else {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.width = Math.floor(width * dpr);\n captureCanvas.height = Math.floor(height * dpr);\n\n // Enable HTML-in-Canvas mode via layoutsubtree attribute/property\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n\n captureCanvas.appendChild(container);\n\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n document.body.appendChild(captureCanvas);\n shouldCleanup = true;\n }\n\n const t1 = performance.now();\n defaultProfiler.addTime(\"setup\", t1 - t0);\n\n try {\n // Force style calculation to ensure CSS is computed before capture\n // This ensures both canvas and container are laid out (required for drawElementImage)\n getComputedStyle(container).opacity;\n\n // When reusing canvas with layoutsubtree, wait for initial layout (first use only)\n // Use a WeakSet to track canvases that have been initialized\n if (\n reuseCanvas &&\n (captureCanvas as any).layoutSubtree &&\n !_layoutInitializedCanvases.has(captureCanvas)\n ) {\n await waitForFrame();\n _layoutInitializedCanvases.add(captureCanvas);\n\n // Canvas may have been detached during async wait (e.g., test cleanup)\n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n\n const ctx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n ctx.drawElementImage(container, 0, 0);\n } finally {\n // Only clean up if we created the canvas\n if (shouldCleanup && captureCanvas.parentNode) {\n captureCanvas.parentNode.removeChild(captureCanvas);\n }\n }\n\n const t2 = performance.now();\n defaultProfiler.addTime(\"draw\", t2 - t1);\n\n // If DPR is 1, no downsampling needed - return as-is\n if (dpr === 1) {\n defaultProfiler.incrementRenderCount();\n return captureCanvas;\n }\n\n // Downsample to logical pixel dimensions to match foreignObject path output\n // This ensures consistent behavior regardless of which rendering path is used\n const outputCanvas = document.createElement(\"canvas\");\n outputCanvas.width = width;\n outputCanvas.height = height;\n\n const outputCtx = outputCanvas.getContext(\"2d\")!;\n // Draw the DPR-scaled capture onto the 1x output canvas\n outputCtx.drawImage(\n captureCanvas,\n 0,\n 0,\n captureCanvas.width,\n captureCanvas.height, // source (full DPR capture)\n 0,\n 0,\n width,\n height, // destination (logical pixels)\n );\n\n const t3 = performance.now();\n defaultProfiler.addTime(\"downsample\", t3 - t2);\n defaultProfiler.incrementRenderCount();\n\n return outputCanvas;\n}\n"],"mappings":"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"}
@@ -1,9 +1,23 @@
1
- import { isVisibleAtTime } from "../previewTypes.js";
2
- import { collectDocumentStyles } from "../renderTimegroupPreview.js";
3
1
  import { encodeCanvasesInParallel } from "../encoding/canvasEncoder.js";
2
+ import { isTemporal, isVisibleAtTime } from "../previewTypes.js";
3
+ import { ScaleConfig } from "./ScaleConfig.js";
4
4
 
5
5
  //#region src/preview/rendering/serializeTimelineDirect.ts
6
6
  /**
7
+ * Collect document styles for shadow DOM injection.
8
+ */
9
+ function collectDocumentStyles() {
10
+ const rules = [];
11
+ try {
12
+ for (const sheet of document.styleSheets) try {
13
+ if (sheet.cssRules) for (const rule of sheet.cssRules) rules.push(rule.cssText);
14
+ } catch {}
15
+ } catch (e) {
16
+ console.warn("[collectDocumentStyles] Failed to access document.styleSheets:", e);
17
+ }
18
+ return rules.join("\n");
19
+ }
20
+ /**
7
21
  * Elements to skip entirely when serializing.
8
22
  * NOTE: SLOT is NOT skipped - it's handled specially to serialize light DOM children.
9
23
  */
@@ -129,27 +143,50 @@ function escapeXML(str) {
129
143
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
130
144
  }
131
145
  /**
146
+ * Resolve the natural display value for an element that has display:none
147
+ * set as an inline style (e.g., from temporal visibility via updateAnimations).
148
+ *
149
+ * Temporarily removes the inline display override so getComputedStyle falls
150
+ * through to the element's stylesheet rules (including shadow DOM :host styles),
151
+ * reads the natural value, then restores the override.
152
+ */
153
+ function resolveNaturalDisplay(element) {
154
+ const htmlEl = element;
155
+ if (htmlEl.style?.getPropertyValue("display") === "none" && htmlEl.style) {
156
+ htmlEl.style.removeProperty("display");
157
+ const natural = getComputedStyle(element).getPropertyValue("display");
158
+ htmlEl.style.setProperty("display", "none");
159
+ return natural || "block";
160
+ }
161
+ return "block";
162
+ }
163
+ /**
132
164
  * Serialize computed styles as inline style string.
133
- * Handles display:none block conversion for non-caption elements
134
- * (temporal visibility is handled separately).
165
+ * Handles display:none recovery for non-caption elements by resolving
166
+ * the element's natural display value from its stylesheet rules.
167
+ * @param element - The element to serialize styles for
168
+ * @param styles - Optional pre-computed CSSStyleDeclaration (avoids redundant getComputedStyle calls)
135
169
  */
136
- function serializeComputedStyles(element) {
137
- const styles = getComputedStyle(element);
170
+ function serializeComputedStyles(element, styles) {
171
+ const computed = styles ?? getComputedStyle(element);
138
172
  const styleParts = [];
139
173
  const tagName = element.tagName;
140
174
  const isCaptionChild = CAPTION_CHILD_TAGS.has(tagName);
175
+ const htmlEl = element;
176
+ const hasExplicitWidth = !!htmlEl.style?.getPropertyValue("width");
177
+ const hasExplicitHeight = !!htmlEl.style?.getPropertyValue("height");
141
178
  for (const prop of SERIALIZED_STYLE_PROPERTIES) {
142
- const value = styles[prop];
179
+ const kebab = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
180
+ const value = computed.getPropertyValue(kebab);
143
181
  if (!value || value === "") continue;
144
182
  let finalValue = value;
145
183
  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";
184
+ if (value === "none" && !isCaptionChild) finalValue = resolveNaturalDisplay(element);
149
185
  }
150
186
  if (prop === "visibility") finalValue = "visible";
151
187
  if (prop === "clipPath") continue;
152
- const kebab = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
188
+ if (prop === "width" && !hasExplicitWidth) continue;
189
+ if (prop === "height" && !hasExplicitHeight) continue;
153
190
  styleParts.push(`${kebab}:${finalValue}`);
154
191
  }
155
192
  styleParts.push("animation:none", "transition:none");
@@ -168,75 +205,153 @@ function serializeAttributes(element, parts) {
168
205
  /**
169
206
  * Check if a canvas element should preserve alpha channel.
170
207
  * EF-WAVEFORM always needs alpha, EF-IMAGE checks hasAlpha property.
208
+ * EF-SURFACE needs alpha because:
209
+ * 1. Without a target, its canvas is transparent and CSS background should show through
210
+ * 2. The target element may have transparent content
211
+ * Raw canvas elements must preserve alpha - we don't know what they contain.
171
212
  */
172
213
  function shouldPreserveAlpha(sourceElement) {
173
214
  const tagName = sourceElement.tagName;
174
215
  if (tagName === "EF-WAVEFORM") return true;
216
+ if (tagName === "EF-SURFACE") return true;
175
217
  if (tagName === "EF-IMAGE") return "hasAlpha" in sourceElement && sourceElement.hasAlpha === true;
218
+ if (sourceElement instanceof HTMLCanvasElement) return true;
176
219
  return false;
177
220
  }
178
221
  /**
222
+ * Find the capture proxy canvas for an offscreen-rendered canvas.
223
+ * When a canvas is transferred to offscreen via transferControlToOffscreen(),
224
+ * the main thread can no longer read pixels from it. OffscreenCompositionCanvas
225
+ * creates a hidden capture canvas (marked with data-offscreen-capture) that
226
+ * receives ImageBitmap frames from the worker.
227
+ */
228
+ function findCaptureProxy(canvas) {
229
+ const container = canvas.parentElement;
230
+ if (!container) return null;
231
+ return container.querySelector("canvas[data-offscreen-capture=\"true\"]");
232
+ }
233
+ /**
234
+ * Read pixels directly from a WebGL canvas's drawing buffer via gl.readPixels().
235
+ *
236
+ * drawImage(webglCanvas) reads from the compositor's "presented" surface, which
237
+ * is only refreshed during requestAnimationFrame / compositing cycles. In hidden
238
+ * browser tabs, compositing is suspended, so drawImage returns stale pixels even
239
+ * though gl.render() produced new content in the drawing buffer.
240
+ *
241
+ * readPixels() reads from the drawing buffer directly, bypassing the compositor.
242
+ *
243
+ * Returns null for non-WebGL canvases (getContext returns null when a different
244
+ * context type is already active).
245
+ */
246
+ function readWebGLPixels(canvas) {
247
+ const gl = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
248
+ if (!gl) return null;
249
+ const width = canvas.width;
250
+ const height = canvas.height;
251
+ if (width === 0 || height === 0) return null;
252
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
253
+ const pixels = new Uint8Array(width * height * 4);
254
+ gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
255
+ const rowSize = width * 4;
256
+ const halfHeight = Math.floor(height / 2);
257
+ const temp = new Uint8Array(rowSize);
258
+ for (let y = 0; y < halfHeight; y++) {
259
+ const topOffset = y * rowSize;
260
+ const bottomOffset = (height - 1 - y) * rowSize;
261
+ temp.set(pixels.subarray(topOffset, topOffset + rowSize));
262
+ pixels.set(pixels.subarray(bottomOffset, bottomOffset + rowSize), topOffset);
263
+ pixels.set(temp, bottomOffset);
264
+ }
265
+ return new Uint8ClampedArray(pixels.buffer);
266
+ }
267
+ /**
179
268
  * Create a snapshot copy of a canvas's current pixels.
180
269
  * This captures the pixels synchronously before any async encoding,
181
270
  * preventing race conditions where the source canvas is modified.
271
+ *
272
+ * For WebGL canvases, uses gl.readPixels() to bypass the compositor's
273
+ * presentation layer (which is suspended in hidden browser tabs).
274
+ *
275
+ * For offscreen-rendered canvases, this automatically uses the capture proxy
276
+ * canvas instead of the transferred display canvas.
182
277
  */
183
278
  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));
279
+ const sourceCanvas = findCaptureProxy(canvas) ?? canvas;
280
+ const targetWidth = Math.max(1, Math.floor(sourceCanvas.width * scale));
281
+ const targetHeight = Math.max(1, Math.floor(sourceCanvas.height * scale));
186
282
  const copy = document.createElement("canvas");
187
283
  copy.width = targetWidth;
188
284
  copy.height = targetHeight;
189
285
  if (preserveAlpha) copy.dataset.preserveAlpha = "true";
190
286
  const ctx = copy.getContext("2d");
191
- if (ctx && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0, targetWidth, targetHeight);
287
+ if (ctx && sourceCanvas.width > 0 && sourceCanvas.height > 0) {
288
+ const glPixels = document.hidden ? readWebGLPixels(sourceCanvas) : null;
289
+ if (glPixels) {
290
+ const srcW = sourceCanvas.width;
291
+ const srcH = sourceCanvas.height;
292
+ const imageData = new ImageData(glPixels, srcW, srcH);
293
+ if (targetWidth === srcW && targetHeight === srcH) ctx.putImageData(imageData, 0, 0);
294
+ else {
295
+ const temp = document.createElement("canvas");
296
+ temp.width = srcW;
297
+ temp.height = srcH;
298
+ temp.getContext("2d").putImageData(imageData, 0, 0);
299
+ ctx.drawImage(temp, 0, 0, targetWidth, targetHeight);
300
+ }
301
+ } else ctx.drawImage(sourceCanvas, 0, 0, targetWidth, targetHeight);
302
+ }
192
303
  return copy;
193
304
  }
194
305
  /**
195
306
  * Serialize a canvas element as an <img> with base64 data URL.
196
307
  * Creates a snapshot of current pixels before async encoding to prevent race conditions.
197
- *
308
+ *
198
309
  * OPTIMIZATION: Calculate optimal encoding resolution based on:
199
310
  * 1. CSS display size (how big it actually appears)
200
311
  * 2. Video export scale (output resolution multiplier)
201
312
  * 3. Quality multiplier (for sharpness, default 1.5x)
202
313
  */
203
314
  function serializeCanvas(sourceElement, canvas, parts, canvasJobs, options) {
204
- const width = canvas.width;
205
- const height = canvas.height;
315
+ const sourceCanvas = findCaptureProxy(canvas) ?? canvas;
316
+ const width = sourceCanvas.width;
317
+ const height = sourceCanvas.height;
206
318
  if (width === 0 || height === 0) return;
207
- const styleStr = serializeComputedStyles(sourceElement);
208
319
  const computedStyle = getComputedStyle(sourceElement);
320
+ const styleStr = serializeComputedStyles(sourceElement, computedStyle);
209
321
  const computedWidth = computedStyle.width;
210
322
  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(";");
323
+ const filteredParts = (styleStr ? styleStr.split(";").filter((s) => s.trim()) : []).filter((s) => {
324
+ const trimmed = s.trim();
325
+ return !trimmed.startsWith("width:") && !trimmed.startsWith("height:");
326
+ });
327
+ const displayWidth = computedWidth || `${width}px`;
328
+ const displayHeight = computedHeight || `${height}px`;
329
+ filteredParts.push(`width:${displayWidth}`);
330
+ filteredParts.push(`height:${displayHeight}`);
331
+ filteredParts.push(`display:block`);
332
+ const finalStyle = filteredParts.join(";");
218
333
  const preserveAlpha = shouldPreserveAlpha(sourceElement);
219
- let optimalScale = options.canvasScale;
220
- const qualityMultiplier = 1.5;
334
+ let optimalScale = options.scaleConfig.exportScale;
221
335
  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);
336
+ const cssWidth = parseFloat(computedWidth) || sourceCanvas.width;
337
+ const cssHeight = parseFloat(computedHeight) || sourceCanvas.height;
338
+ optimalScale = options.scaleConfig.computeCanvasScale({
339
+ naturalWidth: sourceCanvas.width,
340
+ naturalHeight: sourceCanvas.height,
341
+ displayWidth: cssWidth,
342
+ displayHeight: cssHeight
343
+ });
228
344
  } catch (e) {
229
345
  console.warn(`[serializeCanvas] Failed to get computed style for ${sourceElement.tagName}:`, e);
230
346
  }
231
347
  const snapshot = snapshotCanvas(canvas, optimalScale, preserveAlpha);
232
348
  parts.push(`<img style="${escapeXML(finalStyle)}" src="`);
233
349
  const promiseIndex = parts.length;
234
- const sourceMap = /* @__PURE__ */ new WeakMap();
235
- sourceMap.set(snapshot, sourceElement);
350
+ options.sourceMap.set(snapshot, sourceElement);
236
351
  const encodePromise = encodeCanvasesInParallel([snapshot], {
237
352
  scale: 1,
238
353
  renderContext: options.renderContext,
239
- sourceMap
354
+ sourceMap: options.sourceMap
240
355
  }).then((results) => results[0]?.dataUrl || "");
241
356
  parts.push(encodePromise);
242
357
  canvasJobs.push({
@@ -280,7 +395,8 @@ function serializeElement(element, parts, canvasJobs, options, parentIsSVG = fal
280
395
  serializeSlottedContent(slotHost, parts, canvasJobs, options, parentIsSVG);
281
396
  return;
282
397
  }
283
- if (!isTemporallyVisible(element, options.timeMs)) return;
398
+ if (!isVisibleAtTime(element, options.timeMs)) return;
399
+ if (isTemporal(element) && element.style?.getPropertyValue("display") === "none") return;
284
400
  if (element.tagName.includes("-") && element.shadowRoot) {
285
401
  const shadowCanvas = element.shadowRoot.querySelector("canvas");
286
402
  if (shadowCanvas) {
@@ -292,9 +408,11 @@ function serializeElement(element, parts, canvasJobs, options, parentIsSVG = fal
292
408
  serializeImageAsCanvas(element, shadowImg, parts, canvasJobs, options);
293
409
  return;
294
410
  }
295
- const computedDisplay = getComputedStyle(element).display;
411
+ const computedStyle = getComputedStyle(element);
412
+ let computedDisplay = computedStyle.display;
413
+ if (computedDisplay === "none") computedDisplay = resolveNaturalDisplay(element);
296
414
  const containerTag = computedDisplay === "inline" || computedDisplay === "inline-block" || computedDisplay === "inline-flex" ? "span" : "div";
297
- let styleStr$1 = serializeComputedStyles(element);
415
+ let styleStr$1 = serializeComputedStyles(element, computedStyle);
298
416
  parts.push(`<${containerTag}`);
299
417
  for (const attr of element.attributes) {
300
418
  const name = attr.name.toLowerCase();
@@ -334,46 +452,84 @@ function serializeElement(element, parts, canvasJobs, options, parentIsSVG = fal
334
452
  parts.push(`</${tagName}>`);
335
453
  }
336
454
  /**
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.
455
+ * TextEncoder instance for SVG-to-base64 encoding.
456
+ * encode() converts to UTF-8 bytes in a single native call, then we
457
+ * base64-encode the bytes. ~33% overhead vs ~200% for percent-encoding.
339
458
  */
340
- function isTemporallyVisible(element, timeMs) {
341
- if (!isVisibleAtTime(element, timeMs)) return false;
342
- return true;
343
- }
459
+ const textEncoder = new TextEncoder();
344
460
  /**
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
461
+ * Synchronous DOM capture phase. Walks the element tree, snapshots canvas
462
+ * pixels, and kicks off async encoding. Returns parts array containing
463
+ * string fragments and encoding promises.
464
+ *
465
+ * After this function returns, the source element's DOM is no longer
466
+ * referenced the clone can safely be seeked to the next frame.
467
+ *
468
+ * SCALING ARCHITECTURE (unified via ScaleConfig):
469
+ *
470
+ * ScaleConfig centralizes all scaling logic and provides:
471
+ * 1. Output SVG dimensions (width * exportScale, height * exportScale)
472
+ * 2. DOM scaling wrapper (CSS transform:scale when exportScale < 1)
473
+ * 3. Per-canvas optimal encoding scale via computeCanvasScale()
474
+ *
475
+ * Canvas scaling is independent from DOM scaling because:
476
+ * - Canvas elements have intrinsic pixel dimensions and can be downsampled
477
+ * efficiently before encoding (prevents encoding 1920px at full resolution
478
+ * when displayed at 420px)
479
+ * - DOM content has no intrinsic resolution and must be scaled via CSS
480
+ * transforms, which the browser handles during SVG foreignObject rendering
481
+ *
482
+ * Example: 1920x1080 @ 0.5 export scale
483
+ * - Output SVG: 960x540
484
+ * - DOM wrapper: transform:scale(0.5) on 1920x1080 content
485
+ * - Canvas (1920px displayed at 420px): encoded at ~0.16x (315px)
486
+ * via computeCanvasScale(420/1920 * 0.5 * 1.5 quality = 0.164)
352
487
  */
353
- async function serializeTimelineToXHTML(timeline, width, height, options) {
488
+ function captureElementParts(element, width, height, options) {
354
489
  const parts = [];
355
490
  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;">`);
491
+ const sourceMap = /* @__PURE__ */ new WeakMap();
492
+ const scaleConfig = ScaleConfig.fromOptions(width, height, options.canvasScale);
493
+ const documentStyles = options.renderContext?.getCachedDocumentStyles() ?? collectDocumentStyles();
494
+ if (options.renderContext && documentStyles) options.renderContext.setCachedDocumentStyles(documentStyles);
495
+ parts.push(`<div xmlns="http://www.w3.org/1999/xhtml" style="width:${scaleConfig.outputWidth}px;height:${scaleConfig.outputHeight}px;overflow:hidden;position:relative;">`);
358
496
  if (documentStyles) parts.push(`<style type="text/css"><![CDATA[${documentStyles}]]></style>`);
359
- serializeElement(timeline, parts, canvasJobs, options);
497
+ const domTransform = scaleConfig.getDOMTransform();
498
+ if (domTransform) {
499
+ const wrapperDims = scaleConfig.getDOMWrapperDimensions();
500
+ parts.push(`<div style="transform:${domTransform};transform-origin:0 0;width:${wrapperDims.width}px;height:${wrapperDims.height}px;">`);
501
+ }
502
+ serializeElement(element, parts, canvasJobs, {
503
+ renderContext: options.renderContext,
504
+ timeMs: options.timeMs,
505
+ scaleConfig,
506
+ sourceMap
507
+ });
508
+ if (domTransform) parts.push("</div>");
360
509
  parts.push("</div>");
361
- return (await Promise.all(parts)).join("");
510
+ return parts;
362
511
  }
363
512
  /**
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
513
+ * Synchronous capture with deferred data URI encoding.
514
+ *
515
+ * Walks the DOM and snapshots canvas pixels synchronously, then returns
516
+ * a promise that resolves to the SVG data URI once async canvas-to-base64
517
+ * encoding completes. The source element is NOT referenced after this
518
+ * function returns the caller can immediately mutate/seek the clone.
371
519
  */
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)}`;
520
+ function captureTimelineToDataUri(element, width, height, options) {
521
+ const scaleConfig = ScaleConfig.fromOptions(width, height, options.canvasScale);
522
+ const parts = captureElementParts(element, width, height, options);
523
+ return Promise.all(parts).then((resolvedParts) => {
524
+ const xhtml = resolvedParts.join("");
525
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${scaleConfig.outputWidth}" height="${scaleConfig.outputHeight}"><foreignObject x="0" y="0" width="${scaleConfig.outputWidth}" height="${scaleConfig.outputHeight}">${xhtml}</foreignObject></svg>`;
526
+ const bytes = textEncoder.encode(svg);
527
+ let binary = "";
528
+ for (let i = 0; i < bytes.length; i += 8192) binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192));
529
+ return `data:image/svg+xml;base64,${btoa(binary)}`;
530
+ });
375
531
  }
376
532
 
377
533
  //#endregion
378
- export { serializeTimelineToDataUri };
534
+ export { captureTimelineToDataUri };
379
535
  //# sourceMappingURL=serializeTimelineDirect.js.map