@editframe/elements 0.33.0-beta → 0.34.5-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 (251) hide show
  1. package/dist/EF_FRAMEGEN.js +5 -3
  2. package/dist/EF_FRAMEGEN.js.map +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.94.0 → _@oxc-project_runtime@0.95.0}/helpers/decorate.js +1 -1
  4. package/dist/canvas/EFCanvas.d.ts +7 -4
  5. package/dist/canvas/EFCanvas.js +1 -1
  6. package/dist/canvas/EFCanvasItem.d.ts +4 -4
  7. package/dist/canvas/EFCanvasItem.js +1 -1
  8. package/dist/canvas/overlays/SelectionOverlay.d.ts +95 -0
  9. package/dist/canvas/overlays/SelectionOverlay.js +1 -1
  10. package/dist/canvas/selection/SelectionController.js +7 -11
  11. package/dist/canvas/selection/SelectionController.js.map +1 -1
  12. package/dist/elements/EFAudio.d.ts +25 -7
  13. package/dist/elements/EFAudio.js +31 -61
  14. package/dist/elements/EFAudio.js.map +1 -1
  15. package/dist/elements/EFCaptions.d.ts +65 -52
  16. package/dist/elements/EFCaptions.js +186 -400
  17. package/dist/elements/EFCaptions.js.map +1 -1
  18. package/dist/elements/EFImage.d.ts +34 -6
  19. package/dist/elements/EFImage.js +114 -79
  20. package/dist/elements/EFImage.js.map +1 -1
  21. package/dist/elements/EFMedia/AssetIdMediaEngine.js +17 -17
  22. package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
  23. package/dist/elements/EFMedia/AssetMediaEngine.js +41 -25
  24. package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
  25. package/dist/elements/EFMedia/BaseMediaEngine.js +4 -4
  26. package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
  27. package/dist/elements/EFMedia/BufferedSeekingInput.js +1 -1
  28. package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
  29. package/dist/elements/EFMedia/JitMediaEngine.js +31 -17
  30. package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
  31. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -3
  32. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  33. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +17 -9
  34. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
  35. package/dist/elements/EFMedia.d.ts +66 -20
  36. package/dist/elements/EFMedia.js +412 -30
  37. package/dist/elements/EFMedia.js.map +1 -1
  38. package/dist/elements/EFPanZoom.d.ts +4 -4
  39. package/dist/elements/EFPanZoom.js +1 -1
  40. package/dist/elements/EFSourceMixin.js +43 -15
  41. package/dist/elements/EFSourceMixin.js.map +1 -1
  42. package/dist/elements/EFSurface.d.ts +23 -10
  43. package/dist/elements/EFSurface.js +64 -22
  44. package/dist/elements/EFSurface.js.map +1 -1
  45. package/dist/elements/EFTemporal.d.ts +8 -2
  46. package/dist/elements/EFTemporal.js +42 -31
  47. package/dist/elements/EFTemporal.js.map +1 -1
  48. package/dist/elements/EFText.d.ts +5 -4
  49. package/dist/elements/EFText.js +11 -2
  50. package/dist/elements/EFText.js.map +1 -1
  51. package/dist/elements/EFTextSegment.d.ts +4 -4
  52. package/dist/elements/EFTextSegment.js +1 -1
  53. package/dist/elements/EFThumbnailStrip.d.ts +4 -4
  54. package/dist/elements/EFThumbnailStrip.js +1 -1
  55. package/dist/elements/EFTimegroup.d.ts +22 -8
  56. package/dist/elements/EFTimegroup.js +203 -115
  57. package/dist/elements/EFTimegroup.js.map +1 -1
  58. package/dist/elements/EFVideo.d.ts +57 -20
  59. package/dist/elements/EFVideo.js +324 -72
  60. package/dist/elements/EFVideo.js.map +1 -1
  61. package/dist/elements/EFWaveform.d.ts +33 -7
  62. package/dist/elements/EFWaveform.js +103 -59
  63. package/dist/elements/EFWaveform.js.map +1 -1
  64. package/dist/elements/renderTemporalAudio.js +14 -3
  65. package/dist/elements/renderTemporalAudio.js.map +1 -1
  66. package/dist/getRenderInfo.d.ts +2 -2
  67. package/dist/gui/ContextMixin.js +1 -1
  68. package/dist/gui/Controllable.d.ts +2 -0
  69. package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
  70. package/dist/gui/EFActiveRootTemporal.js +1 -1
  71. package/dist/gui/EFConfiguration.d.ts +4 -4
  72. package/dist/gui/EFConfiguration.js +1 -1
  73. package/dist/gui/EFControls.d.ts +2 -2
  74. package/dist/gui/EFControls.js +1 -1
  75. package/dist/gui/EFDial.d.ts +4 -4
  76. package/dist/gui/EFDial.js +1 -1
  77. package/dist/gui/EFFilmstrip.d.ts +3 -2
  78. package/dist/gui/EFFilmstrip.js +1 -1
  79. package/dist/gui/EFFitScale.js +1 -1
  80. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  81. package/dist/gui/EFFocusOverlay.js +1 -1
  82. package/dist/gui/EFOverlayItem.d.ts +4 -4
  83. package/dist/gui/EFOverlayItem.js +1 -1
  84. package/dist/gui/EFOverlayLayer.d.ts +4 -4
  85. package/dist/gui/EFOverlayLayer.js +1 -1
  86. package/dist/gui/EFPause.d.ts +4 -4
  87. package/dist/gui/EFPause.js +1 -1
  88. package/dist/gui/EFPlay.d.ts +4 -4
  89. package/dist/gui/EFPlay.js +1 -1
  90. package/dist/gui/EFPreview.d.ts +4 -4
  91. package/dist/gui/EFPreview.js +1 -1
  92. package/dist/gui/EFResizableBox.d.ts +4 -4
  93. package/dist/gui/EFResizableBox.js +1 -1
  94. package/dist/gui/EFScrubber.d.ts +4 -4
  95. package/dist/gui/EFScrubber.js +1 -1
  96. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  97. package/dist/gui/EFTimeDisplay.js +1 -1
  98. package/dist/gui/EFTimelineRuler.d.ts +4 -4
  99. package/dist/gui/EFTimelineRuler.js +1 -1
  100. package/dist/gui/EFToggleLoop.d.ts +4 -4
  101. package/dist/gui/EFToggleLoop.js +1 -1
  102. package/dist/gui/EFTogglePlay.d.ts +4 -4
  103. package/dist/gui/EFTogglePlay.js +1 -1
  104. package/dist/gui/EFTransformHandles.d.ts +4 -4
  105. package/dist/gui/EFTransformHandles.js +1 -1
  106. package/dist/gui/EFWorkbench.d.ts +5 -4
  107. package/dist/gui/EFWorkbench.js +1 -1
  108. package/dist/gui/PlaybackController.d.ts +10 -2
  109. package/dist/gui/PlaybackController.js +52 -30
  110. package/dist/gui/PlaybackController.js.map +1 -1
  111. package/dist/gui/TWMixin.js +1 -1
  112. package/dist/gui/TWMixin.js.map +1 -1
  113. package/dist/gui/TargetOrContextMixin.js +1 -1
  114. package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
  115. package/dist/gui/hierarchy/EFHierarchy.js +1 -1
  116. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +3 -3
  117. package/dist/gui/hierarchy/EFHierarchyItem.js +1 -1
  118. package/dist/gui/timeline/EFTimeline.d.ts +6 -2
  119. package/dist/gui/timeline/EFTimeline.js +1 -1
  120. package/dist/gui/timeline/EFTimelineRow.d.ts +57 -0
  121. package/dist/gui/timeline/EFTimelineRow.js +1 -1
  122. package/dist/gui/timeline/TrimHandles.d.ts +4 -4
  123. package/dist/gui/timeline/TrimHandles.js +1 -1
  124. package/dist/gui/timeline/tracks/AudioTrack.d.ts +2 -0
  125. package/dist/gui/timeline/tracks/AudioTrack.js +1 -1
  126. package/dist/gui/timeline/tracks/CaptionsTrack.d.ts +58 -0
  127. package/dist/gui/timeline/tracks/CaptionsTrack.js +1 -1
  128. package/dist/gui/timeline/tracks/HTMLTrack.d.ts +13 -0
  129. package/dist/gui/timeline/tracks/HTMLTrack.js +1 -1
  130. package/dist/gui/timeline/tracks/ImageTrack.d.ts +14 -0
  131. package/dist/gui/timeline/tracks/ImageTrack.js +1 -1
  132. package/dist/gui/timeline/tracks/TextTrack.d.ts +26 -0
  133. package/dist/gui/timeline/tracks/TextTrack.js +1 -1
  134. package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +47 -0
  135. package/dist/gui/timeline/tracks/TimegroupTrack.js +4 -12
  136. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
  137. package/dist/gui/timeline/tracks/TrackItem.d.ts +81 -0
  138. package/dist/gui/timeline/tracks/TrackItem.js +1 -1
  139. package/dist/gui/timeline/tracks/VideoTrack.d.ts +25 -0
  140. package/dist/gui/timeline/tracks/VideoTrack.js +1 -1
  141. package/dist/gui/timeline/tracks/WaveformTrack.d.ts +14 -0
  142. package/dist/gui/timeline/tracks/WaveformTrack.js +1 -1
  143. package/dist/gui/timeline/tracks/ensureTrackItemInit.d.ts +1 -0
  144. package/dist/gui/timeline/tracks/preloadTracks.d.ts +9 -0
  145. package/dist/gui/tree/EFTree.d.ts +5 -4
  146. package/dist/gui/tree/EFTree.js +1 -1
  147. package/dist/gui/tree/EFTreeItem.d.ts +4 -4
  148. package/dist/gui/tree/EFTreeItem.js +1 -1
  149. package/dist/index.d.ts +4 -1
  150. package/dist/preview/AdaptiveResolutionTracker.js +6 -14
  151. package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
  152. package/dist/preview/FrameController.d.ts +123 -0
  153. package/dist/preview/FrameController.js +216 -0
  154. package/dist/preview/FrameController.js.map +1 -0
  155. package/dist/preview/RenderContext.d.ts +1 -0
  156. package/dist/preview/RenderContext.js +193 -0
  157. package/dist/preview/RenderContext.js.map +1 -0
  158. package/dist/preview/encoding/canvasEncoder.js +166 -0
  159. package/dist/preview/encoding/canvasEncoder.js.map +1 -0
  160. package/dist/preview/encoding/mainThreadEncoder.js +39 -0
  161. package/dist/preview/encoding/mainThreadEncoder.js.map +1 -0
  162. package/dist/preview/encoding/types.d.ts +1 -0
  163. package/dist/preview/encoding/workerEncoder.js +58 -0
  164. package/dist/preview/encoding/workerEncoder.js.map +1 -0
  165. package/dist/preview/logger.js +41 -0
  166. package/dist/preview/logger.js.map +1 -0
  167. package/dist/preview/previewTypes.js +11 -10
  168. package/dist/preview/previewTypes.js.map +1 -1
  169. package/dist/preview/renderTimegroupPreview.js +259 -236
  170. package/dist/preview/renderTimegroupPreview.js.map +1 -1
  171. package/dist/preview/renderTimegroupToCanvas.d.ts +5 -0
  172. package/dist/preview/renderTimegroupToCanvas.js +99 -489
  173. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  174. package/dist/preview/renderTimegroupToVideo.d.ts +1 -0
  175. package/dist/preview/renderTimegroupToVideo.js +80 -22
  176. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  177. package/dist/preview/renderers.js.map +1 -1
  178. package/dist/preview/rendering/inlineImages.js +56 -0
  179. package/dist/preview/rendering/inlineImages.js.map +1 -0
  180. package/dist/preview/rendering/renderToImage.d.ts +1 -0
  181. package/dist/preview/rendering/renderToImage.js +120 -0
  182. package/dist/preview/rendering/renderToImage.js.map +1 -0
  183. package/dist/preview/rendering/renderToImageForeignObject.js +135 -0
  184. package/dist/preview/rendering/renderToImageForeignObject.js.map +1 -0
  185. package/dist/preview/rendering/renderToImageNative.d.ts +1 -0
  186. package/dist/preview/rendering/renderToImageNative.js +129 -0
  187. package/dist/preview/rendering/renderToImageNative.js.map +1 -0
  188. package/dist/preview/rendering/svgSerializer.js +43 -0
  189. package/dist/preview/rendering/svgSerializer.js.map +1 -0
  190. package/dist/preview/rendering/types.d.ts +2 -0
  191. package/dist/preview/statsTrackingStrategy.js +3 -1
  192. package/dist/preview/statsTrackingStrategy.js.map +1 -1
  193. package/dist/preview/workers/WorkerPool.js +8 -57
  194. package/dist/preview/workers/WorkerPool.js.map +1 -1
  195. package/dist/render/EFRenderAPI.d.ts +35 -0
  196. package/dist/render/EFRenderAPI.js +1 -0
  197. package/dist/render/EFRenderAPI.js.map +1 -1
  198. package/dist/sandbox/PlaybackControls.d.ts +1 -0
  199. package/dist/sandbox/ScenarioRunner.d.ts +1 -0
  200. package/dist/sandbox/defineSandbox.d.ts +1 -0
  201. package/dist/sandbox/index.d.ts +3 -0
  202. package/dist/style.css +3 -0
  203. package/dist/transcoding/types/index.d.ts +6 -3
  204. package/package.json +2 -3
  205. package/test/EFVideo.framegen.browsertest.ts +8 -1
  206. package/test/profilingPlugin.ts +1 -3
  207. package/test/setup.ts +23 -1
  208. package/dist/EF_INTERACTIVE.js +0 -7
  209. package/dist/EF_INTERACTIVE.js.map +0 -1
  210. package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +0 -50
  211. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.d.ts +0 -12
  212. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +0 -104
  213. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +0 -1
  214. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +0 -168
  215. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +0 -1
  216. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +0 -46
  217. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +0 -1
  218. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +0 -49
  219. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +0 -1
  220. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +0 -30
  221. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +0 -1
  222. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +0 -49
  223. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +0 -1
  224. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +0 -47
  225. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +0 -1
  226. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +0 -140
  227. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +0 -1
  228. package/dist/elements/EFMedia/shared/BufferUtils.d.ts +0 -13
  229. package/dist/elements/EFMedia/shared/BufferUtils.js +0 -86
  230. package/dist/elements/EFMedia/shared/BufferUtils.js.map +0 -1
  231. package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +0 -17
  232. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +0 -90
  233. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +0 -1
  234. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +0 -80
  235. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +0 -1
  236. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +0 -49
  237. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +0 -1
  238. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +0 -58
  239. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +0 -1
  240. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +0 -71
  241. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +0 -1
  242. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +0 -52
  243. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +0 -1
  244. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +0 -50
  245. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +0 -1
  246. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +0 -109
  247. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +0 -1
  248. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.d.ts +0 -12
  249. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +0 -97
  250. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +0 -1
  251. package/dist/elements/SampleBuffer.d.ts +0 -19
@@ -1,17 +1,17 @@
1
- import { DEFAULT_BLOCKING_TIMEOUT_MS, DEFAULT_HEIGHT, DEFAULT_THUMBNAIL_SCALE, DEFAULT_WIDTH, JPEG_QUALITY_HIGH, JPEG_QUALITY_MEDIUM, createPreviewContainer, isVisibleAtTime } from "./previewTypes.js";
2
- import { buildCloneStructure, collectDocumentStyles, overrideRootCloneStyles, syncStyles } from "./renderTimegroupPreview.js";
1
+ import { FrameController } from "./FrameController.js";
2
+ import { logger } from "./logger.js";
3
+ import { DEFAULT_BLOCKING_TIMEOUT_MS, DEFAULT_HEIGHT, DEFAULT_THUMBNAIL_SCALE, DEFAULT_WIDTH, createPreviewContainer, isVisibleAtTime } from "./previewTypes.js";
4
+ import { buildCloneStructure, collectDocumentStyles, overrideRootCloneStyles, removeHiddenNodesForSerialization, restoreHiddenNodes, syncStyles } from "./renderTimegroupPreview.js";
3
5
  import { getEffectiveRenderMode } from "./renderers.js";
4
- import { WorkerPool, encodeCanvasInWorker } from "./workers/WorkerPool.js";
5
- import { getEncoderWorkerUrl } from "./workers/encoderWorkerInline.js";
6
+ import { RenderContext } from "./RenderContext.js";
6
7
  import { defaultProfiler } from "./RenderProfiler.js";
8
+ import { createDprCanvas, renderToImageNative } from "./rendering/renderToImageNative.js";
9
+ import { clearInlineImageCache } from "./rendering/inlineImages.js";
10
+ import { loadImageFromDataUri, renderToImage, renderToImageDirect } from "./rendering/renderToImage.js";
7
11
 
8
12
  //#region src/preview/renderTimegroupToCanvas.ts
9
13
  /** Number of rows to sample when checking canvas content */
10
14
  const CANVAS_SAMPLE_STRIP_HEIGHT = 4;
11
- /** Interval between profiling log outputs (ms) */
12
- const PROFILING_LOG_INTERVAL_MS = 2e3;
13
- /** Maximum number of cached inline images before eviction */
14
- const MAX_INLINE_IMAGE_CACHE_SIZE = 100;
15
15
  /**
16
16
  * Error thrown when video content is not ready within the blocking timeout.
17
17
  */
@@ -24,159 +24,27 @@ var ContentNotReadyError = class extends Error {
24
24
  this.name = "ContentNotReadyError";
25
25
  }
26
26
  };
27
- /** Image cache for inlining external images as data URIs (foreignObject path) */
28
- const _inlineImageCache = /* @__PURE__ */ new Map();
29
- /** Track canvases that have been initialized for layoutsubtree (only need to wait once) */
30
- const _layoutInitializedCanvases = /* @__PURE__ */ new WeakSet();
31
- let _xmlSerializer = null;
32
- let _textEncoder = null;
33
- let _workerPool = null;
34
- let _workerPoolWarningLogged = false;
35
27
  /**
36
- * Get or create the worker pool for canvas encoding.
37
- * Returns null if workers are not available.
28
+ * Module-level state for render operations.
38
29
  */
39
- function getWorkerPool() {
40
- if (_workerPool) return _workerPool;
41
- if (typeof Worker === "undefined" || typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") {
42
- if (!_workerPoolWarningLogged) {
43
- _workerPoolWarningLogged = true;
44
- console.warn("[renderTimegroupToCanvas] Web Workers or OffscreenCanvas not available, using main thread fallback");
45
- }
46
- return null;
47
- }
48
- try {
49
- _workerPool = new WorkerPool(getEncoderWorkerUrl());
50
- if (!_workerPool.isAvailable()) {
51
- const reason = _workerPool.workerCount === 0 ? "no workers created (check console for errors)" : "workers not available";
52
- _workerPool = null;
53
- if (!_workerPoolWarningLogged) {
54
- _workerPoolWarningLogged = true;
55
- console.warn(`[renderTimegroupToCanvas] Worker pool initialization failed (${reason}), using main thread fallback`);
56
- }
57
- }
58
- } catch (error) {
59
- _workerPool = null;
60
- if (!_workerPoolWarningLogged) {
61
- _workerPoolWarningLogged = true;
62
- const errorMessage = error instanceof Error ? error.message : String(error);
63
- console.warn(`[renderTimegroupToCanvas] Failed to create worker pool: ${errorMessage} - using main thread fallback`);
64
- }
65
- }
66
- return _workerPool;
67
- }
68
- /**
69
- * Encode a single canvas to a data URL (fallback implementation for main thread).
70
- */
71
- function encodeCanvasOnMainThread(canvas, canvasScale) {
72
- try {
73
- if (canvas.width === 0 || canvas.height === 0) return null;
74
- const preserveAlpha = canvas.dataset.preserveAlpha === "true";
75
- let dataUrl;
76
- if (canvasScale < 1) {
77
- const scaledWidth = Math.floor(canvas.width * canvasScale);
78
- const scaledHeight = Math.floor(canvas.height * canvasScale);
79
- const scaledCanvas = document.createElement("canvas");
80
- scaledCanvas.width = scaledWidth;
81
- scaledCanvas.height = scaledHeight;
82
- const scaledCtx = scaledCanvas.getContext("2d");
83
- if (scaledCtx) {
84
- scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);
85
- const quality = canvasScale < .5 ? JPEG_QUALITY_MEDIUM : JPEG_QUALITY_HIGH;
86
- dataUrl = preserveAlpha ? scaledCanvas.toDataURL("image/png") : scaledCanvas.toDataURL("image/jpeg", quality);
87
- } else dataUrl = preserveAlpha ? canvas.toDataURL("image/png") : canvas.toDataURL("image/jpeg", JPEG_QUALITY_HIGH);
88
- } else dataUrl = preserveAlpha ? canvas.toDataURL("image/png") : canvas.toDataURL("image/jpeg", JPEG_QUALITY_HIGH);
89
- return {
90
- dataUrl,
91
- preserveAlpha
92
- };
93
- } catch (e) {
94
- return null;
30
+ const renderState = {
31
+ inlineImageCache: /* @__PURE__ */ new Map(),
32
+ layoutInitializedCanvases: /* @__PURE__ */ new WeakSet(),
33
+ xmlSerializer: new XMLSerializer(),
34
+ textEncoder: new TextEncoder(),
35
+ metrics: {
36
+ inlineImageCacheHits: 0,
37
+ inlineImageCacheMisses: 0,
38
+ inlineImageCacheEvictions: 0
95
39
  }
96
- }
97
- /**
98
- * Encode canvases to data URLs in parallel using worker pool.
99
- * Falls back to main thread encoding if workers are unavailable.
100
- */
101
- async function encodeCanvasesInParallel(canvases, canvasScale = 1) {
102
- const workerPool = getWorkerPool();
103
- if (!workerPool) {
104
- const results = [];
105
- for (const canvas of canvases) {
106
- const encoded = encodeCanvasOnMainThread(canvas, canvasScale);
107
- if (encoded) results.push({
108
- canvas,
109
- ...encoded
110
- });
111
- }
112
- return results;
113
- }
114
- const encodingTasks = canvases.map(async (canvas) => {
115
- try {
116
- if (canvas.width === 0 || canvas.height === 0) return null;
117
- const preserveAlpha = canvas.dataset.preserveAlpha === "true";
118
- let sourceCanvas = canvas;
119
- if (canvasScale < 1) {
120
- const scaledWidth = Math.floor(canvas.width * canvasScale);
121
- const scaledHeight = Math.floor(canvas.height * canvasScale);
122
- const scaledCanvas = document.createElement("canvas");
123
- scaledCanvas.width = scaledWidth;
124
- scaledCanvas.height = scaledHeight;
125
- const scaledCtx = scaledCanvas.getContext("2d");
126
- if (scaledCtx) {
127
- scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);
128
- sourceCanvas = scaledCanvas;
129
- }
130
- }
131
- return {
132
- canvas,
133
- dataUrl: await workerPool.execute((worker) => encodeCanvasInWorker(worker, sourceCanvas, preserveAlpha)),
134
- preserveAlpha
135
- };
136
- } catch (error) {
137
- const encoded = encodeCanvasOnMainThread(canvas, canvasScale);
138
- if (encoded) return {
139
- canvas,
140
- ...encoded
141
- };
142
- return null;
143
- }
144
- });
145
- return (await Promise.all(encodingTasks)).filter((r) => r !== null);
146
- }
40
+ };
147
41
  /**
148
- * Fast base64 encoding directly from Uint8Array.
149
- * Avoids the overhead of converting to binary string first.
150
- * Uses lookup table for optimal performance.
42
+ * Reset cache metrics to zero.
151
43
  */
152
- function encodeBase64Fast(bytes) {
153
- const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
154
- let result = "";
155
- let i = 0;
156
- const len = bytes.length;
157
- while (i < len - 2) {
158
- const byte1 = bytes[i++];
159
- const byte2 = bytes[i++];
160
- const byte3 = bytes[i++];
161
- const bitmap = byte1 << 16 | byte2 << 8 | byte3;
162
- result += base64Chars.charAt(bitmap >> 18 & 63);
163
- result += base64Chars.charAt(bitmap >> 12 & 63);
164
- result += base64Chars.charAt(bitmap >> 6 & 63);
165
- result += base64Chars.charAt(bitmap & 63);
166
- }
167
- if (i < len) {
168
- const byte1 = bytes[i++];
169
- const bitmap = byte1 << 16;
170
- result += base64Chars.charAt(bitmap >> 18 & 63);
171
- result += base64Chars.charAt(bitmap >> 12 & 63);
172
- if (i < len) {
173
- const byte2 = bytes[i++];
174
- const bitmap2 = byte1 << 16 | byte2 << 8;
175
- result += base64Chars.charAt(bitmap2 >> 6 & 63);
176
- result += "=";
177
- } else result += "==";
178
- }
179
- return result;
44
+ function resetCacheMetrics() {
45
+ renderState.metrics.inlineImageCacheHits = 0;
46
+ renderState.metrics.inlineImageCacheMisses = 0;
47
+ renderState.metrics.inlineImageCacheEvictions = 0;
180
48
  }
181
49
  /**
182
50
  * Reset all module state including profiling counters, caches, and logging flags.
@@ -184,22 +52,8 @@ function encodeBase64Fast(bytes) {
184
52
  */
185
53
  function resetRenderState() {
186
54
  defaultProfiler.reset();
187
- _inlineImageCache.clear();
188
- }
189
- /**
190
- * Create a canvas element with proper DPR handling.
191
- * Buffer size is based on renderWidth/renderHeight (internal resolution).
192
- * CSS size is based on fullWidth/fullHeight (logical display size).
193
- */
194
- function createDprCanvas(options) {
195
- const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;
196
- const dpr = options.dpr ?? window.devicePixelRatio ?? 1;
197
- const canvas = document.createElement("canvas");
198
- canvas.width = Math.floor(renderWidth * scale * dpr);
199
- canvas.height = Math.floor(renderHeight * scale * dpr);
200
- canvas.style.width = `${Math.floor(fullWidth * scale)}px`;
201
- canvas.style.height = `${Math.floor(fullHeight * scale)}px`;
202
- return canvas;
55
+ clearInlineImageCache();
56
+ resetCacheMetrics();
203
57
  }
204
58
  /**
205
59
  * Create a debug label for showing render info.
@@ -234,135 +88,12 @@ function updateDebugLabel(label, renderWidth, renderHeight, resolutionScale) {
234
88
  label.textContent = `Render: ${renderWidth}x${renderHeight} (${Math.round(resolutionScale * 100)}%)`;
235
89
  }
236
90
  /**
237
- * Common SVG foreignObject serialization pipeline.
238
- * Handles canvas encoding, serialization, and base64 encoding.
239
- *
240
- * @param container - The HTML element to serialize
241
- * @param width - Output width
242
- * @param height - Output height
243
- * @param options - Serialization options
244
- * @returns Serialization result with data URI and restore function
245
- */
246
- async function serializeToSvgDataUri(container, width, height, options = {}) {
247
- const { canvasScale = 1, inlineImages: shouldInlineImages = false, logEarlyRenders = false } = options;
248
- const canvasRestoreInfo = [];
249
- const canvasStart = performance.now();
250
- const encodedResults = await encodeCanvasesInParallel(Array.from(container.querySelectorAll("canvas")), canvasScale);
251
- for (const { canvas, dataUrl } of encodedResults) try {
252
- const img = document.createElement("img");
253
- img.src = dataUrl;
254
- img.width = canvas.width;
255
- img.height = canvas.height;
256
- const style = canvas.getAttribute("style");
257
- if (style) img.setAttribute("style", style);
258
- const parent = canvas.parentNode;
259
- if (parent) {
260
- const nextSibling = canvas.nextSibling;
261
- parent.replaceChild(img, canvas);
262
- canvasRestoreInfo.push({
263
- canvas,
264
- parent,
265
- nextSibling,
266
- img
267
- });
268
- }
269
- } catch {}
270
- defaultProfiler.addTime("canvasEncode", performance.now() - canvasStart);
271
- if (shouldInlineImages) {
272
- const inlineStart = performance.now();
273
- await inlineImages(container);
274
- defaultProfiler.addTime("inline", performance.now() - inlineStart);
275
- }
276
- const serializeStart = performance.now();
277
- const wrapper = document.createElement("div");
278
- wrapper.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
279
- wrapper.setAttribute("style", `width:${width}px;height:${height}px;overflow:hidden;position:relative;`);
280
- wrapper.appendChild(container);
281
- if (!_xmlSerializer) _xmlSerializer = new XMLSerializer();
282
- const serialized = _xmlSerializer.serializeToString(wrapper);
283
- defaultProfiler.addTime("serialize", performance.now() - serializeStart);
284
- const restore = () => {
285
- const restoreStart = performance.now();
286
- wrapper.removeChild(container);
287
- for (const { canvas, parent, nextSibling, img } of canvasRestoreInfo) if (img.parentNode === parent) if (nextSibling) {
288
- parent.insertBefore(canvas, nextSibling);
289
- parent.removeChild(img);
290
- } else parent.replaceChild(canvas, img);
291
- defaultProfiler.addTime("restore", performance.now() - restoreStart);
292
- };
293
- if (logEarlyRenders && defaultProfiler.isEarlyRender(2)) console.log(`[serializeToSvgDataUri] FO serialized: ${serialized.length} chars`);
294
- const base64Start = performance.now();
295
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><foreignObject width="100%" height="100%">${serialized}</foreignObject></svg>`;
296
- if (!_textEncoder) _textEncoder = new TextEncoder();
297
- const utf8Bytes = _textEncoder.encode(svg);
298
- let base64;
299
- if (typeof Uint8Array.prototype.toBase64 === "function") base64 = utf8Bytes.toBase64();
300
- else base64 = encodeBase64Fast(utf8Bytes);
301
- const dataUri = `data:image/svg+xml;base64,${base64}`;
302
- defaultProfiler.addTime("base64", performance.now() - base64Start);
303
- return {
304
- dataUri,
305
- restore
306
- };
307
- }
308
- /**
309
- * Inline all images in a container as base64 data URIs.
310
- * SVG foreignObject can't load external images due to security restrictions.
311
- * Uses an LRU-style cache with size limits to prevent memory leaks.
312
- */
313
- async function inlineImages(container) {
314
- const images = container.querySelectorAll("img");
315
- for (const image of images) {
316
- const src = image.getAttribute("src");
317
- if (!src || src.startsWith("data:")) continue;
318
- const cached = _inlineImageCache.get(src);
319
- if (cached) {
320
- image.setAttribute("src", cached);
321
- continue;
322
- }
323
- try {
324
- const dataUrl = await blobToDataURL(await (await fetch(src)).blob());
325
- image.setAttribute("src", dataUrl);
326
- if (_inlineImageCache.size >= MAX_INLINE_IMAGE_CACHE_SIZE) {
327
- const firstKey = _inlineImageCache.keys().next().value;
328
- if (firstKey) _inlineImageCache.delete(firstKey);
329
- }
330
- _inlineImageCache.set(src, dataUrl);
331
- } catch (e) {
332
- console.warn("Failed to inline image:", src, e);
333
- }
334
- }
335
- }
336
- /**
337
- * Convert a Blob to a data URL.
338
- */
339
- function blobToDataURL(blob) {
340
- return new Promise((resolve, reject) => {
341
- const reader = new FileReader();
342
- reader.onload = () => resolve(reader.result);
343
- reader.onerror = reject;
344
- reader.readAsDataURL(blob);
345
- });
346
- }
347
- /**
348
91
  * Wait for next animation frame (allows browser to complete layout)
349
92
  */
350
93
  function waitForFrame() {
351
94
  return new Promise((resolve) => requestAnimationFrame(() => resolve()));
352
95
  }
353
96
  /**
354
- * Wait for multiple animation frames to ensure all paints are flushed.
355
- * This is necessary because video frame decoding and canvas painting may
356
- * happen asynchronously even after seek() returns.
357
- */
358
- function waitForPaintFlush() {
359
- return new Promise((resolve) => {
360
- requestAnimationFrame(() => {
361
- requestAnimationFrame(() => resolve());
362
- });
363
- });
364
- }
365
- /**
366
97
  * Check if a canvas has any rendered content (not all transparent/uninitialized).
367
98
  * Returns true if there's ANY non-transparent pixel.
368
99
  */
@@ -433,157 +164,6 @@ async function waitForVideoContent(timegroup, timeMs, maxWaitMs) {
433
164
  };
434
165
  }
435
166
  /**
436
- * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).
437
- * This is much faster than the foreignObject approach and avoids canvas tainting.
438
- *
439
- * Note: The native API renders at device pixel ratio, so we capture at DPR scale
440
- * and then downsample to logical pixels to match the foreignObject path's output.
441
- *
442
- * @param container - The HTML element to render
443
- * @param width - Target width in logical pixels
444
- * @param height - Target height in logical pixels
445
- * @param options - Rendering options (skipWait for batch mode)
446
- *
447
- * @see https://github.com/WICG/html-in-canvas
448
- */
449
- async function renderToImageNative(container, width, height, options = {}) {
450
- const t0 = performance.now();
451
- const { waitForPaint = false, reuseCanvas, skipDprScaling = false } = options;
452
- const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;
453
- let captureCanvas;
454
- let shouldCleanup = false;
455
- if (reuseCanvas) {
456
- captureCanvas = reuseCanvas;
457
- const dpr$1 = skipDprScaling ? 1 : window.devicePixelRatio || 1;
458
- const targetWidth = Math.floor(width * dpr$1);
459
- const targetHeight = Math.floor(height * dpr$1);
460
- if (captureCanvas.width !== targetWidth) captureCanvas.width = targetWidth;
461
- if (captureCanvas.height !== targetHeight) captureCanvas.height = targetHeight;
462
- captureCanvas.style.width = `${width}px`;
463
- captureCanvas.style.height = `${height}px`;
464
- if (!captureCanvas.hasAttribute("layoutsubtree")) {
465
- captureCanvas.setAttribute("layoutsubtree", "");
466
- captureCanvas.layoutSubtree = true;
467
- }
468
- if (!captureCanvas.parentNode) document.body.appendChild(captureCanvas);
469
- if (container.parentElement !== captureCanvas) captureCanvas.appendChild(container);
470
- if (getComputedStyle(container).display === "none") container.style.display = "block";
471
- captureCanvas.offsetHeight;
472
- container.offsetHeight;
473
- getComputedStyle(captureCanvas).opacity;
474
- getComputedStyle(container).opacity;
475
- } else {
476
- captureCanvas = document.createElement("canvas");
477
- captureCanvas.width = Math.floor(width * dpr);
478
- captureCanvas.height = Math.floor(height * dpr);
479
- captureCanvas.setAttribute("layoutsubtree", "");
480
- captureCanvas.layoutSubtree = true;
481
- captureCanvas.appendChild(container);
482
- captureCanvas.style.cssText = `
483
- position: fixed;
484
- left: 0;
485
- top: 0;
486
- width: ${width}px;
487
- height: ${height}px;
488
- opacity: 0;
489
- pointer-events: none;
490
- z-index: -9999;
491
- `;
492
- document.body.appendChild(captureCanvas);
493
- shouldCleanup = true;
494
- }
495
- const t1 = performance.now();
496
- defaultProfiler.addTime("setup", t1 - t0);
497
- try {
498
- getComputedStyle(container).opacity;
499
- if (reuseCanvas && captureCanvas.layoutSubtree && !_layoutInitializedCanvases.has(captureCanvas)) {
500
- await waitForFrame();
501
- _layoutInitializedCanvases.add(captureCanvas);
502
- if (!captureCanvas.parentNode) return captureCanvas;
503
- }
504
- if (waitForPaint) {
505
- await waitForPaintFlush();
506
- if (!captureCanvas.parentNode) return captureCanvas;
507
- }
508
- captureCanvas.getContext("2d").drawElementImage(container, 0, 0);
509
- } finally {
510
- if (shouldCleanup && captureCanvas.parentNode) captureCanvas.parentNode.removeChild(captureCanvas);
511
- }
512
- const t2 = performance.now();
513
- defaultProfiler.addTime("draw", t2 - t1);
514
- if (dpr === 1) {
515
- defaultProfiler.incrementRenderCount();
516
- return captureCanvas;
517
- }
518
- const outputCanvas = document.createElement("canvas");
519
- outputCanvas.width = width;
520
- outputCanvas.height = height;
521
- outputCanvas.getContext("2d").drawImage(captureCanvas, 0, 0, captureCanvas.width, captureCanvas.height, 0, 0, width, height);
522
- const t3 = performance.now();
523
- defaultProfiler.addTime("downsample", t3 - t2);
524
- defaultProfiler.incrementRenderCount();
525
- defaultProfiler.shouldLogByTime(PROFILING_LOG_INTERVAL_MS);
526
- return outputCanvas;
527
- }
528
- /**
529
- * Render HTML content to an image (or canvas) for drawing.
530
- *
531
- * Supports two rendering modes (configurable via previewSettings):
532
- * - "native": Chrome's experimental drawElementImage API (fastest when available)
533
- * - "foreignObject": SVG foreignObject serialization (fallback, works everywhere)
534
- *
535
- * @param container - The HTML element to render
536
- * @param width - Target width in logical pixels
537
- * @param height - Target height in logical pixels
538
- * @param options - Rendering options
539
- * @returns HTMLCanvasElement when using native, HTMLImageElement when using foreignObject
540
- */
541
- async function renderToImage(container, width, height, options) {
542
- if (getEffectiveRenderMode() === "native") return renderToImageNative(container, width, height, options);
543
- const originalCanvases = Array.from(container.querySelectorAll("canvas"));
544
- const clone = container.cloneNode(true);
545
- const clonedCanvases = clone.querySelectorAll("canvas");
546
- const canvasScale = options?.canvasScale ?? 1;
547
- const canvasStart = performance.now();
548
- const encodedResults = await encodeCanvasesInParallel(originalCanvases, canvasScale);
549
- for (let i = 0; i < originalCanvases.length; i++) {
550
- const srcCanvas = originalCanvases[i];
551
- const dstCanvas = clonedCanvases[i];
552
- const encoded = encodedResults.find((r) => r.canvas === srcCanvas);
553
- if (!srcCanvas || !dstCanvas || !encoded) continue;
554
- try {
555
- const img = document.createElement("img");
556
- img.src = encoded.dataUrl;
557
- img.width = srcCanvas.width;
558
- img.height = srcCanvas.height;
559
- const style = dstCanvas.getAttribute("style");
560
- if (style) img.setAttribute("style", style);
561
- dstCanvas.parentNode?.replaceChild(img, dstCanvas);
562
- } catch {}
563
- }
564
- defaultProfiler.addTime("canvasEncode", performance.now() - canvasStart);
565
- const inlineStart = performance.now();
566
- await inlineImages(clone);
567
- defaultProfiler.addTime("inline", performance.now() - inlineStart);
568
- const { dataUri } = await serializeToSvgDataUri(clone, width, height);
569
- return loadImageFromDataUri(dataUri);
570
- }
571
- /**
572
- * Load an image from a data URI. Returns a Promise that resolves when loaded.
573
- */
574
- function loadImageFromDataUri(dataUri) {
575
- const img = new Image();
576
- const imageLoadStart = performance.now();
577
- return new Promise((resolve, reject) => {
578
- img.onload = () => {
579
- defaultProfiler.addTime("imageLoad", performance.now() - imageLoadStart);
580
- resolve(img);
581
- };
582
- img.onerror = reject;
583
- img.src = dataUri;
584
- });
585
- }
586
- /**
587
167
  * Captures a frame from an already-seeked render clone.
588
168
  * Used internally by captureBatch for efficiency (reuses one clone across all captures).
589
169
  *
@@ -606,48 +186,58 @@ async function captureFromClone(renderClone, renderContainer, options = {}) {
606
186
  const ctx = canvas.getContext("2d");
607
187
  if (!ctx) throw new Error("Failed to get canvas 2d context");
608
188
  const timeMs = renderClone.currentTimeMs;
189
+ await new FrameController(renderClone).renderFrame(timeMs, { waitForLitUpdate: false });
609
190
  if (contentReadyMode === "blocking") {
610
191
  const result = await waitForVideoContent(renderClone, timeMs, blockingTimeoutMs);
611
192
  if (!result.ready) throw new ContentNotReadyError(timeMs, blockingTimeoutMs, result.blankVideos);
612
193
  }
613
- let image;
614
- if (getEffectiveRenderMode() === "native") {
615
- renderContainer.style.cssText = `
616
- position: fixed;
617
- left: 0;
618
- top: 0;
619
- width: ${width}px;
620
- height: ${height}px;
621
- pointer-events: none;
622
- overflow: hidden;
623
- `;
624
- image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: scale < 1 });
625
- } else {
626
- const t0 = performance.now();
627
- const { container, syncState } = buildCloneStructure(renderClone, timeMs);
628
- const buildTime = performance.now() - t0;
629
- const bgSource = originalTimegroup ?? renderClone;
630
- const previewContainer = createPreviewContainer({
631
- width,
632
- height,
633
- background: getComputedStyle(bgSource).background || "#000"
634
- });
635
- const t1 = performance.now();
636
- const styleEl = document.createElement("style");
637
- styleEl.textContent = collectDocumentStyles();
638
- const stylesTime = performance.now() - t1;
639
- previewContainer.appendChild(styleEl);
640
- previewContainer.appendChild(container);
641
- overrideRootCloneStyles(syncState, true);
642
- const t2 = performance.now();
643
- image = await renderToImage(previewContainer, width, height, { canvasScale: scale });
644
- const renderTime = performance.now() - t2;
645
- console.log(`[captureFromClone] build=${buildTime.toFixed(0)}ms, styles=${stylesTime.toFixed(0)}ms, render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`);
194
+ const renderContext = new RenderContext();
195
+ try {
196
+ let image;
197
+ if (getEffectiveRenderMode() === "native") {
198
+ renderContainer.style.cssText = `
199
+ position: fixed;
200
+ left: 0;
201
+ top: 0;
202
+ width: ${width}px;
203
+ height: ${height}px;
204
+ pointer-events: none;
205
+ overflow: hidden;
206
+ `;
207
+ image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: true });
208
+ } else {
209
+ const t0 = performance.now();
210
+ const { container, syncState } = buildCloneStructure(renderClone, timeMs);
211
+ const buildTime = performance.now() - t0;
212
+ const bgSource = originalTimegroup ?? renderClone;
213
+ const previewContainer = createPreviewContainer({
214
+ width,
215
+ height,
216
+ background: getComputedStyle(bgSource).background || "#000"
217
+ });
218
+ const t1 = performance.now();
219
+ const styleEl = document.createElement("style");
220
+ styleEl.textContent = collectDocumentStyles();
221
+ const stylesTime = performance.now() - t1;
222
+ previewContainer.appendChild(styleEl);
223
+ previewContainer.appendChild(container);
224
+ overrideRootCloneStyles(syncState, true);
225
+ const t2 = performance.now();
226
+ image = await renderToImage(previewContainer, width, height, {
227
+ canvasScale: scale,
228
+ renderContext,
229
+ sourceMap: syncState.canvasSourceMap
230
+ });
231
+ const renderTime = performance.now() - t2;
232
+ logger.debug(`[captureFromClone] build=${buildTime.toFixed(0)}ms, styles=${stylesTime.toFixed(0)}ms, render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`);
233
+ }
234
+ const srcWidth = image.width;
235
+ const srcHeight = image.height;
236
+ ctx.drawImage(image, 0, 0, srcWidth, srcHeight, 0, 0, canvas.width, canvas.height);
237
+ return canvas;
238
+ } finally {
239
+ renderContext.dispose();
646
240
  }
647
- const srcWidth = image.width;
648
- const srcHeight = image.height;
649
- ctx.drawImage(image, 0, 0, srcWidth, srcHeight, 0, 0, canvas.width, canvas.height);
650
- return canvas;
651
241
  }
652
242
  /**
653
243
  * Captures a single frame from a timegroup at a specific time.
@@ -745,6 +335,9 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
745
335
  overrideRootCloneStyles(syncState);
746
336
  let rendering = false;
747
337
  let lastTimeMs = -1;
338
+ let disposed = false;
339
+ const renderContext = new RenderContext();
340
+ const frameController = new FrameController(timegroup);
748
341
  let hasLoggedScale = false;
749
342
  let pendingResolutionScale = null;
750
343
  /**
@@ -779,7 +372,7 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
779
372
  };
780
373
  const getResolutionScale = () => pendingResolutionScale ?? currentResolutionScale;
781
374
  const refresh = async () => {
782
- if (rendering) return;
375
+ if (rendering || disposed) return;
783
376
  const sourceTimeMs = timegroup.currentTimeMs ?? 0;
784
377
  const userTimeMs = timegroup.userTimeMs ?? 0;
785
378
  if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;
@@ -790,14 +383,21 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
790
383
  if (!hasLoggedScale) {
791
384
  hasLoggedScale = true;
792
385
  const mode = getEffectiveRenderMode();
793
- console.log(`[renderTimegroupToCanvas] Resolution scale: ${currentResolutionScale} (${width}x${height} → ${renderWidth}x${renderHeight}), canvas buffer: ${canvas.width}x${canvas.height}, CSS size: ${canvas.style.width}x${canvas.style.height}, renderMode: ${mode}`);
386
+ logger.debug(`[renderTimegroupToCanvas] Resolution scale: ${currentResolutionScale} (${width}x${height} → ${renderWidth}x${renderHeight}), canvas buffer: ${canvas.width}x${canvas.height}, CSS size: ${canvas.style.width}x${canvas.style.height}, renderMode: ${mode}`);
794
387
  }
795
388
  try {
389
+ await frameController.renderFrame(userTimeMs);
796
390
  syncStyles(syncState, toAbsoluteTime(timegroup, userTimeMs));
797
391
  overrideRootCloneStyles(syncState);
392
+ const removedNodes = removeHiddenNodesForSerialization(syncState);
798
393
  const t0 = performance.now();
799
- const image = await renderToImage(previewContainer, renderWidth, renderHeight, { canvasScale: currentResolutionScale });
394
+ const image = await renderToImage(previewContainer, renderWidth, renderHeight, {
395
+ canvasScale: currentResolutionScale,
396
+ renderContext,
397
+ sourceMap: syncState.canvasSourceMap
398
+ });
800
399
  const renderTime = performance.now() - t0;
400
+ restoreHiddenNodes(removedNodes);
801
401
  const targetWidth = Math.floor(renderWidth * scale * dpr);
802
402
  const targetHeight = Math.floor(renderHeight * scale * dpr);
803
403
  if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
@@ -809,14 +409,23 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
809
409
  ctx.drawImage(image, 0, 0);
810
410
  ctx.restore();
811
411
  defaultProfiler.incrementRenderCount();
812
- if (defaultProfiler.shouldLogByFrameCount(60)) console.log(`[renderTimegroupToCanvas] Frame render: ${renderTime.toFixed(1)}ms (resolutionScale=${currentResolutionScale}, image=${image.width}x${image.height})`);
412
+ if (defaultProfiler.shouldLogByFrameCount(60)) logger.debug(`[renderTimegroupToCanvas] Frame render: ${renderTime.toFixed(1)}ms (resolutionScale=${currentResolutionScale}, image=${image.width}x${image.height})`);
813
413
  updateDebugLabel(debugLabel, renderWidth, renderHeight, currentResolutionScale);
814
414
  } catch (e) {
815
- console.error("Canvas preview render failed:", e);
415
+ logger.error("Canvas preview render failed:", e);
816
416
  } finally {
817
417
  rendering = false;
818
418
  }
819
419
  };
420
+ /**
421
+ * Dispose the preview and release resources.
422
+ */
423
+ const dispose = () => {
424
+ if (disposed) return;
425
+ disposed = true;
426
+ frameController.abort();
427
+ renderContext.dispose();
428
+ };
820
429
  refresh();
821
430
  return {
822
431
  container: wrapperContainer,
@@ -824,7 +433,8 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
824
433
  refresh,
825
434
  syncState,
826
435
  setResolutionScale,
827
- getResolutionScale
436
+ getResolutionScale,
437
+ dispose
828
438
  };
829
439
  }
830
440