@editframe/elements 0.30.2-beta.0 → 0.31.0-beta.1

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 (326) hide show
  1. package/dist/EF_FRAMEGEN.d.ts +5 -0
  2. package/dist/EF_FRAMEGEN.js +20 -4
  3. package/dist/EF_FRAMEGEN.js.map +1 -1
  4. package/dist/EF_INTERACTIVE.js.map +1 -1
  5. package/dist/_virtual/rolldown_runtime.js +27 -0
  6. package/dist/canvas/EFCanvas.d.ts +311 -0
  7. package/dist/canvas/EFCanvas.js +1089 -0
  8. package/dist/canvas/EFCanvas.js.map +1 -0
  9. package/dist/canvas/EFCanvasItem.d.ts +55 -0
  10. package/dist/canvas/EFCanvasItem.js +72 -0
  11. package/dist/canvas/EFCanvasItem.js.map +1 -0
  12. package/dist/canvas/api/CanvasAPI.d.ts +115 -0
  13. package/dist/canvas/api/CanvasAPI.js +182 -0
  14. package/dist/canvas/api/CanvasAPI.js.map +1 -0
  15. package/dist/canvas/api/types.d.ts +42 -0
  16. package/dist/canvas/coordinateTransform.js +90 -0
  17. package/dist/canvas/coordinateTransform.js.map +1 -0
  18. package/dist/canvas/getElementBounds.js +40 -0
  19. package/dist/canvas/getElementBounds.js.map +1 -0
  20. package/dist/canvas/overlays/SelectionOverlay.js +265 -0
  21. package/dist/canvas/overlays/SelectionOverlay.js.map +1 -0
  22. package/dist/canvas/overlays/overlayState.js +153 -0
  23. package/dist/canvas/overlays/overlayState.js.map +1 -0
  24. package/dist/canvas/selection/SelectionController.js +105 -0
  25. package/dist/canvas/selection/SelectionController.js.map +1 -0
  26. package/dist/canvas/selection/SelectionModel.d.ts +98 -0
  27. package/dist/canvas/selection/SelectionModel.js +229 -0
  28. package/dist/canvas/selection/SelectionModel.js.map +1 -0
  29. package/dist/canvas/selection/selectionContext.d.ts +31 -0
  30. package/dist/canvas/selection/selectionContext.js +12 -0
  31. package/dist/canvas/selection/selectionContext.js.map +1 -0
  32. package/dist/elements/ContainerInfo.d.ts +29 -0
  33. package/dist/elements/ContainerInfo.js +30 -0
  34. package/dist/elements/ContainerInfo.js.map +1 -0
  35. package/dist/elements/EFAudio.d.ts +13 -3
  36. package/dist/elements/EFAudio.js +64 -10
  37. package/dist/elements/EFAudio.js.map +1 -1
  38. package/dist/elements/EFCaptions.d.ts +18 -16
  39. package/dist/elements/EFCaptions.js +110 -19
  40. package/dist/elements/EFCaptions.js.map +1 -1
  41. package/dist/elements/EFImage.d.ts +16 -6
  42. package/dist/elements/EFImage.js +79 -9
  43. package/dist/elements/EFImage.js.map +1 -1
  44. package/dist/elements/EFMedia/AssetIdMediaEngine.js +51 -4
  45. package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
  46. package/dist/elements/EFMedia/AssetMediaEngine.js +125 -52
  47. package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
  48. package/dist/elements/EFMedia/BaseMediaEngine.js +24 -6
  49. package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
  50. package/dist/elements/EFMedia/JitMediaEngine.js +12 -8
  51. package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
  52. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +46 -7
  53. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +1 -1
  54. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +98 -73
  55. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +1 -1
  56. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +28 -5
  57. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +1 -1
  58. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +18 -6
  59. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +1 -1
  60. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +8 -2
  61. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +1 -1
  62. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +31 -6
  63. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +1 -1
  64. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +28 -5
  65. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +1 -1
  66. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +97 -72
  67. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +1 -1
  68. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -1
  69. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  70. package/dist/elements/EFMedia/shared/BufferUtils.js +1 -1
  71. package/dist/elements/EFMedia/shared/BufferUtils.js.map +1 -1
  72. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +25 -14
  73. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  74. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +47 -16
  75. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +1 -1
  76. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +37 -19
  77. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
  78. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +65 -21
  79. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
  80. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +8 -3
  81. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +1 -1
  82. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +32 -9
  83. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +1 -1
  84. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +33 -10
  85. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +1 -1
  86. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +23 -8
  87. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +1 -1
  88. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +34 -10
  89. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +1 -1
  90. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +31 -8
  91. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +1 -1
  92. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +31 -114
  93. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +1 -1
  94. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +44 -8
  95. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +1 -1
  96. package/dist/elements/EFMedia.d.ts +18 -7
  97. package/dist/elements/EFMedia.js +23 -3
  98. package/dist/elements/EFMedia.js.map +1 -1
  99. package/dist/elements/EFPanZoom.d.ts +96 -0
  100. package/dist/elements/EFPanZoom.js +290 -0
  101. package/dist/elements/EFPanZoom.js.map +1 -0
  102. package/dist/elements/EFSourceMixin.js +7 -6
  103. package/dist/elements/EFSourceMixin.js.map +1 -1
  104. package/dist/elements/EFSurface.d.ts +6 -6
  105. package/dist/elements/EFSurface.js +7 -2
  106. package/dist/elements/EFSurface.js.map +1 -1
  107. package/dist/elements/EFTemporal.d.ts +2 -1
  108. package/dist/elements/EFTemporal.js +192 -71
  109. package/dist/elements/EFTemporal.js.map +1 -1
  110. package/dist/elements/EFText.d.ts +5 -4
  111. package/dist/elements/EFText.js +102 -13
  112. package/dist/elements/EFText.js.map +1 -1
  113. package/dist/elements/EFTextSegment.d.ts +32 -6
  114. package/dist/elements/EFTextSegment.js +53 -15
  115. package/dist/elements/EFTextSegment.js.map +1 -1
  116. package/dist/elements/EFThumbnailStrip.d.ts +129 -56
  117. package/dist/elements/EFThumbnailStrip.js +605 -359
  118. package/dist/elements/EFThumbnailStrip.js.map +1 -1
  119. package/dist/elements/EFTimegroup.d.ts +233 -25
  120. package/dist/elements/EFTimegroup.js +865 -144
  121. package/dist/elements/EFTimegroup.js.map +1 -1
  122. package/dist/elements/EFVideo.d.ts +42 -5
  123. package/dist/elements/EFVideo.js +165 -11
  124. package/dist/elements/EFVideo.js.map +1 -1
  125. package/dist/elements/EFWaveform.d.ts +6 -6
  126. package/dist/elements/EFWaveform.js +2 -1
  127. package/dist/elements/EFWaveform.js.map +1 -1
  128. package/dist/elements/ElementPositionInfo.d.ts +35 -0
  129. package/dist/elements/ElementPositionInfo.js +49 -0
  130. package/dist/elements/ElementPositionInfo.js.map +1 -0
  131. package/dist/elements/FetchMixin.js +16 -1
  132. package/dist/elements/FetchMixin.js.map +1 -1
  133. package/dist/elements/SessionThumbnailCache.js +154 -0
  134. package/dist/elements/SessionThumbnailCache.js.map +1 -0
  135. package/dist/elements/TargetController.js +3 -1
  136. package/dist/elements/TargetController.js.map +1 -1
  137. package/dist/elements/TimegroupController.js +9 -3
  138. package/dist/elements/TimegroupController.js.map +1 -1
  139. package/dist/elements/findRootTemporal.js +30 -0
  140. package/dist/elements/findRootTemporal.js.map +1 -0
  141. package/dist/elements/renderTemporalAudio.js +18 -5
  142. package/dist/elements/renderTemporalAudio.js.map +1 -1
  143. package/dist/elements/updateAnimations.js +171 -28
  144. package/dist/elements/updateAnimations.js.map +1 -1
  145. package/dist/getRenderInfo.d.ts +2 -2
  146. package/dist/gui/ContextMixin.js +4 -2
  147. package/dist/gui/ContextMixin.js.map +1 -1
  148. package/dist/gui/Controllable.js +74 -1
  149. package/dist/gui/Controllable.js.map +1 -1
  150. package/dist/gui/EFActiveRootTemporal.d.ts +50 -0
  151. package/dist/gui/EFActiveRootTemporal.js +94 -0
  152. package/dist/gui/EFActiveRootTemporal.js.map +1 -0
  153. package/dist/gui/EFConfiguration.d.ts +7 -1
  154. package/dist/gui/EFConfiguration.js.map +1 -1
  155. package/dist/gui/EFControls.d.ts +2 -2
  156. package/dist/gui/EFControls.js +109 -13
  157. package/dist/gui/EFControls.js.map +1 -1
  158. package/dist/gui/EFDial.d.ts +4 -4
  159. package/dist/gui/EFFilmstrip.d.ts +11 -214
  160. package/dist/gui/EFFilmstrip.js +53 -1152
  161. package/dist/gui/EFFilmstrip.js.map +1 -1
  162. package/dist/gui/EFFitScale.d.ts +3 -3
  163. package/dist/gui/EFFitScale.js +39 -12
  164. package/dist/gui/EFFitScale.js.map +1 -1
  165. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  166. package/dist/gui/EFOverlayItem.d.ts +48 -0
  167. package/dist/gui/EFOverlayItem.js +97 -0
  168. package/dist/gui/EFOverlayItem.js.map +1 -0
  169. package/dist/gui/EFOverlayLayer.d.ts +70 -0
  170. package/dist/gui/EFOverlayLayer.js +104 -0
  171. package/dist/gui/EFOverlayLayer.js.map +1 -0
  172. package/dist/gui/EFPause.d.ts +4 -4
  173. package/dist/gui/EFPlay.d.ts +4 -4
  174. package/dist/gui/EFResizableBox.d.ts +12 -16
  175. package/dist/gui/EFResizableBox.js +109 -451
  176. package/dist/gui/EFResizableBox.js.map +1 -1
  177. package/dist/gui/EFScrubber.d.ts +30 -5
  178. package/dist/gui/EFScrubber.js +224 -31
  179. package/dist/gui/EFScrubber.js.map +1 -1
  180. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  181. package/dist/gui/EFTimeDisplay.js +4 -1
  182. package/dist/gui/EFTimeDisplay.js.map +1 -1
  183. package/dist/gui/EFTimelineRuler.d.ts +71 -0
  184. package/dist/gui/EFTimelineRuler.js +320 -0
  185. package/dist/gui/EFTimelineRuler.js.map +1 -0
  186. package/dist/gui/EFToggleLoop.d.ts +4 -4
  187. package/dist/gui/EFTogglePlay.d.ts +4 -4
  188. package/dist/gui/EFTransformHandles.d.ts +91 -0
  189. package/dist/gui/EFTransformHandles.js +393 -0
  190. package/dist/gui/EFTransformHandles.js.map +1 -0
  191. package/dist/gui/EFWorkbench.d.ts +178 -0
  192. package/dist/gui/EFWorkbench.js +2067 -22
  193. package/dist/gui/EFWorkbench.js.map +1 -1
  194. package/dist/gui/FitScaleHelpers.d.ts +31 -0
  195. package/dist/gui/FitScaleHelpers.js +41 -0
  196. package/dist/gui/FitScaleHelpers.js.map +1 -0
  197. package/dist/gui/PlaybackController.d.ts +2 -1
  198. package/dist/gui/PlaybackController.js +46 -15
  199. package/dist/gui/PlaybackController.js.map +1 -1
  200. package/dist/gui/TWMixin.js +1 -1
  201. package/dist/gui/TWMixin.js.map +1 -1
  202. package/dist/gui/hierarchy/EFHierarchy.d.ts +65 -0
  203. package/dist/gui/hierarchy/EFHierarchy.js +338 -0
  204. package/dist/gui/hierarchy/EFHierarchy.js.map +1 -0
  205. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +118 -0
  206. package/dist/gui/hierarchy/EFHierarchyItem.js +551 -0
  207. package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -0
  208. package/dist/gui/hierarchy/hierarchyContext.d.ts +38 -0
  209. package/dist/gui/hierarchy/hierarchyContext.js +8 -0
  210. package/dist/gui/hierarchy/hierarchyContext.js.map +1 -0
  211. package/dist/gui/icons.js +34 -0
  212. package/dist/gui/icons.js.map +1 -0
  213. package/dist/gui/panZoomTransformContext.js +12 -0
  214. package/dist/gui/panZoomTransformContext.js.map +1 -0
  215. package/dist/gui/previewSettingsContext.js +12 -0
  216. package/dist/gui/previewSettingsContext.js.map +1 -0
  217. package/dist/gui/timeline/EFTimeline.d.ts +270 -0
  218. package/dist/gui/timeline/EFTimeline.js +1369 -0
  219. package/dist/gui/timeline/EFTimeline.js.map +1 -0
  220. package/dist/gui/timeline/EFTimelineRow.js +374 -0
  221. package/dist/gui/timeline/EFTimelineRow.js.map +1 -0
  222. package/dist/gui/timeline/TrimHandles.d.ts +36 -0
  223. package/dist/gui/timeline/TrimHandles.js +204 -0
  224. package/dist/gui/timeline/TrimHandles.js.map +1 -0
  225. package/dist/gui/timeline/flattenHierarchy.js +31 -0
  226. package/dist/gui/timeline/flattenHierarchy.js.map +1 -0
  227. package/dist/gui/timeline/timelineStateContext.d.ts +26 -0
  228. package/dist/gui/timeline/timelineStateContext.js +42 -0
  229. package/dist/gui/timeline/timelineStateContext.js.map +1 -0
  230. package/dist/gui/timeline/tracks/AudioTrack.js +264 -0
  231. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -0
  232. package/dist/gui/timeline/tracks/CaptionsTrack.js +595 -0
  233. package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -0
  234. package/dist/gui/timeline/tracks/HTMLTrack.js +19 -0
  235. package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -0
  236. package/dist/gui/timeline/tracks/ImageTrack.js +53 -0
  237. package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -0
  238. package/dist/gui/timeline/tracks/TextTrack.js +250 -0
  239. package/dist/gui/timeline/tracks/TextTrack.js.map +1 -0
  240. package/dist/gui/timeline/tracks/TimegroupTrack.js +143 -0
  241. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -0
  242. package/dist/gui/timeline/tracks/TrackItem.js +269 -0
  243. package/dist/gui/timeline/tracks/TrackItem.js.map +1 -0
  244. package/dist/gui/timeline/tracks/VideoTrack.js +265 -0
  245. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -0
  246. package/dist/gui/timeline/tracks/WaveformTrack.js +19 -0
  247. package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -0
  248. package/dist/gui/timeline/tracks/ensureTrackItemInit.js +1 -0
  249. package/dist/gui/timeline/tracks/preloadTracks.js +9 -0
  250. package/dist/gui/timeline/tracks/renderTrackChildren.js +119 -0
  251. package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -0
  252. package/dist/gui/timeline/tracks/waveformUtils.js +80 -0
  253. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -0
  254. package/dist/gui/transformCalculations.js +217 -0
  255. package/dist/gui/transformCalculations.js.map +1 -0
  256. package/dist/gui/transformUtils.d.ts +37 -0
  257. package/dist/gui/transformUtils.js +77 -0
  258. package/dist/gui/transformUtils.js.map +1 -0
  259. package/dist/gui/tree/EFTree.d.ts +59 -0
  260. package/dist/gui/tree/EFTree.js +174 -0
  261. package/dist/gui/tree/EFTree.js.map +1 -0
  262. package/dist/gui/tree/EFTreeItem.d.ts +38 -0
  263. package/dist/gui/tree/EFTreeItem.js +146 -0
  264. package/dist/gui/tree/EFTreeItem.js.map +1 -0
  265. package/dist/gui/tree/treeContext.d.ts +60 -0
  266. package/dist/gui/tree/treeContext.js +23 -0
  267. package/dist/gui/tree/treeContext.js.map +1 -0
  268. package/dist/index.d.ts +32 -8
  269. package/dist/index.js +30 -6
  270. package/dist/index.js.map +1 -1
  271. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +688 -0
  272. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +1 -0
  273. package/dist/node_modules/react/cjs/react.development.js +1521 -0
  274. package/dist/node_modules/react/cjs/react.development.js.map +1 -0
  275. package/dist/node_modules/react/index.js +13 -0
  276. package/dist/node_modules/react/index.js.map +1 -0
  277. package/dist/node_modules/react/jsx-runtime.js +13 -0
  278. package/dist/node_modules/react/jsx-runtime.js.map +1 -0
  279. package/dist/preview/AdaptiveResolutionTracker.js +228 -0
  280. package/dist/preview/AdaptiveResolutionTracker.js.map +1 -0
  281. package/dist/preview/RenderProfiler.js +135 -0
  282. package/dist/preview/RenderProfiler.js.map +1 -0
  283. package/dist/preview/previewSettings.js +131 -0
  284. package/dist/preview/previewSettings.js.map +1 -0
  285. package/dist/preview/previewTypes.js +64 -0
  286. package/dist/preview/previewTypes.js.map +1 -0
  287. package/dist/preview/renderTimegroupPreview.js +656 -0
  288. package/dist/preview/renderTimegroupPreview.js.map +1 -0
  289. package/dist/preview/renderTimegroupToCanvas.d.ts +37 -0
  290. package/dist/preview/renderTimegroupToCanvas.js +833 -0
  291. package/dist/preview/renderTimegroupToCanvas.js.map +1 -0
  292. package/dist/preview/renderTimegroupToVideo.d.ts +39 -0
  293. package/dist/preview/renderTimegroupToVideo.js +274 -0
  294. package/dist/preview/renderTimegroupToVideo.js.map +1 -0
  295. package/dist/preview/renderers.js +16 -0
  296. package/dist/preview/renderers.js.map +1 -0
  297. package/dist/preview/statsTrackingStrategy.js +201 -0
  298. package/dist/preview/statsTrackingStrategy.js.map +1 -0
  299. package/dist/preview/thumbnailCacheSettings.js +52 -0
  300. package/dist/preview/thumbnailCacheSettings.js.map +1 -0
  301. package/dist/preview/workers/WorkerPool.js +178 -0
  302. package/dist/preview/workers/WorkerPool.js.map +1 -0
  303. package/dist/preview/workers/encoderWorkerInline.js +103 -0
  304. package/dist/preview/workers/encoderWorkerInline.js.map +1 -0
  305. package/dist/sandbox/PlaybackControls.js +10 -0
  306. package/dist/sandbox/PlaybackControls.js.map +1 -0
  307. package/dist/sandbox/ScenarioRunner.js +1 -0
  308. package/dist/sandbox/index.js +2 -0
  309. package/dist/style.css +71 -67
  310. package/dist/transcoding/types/index.d.ts +2 -1
  311. package/dist/transcoding/utils/UrlGenerator.d.ts +6 -1
  312. package/dist/transcoding/utils/UrlGenerator.js +12 -3
  313. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  314. package/dist/utils/LRUCache.js +1 -375
  315. package/dist/utils/LRUCache.js.map +1 -1
  316. package/dist/utils/frameTime.js +14 -0
  317. package/dist/utils/frameTime.js.map +1 -0
  318. package/package.json +3 -3
  319. package/test/profilingPlugin.ts +223 -0
  320. package/test/recordReplayProxyPlugin.js +22 -27
  321. package/test/thumbnail-performance-test.html +116 -0
  322. package/test/visualRegressionUtils.ts +286 -0
  323. package/types.json +1 -1
  324. package/dist/elements/TimegroupController.d.ts +0 -18
  325. package/dist/msToTimeCode.js +0 -17
  326. package/dist/msToTimeCode.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"EFWorkbench.js","names":["EFWorkbench"],"sources":["../../src/gui/EFWorkbench.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, eventOptions, property } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\n\nimport { ContextMixin } from \"./ContextMixin.js\";\nimport { TWMixin } from \"./TWMixin.js\";\n\n@customElement(\"ef-workbench\")\nexport class EFWorkbench extends ContextMixin(TWMixin(LitElement)) {\n static styles = [\n css`\n :host {\n display: block;\n width: 100%;\n height: 100%;\n \n /* Light mode colors */\n --workbench-bg: rgb(30 41 59); /* slate-800 */\n --workbench-overlay-border: rgb(59 130 246); /* blue-500 */\n --workbench-overlay-bg: rgb(191 219 254); /* blue-200 */\n }\n \n :host(.dark), :host-context(.dark) {\n /* Dark mode colors */\n --workbench-bg: rgb(2 6 23); /* slate-950 */\n --workbench-overlay-border: rgb(96 165 250); /* blue-400 */\n --workbench-overlay-bg: rgb(30 58 138); /* blue-900 */\n }\n `,\n ];\n\n @property({ type: Boolean })\n rendering = false;\n\n focusOverlay = createRef<HTMLDivElement>();\n\n @eventOptions({ passive: false, capture: true })\n handleStageWheel(event: WheelEvent) {\n event.preventDefault();\n }\n\n connectedCallback(): void {\n document.body.style.width = \"100%\";\n document.body.style.height = \"100%\";\n document.documentElement.style.width = \"100%\";\n document.documentElement.style.height = \"100%\";\n super.connectedCallback();\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n document.body.style.width = \"\";\n document.body.style.height = \"\";\n document.documentElement.style.width = \"\";\n document.documentElement.style.height = \"\";\n }\n\n update(\n changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,\n ): void {\n super.update(changedProperties);\n\n if (changedProperties.has(\"focusedElement\")) {\n this.drawOverlays();\n }\n }\n\n drawOverlays = () => {\n const focusOverlay = this.focusOverlay.value;\n if (focusOverlay) {\n if (this.focusedElement) {\n focusOverlay.style.display = \"block\";\n const rect = this.focusedElement.getBoundingClientRect();\n Object.assign(focusOverlay.style, {\n position: \"fixed\",\n top: `${rect.top}px`,\n left: `${rect.left}px`,\n width: `${rect.width}px`,\n height: `${rect.height}px`,\n });\n requestAnimationFrame(this.drawOverlays);\n } else {\n focusOverlay.style.display = \"none\";\n }\n }\n };\n\n render() {\n // TODO: this.rendering is not correctly set when using the framegen bridge\n // so to hack we're checking for the existence of EF_RENDERING on the window\n if (\n this.rendering ||\n (typeof window !== \"undefined\" && window.EF_RENDERING?.() === true)\n ) {\n return html`\n <slot class=\"fixed inset-0 h-full w-full\" name=\"canvas\"></slot>\n `;\n }\n return html`\n <div\n class=\"grid h-full w-full\"\n style=\"grid-template-rows: 1fr 300px; grid-template-columns: 100%; background-color: var(--workbench-bg);\"\n >\n <div\n class=\"relative h-full w-full overflow-hidden\"\n @wheel=${this.handleStageWheel}\n >\n <ef-fit-scale class=\"h-full grid place-content-center\">\n <slot name=\"canvas\" class=\"contents\"></slot>\n </ef-fit-scale>\n <div\n class=\"border bg-opacity-20 absolute\"\n style=\"border-color: var(--workbench-overlay-border); background-color: var(--workbench-overlay-bg);\"\n ${ref(this.focusOverlay)}\n ></div>\n </div>\n\n <slot class=\"overflow inline-block\" name=\"timeline\"></slot>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-workbench\": EFWorkbench;\n }\n}\n"],"mappings":";;;;;;;;AAQO,wBAAMA,sBAAoB,aAAa,QAAQ,WAAW,CAAC,CAAC;;;mBAwBrD;sBAEG,WAA2B;4BAiCrB;GACnB,MAAM,eAAe,KAAK,aAAa;AACvC,OAAI,aACF,KAAI,KAAK,gBAAgB;AACvB,iBAAa,MAAM,UAAU;IAC7B,MAAM,OAAO,KAAK,eAAe,uBAAuB;AACxD,WAAO,OAAO,aAAa,OAAO;KAChC,UAAU;KACV,KAAK,GAAG,KAAK,IAAI;KACjB,MAAM,GAAG,KAAK,KAAK;KACnB,OAAO,GAAG,KAAK,MAAM;KACrB,QAAQ,GAAG,KAAK,OAAO;KACxB,CAAC;AACF,0BAAsB,KAAK,aAAa;SAExC,cAAa,MAAM,UAAU;;;;gBAzEnB,CACd,GAAG;;;;;;;;;;;;;;;;;;MAmBJ;;CAOD,AACA,iBAAiB,OAAmB;AAClC,QAAM,gBAAgB;;CAGxB,oBAA0B;AACxB,WAAS,KAAK,MAAM,QAAQ;AAC5B,WAAS,KAAK,MAAM,SAAS;AAC7B,WAAS,gBAAgB,MAAM,QAAQ;AACvC,WAAS,gBAAgB,MAAM,SAAS;AACxC,QAAM,mBAAmB;;CAG3B,uBAA6B;AAC3B,QAAM,sBAAsB;AAC5B,WAAS,KAAK,MAAM,QAAQ;AAC5B,WAAS,KAAK,MAAM,SAAS;AAC7B,WAAS,gBAAgB,MAAM,QAAQ;AACvC,WAAS,gBAAgB,MAAM,SAAS;;CAG1C,OACE,mBACM;AACN,QAAM,OAAO,kBAAkB;AAE/B,MAAI,kBAAkB,IAAI,iBAAiB,CACzC,MAAK,cAAc;;CAwBvB,SAAS;AAGP,MACE,KAAK,aACJ,OAAO,WAAW,eAAe,OAAO,gBAAgB,KAAK,KAE9D,QAAO,IAAI;;;AAIb,SAAO,IAAI;;;;;;;mBAOI,KAAK,iBAAiB;;;;;;;;cAQ3B,IAAI,KAAK,aAAa,CAAC;;;;;;;;;YAlFlC,SAAS,EAAE,MAAM,SAAS,CAAC;YAK3B,aAAa;CAAE,SAAS;CAAO,SAAS;CAAM,CAAC;0BA7BjD,cAAc,eAAe"}
1
+ {"version":3,"file":"EFWorkbench.js","names":["EFWorkbench","state","node: Element | null","thumbnailImageCache","statusColor: string","statusText: string"],"sources":["../../src/gui/EFWorkbench.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, eventOptions, property, state } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\n\nimport { ContextMixin } from \"./ContextMixin.js\";\nimport { TWMixin } from \"./TWMixin.js\";\nimport { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport { findRootTemporal } from \"../elements/findRootTemporal.js\";\nimport { renderTimegroupToCanvas, type CanvasPreviewResult } from \"../preview/renderTimegroupToCanvas.js\";\nimport { renderTimegroupPreview } from \"../preview/renderTimegroupPreview.js\";\nimport { renderTimegroupToVideo, type RenderToVideoOptions, type RenderProgress, RenderCancelledError } from \"../preview/renderTimegroupToVideo.js\";\nimport { \n isNativeCanvasApiAvailable, \n getPreviewPresentationMode,\n setPreviewPresentationMode,\n type PreviewPresentationMode,\n getRenderMode,\n setRenderMode,\n type RenderMode,\n getPreviewResolutionScale,\n setPreviewResolutionScale,\n type PreviewResolutionScale,\n getShowStats,\n onPreviewSettingsChanged,\n} from \"../preview/previewSettings.js\";\nimport { setShowStats } from \"../preview/previewSettings.js\";\nimport {\n createStatsTrackingStrategy,\n type StatsTrackingStrategy,\n} from \"../preview/statsTrackingStrategy.js\";\nimport {\n getThumbnailCacheMaxSize,\n setThumbnailCacheMaxSize,\n onThumbnailCacheSettingsChanged,\n} from \"../preview/thumbnailCacheSettings.js\";\nimport { thumbnailImageCache } from \"../elements/EFThumbnailStrip.js\";\nimport type { ThumbnailCacheStats } from \"../elements/thumbnailCache.js\";\nimport { AdaptiveResolutionTracker } from \"../preview/AdaptiveResolutionTracker.js\";\nimport { provide } from \"@lit/context\";\nimport { previewSettingsContext, type PreviewSettings } from \"./previewSettingsContext.js\";\nimport { phosphorIcon, ICONS } from \"./icons.js\";\n\n// Side-effect import for template usage (pan-zoom is created in light DOM by wrapWithWorkbench)\nimport \"./EFFitScale.js\";\n\n/** Debounce delay before considering the preview \"at rest\" after motion stops */\nconst REST_DEBOUNCE_MS = 200;\n\n@customElement(\"ef-workbench\")\nexport class EFWorkbench extends ContextMixin(TWMixin(LitElement)) {\n static styles = [\n css`\n :host {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n \n /* Light mode colors */\n --workbench-bg: rgb(30 41 59); /* slate-800 */\n --workbench-overlay-border: rgb(59 130 246); /* blue-500 */\n --workbench-overlay-bg: rgb(191 219 254); /* blue-200 */\n --toolbar-bg: rgb(15 23 42); /* slate-900 */\n --toolbar-border: rgba(148, 163, 184, 0.2);\n }\n \n :host(.dark), :host-context(.dark) {\n /* Dark mode colors */\n --workbench-bg: rgb(2 6 23); /* slate-950 */\n --workbench-overlay-border: rgb(96 165 250); /* blue-400 */\n --workbench-overlay-bg: rgb(30 58 138); /* blue-900 */\n --toolbar-bg: rgb(2 6 23);\n --toolbar-border: rgba(148, 163, 184, 0.15);\n }\n \n .toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 8px 12px;\n background: var(--toolbar-bg);\n border-bottom: 1px solid var(--toolbar-border);\n flex-shrink: 0;\n font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;\n position: relative;\n z-index: 20;\n }\n \n .toolbar-left {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n \n .toolbar-right {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n \n .toolbar-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 6px 12px;\n background: rgba(51, 65, 85, 0.6);\n border: 1px solid rgba(148, 163, 184, 0.2);\n border-radius: 6px;\n color: #e2e8f0;\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n }\n \n .toolbar-btn:hover {\n background: rgba(51, 65, 85, 0.9);\n border-color: rgba(148, 163, 184, 0.3);\n }\n \n .toolbar-btn.active {\n background: rgba(59, 130, 246, 0.2);\n border-color: rgba(59, 130, 246, 0.4);\n color: #60a5fa;\n }\n \n .toolbar-btn.primary {\n background: linear-gradient(135deg, #3b82f6, #2563eb);\n border-color: transparent;\n color: white;\n font-weight: 600;\n }\n \n .toolbar-btn.primary:hover {\n background: linear-gradient(135deg, #60a5fa, #3b82f6);\n }\n \n .toolbar-icon-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n padding: 0;\n background: rgba(51, 65, 85, 0.6);\n border: 1px solid rgba(148, 163, 184, 0.2);\n border-radius: 6px;\n color: #e2e8f0;\n cursor: pointer;\n transition: all 0.15s ease;\n }\n \n .toolbar-icon-btn:hover {\n background: rgba(51, 65, 85, 0.9);\n border-color: rgba(148, 163, 184, 0.3);\n }\n \n .toolbar-icon-btn.active {\n background: rgba(59, 130, 246, 0.2);\n border-color: rgba(59, 130, 246, 0.4);\n color: #60a5fa;\n }\n \n .mode-indicator {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 8px;\n border-radius: 10px;\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n white-space: nowrap;\n }\n \n .mode-indicator.dom {\n background: rgba(34, 197, 94, 0.2);\n color: #4ade80;\n border: 1px solid rgba(34, 197, 94, 0.3);\n }\n \n .mode-indicator.canvas {\n background: rgba(168, 85, 247, 0.2);\n color: #c084fc;\n border: 1px solid rgba(168, 85, 247, 0.3);\n }\n \n .canvas-container {\n position: relative;\n overflow: hidden;\n flex: 1;\n display: grid;\n grid-template-columns: 100%;\n grid-template-rows: 100%;\n min-height: 0;\n }\n \n .canvas-container ::slotted(*) {\n width: 100%;\n height: 100%;\n grid-column: 1;\n grid-row: 1;\n }\n \n .clone-overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: 1;\n }\n \n .clone-content {\n position: absolute;\n transform-origin: 0 0;\n }\n \n .playback-stats {\n position: absolute;\n top: 8px;\n left: 8px;\n width: 200px;\n background: rgba(0, 0, 0, 0.75);\n backdrop-filter: blur(4px);\n border-radius: 6px;\n padding: 8px 12px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, monospace;\n font-size: 11px;\n color: #e2e8f0;\n z-index: 10;\n pointer-events: none;\n line-height: 1.5;\n }\n \n .playback-stats .stat-row {\n display: flex;\n justify-content: space-between;\n gap: 8px;\n }\n \n .playback-stats .stat-label {\n color: #94a3b8;\n flex-shrink: 0;\n width: 85px;\n }\n \n .playback-stats .stat-value {\n font-weight: 600;\n text-align: right;\n flex: 1;\n font-variant-numeric: tabular-nums;\n }\n \n .playback-stats .stat-value.good {\n color: #4ade80;\n }\n \n .playback-stats .stat-value.warning {\n color: #fbbf24;\n }\n \n .playback-stats .stat-value.bad {\n color: #f87171;\n }\n \n .pressure-histogram {\n display: flex;\n align-items: flex-end;\n gap: 1px;\n height: 24px;\n margin-top: 8px;\n padding-top: 8px;\n border-top: 1px solid rgba(148, 163, 184, 0.2);\n }\n \n .pressure-histogram .bar {\n flex: 1;\n min-width: 2px;\n max-width: 4px;\n border-radius: 1px 1px 0 0;\n transition: height 0.1s ease-out;\n }\n \n .pressure-histogram .bar.nominal {\n background: #4ade80;\n height: 25%;\n }\n \n .pressure-histogram .bar.fair {\n background: #a3e635;\n height: 50%;\n }\n \n .pressure-histogram .bar.serious {\n background: #fbbf24;\n height: 75%;\n }\n \n .pressure-histogram .bar.critical {\n background: #f87171;\n height: 100%;\n }\n \n .pressure-histogram-label {\n display: flex;\n justify-content: space-between;\n margin-top: 4px;\n font-size: 9px;\n color: #64748b;\n }\n \n .dropdown-panel {\n position: fixed;\n margin: 0;\n padding: 14px 16px;\n min-width: 260px;\n max-width: calc(100vw - 32px);\n background: linear-gradient(135deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.98));\n border: 1px solid rgba(148, 163, 184, 0.3);\n border-radius: 10px;\n backdrop-filter: blur(12px);\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n }\n \n .dropdown-panel::backdrop {\n background: transparent;\n }\n \n .dropdown-panel:popover-open {\n /* Animation for opening */\n animation: popover-fade-in 0.15s ease-out;\n }\n \n @keyframes popover-fade-in {\n from {\n opacity: 0;\n transform: translateY(-4px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n \n .dropdown-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 12px;\n padding-bottom: 10px;\n border-bottom: 1px solid rgba(148, 163, 184, 0.15);\n }\n \n .dropdown-title {\n color: #e2e8f0;\n font-size: 13px;\n font-weight: 600;\n }\n \n .dropdown-close {\n background: transparent;\n border: none;\n color: #64748b;\n cursor: pointer;\n padding: 2px;\n line-height: 1;\n font-size: 14px;\n transition: color 0.15s;\n }\n \n .dropdown-close:hover {\n color: #94a3b8;\n }\n `,\n ];\n\n @property({ type: Boolean })\n rendering = false;\n \n @state()\n private panZoomTransform = { x: 0, y: 0, scale: 1 };\n \n @state()\n private isExporting = false;\n \n @state()\n private exportProgress: RenderProgress | null = null;\n \n @state()\n private exportStatus: \"idle\" | \"rendering\" | \"complete\" | \"error\" | \"cancelled\" = \"idle\";\n \n \n @provide({ context: previewSettingsContext })\n @state()\n private previewSettings: PreviewSettings = {\n presentationMode: getPreviewPresentationMode(),\n renderMode: getRenderMode(),\n resolutionScale: getPreviewResolutionScale(),\n showStats: getShowStats(),\n thumbnailCacheMaxSize: getThumbnailCacheMaxSize(),\n };\n \n // Local state mirrors for direct access (context is primary source of truth)\n @state()\n private renderMode: RenderMode = this.previewSettings.renderMode;\n \n @state()\n private presentationMode: PreviewPresentationMode = this.previewSettings.presentationMode;\n \n @state()\n private previewResolutionScale: PreviewResolutionScale = this.previewSettings.resolutionScale;\n \n @state()\n private debugThumbnailTimestamps = false;\n \n @state()\n private thumbnailCacheMaxSize = this.previewSettings.thumbnailCacheMaxSize;\n \n @state()\n private thumbnailCacheStats: ThumbnailCacheStats | null = null;\n \n private cacheStatsUpdateInterval: number | null = null;\n \n @state()\n private exportOptions = {\n includeAudio: true,\n scale: 1,\n useInOut: false,\n inMs: 0,\n outMs: 0,\n };\n \n private exportAbortController: AbortController | null = null;\n \n // Motion state tracking for adaptive resolution\n @state()\n private isPlaying = false;\n \n @state()\n private isScrubbing = false;\n \n @state()\n private isAtRest = true;\n \n /**\n * Current adaptive resolution scale (only used when previewResolutionScale === \"auto\")\n */\n @state()\n private currentAdaptiveScale: number = 1;\n \n /**\n * Playback stats for display (FPS, dropped frames, etc.)\n * Mirrors previewSettings.showStats for direct access\n */\n @state()\n private showStats: boolean = this.previewSettings.showStats;\n \n private statsStrategy: StatsTrackingStrategy | null = null;\n \n /**\n * Reference for tracking scrubbing state from EFScrubber.\n * Pass this to <ef-scrubber isScrubbingRef={...}> to enable motion detection.\n */\n readonly isScrubbingRef = { current: false };\n \n private restDebounceTimer: number | null = null;\n private playingCheckInterval: number | null = null;\n private adaptiveTracker: AdaptiveResolutionTracker | null = null;\n private savePanZoomDebounceTimer: number | null = null;\n \n // Clone overlay (computed styles preview on top of hidden original)\n private cloneOverlayRef = createRef<HTMLDivElement>();\n private cloneRefresh: (() => void) | null = null;\n private cloneAnimationFrame: number | null = null;\n private cloneRootElement: HTMLElement | null = null;\n private cloneTimegroup: EFTimegroup | null = null;\n private structureObserver: MutationObserver | null = null;\n private rebuildPending = false;\n \n // Canvas renderer (kept for thumbnail generation, not displayed)\n private canvasRefresh: (() => Promise<void>) | null = null;\n \n // Canvas preview mode state\n private canvasPreviewRef = createRef<HTMLDivElement>();\n private canvasPreviewResult: CanvasPreviewResult | null = null;\n private canvasAnimationFrame: number | null = null;\n \n private boundHandleTransformChanged = this.handleTransformChanged.bind(this);\n\n focusOverlay = createRef<HTMLDivElement>();\n\n @eventOptions({ passive: false, capture: true })\n handleStageWheel(event: WheelEvent) {\n event.preventDefault();\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n // Listen for pan-zoom transform changes\n this.addEventListener(\"transform-changed\", this.boundHandleTransformChanged as EventListener);\n \n // Start motion state polling (checks playing state and scrubbing ref)\n this.startMotionStateTracking();\n \n // Initialize adaptive tracker\n // Scale changes directly update the canvas resolution - no expensive reinit needed\n this.adaptiveTracker = new AdaptiveResolutionTracker({\n onScaleChange: (scale) => {\n const oldScale = this.currentAdaptiveScale;\n this.currentAdaptiveScale = scale;\n \n // Directly update resolution if in auto mode, canvas mode, and in motion\n if (this.previewResolutionScale === \"auto\" && this.presentationMode === \"canvas\" && !this.isAtRest) {\n // Use the new dynamic setResolutionScale - instant, no DOM rebuild\n if (this.canvasPreviewResult) {\n this.canvasPreviewResult.setResolutionScale(scale);\n console.log(`[EFWorkbench] Resolution changed ${(oldScale * 100).toFixed(0)}% → ${(scale * 100).toFixed(0)}% (instant)`);\n }\n } else {\n console.log(`[EFWorkbench] Adaptive scale updated to ${(scale * 100).toFixed(0)}% (no change: atRest=${this.isAtRest}, mode=${this.presentationMode})`);\n }\n },\n });\n \n // Initialize thumbnail cache stats\n this.updateThumbnailCacheStats();\n this.startCacheStatsUpdates();\n \n // Listen for cache settings changes (keep for thumbnail cache updates)\n onThumbnailCacheSettingsChanged(() => {\n const newSize = getThumbnailCacheMaxSize();\n this.thumbnailCacheMaxSize = newSize;\n this.previewSettings = { ...this.previewSettings, thumbnailCacheMaxSize: newSize };\n thumbnailImageCache.setMaxSize(newSize);\n this.updateThumbnailCacheStats();\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n \n // Stop stats tracking\n if (this.statsStrategy) {\n this.statsStrategy.stop();\n this.statsStrategy = null;\n }\n \n // Clean up current mode\n if (this.presentationMode === \"clone\") {\n this.stopCloneOverlay();\n } else if (this.presentationMode === \"dom\") {\n this.stopDomMode();\n } else if (this.presentationMode === \"canvas\") {\n this.stopCanvasMode();\n }\n \n // Restore timegroup visibility\n const timegroup = this.getTimegroup();\n if (timegroup) {\n timegroup.style.clipPath = \"\";\n timegroup.style.pointerEvents = \"\";\n }\n \n this.removeEventListener(\"transform-changed\", this.boundHandleTransformChanged as EventListener);\n \n // Clean up motion state tracking\n this.stopMotionStateTracking();\n \n // Clean up cache stats updates\n this.stopCacheStatsUpdates();\n \n // Clean up adaptive tracker\n if (this.adaptiveTracker) {\n this.adaptiveTracker.dispose();\n this.adaptiveTracker = null;\n }\n \n // Save pan/zoom state before disconnecting\n if (this.savePanZoomDebounceTimer !== null) {\n clearTimeout(this.savePanZoomDebounceTimer);\n this.savePanZoomDebounceTimer = null;\n }\n this.savePreviewPanZoom();\n }\n \n protected firstUpdated(): void {\n // Restore preview pan/zoom from localStorage\n // Wait for timegroup to be available\n requestAnimationFrame(() => {\n this.restorePreviewPanZoom();\n });\n \n // Initialize based on current presentation mode\n if (this.presentationMode === \"clone\") {\n this.initCloneOverlay();\n } else if (this.presentationMode === \"dom\") {\n this.initDomMode();\n } else if (this.presentationMode === \"canvas\") {\n this.initCanvasMode();\n }\n }\n \n // Track zoom for detecting changes that need canvas reinit\n private lastCanvasZoom = 1;\n private zoomReinitTimeout: number | null = null;\n \n private handleTransformChanged(e: CustomEvent<{ x: number; y: number; scale: number }>) {\n this.panZoomTransform = e.detail;\n \n // Save pan/zoom state to localStorage\n this.debouncedSavePreviewPanZoom();\n \n // Update overlay transform based on current mode\n if (this.presentationMode === \"clone\") {\n this.updateCloneTransform();\n } else if (this.presentationMode === \"canvas\") {\n this.updateCanvasTransform();\n \n // Check if zoom changed enough to warrant re-rendering at new resolution\n // Only reinit if zoom changed by >25% from last init\n const zoomRatio = e.detail.scale / this.lastCanvasZoom;\n if (zoomRatio < 0.75 || zoomRatio > 1.33) {\n // Debounce to avoid thrashing during zoom gestures\n if (this.zoomReinitTimeout !== null) {\n clearTimeout(this.zoomReinitTimeout);\n }\n this.zoomReinitTimeout = window.setTimeout(() => {\n this.zoomReinitTimeout = null;\n if (this.presentationMode === \"canvas\") {\n this.lastCanvasZoom = this.panZoomTransform.scale;\n this.stopCanvasMode();\n this.initCanvasMode();\n }\n }, 500); // Wait 500ms after zoom stops\n }\n }\n }\n \n private getTimegroup(): EFTimegroup | null {\n // Find the timegroup in our canvas slot\n const canvas = this.querySelector(\"[slot='canvas']\");\n if (!canvas) return null;\n return canvas.querySelector(\"ef-timegroup\") as EFTimegroup | null;\n }\n\n /**\n * Get the root timegroup ID for localStorage key generation.\n * Returns null if no root timegroup is found or it has no ID.\n */\n private getRootTimegroupId(): string | null {\n const timegroup = this.getTimegroup();\n if (!timegroup) return null;\n \n const rootTemporal = findRootTemporal(timegroup);\n if (rootTemporal instanceof EFTimegroup && rootTemporal.id) {\n return rootTemporal.id;\n }\n \n return null;\n }\n\n /**\n * Get localStorage key for preview pan/zoom state.\n */\n private getPreviewPanZoomStorageKey(): string | null {\n const rootId = this.getRootTimegroupId();\n return rootId ? `ef-workbench-panzoom-${rootId}` : null;\n }\n\n /**\n * Save preview pan/zoom to localStorage.\n */\n private savePreviewPanZoom(): void {\n const storageKey = this.getPreviewPanZoomStorageKey();\n if (!storageKey) return;\n\n try {\n const state = {\n x: this.panZoomTransform.x,\n y: this.panZoomTransform.y,\n scale: this.panZoomTransform.scale,\n };\n localStorage.setItem(storageKey, JSON.stringify(state));\n } catch (error) {\n console.warn(\"Failed to save preview pan/zoom to localStorage\", error);\n }\n }\n\n /**\n * Restore preview pan/zoom from localStorage.\n */\n private restorePreviewPanZoom(): void {\n const storageKey = this.getPreviewPanZoomStorageKey();\n if (!storageKey) return;\n\n try {\n const stored = localStorage.getItem(storageKey);\n if (!stored) return;\n\n const state = JSON.parse(stored);\n if (\n typeof state.x === \"number\" &&\n typeof state.y === \"number\" &&\n typeof state.scale === \"number\" &&\n state.scale > 0\n ) {\n // Clamp scale to valid range [0.1, 5]\n const clampedScale = Math.max(0.1, Math.min(5, state.scale));\n this.panZoomTransform = {\n x: state.x,\n y: state.y,\n scale: clampedScale,\n };\n \n // Apply transform to pan-zoom element if it exists\n requestAnimationFrame(() => {\n const panZoomElement = this.querySelector(\"ef-pan-zoom\");\n if (panZoomElement) {\n (panZoomElement as any).x = this.panZoomTransform.x;\n (panZoomElement as any).y = this.panZoomTransform.y;\n (panZoomElement as any).scale = this.panZoomTransform.scale;\n }\n \n // Update transforms based on current mode\n if (this.presentationMode === \"clone\") {\n this.updateCloneTransform();\n } else if (this.presentationMode === \"canvas\") {\n this.updateCanvasTransform();\n }\n });\n }\n } catch (error) {\n console.warn(\"Failed to restore preview pan/zoom from localStorage\", error);\n }\n }\n\n /**\n * Debounced save of preview pan/zoom to avoid excessive localStorage writes.\n */\n private debouncedSavePreviewPanZoom(): void {\n if (this.savePanZoomDebounceTimer !== null) {\n clearTimeout(this.savePanZoomDebounceTimer);\n }\n this.savePanZoomDebounceTimer = window.setTimeout(() => {\n this.savePanZoomDebounceTimer = null;\n this.savePreviewPanZoom();\n }, 200);\n }\n \n // ==================== Motion State Detection ====================\n \n /**\n * Start polling for motion state (playing/scrubbing).\n * We use polling because:\n * - Playing state comes from timegroup's playbackController\n * - Scrubbing state comes from isScrubbingRef (set by EFScrubber)\n */\n private startMotionStateTracking(): void {\n if (this.playingCheckInterval !== null) return;\n \n this.playingCheckInterval = window.setInterval(() => {\n this.updateMotionState();\n }, 50); // Check every 50ms for responsive motion detection\n }\n \n private stopMotionStateTracking(): void {\n if (this.playingCheckInterval !== null) {\n clearInterval(this.playingCheckInterval);\n this.playingCheckInterval = null;\n }\n if (this.restDebounceTimer !== null) {\n clearTimeout(this.restDebounceTimer);\n this.restDebounceTimer = null;\n }\n }\n \n /**\n * Update motion state by checking timegroup and scrubbing ref.\n */\n private updateMotionState(): void {\n const timegroup = this.getTimegroup();\n const wasPlaying = this.isPlaying;\n const wasScrubbing = this.isScrubbing;\n \n // Check playing state from timegroup\n this.isPlaying = timegroup?.playing ?? false;\n \n // Check scrubbing state from ref\n this.isScrubbing = this.isScrubbingRef.current;\n \n const wasInMotion = wasPlaying || wasScrubbing;\n const isInMotion = this.isPlaying || this.isScrubbing;\n \n // Handle motion state transitions\n if (isInMotion && !wasInMotion) {\n // Started moving - immediately mark as not at rest\n this.handleMotionStart();\n } else if (!isInMotion && wasInMotion) {\n // Stopped moving - start debounce timer for rest\n this.handleMotionStop();\n }\n }\n \n /**\n * Called when motion starts (playing or scrubbing began).\n */\n private handleMotionStart(): void {\n // Cancel any pending rest transition\n if (this.restDebounceTimer !== null) {\n clearTimeout(this.restDebounceTimer);\n this.restDebounceTimer = null;\n }\n\n // Mark as in motion immediately\n this.isAtRest = false;\n\n // For auto mode, initialize the tracker at the current display scale\n // so it doesn't have to step down from 100% to reach it.\n if (this.previewResolutionScale === \"auto\" && this.adaptiveTracker) {\n const timegroup = this.getTimegroup();\n if (timegroup) {\n const compositionWidth = timegroup.offsetWidth || 1920;\n const compositionHeight = timegroup.offsetHeight || 1080;\n const rect = timegroup.getBoundingClientRect();\n const displayScale = Math.min(\n rect.width / compositionWidth,\n rect.height / compositionHeight\n );\n\n // Initialize tracker at display scale so it can immediately start\n // scaling down if there are performance issues\n this.adaptiveTracker.initializeAtScale(displayScale);\n this.currentAdaptiveScale = this.adaptiveTracker.getRecommendedScale();\n\n // Set canvas to the initial adaptive scale (instant - no rebuild)\n if (this.canvasPreviewResult) {\n this.canvasPreviewResult.setResolutionScale(this.currentAdaptiveScale);\n }\n\n console.log(`[EFWorkbench] Motion started, set resolution to ${(this.currentAdaptiveScale * 100).toFixed(0)}% (displayScale=${(displayScale * 100).toFixed(0)}%)`);\n }\n }\n\n console.log(`[EFWorkbench] Motion started (playing=${this.isPlaying}, scrubbing=${this.isScrubbing})`);\n }\n \n /**\n * Called when motion stops (not playing and not scrubbing).\n * Starts a debounce timer before transitioning to rest state.\n */\n private handleMotionStop(): void {\n // Start debounce timer\n if (this.restDebounceTimer !== null) {\n clearTimeout(this.restDebounceTimer);\n }\n \n this.restDebounceTimer = window.setTimeout(() => {\n this.restDebounceTimer = null;\n this.transitionToRest();\n }, REST_DEBOUNCE_MS);\n }\n \n /**\n * Called after debounce period when we're confirmed to be at rest.\n */\n private transitionToRest(): void {\n this.isAtRest = true;\n console.log(\"[EFWorkbench] Transitioned to rest state\");\n\n // If in auto mode, set full resolution (instant - no rebuild needed)\n if (this.previewResolutionScale === \"auto\" && this.presentationMode === \"canvas\") {\n // Reset tracker and set full resolution\n this.adaptiveTracker?.reset();\n this.currentAdaptiveScale = 1;\n \n // Use instant resolution change - no DOM rebuild\n if (this.canvasPreviewResult) {\n this.canvasPreviewResult.setResolutionScale(1);\n console.log(\"[EFWorkbench] Set full resolution for rest state (instant)\");\n }\n }\n }\n \n /**\n * Get the effective resolution scale based on current mode and motion state.\n * For \"auto\" mode, returns full resolution at rest, adaptive scale in motion.\n */\n private getEffectiveResolutionScale(timegroup: EFTimegroup, canvasContainer: HTMLElement): number {\n // For non-auto modes, use the existing logic\n if (this.previewResolutionScale !== \"auto\") {\n return this.getResolutionScale(timegroup, canvasContainer);\n }\n \n // Auto mode: full resolution at rest, adaptive in motion\n const compositionWidth = timegroup.offsetWidth || 1920;\n const compositionHeight = timegroup.offsetHeight || 1080;\n const rect = timegroup.getBoundingClientRect();\n const displayedWidth = rect.width;\n const displayedHeight = rect.height;\n const displayScale = Math.min(\n displayedWidth / compositionWidth,\n displayedHeight / compositionHeight\n );\n \n if (this.isAtRest) {\n // At rest: use display scale (full resolution for current display size)\n const scale = Math.max(0.1, Math.min(1, displayScale));\n console.log(`[EFWorkbench] Auto mode (at rest): using display scale ${(scale * 100).toFixed(1)}%`);\n return scale;\n } else {\n // In motion: use adaptive scale (may be reduced to prevent dropped frames)\n const adaptiveScale = this.currentAdaptiveScale;\n const targetScale = Math.min(displayScale, adaptiveScale);\n const scale = Math.max(0.1, Math.min(1, targetScale));\n console.log(`[EFWorkbench] Auto mode (in motion): adaptive=${adaptiveScale}, display=${displayScale.toFixed(2)}, final=${(scale * 100).toFixed(1)}%`);\n return scale;\n }\n }\n \n \n /**\n * Apply settings when dependencies are ready.\n * Called from updated() hook when settings change or dependencies become available.\n */\n private applySettings(): void {\n // Sync local state from context (for direct property access)\n this.presentationMode = this.previewSettings.presentationMode;\n this.renderMode = this.previewSettings.renderMode;\n this.previewResolutionScale = this.previewSettings.resolutionScale;\n this.showStats = this.previewSettings.showStats;\n this.thumbnailCacheMaxSize = this.previewSettings.thumbnailCacheMaxSize;\n \n // Apply stats strategy if enabled and dependencies are ready\n this.updateStatsStrategy();\n }\n \n /**\n * Update or create stats tracking strategy based on current mode and settings.\n */\n private updateStatsStrategy(): void {\n // Stop existing strategy\n if (this.statsStrategy) {\n this.statsStrategy.stop();\n this.statsStrategy = null;\n }\n \n // Only create strategy if stats are enabled\n if (!this.showStats) {\n return;\n }\n \n const timegroup = this.getTimegroup();\n if (!timegroup || !this.adaptiveTracker) {\n return;\n }\n \n const compositionWidth = timegroup.offsetWidth || 1920;\n const compositionHeight = timegroup.offsetHeight || 1080;\n \n // Create strategy based on current mode\n const strategy = createStatsTrackingStrategy(this.presentationMode, {\n timegroup,\n adaptiveTracker: this.adaptiveTracker,\n canvasPreviewResult: this.canvasPreviewResult ?? undefined,\n compositionWidth,\n compositionHeight,\n getResolutionScale: this.canvasPreviewResult?.getResolutionScale,\n isAtRest: () => this.isAtRest,\n isExporting: () => this.isExporting,\n });\n \n if (strategy) {\n this.statsStrategy = strategy;\n strategy.start();\n }\n }\n \n // ==================== End Motion State Detection ====================\n \n private initCloneOverlay() {\n // Don't initialize if we're no longer in clone mode\n if (this.presentationMode !== \"clone\") return;\n \n const timegroup = this.getTimegroup();\n const cloneContainer = this.cloneOverlayRef.value;\n \n // Wait for both timegroup and container to be available\n if (!timegroup || !cloneContainer) {\n // Retry after a short delay\n setTimeout(() => this.initCloneOverlay(), 100);\n return;\n }\n \n // Store reference to timegroup\n this.cloneTimegroup = timegroup;\n \n // Disable the timegroup's own proxy mode - workbench handles cloning\n (timegroup as any).proxyMode = false;\n \n // Ensure timegroup and its children have finished their initial render\n // before building the clone (custom elements need their shadow DOM ready)\n timegroup.updateComplete.then(() => {\n // Double-check we're still in clone mode\n if (this.presentationMode !== \"clone\") return;\n this.finishCloneSetup(timegroup, cloneContainer);\n });\n }\n \n private finishCloneSetup(timegroup: EFTimegroup, cloneContainer: HTMLDivElement) {\n \n // Hide the original timegroup but keep it rendering (critical for video frame decoding)\n // Using clip-path instead of opacity ensures video elements continue to decode frames\n timegroup.style.clipPath = \"inset(100%)\";\n timegroup.style.pointerEvents = \"none\";\n \n // Show the clone overlay container\n cloneContainer.style.display = \"block\";\n \n // Build initial clone\n this.rebuildClone(timegroup);\n \n // Watch for structural changes to rebuild clone\n this.setupStructureObserver(timegroup);\n }\n \n private rebuildClone(timegroup: EFTimegroup) {\n // Don't rebuild if we're not in clone mode\n if (this.presentationMode !== \"clone\") return;\n \n const container = this.cloneOverlayRef.value;\n if (!container) return;\n \n try {\n const { container: previewContainer, refresh } = renderTimegroupPreview(timegroup);\n \n container.innerHTML = \"\";\n previewContainer.classList.add(\"clone-content\");\n container.appendChild(previewContainer);\n this.cloneRefresh = refresh;\n \n // Store reference to the root clone element (first child of preview container)\n this.cloneRootElement = previewContainer.firstElementChild as HTMLElement ?? null;\n \n // Ensure the clone root is visible and properly positioned\n // (opacity and position values get copied from hidden original which are wrong for clone context)\n if (this.cloneRootElement) {\n this.cloneRootElement.style.opacity = \"1\";\n this.cloneRootElement.style.clipPath = \"none\";\n this.cloneRootElement.style.position = \"relative\";\n this.cloneRootElement.style.inset = \"auto\";\n this.cloneRootElement.style.top = \"0\";\n this.cloneRootElement.style.right = \"auto\";\n this.cloneRootElement.style.bottom = \"auto\";\n this.cloneRootElement.style.left = \"0\";\n }\n \n // Apply current transform\n this.updateCloneTransform();\n \n // Re-observe shadow roots (new ones may have been created)\n this.observeShadowRoots(timegroup);\n \n // Start the sync loop if not already running\n if (this.cloneAnimationFrame === null) {\n this.startCloneLoop();\n }\n } catch (e) {\n console.error(\"Failed to build clone:\", e);\n }\n }\n \n private setupStructureObserver(timegroup: EFTimegroup) {\n // Clean up existing observer\n if (this.structureObserver) {\n this.structureObserver.disconnect();\n }\n \n // Watch for structural changes (child additions/removals)\n // No special handling needed for batch operations - preview uses userTimeMs\n // which doesn't change during thumbnail/export captures\n this.structureObserver = new MutationObserver((mutations) => {\n // Don't process if we're no longer in clone mode\n if (this.presentationMode !== \"clone\") return;\n \n // Check if any mutation added/removed nodes\n const hasStructuralChange = mutations.some(m => \n m.type === \"childList\" && (m.addedNodes.length > 0 || m.removedNodes.length > 0)\n );\n \n if (hasStructuralChange && !this.rebuildPending) {\n this.rebuildPending = true;\n // Debounce rebuilds to batch rapid changes\n requestAnimationFrame(() => {\n this.rebuildPending = false;\n if (this.presentationMode === \"clone\") {\n this.rebuildClone(timegroup);\n }\n });\n }\n });\n \n // Observe the timegroup and all descendants for child changes\n this.structureObserver.observe(timegroup, {\n childList: true,\n subtree: true,\n });\n \n // Also observe shadow roots of custom elements\n this.observeShadowRoots(timegroup);\n }\n \n private observeShadowRoots(root: Element) {\n if (!this.structureObserver) return;\n \n // Walk the tree and observe any shadow roots\n const walker = document.createTreeWalker(\n root,\n NodeFilter.SHOW_ELEMENT,\n null\n );\n \n let node: Element | null = root;\n while (node) {\n if (node.shadowRoot) {\n this.structureObserver.observe(node.shadowRoot, {\n childList: true,\n subtree: true,\n });\n }\n node = walker.nextNode() as Element | null;\n }\n }\n \n private updateCloneTransform() {\n if (this.presentationMode !== \"clone\") return;\n \n const container = this.cloneOverlayRef.value;\n if (!container) return;\n \n const cloneContent = container.querySelector(\".clone-content\") as HTMLElement;\n if (!cloneContent) return;\n \n const { x, y, scale } = this.panZoomTransform;\n cloneContent.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;\n }\n \n private startCloneLoop() {\n const loop = () => {\n // Stop the loop if we're no longer in clone mode\n if (this.presentationMode !== \"clone\" || !this.cloneRefresh) {\n this.cloneAnimationFrame = null;\n return;\n }\n \n // Skip sync during export to avoid wasting CPU\n // cloneRefresh uses userTimeMs which doesn't change during batch captures\n if (!this.isExporting) {\n this.cloneRefresh();\n // Restore visibility and position on root clone \n // (syncing copies clip-path and position values from the hidden original,\n // which are relative to the workbench, not the clone container)\n if (this.cloneRootElement) {\n this.cloneRootElement.style.clipPath = \"none\";\n this.cloneRootElement.style.opacity = \"1\";\n // Reset position to fill the preview container\n this.cloneRootElement.style.position = \"relative\";\n this.cloneRootElement.style.inset = \"auto\";\n this.cloneRootElement.style.top = \"0\";\n this.cloneRootElement.style.right = \"auto\";\n this.cloneRootElement.style.bottom = \"auto\";\n this.cloneRootElement.style.left = \"0\";\n }\n }\n this.cloneAnimationFrame = requestAnimationFrame(loop);\n };\n this.cloneAnimationFrame = requestAnimationFrame(loop);\n }\n \n private stopCloneOverlay() {\n if (this.cloneAnimationFrame !== null) {\n cancelAnimationFrame(this.cloneAnimationFrame);\n this.cloneAnimationFrame = null;\n }\n if (this.structureObserver) {\n this.structureObserver.disconnect();\n this.structureObserver = null;\n }\n this.cloneRefresh = null;\n this.cloneRootElement = null;\n this.cloneTimegroup = null;\n this.rebuildPending = false;\n \n // Clear and hide the clone overlay container\n const container = this.cloneOverlayRef.value;\n if (container) {\n container.innerHTML = \"\";\n container.style.display = \"none\";\n }\n }\n \n private async handlePresentationModeChange(mode: PreviewPresentationMode) {\n if (mode === this.presentationMode) return;\n \n const previousMode = this.presentationMode;\n \n // Stop previous mode (this will stop stats strategy)\n if (previousMode === \"clone\") {\n this.stopCloneOverlay();\n } else if (previousMode === \"dom\") {\n this.stopDomMode();\n } else if (previousMode === \"canvas\") {\n this.stopCanvasMode();\n }\n \n // Update context and persist\n setPreviewPresentationMode(mode);\n this.previewSettings = { ...this.previewSettings, presentationMode: mode };\n \n // Wait for Lit to re-render (removes old overlay, adds new one if needed)\n await this.updateComplete;\n \n // Start new mode after DOM is updated\n if (mode === \"clone\") {\n this.initCloneOverlay();\n } else if (mode === \"dom\") {\n this.initDomMode();\n } else if (mode === \"canvas\") {\n this.initCanvasMode();\n }\n // Stats strategy will be created by applySettings() when dependencies are ready\n }\n \n private initDomMode() {\n // Don't initialize if we're no longer in dom mode\n if (this.presentationMode !== \"dom\") return;\n \n const timegroup = this.getTimegroup();\n if (!timegroup) {\n setTimeout(() => this.initDomMode(), 100);\n return;\n }\n \n // Pause the ef-fit-scale to prevent it from applying transforms\n const fitScale = this.querySelector(\"[slot='canvas']\") as any;\n if (fitScale?.removeScale && fitScale?.paused !== undefined) {\n fitScale.paused = true;\n fitScale.removeScale();\n }\n \n // Disable the timegroup's own proxy mode (it may have been enabled by thumbnail strip)\n (timegroup as any).proxyMode = false;\n \n // Show the original timegroup directly\n timegroup.style.clipPath = \"\";\n timegroup.style.pointerEvents = \"\";\n \n // Settings will be applied via applySettings() when dependencies are ready\n // This happens automatically in updated() hook when timegroup becomes available\n }\n \n private stopDomMode() {\n const timegroup = this.getTimegroup();\n if (timegroup) {\n // Hide the original again\n timegroup.style.clipPath = \"inset(100%)\";\n timegroup.style.pointerEvents = \"none\";\n }\n \n // Resume the ef-fit-scale\n const fitScale = this.querySelector(\"[slot='canvas']\") as any;\n if (fitScale?.paused !== undefined) {\n fitScale.paused = false;\n }\n \n // Stop stats tracking\n if (this.statsStrategy) {\n this.statsStrategy.stop();\n this.statsStrategy = null;\n }\n }\n \n /**\n * Get the resolution scale for canvas rendering (for fixed scale modes).\n * \n * Logic:\n * - Get actual displayed size from getBoundingClientRect()\n * - For \"Full\": render at displayed size (1:1 pixel mapping)\n * - For other settings: render at that % of displayed size\n * - Never exceed composition size (100%)\n * \n * Note: For \"auto\" mode, use getEffectiveResolutionScale() instead.\n */\n private getResolutionScale(\n timegroup: EFTimegroup,\n _canvasContainer: HTMLElement\n ): number {\n // For \"auto\" mode, delegate to getEffectiveResolutionScale\n if (this.previewResolutionScale === \"auto\") {\n return this.getEffectiveResolutionScale(timegroup, _canvasContainer);\n }\n \n // Composition size = the native resolution (offsetWidth/Height gives CSS layout size)\n const compositionWidth = timegroup.offsetWidth || 1920;\n const compositionHeight = timegroup.offsetHeight || 1080;\n \n // Displayed size = actual screen pixels after all transforms\n const rect = timegroup.getBoundingClientRect();\n const displayedWidth = rect.width;\n const displayedHeight = rect.height;\n \n // Display scale = displayed / composition (how much the composition is scaled down for display)\n const displayScale = Math.min(\n displayedWidth / compositionWidth,\n displayedHeight / compositionHeight\n );\n \n // For \"Full\", we want to render at displayed size (displayScale of composition)\n // For other settings, we want min(displayScale, setting) of composition\n // But we should never exceed 100% of composition\n const targetScale = this.previewResolutionScale === 1 \n ? displayScale // Full = match display\n : Math.min(displayScale, this.previewResolutionScale); // Others = min of display and setting\n \n // Clamp to reasonable bounds [10%, 100%]\n const finalScale = Math.max(0.1, Math.min(1, targetScale));\n \n const renderWidth = Math.floor(compositionWidth * finalScale);\n const renderHeight = Math.floor(compositionHeight * finalScale);\n \n console.log(`[EFWorkbench] Resolution scale:\n Composition (offsetWidth×offsetHeight): ${compositionWidth}×${compositionHeight}\n Displayed (boundingRect): ${Math.round(displayedWidth)}×${Math.round(displayedHeight)}\n Display scale: ${(displayScale * 100).toFixed(1)}%\n Setting: ${this.previewResolutionScale === 1 ? \"Full\" : `${Math.round((this.previewResolutionScale as number) * 100)}%`}\n Final: ${(finalScale * 100).toFixed(1)}% → ${renderWidth}×${renderHeight}`);\n \n return finalScale;\n }\n \n private initCanvasMode() {\n // Don't initialize if we're no longer in canvas mode\n if (this.presentationMode !== \"canvas\") return;\n \n const timegroup = this.getTimegroup();\n const canvasContainer = this.canvasPreviewRef.value;\n \n // Wait for both timegroup and container to be available\n if (!timegroup || !canvasContainer) {\n setTimeout(() => this.initCanvasMode(), 100);\n return;\n }\n \n // Don't wait for timegroup initialization here - it can cause deadlocks when\n // localStorage restoration triggers seeks that wait for waitForMediaDurations().\n // The canvas refresh loop already handles this by checking if timegroup is ready:\n // - refresh() checks if sourceTimeMs and userTimeMs are synchronized\n // - If they're not synchronized (seek in progress), refresh() returns early\n // - Once the seek completes and times are synchronized, refresh() will render\n // This avoids blocking the main thread while still ensuring correct rendering.\n \n // Disable the timegroup's own proxy mode - workbench handles canvas rendering\n (timegroup as any).proxyMode = false;\n \n // Hide the original timegroup\n timegroup.style.clipPath = \"inset(100%)\";\n timegroup.style.pointerEvents = \"none\";\n \n // Show the canvas container\n canvasContainer.style.display = \"block\";\n \n // Get initial resolution scale based on display size, user setting, and motion state\n const initialResolutionScale = this.previewResolutionScale === \"auto\"\n ? this.getEffectiveResolutionScale(timegroup, canvasContainer)\n : this.getResolutionScale(timegroup, canvasContainer);\n \n // Track zoom level for detecting significant changes\n this.lastCanvasZoom = this.panZoomTransform.scale;\n \n // Store composition dimensions for stats calculation\n const compositionWidth = timegroup.offsetWidth || 1920;\n const compositionHeight = timegroup.offsetHeight || 1080;\n \n try {\n // Create canvas preview - this builds the clone structure ONCE\n const result = renderTimegroupToCanvas(timegroup, {\n scale: 1,\n resolutionScale: initialResolutionScale,\n });\n \n // Store the full result for dynamic resolution changes\n this.canvasPreviewResult = result;\n \n const { container, canvas, refresh, getResolutionScale } = result;\n \n canvas.classList.add(\"clone-content\");\n \n canvasContainer.innerHTML = \"\";\n canvasContainer.appendChild(container);\n \n // Apply current transform\n this.updateCanvasTransform();\n \n // Start the canvas render loop\n const loop = async () => {\n if (this.presentationMode !== \"canvas\") return;\n \n // Skip refresh during export to avoid wasting CPU\n if (!this.isExporting) {\n try {\n await refresh();\n this.updateCanvasTransform();\n } catch (e) {\n console.error(\"Canvas refresh failed:\", e);\n }\n }\n \n this.canvasAnimationFrame = requestAnimationFrame(loop);\n };\n this.canvasAnimationFrame = requestAnimationFrame(loop);\n \n // Settings will be applied via applySettings() when dependencies are ready\n // This happens automatically in updated() hook when timegroup becomes available\n } catch (e) {\n console.error(\"Failed to init canvas mode:\", e);\n }\n }\n \n private stopCanvasMode() {\n if (this.canvasAnimationFrame !== null) {\n cancelAnimationFrame(this.canvasAnimationFrame);\n this.canvasAnimationFrame = null;\n }\n if (this.zoomReinitTimeout !== null) {\n clearTimeout(this.zoomReinitTimeout);\n this.zoomReinitTimeout = null;\n }\n this.canvasPreviewResult = null;\n \n // Stop stats tracking\n if (this.statsStrategy) {\n this.statsStrategy.stop();\n this.statsStrategy = null;\n }\n \n // Clear and hide the canvas container\n const container = this.canvasPreviewRef.value;\n if (container) {\n container.innerHTML = \"\";\n container.style.display = \"none\";\n }\n }\n \n private updateCanvasTransform() {\n if (this.presentationMode !== \"canvas\") return;\n \n const container = this.canvasPreviewRef.value;\n if (!container) return;\n \n const canvas = container.querySelector(\"canvas\") as HTMLElement;\n if (!canvas) return;\n \n const { x, y, scale } = this.panZoomTransform;\n canvas.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;\n }\n \n // Canvas renderer - kept for thumbnail generation\n initCanvasRenderer(): { canvas: HTMLCanvasElement; refresh: () => Promise<void> } | null {\n const timegroup = this.getTimegroup();\n if (!timegroup) return null;\n \n try {\n const { canvas, refresh } = renderTimegroupToCanvas(timegroup, 1);\n this.canvasRefresh = refresh;\n return { canvas, refresh };\n } catch (e) {\n console.error(\"Failed to init canvas renderer:\", e);\n return null;\n }\n }\n \n /** Start video export with progress tracking */\n async startExport(options: RenderToVideoOptions = {}): Promise<void> {\n const timegroup = this.getTimegroup();\n if (!timegroup) {\n console.error(\"No timegroup found for export\");\n return;\n }\n \n if (this.isExporting) {\n console.warn(\"Export already in progress\");\n return;\n }\n \n this.exportAbortController = new AbortController();\n this.isExporting = true;\n this.exportProgress = null;\n this.exportStatus = \"rendering\";\n \n try {\n await renderTimegroupToVideo(timegroup, {\n ...options,\n signal: this.exportAbortController.signal,\n onProgress: (progress) => {\n this.exportProgress = progress;\n },\n });\n \n this.exportStatus = \"complete\";\n setTimeout(() => {\n this.isExporting = false;\n this.exportProgress = null;\n this.exportStatus = \"idle\";\n this.exportAbortController = null;\n }, 2000);\n } catch (e) {\n if (e instanceof RenderCancelledError) {\n console.log(\"Export cancelled by user\");\n this.exportStatus = \"cancelled\";\n setTimeout(() => {\n this.isExporting = false;\n this.exportProgress = null;\n this.exportStatus = \"idle\";\n this.exportAbortController = null;\n }, 1500);\n } else {\n console.error(\"Export failed:\", e);\n this.exportStatus = \"error\";\n setTimeout(() => {\n this.isExporting = false;\n this.exportProgress = null;\n this.exportStatus = \"idle\";\n this.exportAbortController = null;\n }, 3000);\n }\n }\n }\n \n /** Cancel the current export */\n cancelExport(): void {\n if (this.exportAbortController) {\n this.exportAbortController.abort();\n }\n }\n \n private positionPopover(popover: HTMLElement, anchorId: string) {\n const anchor = this.shadowRoot?.getElementById(anchorId);\n if (!anchor) return;\n \n const anchorRect = anchor.getBoundingClientRect();\n const popoverRect = popover.getBoundingClientRect();\n const padding = 8;\n \n // Position below the anchor\n let top = anchorRect.bottom + padding;\n // Align right edge of popover with right edge of anchor\n let left = anchorRect.right - popoverRect.width;\n \n // Keep within viewport bounds\n if (left < padding) {\n left = padding;\n }\n if (left + popoverRect.width > window.innerWidth - padding) {\n left = window.innerWidth - popoverRect.width - padding;\n }\n if (top + popoverRect.height > window.innerHeight - padding) {\n // Flip to above if doesn't fit below\n top = anchorRect.top - popoverRect.height - padding;\n }\n \n popover.style.top = `${top}px`;\n popover.style.left = `${left}px`;\n }\n \n private handleSettingsPopoverToggle(e: Event) {\n const popover = e.target as HTMLElement;\n if ((e as ToggleEvent).newState === 'open') {\n // Position after the popover is shown so we can measure it\n requestAnimationFrame(() => {\n this.positionPopover(popover, 'settings-btn');\n });\n }\n }\n \n private handleExportPopoverToggle(e: Event) {\n const popover = e.target as HTMLElement;\n if ((e as ToggleEvent).newState === 'open') {\n // Initialize export options when popover opens\n if (this.exportOptions.outMs === 0) {\n const timegroup = this.getTimegroup();\n if (timegroup) {\n this.exportOptions = {\n ...this.exportOptions,\n outMs: timegroup.durationMs,\n };\n }\n }\n // Position after the popover is shown\n requestAnimationFrame(() => {\n this.positionPopover(popover, 'export-btn');\n });\n }\n }\n \n private handleStartExport() {\n this.startExport({\n includeAudio: this.exportOptions.includeAudio,\n scale: this.exportOptions.scale,\n fromMs: this.exportOptions.useInOut ? this.exportOptions.inMs : undefined,\n toMs: this.exportOptions.useInOut ? this.exportOptions.outMs : undefined,\n });\n }\n \n private updateExportOption<K extends keyof typeof this.exportOptions>(\n key: K,\n value: typeof this.exportOptions[K]\n ) {\n this.exportOptions = { ...this.exportOptions, [key]: value };\n }\n \n private formatTime(ms: number): string {\n const totalSeconds = Math.floor(ms / 1000);\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n if (minutes > 0) {\n return `${minutes}:${seconds.toString().padStart(2, \"0\")}`;\n }\n return `${seconds}s`;\n }\n \n private handleCancelClick() {\n this.cancelExport();\n }\n \n private handleRenderModeChange(mode: RenderMode) {\n setRenderMode(mode);\n this.previewSettings = { ...this.previewSettings, renderMode: mode };\n }\n \n private handleResolutionScaleChange(scale: PreviewResolutionScale) {\n console.log(`[EFWorkbench] Resolution scale changed to ${scale}, presentationMode=${this.presentationMode}`);\n setPreviewResolutionScale(scale);\n this.previewSettings = { ...this.previewSettings, resolutionScale: scale };\n \n // Reset adaptive tracker when switching to auto mode\n if (scale === \"auto\") {\n this.adaptiveTracker?.reset();\n this.currentAdaptiveScale = 1;\n }\n \n // Reinitialize canvas mode if active to apply new resolution\n // Note: presentationMode at runtime may be \"clone\" or \"dom\" (not in TS type but used in UI)\n if (this.presentationMode === \"canvas\") {\n console.log(\"[EFWorkbench] Reinitializing canvas mode with new resolution scale\");\n this.stopCanvasMode();\n this.initCanvasMode();\n }\n }\n \n private async updateThumbnailCacheStats() {\n try {\n this.thumbnailCacheStats = await thumbnailImageCache.getStats();\n } catch (error) {\n console.warn(\"Failed to update thumbnail cache stats:\", error);\n }\n }\n \n private startCacheStatsUpdates() {\n // Update stats every 2 seconds\n this.cacheStatsUpdateInterval = window.setInterval(() => {\n this.updateThumbnailCacheStats();\n }, 2000);\n }\n \n private stopCacheStatsUpdates() {\n if (this.cacheStatsUpdateInterval !== null) {\n clearInterval(this.cacheStatsUpdateInterval);\n this.cacheStatsUpdateInterval = null;\n }\n }\n \n private handleThumbnailCacheMaxSizeChange(size: number) {\n setThumbnailCacheMaxSize(size);\n this.previewSettings = { ...this.previewSettings, thumbnailCacheMaxSize: size };\n thumbnailImageCache.setMaxSize(size);\n this.updateThumbnailCacheStats();\n }\n \n private async handleClearThumbnailCache() {\n await thumbnailImageCache.clear();\n await this.updateThumbnailCacheStats();\n }\n \n private formatBytes(bytes: number): string {\n if (bytes < 1024) {\n return `${bytes} B`;\n } else if (bytes < 1024 * 1024) {\n return `${(bytes / 1024).toFixed(1)} KB`;\n } else {\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n }\n }\n \n private handleDebugThumbnailTimestampsToggle(enabled: boolean) {\n this.debugThumbnailTimestamps = enabled;\n // Dispatch event so thumbnail strips can react\n this.dispatchEvent(new CustomEvent(\"ef-debug-thumbnail-timestamps-changed\", {\n detail: { enabled },\n bubbles: true,\n composed: true,\n }));\n }\n \n private handleShowStatsToggle(enabled: boolean) {\n setShowStats(enabled);\n this.previewSettings = { ...this.previewSettings, showStats: enabled };\n // applySettings() will be called automatically via updated() hook when context changes\n }\n \n /**\n * Reset and fit the preview to show all content centered.\n * Finds the pan-zoom element and calls fitToContent() on it.\n */\n private handleFitToContent(): void {\n const panZoomElement = this.querySelector(\"ef-pan-zoom\") as any;\n if (panZoomElement && typeof panZoomElement.fitToContent === \"function\") {\n panZoomElement.fitToContent();\n }\n }\n \n private renderSettingsPopover() {\n const isAvailable = isNativeCanvasApiAvailable();\n \n return html`\n <div \n id=\"settings-popover\" \n popover=\"auto\"\n class=\"dropdown-panel\"\n @toggle=${this.handleSettingsPopoverToggle}\n >\n <div class=\"dropdown-header\">\n <span class=\"dropdown-title\">Preview Settings</span>\n <button class=\"dropdown-close\" popovertarget=\"settings-popover\" popovertargetaction=\"hide\">✕</button>\n </div>\n \n <!-- Presentation Mode Setting -->\n <div style=\"\n background: rgba(51, 65, 85, 0.4);\n border-radius: 8px;\n padding: 12px;\n margin-bottom: 10px;\n \">\n <div style=\"color: #e2e8f0; font-size: 12px; font-weight: 500; margin-bottom: 10px;\">Presentation Mode</div>\n \n <div style=\"display: flex; gap: 4px; background: rgba(30, 41, 59, 0.6); border-radius: 6px; padding: 3px;\">\n <button\n @click=${() => this.handlePresentationModeChange(\"clone\")}\n style=\"\n flex: 1;\n padding: 6px 10px;\n border: none;\n border-radius: 4px;\n font-size: 11px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n background: ${this.presentationMode === \"clone\" ? \"rgba(59, 130, 246, 0.3)\" : \"transparent\"};\n color: ${this.presentationMode === \"clone\" ? \"#60a5fa\" : \"#94a3b8\"};\n border: 1px solid ${this.presentationMode === \"clone\" ? \"rgba(59, 130, 246, 0.4)\" : \"transparent\"};\n \"\n >Clone</button>\n <button\n @click=${() => this.handlePresentationModeChange(\"dom\")}\n style=\"\n flex: 1;\n padding: 6px 10px;\n border: none;\n border-radius: 4px;\n font-size: 11px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n background: ${this.presentationMode === \"dom\" ? \"rgba(34, 197, 94, 0.3)\" : \"transparent\"};\n color: ${this.presentationMode === \"dom\" ? \"#4ade80\" : \"#94a3b8\"};\n border: 1px solid ${this.presentationMode === \"dom\" ? \"rgba(34, 197, 94, 0.4)\" : \"transparent\"};\n \"\n >DOM</button>\n <button\n @click=${() => this.handlePresentationModeChange(\"canvas\")}\n style=\"\n flex: 1;\n padding: 6px 10px;\n border: none;\n border-radius: 4px;\n font-size: 11px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n background: ${this.presentationMode === \"canvas\" ? \"rgba(168, 85, 247, 0.3)\" : \"transparent\"};\n color: ${this.presentationMode === \"canvas\" ? \"#c084fc\" : \"#94a3b8\"};\n border: 1px solid ${this.presentationMode === \"canvas\" ? \"rgba(168, 85, 247, 0.4)\" : \"transparent\"};\n \"\n >Canvas</button>\n </div>\n \n <div style=\"margin-top: 8px; color: #64748b; font-size: 10px; line-height: 1.4;\">\n ${this.presentationMode === \"clone\" \n ? \"Default. Shows a styled clone synced from the hidden original.\" \n : this.presentationMode === \"dom\" \n ? \"Shows the real timegroup DOM directly.\" \n : \"Renders to canvas each frame (experimental).\"}\n </div>\n </div>\n \n <!-- Render Mode Setting -->\n <div style=\"\n background: rgba(51, 65, 85, 0.4);\n border-radius: 8px;\n padding: 12px;\n \">\n <div style=\"display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;\">\n <span style=\"color: #e2e8f0; font-size: 12px; font-weight: 500;\">Render Mode</span>\n ${isAvailable ? html`\n <div style=\"display: flex; align-items: center; gap: 5px;\">\n <span style=\"\n display: inline-block;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: #4ade80;\n \"></span>\n <span style=\"color: #4ade80; font-size: 10px; font-weight: 500;\">\n Native Available\n </span>\n </div>\n ` : ''}\n </div>\n \n <div style=\"display: flex; gap: 4px; background: rgba(30, 41, 59, 0.6); border-radius: 6px; padding: 3px;\">\n <button\n @click=${() => this.handleRenderModeChange(\"foreignObject\")}\n style=\"\n flex: 1;\n padding: 6px 8px;\n border: none;\n border-radius: 4px;\n font-size: 10px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n background: ${this.renderMode === \"foreignObject\" ? \"rgba(59, 130, 246, 0.3)\" : \"transparent\"};\n color: ${this.renderMode === \"foreignObject\" ? \"#60a5fa\" : \"#94a3b8\"};\n border: 1px solid ${this.renderMode === \"foreignObject\" ? \"rgba(59, 130, 246, 0.4)\" : \"transparent\"};\n \"\n >foreignObject</button>\n <button\n @click=${() => this.handleRenderModeChange(\"native\")}\n ?disabled=${!isAvailable}\n style=\"\n flex: 1;\n padding: 6px 8px;\n border: none;\n border-radius: 4px;\n font-size: 10px;\n font-weight: 500;\n cursor: ${isAvailable ? 'pointer' : 'not-allowed'};\n transition: all 0.15s ease;\n background: ${this.renderMode === \"native\" ? \"rgba(34, 197, 94, 0.3)\" : \"transparent\"};\n color: ${this.renderMode === \"native\" ? \"#4ade80\" : isAvailable ? \"#94a3b8\" : \"#64748b\"};\n border: 1px solid ${this.renderMode === \"native\" ? \"rgba(34, 197, 94, 0.4)\" : \"transparent\"};\n opacity: ${isAvailable ? '1' : '0.5'};\n \"\n >native</button>\n </div>\n \n <div style=\"margin-top: 8px; color: #64748b; font-size: 10px; line-height: 1.4;\">\n ${this.renderMode === \"foreignObject\" \n ? \"SVG foreignObject serialization. Works everywhere but slower.\" \n : \"Chrome's drawElementImage API. Fastest, requires chrome://flags/#canvas-draw-element.\"}\n </div>\n </div>\n \n <!-- Preview Resolution Setting -->\n <div style=\"\n background: rgba(51, 65, 85, 0.4);\n border-radius: 8px;\n padding: 12px;\n margin-top: 10px;\n \">\n <div style=\"color: #e2e8f0; font-size: 12px; font-weight: 500; margin-bottom: 10px;\">Preview Resolution</div>\n \n <div style=\"display: flex; gap: 4px; background: rgba(30, 41, 59, 0.6); border-radius: 6px; padding: 3px;\">\n <button\n @click=${() => this.handleResolutionScaleChange(\"auto\")}\n style=\"\n flex: 1;\n padding: 6px 8px;\n border: none;\n border-radius: 4px;\n font-size: 10px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n background: ${this.previewResolutionScale === \"auto\" ? \"rgba(34, 197, 94, 0.3)\" : \"transparent\"};\n color: ${this.previewResolutionScale === \"auto\" ? \"#4ade80\" : \"#94a3b8\"};\n border: 1px solid ${this.previewResolutionScale === \"auto\" ? \"rgba(34, 197, 94, 0.4)\" : \"transparent\"};\n \"\n >Auto</button>\n <button\n @click=${() => this.handleResolutionScaleChange(1)}\n style=\"\n flex: 1;\n padding: 6px 8px;\n border: none;\n border-radius: 4px;\n font-size: 10px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n background: ${this.previewResolutionScale === 1 ? \"rgba(59, 130, 246, 0.3)\" : \"transparent\"};\n color: ${this.previewResolutionScale === 1 ? \"#60a5fa\" : \"#94a3b8\"};\n border: 1px solid ${this.previewResolutionScale === 1 ? \"rgba(59, 130, 246, 0.4)\" : \"transparent\"};\n \"\n >Full</button>\n <button\n @click=${() => this.handleResolutionScaleChange(0.75)}\n style=\"\n flex: 1;\n padding: 6px 8px;\n border: none;\n border-radius: 4px;\n font-size: 10px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n background: ${this.previewResolutionScale === 0.75 ? \"rgba(59, 130, 246, 0.3)\" : \"transparent\"};\n color: ${this.previewResolutionScale === 0.75 ? \"#60a5fa\" : \"#94a3b8\"};\n border: 1px solid ${this.previewResolutionScale === 0.75 ? \"rgba(59, 130, 246, 0.4)\" : \"transparent\"};\n \"\n >3/4</button>\n <button\n @click=${() => this.handleResolutionScaleChange(0.5)}\n style=\"\n flex: 1;\n padding: 6px 8px;\n border: none;\n border-radius: 4px;\n font-size: 10px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n background: ${this.previewResolutionScale === 0.5 ? \"rgba(59, 130, 246, 0.3)\" : \"transparent\"};\n color: ${this.previewResolutionScale === 0.5 ? \"#60a5fa\" : \"#94a3b8\"};\n border: 1px solid ${this.previewResolutionScale === 0.5 ? \"rgba(59, 130, 246, 0.4)\" : \"transparent\"};\n \"\n >1/2</button>\n <button\n @click=${() => this.handleResolutionScaleChange(0.25)}\n style=\"\n flex: 1;\n padding: 6px 8px;\n border: none;\n border-radius: 4px;\n font-size: 10px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n background: ${this.previewResolutionScale === 0.25 ? \"rgba(59, 130, 246, 0.3)\" : \"transparent\"};\n color: ${this.previewResolutionScale === 0.25 ? \"#60a5fa\" : \"#94a3b8\"};\n border: 1px solid ${this.previewResolutionScale === 0.25 ? \"rgba(59, 130, 246, 0.4)\" : \"transparent\"};\n \"\n >1/4</button>\n </div>\n \n <div style=\"margin-top: 8px; color: #64748b; font-size: 10px; line-height: 1.4;\">\n ${this.previewResolutionScale === \"auto\" \n ? `Auto: Full resolution at rest, adaptive during playback/scrub.${!this.isAtRest ? ` Currently: ${Math.round(this.currentAdaptiveScale * 100)}%` : \"\"}`\n : this.previewResolutionScale === 1 \n ? \"Full: Matches display resolution (1:1 pixels, adapts to zoom).\" \n : `${Math.round((this.previewResolutionScale as number) * 100)}%: Reduced quality for faster rendering.`}\n Canvas mode only.\n </div>\n </div>\n \n <!-- Show Performance Stats Setting -->\n <div style=\"\n background: rgba(51, 65, 85, 0.4);\n border-radius: 8px;\n padding: 12px;\n margin-top: 10px;\n \">\n <label style=\"\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n \">\n <input\n type=\"checkbox\"\n ?checked=${this.showStats}\n @change=${(e: Event) => this.handleShowStatsToggle((e.target as HTMLInputElement).checked)}\n style=\"\n width: 14px;\n height: 14px;\n accent-color: #3b82f6;\n cursor: pointer;\n \"\n />\n <span style=\"color: #e2e8f0; font-size: 12px; font-weight: 500;\">Show Performance Stats</span>\n </label>\n \n <div style=\"\n margin-top: 8px;\n color: #64748b;\n font-size: 10px;\n line-height: 1.4;\n \">\n Display FPS, CPU pressure, and performance metrics overlay.\n </div>\n </div>\n \n <!-- Thumbnail Cache Setting -->\n <div \n data-testid=\"thumbnail-cache-section\"\n style=\"\n background: rgba(51, 65, 85, 0.4);\n border-radius: 8px;\n padding: 12px;\n margin-top: 10px;\n \"\n >\n <div style=\"color: #e2e8f0; font-size: 12px; font-weight: 500; margin-bottom: 10px;\">Thumbnail Cache</div>\n \n <!-- Cache Size Input -->\n <div style=\"display: flex; align-items: center; gap: 8px; margin-bottom: 10px;\">\n <label style=\"color: #94a3b8; font-size: 11px; min-width: 80px;\">Max Size:</label>\n <input\n data-testid=\"thumbnail-cache-size\"\n type=\"number\"\n min=\"100\"\n max=\"5000\"\n step=\"100\"\n .value=${String(this.thumbnailCacheMaxSize)}\n @change=${(e: Event) => {\n const value = parseInt((e.target as HTMLInputElement).value, 10);\n if (!isNaN(value) && value >= 100 && value <= 5000) {\n this.handleThumbnailCacheMaxSizeChange(value);\n }\n }}\n style=\"\n flex: 1;\n padding: 4px 8px;\n background: rgba(30, 41, 59, 0.6);\n border: 1px solid rgba(148, 163, 184, 0.2);\n border-radius: 4px;\n color: #e2e8f0;\n font-size: 11px;\n \"\n />\n <span style=\"color: #64748b; font-size: 10px;\">items</span>\n </div>\n \n <!-- Cache Statistics -->\n ${this.thumbnailCacheStats ? html`\n <div \n data-testid=\"thumbnail-cache-stats\"\n style=\"\n background: rgba(30, 41, 59, 0.6);\n border-radius: 6px;\n padding: 8px;\n margin-bottom: 10px;\n font-size: 10px;\n color: #94a3b8;\n \"\n >\n <div style=\"display: flex; justify-content: space-between; margin-bottom: 4px;\">\n <span>Items:</span>\n <span style=\"color: #e2e8f0; font-weight: 500;\">${this.thumbnailCacheStats.itemCount} / ${this.thumbnailCacheStats.maxSize}</span>\n </div>\n <div style=\"display: flex; justify-content: space-between;\">\n <span>Size:</span>\n <span style=\"color: #e2e8f0; font-weight: 500;\">${this.formatBytes(this.thumbnailCacheStats.totalSizeBytes)}</span>\n </div>\n </div>\n ` : ''}\n \n <!-- Clear Cache Button -->\n <button\n data-testid=\"thumbnail-cache-clear\"\n @click=${() => this.handleClearThumbnailCache()}\n style=\"\n width: 100%;\n padding: 6px 10px;\n background: rgba(239, 68, 68, 0.2);\n border: 1px solid rgba(239, 68, 68, 0.4);\n border-radius: 4px;\n color: #f87171;\n font-size: 11px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n \"\n onmouseover=\"this.style.background='rgba(239, 68, 68, 0.3)'\"\n onmouseout=\"this.style.background='rgba(239, 68, 68, 0.2)'\"\n >\n Clear Cache\n </button>\n \n <div style=\"\n margin-top: 8px;\n color: #64748b;\n font-size: 10px;\n line-height: 1.4;\n \">\n Persistent cache survives page reloads. Stored in IndexedDB.\n </div>\n </div>\n \n <!-- Debug Thumbnails Setting -->\n <div style=\"\n background: rgba(51, 65, 85, 0.4);\n border-radius: 8px;\n padding: 12px;\n margin-top: 10px;\n \">\n <label style=\"\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n \">\n <input\n type=\"checkbox\"\n ?checked=${this.debugThumbnailTimestamps}\n @change=${(e: Event) => this.handleDebugThumbnailTimestampsToggle((e.target as HTMLInputElement).checked)}\n style=\"\n width: 14px;\n height: 14px;\n accent-color: #f59e0b;\n cursor: pointer;\n \"\n />\n <span style=\"color: #e2e8f0; font-size: 12px; font-weight: 500;\">Show Thumbnail Timestamps</span>\n </label>\n \n <div style=\"\n margin-top: 8px;\n color: #64748b;\n font-size: 10px;\n line-height: 1.4;\n \">\n Overlays capture timestamps on timeline thumbnails for debugging.\n </div>\n </div>\n </div>\n `;\n }\n \n private renderExportPopover() {\n const timegroup = this.getTimegroup();\n const durationMs = timegroup?.durationMs ?? 0;\n \n return html`\n <div \n id=\"export-popover\" \n popover=\"auto\"\n class=\"dropdown-panel\"\n @toggle=${this.handleExportPopoverToggle}\n >\n <div class=\"dropdown-header\">\n <span class=\"dropdown-title\">Export Settings</span>\n <button class=\"dropdown-close\" popovertarget=\"export-popover\" popovertargetaction=\"hide\">✕</button>\n </div>\n \n <!-- Scale -->\n <div style=\"margin-bottom: 10px;\">\n <label style=\"display: block; color: #94a3b8; font-size: 11px; margin-bottom: 4px;\">Scale</label>\n <select\n style=\"\n width: 100%;\n padding: 6px 10px;\n background: rgba(51, 65, 85, 0.8);\n border: 1px solid rgba(148, 163, 184, 0.2);\n border-radius: 5px;\n color: #e2e8f0;\n font-size: 12px;\n cursor: pointer;\n \"\n .value=${String(this.exportOptions.scale)}\n @change=${(e: Event) => this.updateExportOption(\"scale\", Number((e.target as HTMLSelectElement).value))}\n >\n <option value=\"1\">100% (Full)</option>\n <option value=\"0.75\">75%</option>\n <option value=\"0.5\">50%</option>\n <option value=\"0.25\">25%</option>\n </select>\n </div>\n \n <!-- Audio -->\n <div style=\"margin-bottom: 10px;\">\n <label style=\"display: flex; align-items: center; gap: 8px; cursor: pointer;\">\n <input\n type=\"checkbox\"\n ?checked=${this.exportOptions.includeAudio}\n @change=${(e: Event) => this.updateExportOption(\"includeAudio\", (e.target as HTMLInputElement).checked)}\n style=\"width: 14px; height: 14px; accent-color: #3b82f6;\"\n />\n <span style=\"color: #e2e8f0; font-size: 12px;\">Include Audio</span>\n </label>\n </div>\n \n <!-- In/Out Range -->\n <div style=\"margin-bottom: 12px;\">\n <label style=\"display: flex; align-items: center; gap: 8px; cursor: pointer; margin-bottom: 6px;\">\n <input\n type=\"checkbox\"\n ?checked=${this.exportOptions.useInOut}\n @change=${(e: Event) => this.updateExportOption(\"useInOut\", (e.target as HTMLInputElement).checked)}\n style=\"width: 14px; height: 14px; accent-color: #3b82f6;\"\n />\n <span style=\"color: #e2e8f0; font-size: 12px;\">Custom Range</span>\n </label>\n \n ${this.exportOptions.useInOut ? html`\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 6px;\">\n <div>\n <label style=\"display: block; color: #94a3b8; font-size: 10px; margin-bottom: 2px;\">In (ms)</label>\n <input\n type=\"number\"\n min=\"0\"\n max=${durationMs}\n .value=${String(this.exportOptions.inMs)}\n @change=${(e: Event) => this.updateExportOption(\"inMs\", Number((e.target as HTMLInputElement).value))}\n style=\"\n width: 100%;\n padding: 5px 7px;\n background: rgba(51, 65, 85, 0.8);\n border: 1px solid rgba(148, 163, 184, 0.2);\n border-radius: 4px;\n color: #e2e8f0;\n font-size: 11px;\n font-family: ui-monospace, monospace;\n \"\n />\n </div>\n <div>\n <label style=\"display: block; color: #94a3b8; font-size: 10px; margin-bottom: 2px;\">Out (ms)</label>\n <input\n type=\"number\"\n min=\"0\"\n max=${durationMs}\n .value=${String(this.exportOptions.outMs)}\n @change=${(e: Event) => this.updateExportOption(\"outMs\", Number((e.target as HTMLInputElement).value))}\n style=\"\n width: 100%;\n padding: 5px 7px;\n background: rgba(51, 65, 85, 0.8);\n border: 1px solid rgba(148, 163, 184, 0.2);\n border-radius: 4px;\n color: #e2e8f0;\n font-size: 11px;\n font-family: ui-monospace, monospace;\n \"\n />\n </div>\n </div>\n <div style=\"color: #64748b; font-size: 10px; margin-top: 4px;\">\n Duration: ${this.formatTime(this.exportOptions.outMs - this.exportOptions.inMs)} / ${this.formatTime(durationMs)}\n </div>\n ` : html`\n <div style=\"color: #64748b; font-size: 10px;\">\n Full duration: ${this.formatTime(durationMs)}\n </div>\n `}\n </div>\n \n <!-- Start Export button -->\n <button\n class=\"toolbar-btn primary\"\n style=\"width: 100%; justify-content: center;\"\n @click=${this.handleStartExport}\n popovertarget=\"export-popover\"\n popovertargetaction=\"hide\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polygon points=\"23 7 16 12 23 17 23 7\"></polygon>\n <rect x=\"1\" y=\"5\" width=\"15\" height=\"14\" rx=\"2\" ry=\"2\"></rect>\n </svg>\n Start Export\n </button>\n </div>\n `;\n }\n \n private renderExportProgressPopover() {\n const p = this.exportProgress;\n const progressPercent = p ? Math.round(p.progress * 100) : 0;\n const isComplete = this.exportStatus === \"complete\";\n const isError = this.exportStatus === \"error\";\n const isCancelled = this.exportStatus === \"cancelled\";\n const isRendering = this.exportStatus === \"rendering\";\n \n let statusColor: string;\n let statusText: string;\n \n if (isComplete) {\n statusColor = \"#4ade80\";\n statusText = \"Complete!\";\n } else if (isError) {\n statusColor = \"#f87171\";\n statusText = \"Failed\";\n } else if (isCancelled) {\n statusColor = \"#fbbf24\";\n statusText = \"Cancelled\";\n } else {\n statusColor = \"#60a5fa\";\n statusText = `${progressPercent}%`;\n }\n \n return html`\n <div \n id=\"export-progress-popover\" \n popover=\"manual\"\n class=\"dropdown-panel\" \n style=\"min-width: 240px;\"\n >\n <div class=\"dropdown-header\">\n <span class=\"dropdown-title\">Exporting</span>\n ${isRendering ? html`\n <button \n class=\"dropdown-close\" \n style=\"color: #f87171;\"\n @click=${this.handleCancelClick}\n >Cancel</button>\n ` : null}\n </div>\n \n ${isRendering && p !== null ? html`\n ${p.framePreviewUrl ? html`\n <div style=\"margin-bottom: 10px; display: flex; justify-content: center;\">\n <img \n src=${p.framePreviewUrl} \n alt=\"Current frame\"\n style=\"\n border-radius: 4px;\n border: 1px solid rgba(148, 163, 184, 0.2);\n max-width: 100%;\n height: auto;\n \"\n />\n </div>\n ` : null}\n \n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 6px 12px; margin-bottom: 10px; font-family: ui-monospace, monospace; font-size: 10px;\">\n <div>\n <div style=\"color: #64748b;\">Frames</div>\n <div style=\"color: #e2e8f0;\">${p.currentFrame} / ${p.totalFrames}</div>\n </div>\n <div>\n <div style=\"color: #64748b;\">Time</div>\n <div style=\"color: #e2e8f0;\">${this.formatTime(p.renderedMs)} / ${this.formatTime(p.totalDurationMs)}</div>\n </div>\n <div>\n <div style=\"color: #64748b;\">Speed</div>\n <div style=\"color: ${p.speedMultiplier >= 1 ? \"#4ade80\" : \"#fbbf24\"};\">${p.speedMultiplier.toFixed(2)}x</div>\n </div>\n <div>\n <div style=\"color: #64748b;\">ETA</div>\n <div style=\"color: #e2e8f0;\">${this.formatTime(p.estimatedRemainingMs)}</div>\n </div>\n </div>\n ` : null}\n \n <div style=\"height: 4px; background: rgba(51, 65, 85, 0.8); border-radius: 2px; overflow: hidden;\">\n <div style=\"\n height: 100%;\n width: ${progressPercent}%;\n background: ${statusColor};\n border-radius: 2px;\n transition: width 0.15s ease-out;\n \"></div>\n </div>\n \n <div style=\"text-align: center; margin-top: 6px; font-size: 11px; font-weight: 600; color: ${statusColor};\">\n ${statusText}\n </div>\n </div>\n \n <style>\n @keyframes spin {\n to { transform: rotate(360deg); }\n }\n </style>\n `;\n }\n \n private renderToolbar() {\n return html`\n <div class=\"toolbar\">\n <div class=\"toolbar-left\">\n <!-- Fit to content button -->\n <button \n class=\"toolbar-icon-btn\"\n @click=${this.handleFitToContent}\n title=\"Fit to Content (Reset Zoom & Center)\"\n >\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"></rect>\n <path d=\"M8 12h8M12 8v8\"></path>\n </svg>\n </button>\n </div>\n \n <div class=\"toolbar-right\">\n <!-- Mode indicator (shown when not in default clone mode) -->\n ${this.presentationMode !== \"clone\" ? html`\n <span class=\"mode-indicator ${this.presentationMode}\">\n ${this.presentationMode === \"dom\" ? \"DOM\" : html`\n Canvas ${getRenderMode() === \"native\" \n ? phosphorIcon(ICONS.lightning, 12) \n : phosphorIcon(ICONS.code, 12)}\n `}\n </span>\n ` : null}\n \n <!-- Settings button -->\n <button \n id=\"settings-btn\"\n class=\"toolbar-icon-btn\"\n popovertarget=\"settings-popover\"\n title=\"Preview Settings\"\n >\n ${phosphorIcon(ICONS.gear, 16)}\n </button>\n \n <!-- Export button -->\n ${this.isExporting ? html`\n <button \n id=\"export-btn\"\n class=\"toolbar-btn active\"\n style=\"min-width: 100px;\"\n popovertarget=\"export-progress-popover\"\n >\n <div style=\"width: 12px; height: 12px; border: 2px solid rgba(96, 165, 250, 0.3); border-top-color: #60a5fa; border-radius: 50%; animation: spin 1s linear infinite;\"></div>\n Exporting...\n </button>\n ` : html`\n <button \n id=\"export-btn\"\n class=\"toolbar-btn primary\"\n popovertarget=\"export-popover\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polygon points=\"23 7 16 12 23 17 23 7\"></polygon>\n <rect x=\"1\" y=\"5\" width=\"15\" height=\"14\" rx=\"2\" ry=\"2\"></rect>\n </svg>\n Export\n </button>\n `}\n </div>\n </div>\n \n <!-- Popovers (rendered into top-layer) -->\n ${this.renderSettingsPopover()}\n ${this.renderExportPopover()}\n ${this.renderExportProgressPopover()}\n `;\n }\n\n update(\n changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,\n ): void {\n super.update(changedProperties);\n\n if (changedProperties.has(\"focusedElement\")) {\n this.drawOverlays();\n }\n }\n \n updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {\n super.updated(changedProperties);\n \n // Restore preview pan/zoom when timegroup becomes available\n // Check if timegroup is now available (slot content changed)\n const timegroup = this.getTimegroup();\n if (timegroup && !changedProperties.has(\"panZoomTransform\")) {\n // Only restore if we haven't already restored (avoid overwriting user changes)\n // Check if panZoomTransform is still at default values\n if (\n this.panZoomTransform.x === 0 &&\n this.panZoomTransform.y === 0 &&\n this.panZoomTransform.scale === 1\n ) {\n requestAnimationFrame(() => {\n this.restorePreviewPanZoom();\n });\n }\n }\n \n // Apply settings when dependencies become available or settings change\n if (changedProperties.has(\"previewSettings\") || \n changedProperties.has(\"presentationMode\") ||\n changedProperties.has(\"showStats\")) {\n this.applySettings();\n }\n \n // Show/hide export progress popover based on isExporting state\n if (changedProperties.has(\"isExporting\")) {\n const popover = this.shadowRoot?.getElementById(\"export-progress-popover\") as HTMLElement | null;\n if (popover) {\n if (this.isExporting) {\n popover.showPopover();\n // Position after showing\n requestAnimationFrame(() => {\n this.positionPopover(popover, 'export-btn');\n });\n } else {\n popover.hidePopover();\n }\n }\n }\n }\n\n drawOverlays = () => {\n const focusOverlay = this.focusOverlay.value;\n if (focusOverlay) {\n if (this.focusedElement) {\n focusOverlay.style.display = \"block\";\n const rect = this.focusedElement.getBoundingClientRect();\n Object.assign(focusOverlay.style, {\n position: \"fixed\",\n top: `${rect.top}px`,\n left: `${rect.left}px`,\n width: `${rect.width}px`,\n height: `${rect.height}px`,\n });\n requestAnimationFrame(this.drawOverlays);\n } else {\n focusOverlay.style.display = \"none\";\n }\n }\n };\n \n private renderPlaybackStats() {\n // Only show stats if enabled and strategy is available\n if (!this.showStats || !this.statsStrategy) {\n return null;\n }\n \n const stats = this.statsStrategy.getStats();\n if (!stats) {\n return null;\n }\n \n // Determine FPS color (based on frame interval, not render time)\n const fpsClass = stats.fps >= 55 ? \"good\" : stats.fps >= 25 ? \"warning\" : \"bad\";\n \n // Determine render time color (target is 33ms for 30fps)\n // Only show if supported\n const renderClass = stats.avgRenderTime !== null\n ? (stats.avgRenderTime <= 20 ? \"good\" : stats.avgRenderTime <= 30 ? \"warning\" : \"bad\")\n : \"\";\n \n // Determine headroom color (positive = good, negative = bad)\n // Only show if supported\n const headroomClass = stats.headroom !== null\n ? (stats.headroom >= 10 ? \"good\" : stats.headroom >= 0 ? \"warning\" : \"bad\")\n : \"\";\n \n // Determine pressure color\n const pressureClass = stats.pressureState === \"nominal\" ? \"good\" \n : stats.pressureState === \"fair\" ? \"good\"\n : stats.pressureState === \"serious\" ? \"warning\" \n : \"bad\";\n \n // Resolution scale color (only if supported)\n const scaleClass = stats.resolutionScale !== null\n ? (stats.resolutionScale >= 0.75 ? \"good\" : stats.resolutionScale >= 0.5 ? \"warning\" : \"bad\")\n : \"\";\n \n // Motion state\n const motionState = this.isAtRest ? \"At Rest\" : this.isPlaying ? \"Playing\" : this.isScrubbing ? \"Scrubbing\" : \"Idle\";\n \n // Render pressure histogram bars\n const renderPressureHistogram = () => {\n if (stats.pressureHistory.length === 0) {\n return html`<div style=\"color: #64748b; font-size: 9px;\">No pressure data (API not available)</div>`;\n }\n \n return html`\n <div class=\"pressure-histogram\">\n ${stats.pressureHistory.map((state) => html`\n <div class=\"bar ${state}\"></div>\n `)}\n </div>\n <div class=\"pressure-histogram-label\">\n <span>30s ago</span>\n <span>now</span>\n </div>\n `;\n };\n \n // Helper to pad numbers for consistent width\n const padNum = (n: number, decimals: number, width: number) => {\n const str = n.toFixed(decimals);\n return str.padStart(width, '\\u2007'); // Use figure space for padding\n };\n \n return html`\n <div class=\"playback-stats\">\n <div class=\"stat-row\">\n <span class=\"stat-label\">FPS</span>\n <span class=\"stat-value ${fpsClass}\">${padNum(stats.fps, 1, 5)}</span>\n </div>\n ${this.statsStrategy.supportsStat(\"renderTime\") && stats.avgRenderTime !== null ? html`\n <div class=\"stat-row\">\n <span class=\"stat-label\">Render</span>\n <span class=\"stat-value ${renderClass}\">${padNum(stats.avgRenderTime, 1, 5)}ms</span>\n </div>\n ` : null}\n ${this.statsStrategy.supportsStat(\"headroom\") && stats.headroom !== null ? html`\n <div class=\"stat-row\">\n <span class=\"stat-label\">Headroom</span>\n <span class=\"stat-value ${headroomClass}\">${stats.headroom >= 0 ? '+' : ''}${padNum(stats.headroom, 1, 4)}ms</span>\n </div>\n ` : null}\n <div class=\"stat-row\">\n <span class=\"stat-label\">Resolution</span>\n <span class=\"stat-value\">${stats.renderWidth}×${stats.renderHeight}</span>\n </div>\n ${this.statsStrategy.supportsStat(\"resolutionScale\") && stats.resolutionScale !== null ? html`\n <div class=\"stat-row\">\n <span class=\"stat-label\">Scale</span>\n <span class=\"stat-value ${scaleClass}\">${String(Math.round(stats.resolutionScale * 100)).padStart(3, '\\u2007')}%</span>\n </div>\n ` : null}\n <div class=\"stat-row\">\n <span class=\"stat-label\">CPU</span>\n <span class=\"stat-value ${pressureClass}\">${stats.pressureState}</span>\n </div>\n <div class=\"stat-row\">\n <span class=\"stat-label\">State</span>\n <span class=\"stat-value\">${motionState}</span>\n </div>\n ${this.statsStrategy.supportsStat(\"adaptiveResolution\") && this.previewResolutionScale === \"auto\" && stats.samplesAtCurrentScale !== undefined ? html`\n <div style=\"margin-top: 4px; padding-top: 4px; border-top: 1px solid rgba(148, 163, 184, 0.2);\">\n <div class=\"stat-row\">\n <span class=\"stat-label\">Mode</span>\n <span class=\"stat-value good\">Auto</span>\n </div>\n <div class=\"stat-row\">\n <span class=\"stat-label\">Samples</span>\n <span class=\"stat-value\">${String(stats.samplesAtCurrentScale).padStart(3, '\\u2007')}/60</span>\n </div>\n <div class=\"stat-row\">\n <span class=\"stat-label\">Scale Up</span>\n <span class=\"stat-value ${stats.canScaleUp ? 'good' : ''}\">${stats.canScaleUp ? 'Ready' : 'Waiting'}</span>\n </div>\n <div class=\"stat-row\">\n <span class=\"stat-label\">Scale Down</span>\n <span class=\"stat-value ${stats.canScaleDown ? '' : 'warning'}\">${stats.canScaleDown ? 'Ready' : 'Min'}</span>\n </div>\n </div>\n ` : null}\n \n <!-- CPU Pressure Histogram -->\n <div style=\"margin-top: 8px;\">\n <div style=\"color: #94a3b8; font-size: 10px; margin-bottom: 4px;\">CPU Pressure History</div>\n ${renderPressureHistogram()}\n </div>\n </div>\n `;\n }\n\n render() {\n if (this.rendering) {\n return html`\n <slot class=\"fixed inset-0 h-full w-full\" name=\"canvas\"></slot>\n `;\n }\n return html`\n <div\n class=\"grid overflow-hidden\"\n style=\"flex: 1; min-height: 0; width: 100%; grid-template-rows: auto 1fr 280px; grid-template-columns: 280px 1fr; background-color: var(--workbench-bg);\"\n >\n <!-- Top: Full-width Toolbar -->\n <div style=\"grid-row: 1 / 2; grid-column: 1 / -1;\">\n ${this.renderToolbar()}\n </div>\n \n <!-- Left: Hierarchy Panel -->\n <div\n style=\"grid-row: 2 / 3; grid-column: 1 / 2; background: rgb(30 41 59); border-right: 1px solid rgba(148, 163, 184, 0.2); min-height: 0; max-height: 100%; display: flex; flex-direction: column; overflow: hidden;\"\n >\n <slot name=\"hierarchy\"></slot>\n </div>\n\n <!-- Center: Canvas area -->\n <div\n class=\"canvas-container\"\n style=\"grid-row: 2 / 3; grid-column: 2 / 3; min-height: 0;\"\n @wheel=${this.handleStageWheel}\n >\n <!-- Original timegroup (hidden in clone/canvas mode, visible in dom mode) -->\n <slot name=\"canvas\"></slot>\n \n <!-- Clone overlay (visible in clone mode only) -->\n <div \n class=\"clone-overlay\" \n ${ref(this.cloneOverlayRef)}\n style=\"display: ${this.presentationMode === \"clone\" ? \"block\" : \"none\"}\"\n ></div>\n \n <!-- Canvas preview (visible in canvas mode only) -->\n <div \n class=\"clone-overlay\" \n ${ref(this.canvasPreviewRef)}\n style=\"display: ${this.presentationMode === \"canvas\" ? \"block\" : \"none\"}\"\n ></div>\n \n <!-- Playback stats overlay (visible in canvas mode only) -->\n ${this.renderPlaybackStats()}\n </div>\n\n <!-- Bottom: Timeline -->\n <div\n class=\"overflow-hidden\"\n style=\"grid-row: 3 / 4; grid-column: 1 / -1; width: 100%; border-top: 1px solid rgba(148, 163, 184, 0.2);\"\n >\n <slot name=\"timeline\"></slot>\n </div>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-workbench\": EFWorkbench;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ //#region src/gui/FitScaleHelpers.d.ts
2
+ /**
3
+ * Determines if an element needs FitScale wrapping based on its parent container
4
+ * and whether it has explicit size.
5
+ *
6
+ * FitScale is needed when:
7
+ * - Parent is a grid container (grid or flex display)
8
+ * - Element doesn't have explicit width/height
9
+ *
10
+ * @param parentElement - The parent element (can be HTMLElement or null)
11
+ * @param elementHasExplicitSize - Whether the element has explicit width/height
12
+ * @returns true if FitScale should be applied
13
+ *
14
+ * @public
15
+ */
16
+ declare function needsFitScale(parentElement: HTMLElement | null, elementHasExplicitSize: boolean): boolean;
17
+ /**
18
+ * Determines if an element needs FitScale wrapping by checking computed styles.
19
+ * This is a convenience function that reads the element's computed style to check
20
+ * for explicit size.
21
+ *
22
+ * @param element - The element to check
23
+ * @param parentElement - The parent element (can be HTMLElement or null)
24
+ * @returns true if FitScale should be applied
25
+ *
26
+ * @public
27
+ */
28
+ declare function elementNeedsFitScale(element: HTMLElement, parentElement: HTMLElement | null): boolean;
29
+ //#endregion
30
+ export { elementNeedsFitScale, needsFitScale };
31
+ //# sourceMappingURL=FitScaleHelpers.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { getContainerInfoFromElement } from "../elements/ContainerInfo.js";
2
+
3
+ //#region src/gui/FitScaleHelpers.ts
4
+ /**
5
+ * Determines if an element needs FitScale wrapping based on its parent container
6
+ * and whether it has explicit size.
7
+ *
8
+ * FitScale is needed when:
9
+ * - Parent is a grid container (grid or flex display)
10
+ * - Element doesn't have explicit width/height
11
+ *
12
+ * @param parentElement - The parent element (can be HTMLElement or null)
13
+ * @param elementHasExplicitSize - Whether the element has explicit width/height
14
+ * @returns true if FitScale should be applied
15
+ *
16
+ * @public
17
+ */
18
+ function needsFitScale(parentElement, elementHasExplicitSize) {
19
+ if (!parentElement) return false;
20
+ const containerInfo = getContainerInfoFromElement(parentElement);
21
+ return (containerInfo.displayMode === "grid" || containerInfo.displayMode === "flex") && !elementHasExplicitSize;
22
+ }
23
+ /**
24
+ * Determines if an element needs FitScale wrapping by checking computed styles.
25
+ * This is a convenience function that reads the element's computed style to check
26
+ * for explicit size.
27
+ *
28
+ * @param element - The element to check
29
+ * @param parentElement - The parent element (can be HTMLElement or null)
30
+ * @returns true if FitScale should be applied
31
+ *
32
+ * @public
33
+ */
34
+ function elementNeedsFitScale(element, parentElement) {
35
+ const computedStyle = window.getComputedStyle(element);
36
+ return needsFitScale(parentElement, !!(computedStyle.width && computedStyle.width !== "auto") || !!(computedStyle.height && computedStyle.height !== "auto"));
37
+ }
38
+
39
+ //#endregion
40
+ export { elementNeedsFitScale, needsFitScale };
41
+ //# sourceMappingURL=FitScaleHelpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FitScaleHelpers.js","names":[],"sources":["../../src/gui/FitScaleHelpers.ts"],"sourcesContent":["import type { ContainerInfo } from \"../elements/ContainerInfo.js\";\nimport { getContainerInfoFromElement } from \"../elements/ContainerInfo.js\";\n\n/**\n * Determines if an element needs FitScale wrapping based on its parent container\n * and whether it has explicit size.\n *\n * FitScale is needed when:\n * - Parent is a grid container (grid or flex display)\n * - Element doesn't have explicit width/height\n *\n * @param parentElement - The parent element (can be HTMLElement or null)\n * @param elementHasExplicitSize - Whether the element has explicit width/height\n * @returns true if FitScale should be applied\n *\n * @public\n */\nexport function needsFitScale(\n parentElement: HTMLElement | null,\n elementHasExplicitSize: boolean,\n): boolean {\n if (!parentElement) {\n return false;\n }\n\n const containerInfo = getContainerInfoFromElement(parentElement);\n const isInGridContainer =\n containerInfo.displayMode === \"grid\" ||\n containerInfo.displayMode === \"flex\";\n\n return isInGridContainer && !elementHasExplicitSize;\n}\n\n/**\n * Determines if an element needs FitScale wrapping by checking computed styles.\n * This is a convenience function that reads the element's computed style to check\n * for explicit size.\n *\n * @param element - The element to check\n * @param parentElement - The parent element (can be HTMLElement or null)\n * @returns true if FitScale should be applied\n *\n * @public\n */\nexport function elementNeedsFitScale(\n element: HTMLElement,\n parentElement: HTMLElement | null,\n): boolean {\n const computedStyle = window.getComputedStyle(element);\n const hasExplicitSize =\n !!(computedStyle.width && computedStyle.width !== \"auto\") ||\n !!(computedStyle.height && computedStyle.height !== \"auto\");\n\n return needsFitScale(parentElement, hasExplicitSize);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,SAAgB,cACd,eACA,wBACS;AACT,KAAI,CAAC,cACH,QAAO;CAGT,MAAM,gBAAgB,4BAA4B,cAAc;AAKhE,SAHE,cAAc,gBAAgB,UAC9B,cAAc,gBAAgB,WAEJ,CAAC;;;;;;;;;;;;;AAc/B,SAAgB,qBACd,SACA,eACS;CACT,MAAM,gBAAgB,OAAO,iBAAiB,QAAQ;AAKtD,QAAO,cAAc,eAHnB,CAAC,EAAE,cAAc,SAAS,cAAc,UAAU,WAClD,CAAC,EAAE,cAAc,UAAU,cAAc,WAAW,QAEF"}
@@ -11,7 +11,7 @@ interface PlaybackHost extends HTMLElement, ReactiveControllerHost {
11
11
  taskComplete: Promise<unknown>;
12
12
  };
13
13
  renderAudio?(fromMs: number, toMs: number): Promise<AudioBuffer>;
14
- waitForMediaDurations?(): Promise<void>;
14
+ waitForMediaDurations?(signal?: AbortSignal): Promise<void>;
15
15
  saveTimeToLocalStorage?(time: number): void;
16
16
  loadTimeFromLocalStorage?(): number | undefined;
17
17
  requestUpdate(property?: string): void;
@@ -58,6 +58,7 @@ declare class PlaybackController implements ReactiveController {
58
58
  hostConnected(): void;
59
59
  hostDisconnected(): void;
60
60
  hostUpdated(): void;
61
+ static readonly THROTTLED_FRAME_TASK_MAX_WAITS = 100;
61
62
  runThrottledFrameTask(): Promise<void>;
62
63
  addListener(listener: (event: PlaybackControllerUpdateEvent) => void): void;
63
64
  removeListener(listener: (event: PlaybackControllerUpdateEvent) => void): void;
@@ -19,7 +19,7 @@ import { Task, TaskStatus } from "@lit/task";
19
19
  *
20
20
  * Works with any temporal element (timegroups or standalone media) via PlaybackHost interface
21
21
  */
22
- var PlaybackController = class {
22
+ var PlaybackController = class PlaybackController {
23
23
  #host;
24
24
  #playing = false;
25
25
  #loop = false;
@@ -50,8 +50,10 @@ var PlaybackController = class {
50
50
  autoRun: false,
51
51
  args: () => [this.#pendingSeekTime ?? this.#currentTime],
52
52
  onComplete: () => {},
53
- task: async ([targetTime]) => {
54
- await this.#host.waitForMediaDurations?.();
53
+ task: async ([targetTime], { signal }) => {
54
+ signal?.throwIfAborted();
55
+ await this.#host.waitForMediaDurations?.(signal);
56
+ signal?.throwIfAborted();
55
57
  const newTime = Math.max(0, Math.min(targetTime ?? 0, this.#host.durationMs / 1e3));
56
58
  this.#currentTime = newTime;
57
59
  this.#host.requestUpdate("currentTime");
@@ -60,8 +62,11 @@ var PlaybackController = class {
60
62
  property: "currentTimeMs",
61
63
  value: this.currentTimeMs
62
64
  });
65
+ signal?.throwIfAborted();
63
66
  await this.runThrottledFrameTask();
64
- this.#host.saveTimeToLocalStorage?.(newTime);
67
+ signal?.throwIfAborted();
68
+ if (!(this.#host.isRestoringFromLocalStorage?.() ?? false)) this.#host.saveTimeToLocalStorage?.(newTime);
69
+ else this.#host.setRestoringFromLocalStorage?.(false);
65
70
  this.#seekInProgress = false;
66
71
  return newTime;
67
72
  }
@@ -88,7 +93,9 @@ var PlaybackController = class {
88
93
  const fps = this.#host.fps ?? 30;
89
94
  if (!fps || fps <= 0) return rawTime;
90
95
  const frameDurationS = 1 / fps;
91
- return Math.round(rawTime / frameDurationS) * frameDurationS;
96
+ const quantizedTime = Math.round(rawTime / frameDurationS) * frameDurationS;
97
+ const durationS = this.#host.durationMs / 1e3;
98
+ return Math.max(0, Math.min(quantizedTime, durationS));
92
99
  }
93
100
  set currentTime(time) {
94
101
  time = Math.max(0, Math.min(this.#host.durationMs / 1e3, time));
@@ -150,14 +157,16 @@ var PlaybackController = class {
150
157
  this.currentTime = value / 1e3;
151
158
  }
152
159
  #updatePlaybackTime(timeMs) {
153
- const timeSec = timeMs / 1e3;
160
+ const durationMs = this.#host.durationMs;
161
+ const clampedTimeMs = Math.max(0, Math.min(timeMs, durationMs));
162
+ const timeSec = clampedTimeMs / 1e3;
154
163
  if (this.#currentTime === timeSec) return;
155
164
  this.#currentTime = timeSec;
156
165
  this.#host.requestUpdate("currentTime");
157
- this.#currentTimeMsProvider.setValue(timeMs);
166
+ this.#currentTimeMsProvider.setValue(clampedTimeMs);
158
167
  this.#notifyListeners({
159
168
  property: "currentTimeMs",
160
- value: timeMs
169
+ value: clampedTimeMs
161
170
  });
162
171
  this.runThrottledFrameTask();
163
172
  }
@@ -167,13 +176,24 @@ var PlaybackController = class {
167
176
  pause() {
168
177
  this.setPlaying(false);
169
178
  }
179
+ #removed = false;
170
180
  hostConnected() {
171
- if (this.#playing) this.startPlayback();
172
- else this.#host.waitForMediaDurations?.().then(() => {
173
- const maybeLoadedTime = this.#host.loadTimeFromLocalStorage?.();
174
- if (maybeLoadedTime !== void 0) this.currentTime = maybeLoadedTime;
175
- else if (this.#currentTime === void 0) this.#currentTime = 0;
176
- if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) this.seekTask.run();
181
+ requestAnimationFrame(() => {
182
+ requestAnimationFrame(() => {
183
+ if (this.#removed || this.#host.playbackController !== this) return;
184
+ if (this.#playing) this.startPlayback();
185
+ else this.#host.waitForMediaDurations?.().then(() => {
186
+ if (this.#removed || this.#host.playbackController !== this) return;
187
+ const maybeLoadedTime = this.#host.loadTimeFromLocalStorage?.();
188
+ if (maybeLoadedTime !== void 0) {
189
+ this.#host.setRestoringFromLocalStorage?.(true);
190
+ this.currentTime = maybeLoadedTime;
191
+ } else if (this.#currentTime === void 0) this.#currentTime = 0;
192
+ if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) this.seekTask.run();
193
+ }).catch((err) => {
194
+ if (!(err instanceof DOMException && err.name === "AbortError" || err instanceof Error && (err.name === "AbortError" || err.message.includes("signal is aborted") || err.message.includes("The user aborted a request")))) console.error("Error in PlaybackController hostConnected:", err);
195
+ });
196
+ });
177
197
  });
178
198
  }
179
199
  hostDisconnected() {
@@ -183,10 +203,18 @@ var PlaybackController = class {
183
203
  this.#durationMsProvider.setValue(this.#host.durationMs);
184
204
  this.#currentTimeMsProvider.setValue(this.currentTimeMs);
185
205
  }
206
+ static {
207
+ this.THROTTLED_FRAME_TASK_MAX_WAITS = 100;
208
+ }
186
209
  async runThrottledFrameTask() {
187
210
  if (this.#frameTaskInProgress) {
188
211
  this.#pendingFrameTaskRun = true;
189
- while (this.#frameTaskInProgress) await this.#host.frameTask.taskComplete;
212
+ let waitLoopCount = 0;
213
+ while (this.#frameTaskInProgress) {
214
+ waitLoopCount++;
215
+ if (waitLoopCount > PlaybackController.THROTTLED_FRAME_TASK_MAX_WAITS) break;
216
+ await this.#host.frameTask.taskComplete;
217
+ }
190
218
  return;
191
219
  }
192
220
  this.#frameTaskInProgress = true;
@@ -217,6 +245,7 @@ var PlaybackController = class {
217
245
  for (const listener of this.#listeners) listener(event);
218
246
  }
219
247
  remove() {
248
+ this.#removed = true;
220
249
  this.stopPlayback();
221
250
  this.#listeners.clear();
222
251
  this.#host.removeController(this);
@@ -264,10 +293,12 @@ var PlaybackController = class {
264
293
  this.#pendingAudioContext = null;
265
294
  }
266
295
  async startPlayback() {
296
+ if (this.#removed) return;
267
297
  await this.stopPlayback();
268
298
  const host = this.#host;
269
299
  if (!host) return;
270
300
  if (host.waitForMediaDurations) await host.waitForMediaDurations();
301
+ if (this.#removed) return;
271
302
  const currentMs = this.currentTimeMs;
272
303
  const fromMs = currentMs;
273
304
  const toMs = host.endTimeMs;