@editframe/elements 0.30.1-beta.0 → 0.31.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) 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 +118 -56
  117. package/dist/elements/EFThumbnailStrip.js +522 -358
  118. package/dist/elements/EFThumbnailStrip.js.map +1 -1
  119. package/dist/elements/EFTimegroup.d.ts +223 -27
  120. package/dist/elements/EFTimegroup.js +851 -148
  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 +152 -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 +492 -109
  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 +11 -5
  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/EFPreview.d.ts +4 -4
  175. package/dist/gui/EFResizableBox.d.ts +12 -16
  176. package/dist/gui/EFResizableBox.js +109 -451
  177. package/dist/gui/EFResizableBox.js.map +1 -1
  178. package/dist/gui/EFScrubber.d.ts +30 -5
  179. package/dist/gui/EFScrubber.js +224 -31
  180. package/dist/gui/EFScrubber.js.map +1 -1
  181. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  182. package/dist/gui/EFTimeDisplay.js +4 -1
  183. package/dist/gui/EFTimeDisplay.js.map +1 -1
  184. package/dist/gui/EFTimelineRuler.d.ts +71 -0
  185. package/dist/gui/EFTimelineRuler.js +320 -0
  186. package/dist/gui/EFTimelineRuler.js.map +1 -0
  187. package/dist/gui/EFToggleLoop.d.ts +4 -4
  188. package/dist/gui/EFTogglePlay.d.ts +4 -4
  189. package/dist/gui/EFTransformHandles.d.ts +91 -0
  190. package/dist/gui/EFTransformHandles.js +393 -0
  191. package/dist/gui/EFTransformHandles.js.map +1 -0
  192. package/dist/gui/EFWorkbench.d.ts +182 -4
  193. package/dist/gui/EFWorkbench.js +2067 -22
  194. package/dist/gui/EFWorkbench.js.map +1 -1
  195. package/dist/gui/FitScaleHelpers.d.ts +31 -0
  196. package/dist/gui/FitScaleHelpers.js +41 -0
  197. package/dist/gui/FitScaleHelpers.js.map +1 -0
  198. package/dist/gui/PlaybackController.d.ts +2 -1
  199. package/dist/gui/PlaybackController.js +46 -15
  200. package/dist/gui/PlaybackController.js.map +1 -1
  201. package/dist/gui/TWMixin.js +1 -1
  202. package/dist/gui/TWMixin.js.map +1 -1
  203. package/dist/gui/hierarchy/EFHierarchy.d.ts +65 -0
  204. package/dist/gui/hierarchy/EFHierarchy.js +338 -0
  205. package/dist/gui/hierarchy/EFHierarchy.js.map +1 -0
  206. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +118 -0
  207. package/dist/gui/hierarchy/EFHierarchyItem.js +551 -0
  208. package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -0
  209. package/dist/gui/hierarchy/hierarchyContext.d.ts +38 -0
  210. package/dist/gui/hierarchy/hierarchyContext.js +8 -0
  211. package/dist/gui/hierarchy/hierarchyContext.js.map +1 -0
  212. package/dist/gui/icons.js +34 -0
  213. package/dist/gui/icons.js.map +1 -0
  214. package/dist/gui/panZoomTransformContext.js +12 -0
  215. package/dist/gui/panZoomTransformContext.js.map +1 -0
  216. package/dist/gui/previewSettingsContext.js +12 -0
  217. package/dist/gui/previewSettingsContext.js.map +1 -0
  218. package/dist/gui/timeline/EFTimeline.d.ts +270 -0
  219. package/dist/gui/timeline/EFTimeline.js +1369 -0
  220. package/dist/gui/timeline/EFTimeline.js.map +1 -0
  221. package/dist/gui/timeline/EFTimelineRow.js +374 -0
  222. package/dist/gui/timeline/EFTimelineRow.js.map +1 -0
  223. package/dist/gui/timeline/TrimHandles.d.ts +36 -0
  224. package/dist/gui/timeline/TrimHandles.js +204 -0
  225. package/dist/gui/timeline/TrimHandles.js.map +1 -0
  226. package/dist/gui/timeline/flattenHierarchy.js +31 -0
  227. package/dist/gui/timeline/flattenHierarchy.js.map +1 -0
  228. package/dist/gui/timeline/timelineStateContext.d.ts +26 -0
  229. package/dist/gui/timeline/timelineStateContext.js +42 -0
  230. package/dist/gui/timeline/timelineStateContext.js.map +1 -0
  231. package/dist/gui/timeline/tracks/AudioTrack.js +264 -0
  232. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -0
  233. package/dist/gui/timeline/tracks/CaptionsTrack.js +595 -0
  234. package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -0
  235. package/dist/gui/timeline/tracks/HTMLTrack.js +19 -0
  236. package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -0
  237. package/dist/gui/timeline/tracks/ImageTrack.js +53 -0
  238. package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -0
  239. package/dist/gui/timeline/tracks/TextTrack.js +250 -0
  240. package/dist/gui/timeline/tracks/TextTrack.js.map +1 -0
  241. package/dist/gui/timeline/tracks/TimegroupTrack.js +143 -0
  242. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -0
  243. package/dist/gui/timeline/tracks/TrackItem.js +269 -0
  244. package/dist/gui/timeline/tracks/TrackItem.js.map +1 -0
  245. package/dist/gui/timeline/tracks/VideoTrack.js +265 -0
  246. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -0
  247. package/dist/gui/timeline/tracks/WaveformTrack.js +19 -0
  248. package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -0
  249. package/dist/gui/timeline/tracks/ensureTrackItemInit.js +1 -0
  250. package/dist/gui/timeline/tracks/preloadTracks.js +9 -0
  251. package/dist/gui/timeline/tracks/renderTrackChildren.js +119 -0
  252. package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -0
  253. package/dist/gui/timeline/tracks/waveformUtils.js +80 -0
  254. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -0
  255. package/dist/gui/transformCalculations.js +217 -0
  256. package/dist/gui/transformCalculations.js.map +1 -0
  257. package/dist/gui/transformUtils.d.ts +37 -0
  258. package/dist/gui/transformUtils.js +77 -0
  259. package/dist/gui/transformUtils.js.map +1 -0
  260. package/dist/gui/tree/EFTree.d.ts +59 -0
  261. package/dist/gui/tree/EFTree.js +174 -0
  262. package/dist/gui/tree/EFTree.js.map +1 -0
  263. package/dist/gui/tree/EFTreeItem.d.ts +38 -0
  264. package/dist/gui/tree/EFTreeItem.js +146 -0
  265. package/dist/gui/tree/EFTreeItem.js.map +1 -0
  266. package/dist/gui/tree/treeContext.d.ts +60 -0
  267. package/dist/gui/tree/treeContext.js +23 -0
  268. package/dist/gui/tree/treeContext.js.map +1 -0
  269. package/dist/index.d.ts +32 -8
  270. package/dist/index.js +30 -6
  271. package/dist/index.js.map +1 -1
  272. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +688 -0
  273. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +1 -0
  274. package/dist/node_modules/react/cjs/react.development.js +1521 -0
  275. package/dist/node_modules/react/cjs/react.development.js.map +1 -0
  276. package/dist/node_modules/react/index.js +13 -0
  277. package/dist/node_modules/react/index.js.map +1 -0
  278. package/dist/node_modules/react/jsx-runtime.js +13 -0
  279. package/dist/node_modules/react/jsx-runtime.js.map +1 -0
  280. package/dist/preview/AdaptiveResolutionTracker.js +228 -0
  281. package/dist/preview/AdaptiveResolutionTracker.js.map +1 -0
  282. package/dist/preview/RenderProfiler.js +135 -0
  283. package/dist/preview/RenderProfiler.js.map +1 -0
  284. package/dist/preview/previewSettings.js +131 -0
  285. package/dist/preview/previewSettings.js.map +1 -0
  286. package/dist/preview/previewTypes.js +64 -0
  287. package/dist/preview/previewTypes.js.map +1 -0
  288. package/dist/preview/renderTimegroupPreview.js +656 -0
  289. package/dist/preview/renderTimegroupPreview.js.map +1 -0
  290. package/dist/preview/renderTimegroupToCanvas.d.ts +37 -0
  291. package/dist/preview/renderTimegroupToCanvas.js +840 -0
  292. package/dist/preview/renderTimegroupToCanvas.js.map +1 -0
  293. package/dist/preview/renderTimegroupToVideo.d.ts +39 -0
  294. package/dist/preview/renderTimegroupToVideo.js +274 -0
  295. package/dist/preview/renderTimegroupToVideo.js.map +1 -0
  296. package/dist/preview/renderers.js +16 -0
  297. package/dist/preview/renderers.js.map +1 -0
  298. package/dist/preview/statsTrackingStrategy.js +201 -0
  299. package/dist/preview/statsTrackingStrategy.js.map +1 -0
  300. package/dist/preview/thumbnailCacheSettings.js +52 -0
  301. package/dist/preview/thumbnailCacheSettings.js.map +1 -0
  302. package/dist/preview/workers/WorkerPool.js +178 -0
  303. package/dist/preview/workers/WorkerPool.js.map +1 -0
  304. package/dist/sandbox/PlaybackControls.js +10 -0
  305. package/dist/sandbox/PlaybackControls.js.map +1 -0
  306. package/dist/sandbox/ScenarioRunner.js +1 -0
  307. package/dist/sandbox/index.js +2 -0
  308. package/dist/style.css +66 -69
  309. package/dist/transcoding/types/index.d.ts +2 -1
  310. package/dist/transcoding/utils/UrlGenerator.d.ts +6 -1
  311. package/dist/transcoding/utils/UrlGenerator.js +12 -3
  312. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  313. package/dist/utils/LRUCache.js +1 -375
  314. package/dist/utils/LRUCache.js.map +1 -1
  315. package/dist/utils/frameTime.js +14 -0
  316. package/dist/utils/frameTime.js.map +1 -0
  317. package/package.json +3 -3
  318. package/test/profilingPlugin.ts +223 -0
  319. package/test/recordReplayProxyPlugin.js +22 -27
  320. package/test/thumbnail-performance-test.html +116 -0
  321. package/test/visualRegressionUtils.ts +286 -0
  322. package/types.json +1 -1
  323. package/dist/elements/TimegroupController.d.ts +0 -18
  324. package/dist/msToTimeCode.js +0 -17
  325. package/dist/msToTimeCode.js.map +0 -1
@@ -0,0 +1,201 @@
1
+ //#region src/preview/statsTrackingStrategy.ts
2
+ /**
3
+ * Canvas mode stats tracking strategy.
4
+ * Tracks all stats including render time, headroom, resolution scale, and adaptive resolution.
5
+ */
6
+ var CanvasStatsStrategy = class {
7
+ constructor(options) {
8
+ this.animationFrame = null;
9
+ this.lastStatsUpdateTime = 0;
10
+ this.currentStats = null;
11
+ this.canvasPreviewResult = options.canvasPreviewResult;
12
+ this.adaptiveTracker = options.adaptiveTracker;
13
+ this.compositionWidth = options.compositionWidth;
14
+ this.compositionHeight = options.compositionHeight;
15
+ this.getResolutionScale = options.getResolutionScale;
16
+ this.isAtRest = options.isAtRest;
17
+ this.isExporting = options.isExporting;
18
+ }
19
+ start() {
20
+ if (this.animationFrame !== null) return;
21
+ const { refresh } = this.canvasPreviewResult;
22
+ const loop = async (timestamp) => {
23
+ if (this.animationFrame === null) return;
24
+ if (!this.isExporting()) try {
25
+ const renderStart = performance.now();
26
+ await refresh();
27
+ const renderTime = performance.now() - renderStart;
28
+ if (!this.isAtRest()) this.adaptiveTracker.recordFrame(renderTime, timestamp);
29
+ if (timestamp - this.lastStatsUpdateTime > 100) {
30
+ this.lastStatsUpdateTime = timestamp;
31
+ const currentScale = this.getResolutionScale();
32
+ const renderWidth = Math.floor(this.compositionWidth * currentScale);
33
+ const renderHeight = Math.floor(this.compositionHeight * currentScale);
34
+ this.updateStats(renderWidth, renderHeight, currentScale);
35
+ }
36
+ } catch (e) {
37
+ console.error("Canvas stats tracking failed:", e);
38
+ }
39
+ this.animationFrame = requestAnimationFrame(loop);
40
+ };
41
+ this.animationFrame = requestAnimationFrame(loop);
42
+ }
43
+ stop() {
44
+ if (this.animationFrame !== null) {
45
+ cancelAnimationFrame(this.animationFrame);
46
+ this.animationFrame = null;
47
+ }
48
+ this.currentStats = null;
49
+ }
50
+ getStats() {
51
+ return this.currentStats;
52
+ }
53
+ supportsStat(_stat) {
54
+ return true;
55
+ }
56
+ updateStats(renderWidth, renderHeight, resolutionScale) {
57
+ const trackerStats = this.adaptiveTracker.getStats();
58
+ this.currentStats = {
59
+ fps: trackerStats.fps,
60
+ avgRenderTime: trackerStats.avgRenderTime,
61
+ headroom: trackerStats.headroom,
62
+ pressureState: trackerStats.pressureState,
63
+ pressureHistory: trackerStats.pressureHistory,
64
+ renderWidth,
65
+ renderHeight,
66
+ resolutionScale,
67
+ samplesAtCurrentScale: trackerStats.samplesAtCurrentScale,
68
+ canScaleUp: trackerStats.canScaleUp,
69
+ canScaleDown: trackerStats.canScaleDown
70
+ };
71
+ }
72
+ };
73
+ /**
74
+ * DOM mode stats tracking strategy.
75
+ * Tracks FPS, resolution, CPU pressure, and frame seek time.
76
+ */
77
+ var DomStatsStrategy = class {
78
+ constructor(options) {
79
+ this.animationFrame = null;
80
+ this.lastFrameTime = 0;
81
+ this.frameIntervals = [];
82
+ this.lastStatsUpdateTime = 0;
83
+ this.currentStats = null;
84
+ this.seekStartTime = 0;
85
+ this.seekTimes = [];
86
+ this.frameTaskCleanup = null;
87
+ this.ROLLING_WINDOW_SIZE = 30;
88
+ this.TARGET_FRAME_TIME_MS = 33.33;
89
+ this.timegroup = options.timegroup;
90
+ this.adaptiveTracker = options.adaptiveTracker;
91
+ }
92
+ start() {
93
+ if (this.animationFrame !== null) return;
94
+ const frameTaskCallback = async () => {
95
+ if (this.seekStartTime > 0) {
96
+ const seekTime = performance.now() - this.seekStartTime;
97
+ this.seekTimes.push(seekTime);
98
+ if (this.seekTimes.length > this.ROLLING_WINDOW_SIZE) this.seekTimes.shift();
99
+ this.seekStartTime = 0;
100
+ }
101
+ };
102
+ this.timegroup.addFrameTask(frameTaskCallback);
103
+ this.frameTaskCleanup = () => {};
104
+ let lastCurrentTimeMs = this.timegroup.currentTimeMs;
105
+ const checkSeek = () => {
106
+ const currentTimeMs = this.timegroup.currentTimeMs;
107
+ if (currentTimeMs !== lastCurrentTimeMs) {
108
+ this.seekStartTime = performance.now();
109
+ lastCurrentTimeMs = currentTimeMs;
110
+ }
111
+ };
112
+ const loop = (timestamp) => {
113
+ if (this.animationFrame === null) return;
114
+ checkSeek();
115
+ if (this.lastFrameTime > 0) {
116
+ const interval = timestamp - this.lastFrameTime;
117
+ this.frameIntervals.push(interval);
118
+ if (this.frameIntervals.length > this.ROLLING_WINDOW_SIZE) this.frameIntervals.shift();
119
+ }
120
+ this.lastFrameTime = timestamp;
121
+ if (timestamp - this.lastStatsUpdateTime > 100) {
122
+ this.lastStatsUpdateTime = timestamp;
123
+ this.updateStats();
124
+ }
125
+ this.animationFrame = requestAnimationFrame(loop);
126
+ };
127
+ this.animationFrame = requestAnimationFrame(loop);
128
+ }
129
+ stop() {
130
+ if (this.animationFrame !== null) {
131
+ cancelAnimationFrame(this.animationFrame);
132
+ this.animationFrame = null;
133
+ }
134
+ if (this.frameTaskCleanup) {
135
+ this.frameTaskCleanup();
136
+ this.frameTaskCleanup = null;
137
+ }
138
+ this.lastFrameTime = 0;
139
+ this.frameIntervals = [];
140
+ this.seekStartTime = 0;
141
+ this.seekTimes = [];
142
+ this.currentStats = null;
143
+ }
144
+ getStats() {
145
+ return this.currentStats;
146
+ }
147
+ supportsStat(stat) {
148
+ return stat === "fps" || stat === "resolution" || stat === "cpuPressure" || stat === "renderTime" || stat === "headroom";
149
+ }
150
+ updateStats() {
151
+ const avgFrameInterval = this.frameIntervals.length > 0 ? this.frameIntervals.reduce((a, b) => a + b, 0) / this.frameIntervals.length : 16.67;
152
+ const fps = avgFrameInterval > 0 ? 1e3 / avgFrameInterval : 0;
153
+ const avgSeekTime = this.seekTimes.length > 0 ? this.seekTimes.reduce((a, b) => a + b, 0) / this.seekTimes.length : 0;
154
+ const headroom = avgSeekTime > 0 ? this.TARGET_FRAME_TIME_MS - avgSeekTime : 0;
155
+ const trackerStats = this.adaptiveTracker.getStats();
156
+ const rect = this.timegroup.getBoundingClientRect();
157
+ const renderWidth = Math.round(rect.width);
158
+ const renderHeight = Math.round(rect.height);
159
+ this.currentStats = {
160
+ fps,
161
+ avgRenderTime: avgSeekTime > 0 ? avgSeekTime : null,
162
+ headroom: avgSeekTime > 0 ? headroom : null,
163
+ pressureState: trackerStats.pressureState,
164
+ pressureHistory: trackerStats.pressureHistory,
165
+ renderWidth,
166
+ renderHeight,
167
+ resolutionScale: null
168
+ };
169
+ }
170
+ };
171
+ /**
172
+ * Factory function to create the appropriate stats tracking strategy for a presentation mode.
173
+ * Returns null for modes that don't support stats tracking.
174
+ */
175
+ function createStatsTrackingStrategy(mode, options) {
176
+ switch (mode) {
177
+ case "canvas":
178
+ if (!options.canvasPreviewResult || !options.getResolutionScale || !options.isAtRest || !options.isExporting) return null;
179
+ return new CanvasStatsStrategy({
180
+ canvasPreviewResult: options.canvasPreviewResult,
181
+ adaptiveTracker: options.adaptiveTracker,
182
+ compositionWidth: options.compositionWidth,
183
+ compositionHeight: options.compositionHeight,
184
+ getResolutionScale: options.getResolutionScale,
185
+ isAtRest: options.isAtRest,
186
+ isExporting: options.isExporting
187
+ });
188
+ case "dom":
189
+ case "original": return new DomStatsStrategy({
190
+ timegroup: options.timegroup,
191
+ adaptiveTracker: options.adaptiveTracker
192
+ });
193
+ case "clone":
194
+ case "computed": return null;
195
+ default: return null;
196
+ }
197
+ }
198
+
199
+ //#endregion
200
+ export { createStatsTrackingStrategy };
201
+ //# sourceMappingURL=statsTrackingStrategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statsTrackingStrategy.js","names":[],"sources":["../../src/preview/statsTrackingStrategy.ts"],"sourcesContent":["/**\n * Stats tracking strategy for different presentation modes.\n * \n * Uses strategy pattern to encapsulate mode-specific stats tracking logic,\n * allowing each mode to report what stats it supports and provide its own implementation.\n */\n\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { AdaptiveResolutionTracker } from \"./AdaptiveResolutionTracker.js\";\nimport type { CanvasPreviewResult } from \"./renderTimegroupToCanvas.js\";\nimport type { PreviewPresentationMode } from \"./previewSettings.js\";\n\n/**\n * Stat types that can be tracked.\n */\nexport type StatType =\n | \"fps\"\n | \"renderTime\"\n | \"headroom\"\n | \"resolution\"\n | \"resolutionScale\"\n | \"cpuPressure\"\n | \"adaptiveResolution\";\n\n/**\n * Playback statistics for display.\n */\nexport interface PlaybackStats {\n fps: number;\n avgRenderTime: number | null; // null if not measurable\n headroom: number | null; // null if not applicable\n pressureState: string;\n pressureHistory: string[];\n renderWidth: number;\n renderHeight: number;\n resolutionScale: number | null; // null if not applicable\n samplesAtCurrentScale?: number; // only for adaptive resolution\n canScaleUp?: boolean; // only for adaptive resolution\n canScaleDown?: boolean; // only for adaptive resolution\n}\n\n/**\n * Strategy interface for tracking stats in different presentation modes.\n */\nexport interface StatsTrackingStrategy {\n /** Start tracking stats (called when mode is initialized and stats are enabled) */\n start(): void;\n /** Stop tracking stats (called when mode stops or stats are disabled) */\n stop(): void;\n /** Get current stats, or null if not available */\n getStats(): PlaybackStats | null;\n /** Check if this strategy supports a specific stat type */\n supportsStat(stat: StatType): boolean;\n}\n\n/**\n * Canvas mode stats tracking strategy.\n * Tracks all stats including render time, headroom, resolution scale, and adaptive resolution.\n */\nexport class CanvasStatsStrategy implements StatsTrackingStrategy {\n private canvasPreviewResult: CanvasPreviewResult;\n private adaptiveTracker: AdaptiveResolutionTracker;\n private readonly compositionWidth: number;\n private readonly compositionHeight: number;\n private getResolutionScale: () => number;\n private isAtRest: () => boolean;\n private isExporting: () => boolean;\n \n private animationFrame: number | null = null;\n private lastStatsUpdateTime = 0;\n private currentStats: PlaybackStats | null = null;\n\n constructor(options: {\n canvasPreviewResult: CanvasPreviewResult;\n adaptiveTracker: AdaptiveResolutionTracker;\n compositionWidth: number;\n compositionHeight: number;\n getResolutionScale: () => number;\n isAtRest: () => boolean;\n isExporting: () => boolean;\n }) {\n this.canvasPreviewResult = options.canvasPreviewResult;\n this.adaptiveTracker = options.adaptiveTracker;\n this.compositionWidth = options.compositionWidth;\n this.compositionHeight = options.compositionHeight;\n this.getResolutionScale = options.getResolutionScale;\n this.isAtRest = options.isAtRest;\n this.isExporting = options.isExporting;\n }\n\n start(): void {\n if (this.animationFrame !== null) return;\n \n const { refresh } = this.canvasPreviewResult;\n \n const loop = async (timestamp: number) => {\n if (this.animationFrame === null) return; // Stopped\n \n // Skip refresh during export to avoid wasting CPU\n if (!this.isExporting()) {\n try {\n // Measure actual render time\n const renderStart = performance.now();\n await refresh();\n const renderTime = performance.now() - renderStart;\n \n // Only record frame timing when in motion (playing/scrubbing)\n // This prevents inflated stats at rest and focuses tracking on actual playback\n if (!this.isAtRest()) {\n this.adaptiveTracker.recordFrame(renderTime, timestamp);\n }\n\n // Update playback stats every 100ms (10 times per second)\n if (timestamp - this.lastStatsUpdateTime > 100) {\n this.lastStatsUpdateTime = timestamp;\n // Get CURRENT resolution from the canvas result (may have changed dynamically)\n const currentScale = this.getResolutionScale();\n const renderWidth = Math.floor(this.compositionWidth * currentScale);\n const renderHeight = Math.floor(this.compositionHeight * currentScale);\n this.updateStats(renderWidth, renderHeight, currentScale);\n }\n } catch (e) {\n console.error(\"Canvas stats tracking failed:\", e);\n }\n }\n \n this.animationFrame = requestAnimationFrame(loop);\n };\n \n this.animationFrame = requestAnimationFrame(loop);\n }\n\n stop(): void {\n if (this.animationFrame !== null) {\n cancelAnimationFrame(this.animationFrame);\n this.animationFrame = null;\n }\n this.currentStats = null;\n }\n\n getStats(): PlaybackStats | null {\n return this.currentStats;\n }\n\n supportsStat(_stat: StatType): boolean {\n // Canvas mode supports all stats\n return true;\n }\n\n private updateStats(renderWidth: number, renderHeight: number, resolutionScale: number): void {\n const trackerStats = this.adaptiveTracker.getStats();\n\n this.currentStats = {\n fps: trackerStats.fps,\n avgRenderTime: trackerStats.avgRenderTime,\n headroom: trackerStats.headroom,\n pressureState: trackerStats.pressureState,\n pressureHistory: trackerStats.pressureHistory,\n renderWidth,\n renderHeight,\n resolutionScale,\n samplesAtCurrentScale: trackerStats.samplesAtCurrentScale,\n canScaleUp: trackerStats.canScaleUp,\n canScaleDown: trackerStats.canScaleDown,\n };\n }\n}\n\n/**\n * DOM mode stats tracking strategy.\n * Tracks FPS, resolution, CPU pressure, and frame seek time.\n */\nexport class DomStatsStrategy implements StatsTrackingStrategy {\n private timegroup: EFTimegroup;\n private adaptiveTracker: AdaptiveResolutionTracker;\n \n private animationFrame: number | null = null;\n private lastFrameTime = 0;\n private frameIntervals: number[] = [];\n private lastStatsUpdateTime = 0;\n private currentStats: PlaybackStats | null = null;\n \n // Frame seek time tracking\n private seekStartTime = 0;\n private seekTimes: number[] = [];\n private frameTaskCleanup: (() => void) | null = null;\n \n private readonly ROLLING_WINDOW_SIZE = 30; // ~1 second at 30fps\n private readonly TARGET_FRAME_TIME_MS = 33.33; // 30fps target\n\n constructor(options: {\n timegroup: EFTimegroup;\n adaptiveTracker: AdaptiveResolutionTracker;\n }) {\n this.timegroup = options.timegroup;\n this.adaptiveTracker = options.adaptiveTracker;\n }\n\n start(): void {\n if (this.animationFrame !== null) return;\n \n // Track frame seek times using frameTask callback\n // This measures how long it takes for the timegroup to fully update after a seek\n const frameTaskCallback = async () => {\n if (this.seekStartTime > 0) {\n const seekTime = performance.now() - this.seekStartTime;\n this.seekTimes.push(seekTime);\n if (this.seekTimes.length > this.ROLLING_WINDOW_SIZE) {\n this.seekTimes.shift();\n }\n this.seekStartTime = 0; // Reset after recording\n }\n };\n \n this.timegroup.addFrameTask(frameTaskCallback);\n this.frameTaskCleanup = () => {\n // Note: EFTimegroup doesn't have removeFrameTask, but this is fine\n // The callback will be cleaned up when the timegroup is destroyed\n };\n \n // Track currentTimeMs changes to detect seeks\n let lastCurrentTimeMs = this.timegroup.currentTimeMs;\n const checkSeek = () => {\n const currentTimeMs = this.timegroup.currentTimeMs;\n if (currentTimeMs !== lastCurrentTimeMs) {\n // Seek detected - start timing\n this.seekStartTime = performance.now();\n lastCurrentTimeMs = currentTimeMs;\n }\n };\n \n const loop = (timestamp: number) => {\n if (this.animationFrame === null) return; // Stopped\n \n // Check for seeks\n checkSeek();\n \n // Track frame intervals for FPS calculation\n if (this.lastFrameTime > 0) {\n const interval = timestamp - this.lastFrameTime;\n this.frameIntervals.push(interval);\n if (this.frameIntervals.length > this.ROLLING_WINDOW_SIZE) {\n this.frameIntervals.shift();\n }\n }\n this.lastFrameTime = timestamp;\n \n // Update stats every 100ms (10 times per second)\n if (timestamp - this.lastStatsUpdateTime > 100) {\n this.lastStatsUpdateTime = timestamp;\n this.updateStats();\n }\n \n this.animationFrame = requestAnimationFrame(loop);\n };\n \n this.animationFrame = requestAnimationFrame(loop);\n }\n\n stop(): void {\n if (this.animationFrame !== null) {\n cancelAnimationFrame(this.animationFrame);\n this.animationFrame = null;\n }\n if (this.frameTaskCleanup) {\n this.frameTaskCleanup();\n this.frameTaskCleanup = null;\n }\n this.lastFrameTime = 0;\n this.frameIntervals = [];\n this.seekStartTime = 0;\n this.seekTimes = [];\n this.currentStats = null;\n }\n\n getStats(): PlaybackStats | null {\n return this.currentStats;\n }\n\n supportsStat(stat: StatType): boolean {\n // DOM mode supports: fps, resolution, cpuPressure, renderTime (seek time), headroom\n // Does NOT support: resolutionScale, adaptiveResolution\n return stat === \"fps\" || stat === \"resolution\" || stat === \"cpuPressure\" || stat === \"renderTime\" || stat === \"headroom\";\n }\n\n private updateStats(): void {\n // Calculate FPS from frame intervals\n const avgFrameInterval = this.frameIntervals.length > 0\n ? this.frameIntervals.reduce((a, b) => a + b, 0) / this.frameIntervals.length\n : 16.67;\n const fps = avgFrameInterval > 0 ? 1000 / avgFrameInterval : 0;\n \n // Calculate average seek time (frame update time)\n const avgSeekTime = this.seekTimes.length > 0\n ? this.seekTimes.reduce((a, b) => a + b, 0) / this.seekTimes.length\n : 0;\n \n // Calculate headroom (positive = faster than target, negative = slower)\n const headroom = avgSeekTime > 0\n ? this.TARGET_FRAME_TIME_MS - avgSeekTime\n : 0;\n \n // Get CPU pressure from adaptive tracker\n const trackerStats = this.adaptiveTracker.getStats();\n \n // Calculate displayed resolution from timegroup bounding rect\n const rect = this.timegroup.getBoundingClientRect();\n const renderWidth = Math.round(rect.width);\n const renderHeight = Math.round(rect.height);\n\n this.currentStats = {\n fps,\n avgRenderTime: avgSeekTime > 0 ? avgSeekTime : null,\n headroom: avgSeekTime > 0 ? headroom : null,\n pressureState: trackerStats.pressureState,\n pressureHistory: trackerStats.pressureHistory,\n renderWidth,\n renderHeight,\n resolutionScale: null, // Not applicable in DOM mode\n };\n }\n}\n\n/**\n * Factory function to create the appropriate stats tracking strategy for a presentation mode.\n * Returns null for modes that don't support stats tracking.\n */\nexport function createStatsTrackingStrategy(\n mode: PreviewPresentationMode,\n options: {\n timegroup: EFTimegroup;\n adaptiveTracker: AdaptiveResolutionTracker;\n canvasPreviewResult?: CanvasPreviewResult | null;\n compositionWidth: number;\n compositionHeight: number;\n getResolutionScale?: () => number;\n isAtRest?: () => boolean;\n isExporting?: () => boolean;\n }\n): StatsTrackingStrategy | null {\n switch (mode) {\n case \"canvas\":\n if (!options.canvasPreviewResult || !options.getResolutionScale || !options.isAtRest || !options.isExporting) {\n return null;\n }\n return new CanvasStatsStrategy({\n canvasPreviewResult: options.canvasPreviewResult,\n adaptiveTracker: options.adaptiveTracker,\n compositionWidth: options.compositionWidth,\n compositionHeight: options.compositionHeight,\n getResolutionScale: options.getResolutionScale,\n isAtRest: options.isAtRest,\n isExporting: options.isExporting,\n });\n \n case \"dom\":\n case \"original\": // \"dom\" maps to \"original\" mode\n return new DomStatsStrategy({\n timegroup: options.timegroup,\n adaptiveTracker: options.adaptiveTracker,\n });\n \n case \"clone\":\n case \"computed\": // \"clone\" maps to \"computed\" mode\n // These modes don't support stats tracking\n return null;\n \n default:\n return null;\n }\n}\n"],"mappings":";;;;;AA2DA,IAAa,sBAAb,MAAkE;CAahE,YAAY,SAQT;wBAZqC;6BACV;sBACe;AAW3C,OAAK,sBAAsB,QAAQ;AACnC,OAAK,kBAAkB,QAAQ;AAC/B,OAAK,mBAAmB,QAAQ;AAChC,OAAK,oBAAoB,QAAQ;AACjC,OAAK,qBAAqB,QAAQ;AAClC,OAAK,WAAW,QAAQ;AACxB,OAAK,cAAc,QAAQ;;CAG7B,QAAc;AACZ,MAAI,KAAK,mBAAmB,KAAM;EAElC,MAAM,EAAE,YAAY,KAAK;EAEzB,MAAM,OAAO,OAAO,cAAsB;AACxC,OAAI,KAAK,mBAAmB,KAAM;AAGlC,OAAI,CAAC,KAAK,aAAa,CACrB,KAAI;IAEF,MAAM,cAAc,YAAY,KAAK;AACrC,UAAM,SAAS;IACf,MAAM,aAAa,YAAY,KAAK,GAAG;AAIvC,QAAI,CAAC,KAAK,UAAU,CAClB,MAAK,gBAAgB,YAAY,YAAY,UAAU;AAIzD,QAAI,YAAY,KAAK,sBAAsB,KAAK;AAC9C,UAAK,sBAAsB;KAE3B,MAAM,eAAe,KAAK,oBAAoB;KAC9C,MAAM,cAAc,KAAK,MAAM,KAAK,mBAAmB,aAAa;KACpE,MAAM,eAAe,KAAK,MAAM,KAAK,oBAAoB,aAAa;AACtE,UAAK,YAAY,aAAa,cAAc,aAAa;;YAEpD,GAAG;AACV,YAAQ,MAAM,iCAAiC,EAAE;;AAIrD,QAAK,iBAAiB,sBAAsB,KAAK;;AAGnD,OAAK,iBAAiB,sBAAsB,KAAK;;CAGnD,OAAa;AACX,MAAI,KAAK,mBAAmB,MAAM;AAChC,wBAAqB,KAAK,eAAe;AACzC,QAAK,iBAAiB;;AAExB,OAAK,eAAe;;CAGtB,WAAiC;AAC/B,SAAO,KAAK;;CAGd,aAAa,OAA0B;AAErC,SAAO;;CAGT,AAAQ,YAAY,aAAqB,cAAsB,iBAA+B;EAC5F,MAAM,eAAe,KAAK,gBAAgB,UAAU;AAEpD,OAAK,eAAe;GAClB,KAAK,aAAa;GAClB,eAAe,aAAa;GAC5B,UAAU,aAAa;GACvB,eAAe,aAAa;GAC5B,iBAAiB,aAAa;GAC9B;GACA;GACA;GACA,uBAAuB,aAAa;GACpC,YAAY,aAAa;GACzB,cAAc,aAAa;GAC5B;;;;;;;AAQL,IAAa,mBAAb,MAA+D;CAkB7D,YAAY,SAGT;wBAjBqC;uBAChB;wBACW,EAAE;6BACP;sBACe;uBAGrB;mBACM,EAAE;0BACgB;6BAET;8BACC;AAMtC,OAAK,YAAY,QAAQ;AACzB,OAAK,kBAAkB,QAAQ;;CAGjC,QAAc;AACZ,MAAI,KAAK,mBAAmB,KAAM;EAIlC,MAAM,oBAAoB,YAAY;AACpC,OAAI,KAAK,gBAAgB,GAAG;IAC1B,MAAM,WAAW,YAAY,KAAK,GAAG,KAAK;AAC1C,SAAK,UAAU,KAAK,SAAS;AAC7B,QAAI,KAAK,UAAU,SAAS,KAAK,oBAC/B,MAAK,UAAU,OAAO;AAExB,SAAK,gBAAgB;;;AAIzB,OAAK,UAAU,aAAa,kBAAkB;AAC9C,OAAK,yBAAyB;EAM9B,IAAI,oBAAoB,KAAK,UAAU;EACvC,MAAM,kBAAkB;GACtB,MAAM,gBAAgB,KAAK,UAAU;AACrC,OAAI,kBAAkB,mBAAmB;AAEvC,SAAK,gBAAgB,YAAY,KAAK;AACtC,wBAAoB;;;EAIxB,MAAM,QAAQ,cAAsB;AAClC,OAAI,KAAK,mBAAmB,KAAM;AAGlC,cAAW;AAGX,OAAI,KAAK,gBAAgB,GAAG;IAC1B,MAAM,WAAW,YAAY,KAAK;AAClC,SAAK,eAAe,KAAK,SAAS;AAClC,QAAI,KAAK,eAAe,SAAS,KAAK,oBACpC,MAAK,eAAe,OAAO;;AAG/B,QAAK,gBAAgB;AAGrB,OAAI,YAAY,KAAK,sBAAsB,KAAK;AAC9C,SAAK,sBAAsB;AAC3B,SAAK,aAAa;;AAGpB,QAAK,iBAAiB,sBAAsB,KAAK;;AAGnD,OAAK,iBAAiB,sBAAsB,KAAK;;CAGnD,OAAa;AACX,MAAI,KAAK,mBAAmB,MAAM;AAChC,wBAAqB,KAAK,eAAe;AACzC,QAAK,iBAAiB;;AAExB,MAAI,KAAK,kBAAkB;AACzB,QAAK,kBAAkB;AACvB,QAAK,mBAAmB;;AAE1B,OAAK,gBAAgB;AACrB,OAAK,iBAAiB,EAAE;AACxB,OAAK,gBAAgB;AACrB,OAAK,YAAY,EAAE;AACnB,OAAK,eAAe;;CAGtB,WAAiC;AAC/B,SAAO,KAAK;;CAGd,aAAa,MAAyB;AAGpC,SAAO,SAAS,SAAS,SAAS,gBAAgB,SAAS,iBAAiB,SAAS,gBAAgB,SAAS;;CAGhH,AAAQ,cAAoB;EAE1B,MAAM,mBAAmB,KAAK,eAAe,SAAS,IAClD,KAAK,eAAe,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,eAAe,SACrE;EACJ,MAAM,MAAM,mBAAmB,IAAI,MAAO,mBAAmB;EAG7D,MAAM,cAAc,KAAK,UAAU,SAAS,IACxC,KAAK,UAAU,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,UAAU,SAC3D;EAGJ,MAAM,WAAW,cAAc,IAC3B,KAAK,uBAAuB,cAC5B;EAGJ,MAAM,eAAe,KAAK,gBAAgB,UAAU;EAGpD,MAAM,OAAO,KAAK,UAAU,uBAAuB;EACnD,MAAM,cAAc,KAAK,MAAM,KAAK,MAAM;EAC1C,MAAM,eAAe,KAAK,MAAM,KAAK,OAAO;AAE5C,OAAK,eAAe;GAClB;GACA,eAAe,cAAc,IAAI,cAAc;GAC/C,UAAU,cAAc,IAAI,WAAW;GACvC,eAAe,aAAa;GAC5B,iBAAiB,aAAa;GAC9B;GACA;GACA,iBAAiB;GAClB;;;;;;;AAQL,SAAgB,4BACd,MACA,SAU8B;AAC9B,SAAQ,MAAR;EACE,KAAK;AACH,OAAI,CAAC,QAAQ,uBAAuB,CAAC,QAAQ,sBAAsB,CAAC,QAAQ,YAAY,CAAC,QAAQ,YAC/F,QAAO;AAET,UAAO,IAAI,oBAAoB;IAC7B,qBAAqB,QAAQ;IAC7B,iBAAiB,QAAQ;IACzB,kBAAkB,QAAQ;IAC1B,mBAAmB,QAAQ;IAC3B,oBAAoB,QAAQ;IAC5B,UAAU,QAAQ;IAClB,aAAa,QAAQ;IACtB,CAAC;EAEJ,KAAK;EACL,KAAK,WACH,QAAO,IAAI,iBAAiB;GAC1B,WAAW,QAAQ;GACnB,iBAAiB,QAAQ;GAC1B,CAAC;EAEJ,KAAK;EACL,KAAK,WAEH,QAAO;EAET,QACE,QAAO"}
@@ -0,0 +1,52 @@
1
+ //#region src/preview/thumbnailCacheSettings.ts
2
+ /**
3
+ * Thumbnail cache settings module with localStorage persistence.
4
+ * Manages configuration for the thumbnail cache system.
5
+ */
6
+ const STORAGE_KEY_MAX_SIZE = "ef-thumbnail-cache-max-size";
7
+ /**
8
+ * Default maximum cache size (number of items).
9
+ */
10
+ const DEFAULT_MAX_SIZE = 1e3;
11
+ /**
12
+ * Get the current thumbnail cache max size.
13
+ * Defaults to 1000 if not set.
14
+ */
15
+ function getThumbnailCacheMaxSize() {
16
+ try {
17
+ const stored = localStorage.getItem(STORAGE_KEY_MAX_SIZE);
18
+ if (stored !== null) {
19
+ const parsed = parseInt(stored, 10);
20
+ if (!isNaN(parsed) && parsed > 0) return parsed;
21
+ }
22
+ return DEFAULT_MAX_SIZE;
23
+ } catch {
24
+ return DEFAULT_MAX_SIZE;
25
+ }
26
+ }
27
+ /**
28
+ * Set the thumbnail cache max size.
29
+ * Persists to localStorage and dispatches a change event.
30
+ */
31
+ function setThumbnailCacheMaxSize(size) {
32
+ if (size <= 0) throw new Error("Cache size must be greater than 0");
33
+ try {
34
+ localStorage.setItem(STORAGE_KEY_MAX_SIZE, String(size));
35
+ } catch {}
36
+ window.dispatchEvent(new CustomEvent("ef-thumbnail-cache-settings-changed", { detail: { maxSize: size } }));
37
+ }
38
+ /**
39
+ * Subscribe to thumbnail cache settings changes.
40
+ * @returns Unsubscribe function
41
+ */
42
+ function onThumbnailCacheSettingsChanged(callback) {
43
+ const handler = (event) => {
44
+ callback(event.detail);
45
+ };
46
+ window.addEventListener("ef-thumbnail-cache-settings-changed", handler);
47
+ return () => window.removeEventListener("ef-thumbnail-cache-settings-changed", handler);
48
+ }
49
+
50
+ //#endregion
51
+ export { getThumbnailCacheMaxSize, onThumbnailCacheSettingsChanged, setThumbnailCacheMaxSize };
52
+ //# sourceMappingURL=thumbnailCacheSettings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thumbnailCacheSettings.js","names":[],"sources":["../../src/preview/thumbnailCacheSettings.ts"],"sourcesContent":["/**\n * Thumbnail cache settings module with localStorage persistence.\n * Manages configuration for the thumbnail cache system.\n */\n\nconst STORAGE_KEY_MAX_SIZE = \"ef-thumbnail-cache-max-size\";\n\n/**\n * Default maximum cache size (number of items).\n */\nconst DEFAULT_MAX_SIZE = 1000;\n\n/**\n * Get the current thumbnail cache max size.\n * Defaults to 1000 if not set.\n */\nexport function getThumbnailCacheMaxSize(): number {\n try {\n const stored = localStorage.getItem(STORAGE_KEY_MAX_SIZE);\n if (stored !== null) {\n const parsed = parseInt(stored, 10);\n if (!isNaN(parsed) && parsed > 0) {\n return parsed;\n }\n }\n return DEFAULT_MAX_SIZE;\n } catch {\n return DEFAULT_MAX_SIZE;\n }\n}\n\n/**\n * Set the thumbnail cache max size.\n * Persists to localStorage and dispatches a change event.\n */\nexport function setThumbnailCacheMaxSize(size: number): void {\n if (size <= 0) {\n throw new Error(\"Cache size must be greater than 0\");\n }\n \n try {\n localStorage.setItem(STORAGE_KEY_MAX_SIZE, String(size));\n } catch {\n // localStorage not available\n }\n \n // Dispatch event so components can react to the change\n window.dispatchEvent(new CustomEvent(\"ef-thumbnail-cache-settings-changed\", {\n detail: { maxSize: size }\n }));\n}\n\n/**\n * Subscribe to thumbnail cache settings changes.\n * @returns Unsubscribe function\n */\nexport function onThumbnailCacheSettingsChanged(\n callback: (detail: ThumbnailCacheSettingsChangedDetail) => void\n): () => void {\n const handler = (event: Event) => {\n callback((event as CustomEvent).detail);\n };\n window.addEventListener(\"ef-thumbnail-cache-settings-changed\", handler);\n return () => window.removeEventListener(\"ef-thumbnail-cache-settings-changed\", handler);\n}\n\n/**\n * Detail object for thumbnail cache settings change events.\n */\nexport interface ThumbnailCacheSettingsChangedDetail {\n maxSize?: number;\n}\n"],"mappings":";;;;;AAKA,MAAM,uBAAuB;;;;AAK7B,MAAM,mBAAmB;;;;;AAMzB,SAAgB,2BAAmC;AACjD,KAAI;EACF,MAAM,SAAS,aAAa,QAAQ,qBAAqB;AACzD,MAAI,WAAW,MAAM;GACnB,MAAM,SAAS,SAAS,QAAQ,GAAG;AACnC,OAAI,CAAC,MAAM,OAAO,IAAI,SAAS,EAC7B,QAAO;;AAGX,SAAO;SACD;AACN,SAAO;;;;;;;AAQX,SAAgB,yBAAyB,MAAoB;AAC3D,KAAI,QAAQ,EACV,OAAM,IAAI,MAAM,oCAAoC;AAGtD,KAAI;AACF,eAAa,QAAQ,sBAAsB,OAAO,KAAK,CAAC;SAClD;AAKR,QAAO,cAAc,IAAI,YAAY,uCAAuC,EAC1E,QAAQ,EAAE,SAAS,MAAM,EAC1B,CAAC,CAAC;;;;;;AAOL,SAAgB,gCACd,UACY;CACZ,MAAM,WAAW,UAAiB;AAChC,WAAU,MAAsB,OAAO;;AAEzC,QAAO,iBAAiB,uCAAuC,QAAQ;AACvE,cAAa,OAAO,oBAAoB,uCAAuC,QAAQ"}
@@ -0,0 +1,178 @@
1
+ //#region src/preview/workers/WorkerPool.ts
2
+ /**
3
+ * Worker pool for parallel task execution.
4
+ * Manages a pool of workers and distributes tasks across them.
5
+ */
6
+ const WORKER_TASK_TIMEOUT_MS = 3e4;
7
+ const WORKER_INIT_TEST_TIMEOUT_MS = 2e3;
8
+ var WorkerPool = class {
9
+ constructor(workerScriptUrl, poolSize = navigator.hardwareConcurrency || 4) {
10
+ this.poolSize = poolSize;
11
+ this.workers = [];
12
+ this.availableWorkers = [];
13
+ this.taskQueue = [];
14
+ this.isTerminated = false;
15
+ this.taskIdCounter = 0;
16
+ this.workerUrl = workerScriptUrl;
17
+ if (this.hasBrowserSupport()) this.initializeWorkers();
18
+ }
19
+ /**
20
+ * Check if browser supports workers (before initialization).
21
+ */
22
+ hasBrowserSupport() {
23
+ return typeof Worker !== "undefined" && typeof OffscreenCanvas !== "undefined" && typeof createImageBitmap !== "undefined";
24
+ }
25
+ initializeWorkers() {
26
+ for (let i = 0; i < this.poolSize; i++) try {
27
+ const worker = new Worker(this.workerUrl, { type: "module" });
28
+ let testTimeout = null;
29
+ let testHandler = null;
30
+ const cleanupTest = () => {
31
+ if (testTimeout !== null) {
32
+ clearTimeout(testTimeout);
33
+ testTimeout = null;
34
+ }
35
+ if (testHandler !== null) {
36
+ worker.removeEventListener("message", testHandler);
37
+ testHandler = null;
38
+ }
39
+ };
40
+ testTimeout = window.setTimeout(() => {
41
+ cleanupTest();
42
+ }, WORKER_INIT_TEST_TIMEOUT_MS);
43
+ testHandler = (event) => {
44
+ if (event.data && typeof event.data === "string" && event.data.includes("encoderWorker")) cleanupTest();
45
+ };
46
+ worker.addEventListener("message", testHandler);
47
+ worker.onerror = (error) => {
48
+ cleanupTest();
49
+ console.error(`[WorkerPool] Worker ${i} error:`, {
50
+ message: error.message,
51
+ filename: error.filename,
52
+ lineno: error.lineno,
53
+ colno: error.colno
54
+ });
55
+ };
56
+ worker.onmessageerror = (error) => {
57
+ console.error(`[WorkerPool] Worker ${i} message error:`, error);
58
+ };
59
+ this.workers.push(worker);
60
+ this.availableWorkers.push(worker);
61
+ } catch (error) {
62
+ console.error(`[WorkerPool] Failed to create worker ${i}:`, error instanceof Error ? error.message : String(error));
63
+ }
64
+ if (this.workers.length === 0) {
65
+ console.error(`[WorkerPool] Failed to create any workers. URL: ${this.workerUrl}`);
66
+ console.error(`[WorkerPool] Browser support check:`, {
67
+ Worker: typeof Worker !== "undefined",
68
+ OffscreenCanvas: typeof OffscreenCanvas !== "undefined",
69
+ createImageBitmap: typeof createImageBitmap !== "undefined"
70
+ });
71
+ }
72
+ }
73
+ /**
74
+ * Get the number of workers in the pool.
75
+ */
76
+ get workerCount() {
77
+ return this.workers.length;
78
+ }
79
+ /**
80
+ * Check if workers are available and initialized.
81
+ */
82
+ isAvailable() {
83
+ return this.hasBrowserSupport() && this.workers.length > 0 && !this.isTerminated;
84
+ }
85
+ /**
86
+ * Execute a task using an available worker from the pool.
87
+ */
88
+ async execute(task) {
89
+ if (this.isTerminated) throw new Error("WorkerPool has been terminated");
90
+ if (!this.isAvailable()) throw new Error("Workers not available");
91
+ return new Promise((resolve, reject) => {
92
+ this.taskQueue.push({
93
+ resolve,
94
+ reject,
95
+ task
96
+ });
97
+ this.processQueue();
98
+ });
99
+ }
100
+ processQueue() {
101
+ while (this.availableWorkers.length > 0 && this.taskQueue.length > 0) {
102
+ const worker = this.availableWorkers.shift();
103
+ const queuedTask = this.taskQueue.shift();
104
+ if (!worker || !queuedTask) break;
105
+ const { resolve, reject, task } = queuedTask;
106
+ task(worker).then((result) => {
107
+ resolve(result);
108
+ this.availableWorkers.push(worker);
109
+ this.processQueue();
110
+ }).catch((error) => {
111
+ reject(error instanceof Error ? error : new Error(String(error)));
112
+ this.availableWorkers.push(worker);
113
+ this.processQueue();
114
+ });
115
+ }
116
+ }
117
+ /**
118
+ * Terminate all workers and clear the task queue.
119
+ */
120
+ terminate() {
121
+ this.isTerminated = true;
122
+ for (const { reject } of this.taskQueue) reject(/* @__PURE__ */ new Error("WorkerPool terminated"));
123
+ this.taskQueue = [];
124
+ for (const worker of this.workers) worker.terminate();
125
+ this.workers = [];
126
+ this.availableWorkers = [];
127
+ }
128
+ };
129
+ /**
130
+ * Helper function to encode a canvas using a worker.
131
+ */
132
+ async function encodeCanvasInWorker(worker, canvas, preserveAlpha) {
133
+ return new Promise((resolve, reject) => {
134
+ const taskId = `task-${Date.now()}-${Math.random()}-${performance.now()}`;
135
+ performance.now();
136
+ let timeoutId = null;
137
+ const cleanup = () => {
138
+ if (timeoutId !== null) {
139
+ clearTimeout(timeoutId);
140
+ timeoutId = null;
141
+ }
142
+ worker.removeEventListener("message", messageHandler);
143
+ worker.removeEventListener("messageerror", messageErrorHandler);
144
+ };
145
+ const messageHandler = (event) => {
146
+ const result = event.data;
147
+ if (result.taskId === taskId) {
148
+ cleanup();
149
+ if (result.error) reject(/* @__PURE__ */ new Error(`Worker encoding failed: ${result.error}`));
150
+ else resolve(result.dataUrl);
151
+ }
152
+ };
153
+ const messageErrorHandler = () => {
154
+ cleanup();
155
+ reject(/* @__PURE__ */ new Error("Worker message error"));
156
+ };
157
+ worker.addEventListener("message", messageHandler);
158
+ worker.addEventListener("messageerror", messageErrorHandler);
159
+ timeoutId = window.setTimeout(() => {
160
+ cleanup();
161
+ reject(/* @__PURE__ */ new Error("Worker task timed out"));
162
+ }, WORKER_TASK_TIMEOUT_MS);
163
+ createImageBitmap(canvas).then((bitmap) => {
164
+ worker.postMessage({
165
+ taskId,
166
+ bitmap,
167
+ preserveAlpha
168
+ }, [bitmap]);
169
+ }).catch((error) => {
170
+ cleanup();
171
+ reject(error instanceof Error ? error : new Error(String(error)));
172
+ });
173
+ });
174
+ }
175
+
176
+ //#endregion
177
+ export { WorkerPool, encodeCanvasInWorker };
178
+ //# sourceMappingURL=WorkerPool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkerPool.js","names":["poolSize: number","testTimeout: number | null","testHandler: ((event: MessageEvent) => void) | null","timeoutId: number | null"],"sources":["../../../src/preview/workers/WorkerPool.ts"],"sourcesContent":["/**\n * Worker pool for parallel task execution.\n * Manages a pool of workers and distributes tasks across them.\n */\n\n// Constants\nconst WORKER_TASK_TIMEOUT_MS = 30000;\nconst WORKER_INIT_TEST_TIMEOUT_MS = 2000;\n\ninterface QueuedTask<T> {\n resolve: (value: T) => void;\n reject: (error: Error) => void;\n task: (worker: Worker) => Promise<T>;\n}\n\nexport class WorkerPool {\n private workers: Worker[] = [];\n private availableWorkers: Worker[] = [];\n private taskQueue: QueuedTask<unknown>[] = [];\n private isTerminated = false;\n private workerUrl: string;\n private taskIdCounter = 0;\n\n constructor(\n workerScriptUrl: string,\n private poolSize: number = navigator.hardwareConcurrency || 4,\n ) {\n this.workerUrl = workerScriptUrl;\n\n // Check browser support first, then initialize workers\n if (this.hasBrowserSupport()) {\n this.initializeWorkers();\n }\n }\n\n /**\n * Check if browser supports workers (before initialization).\n */\n private hasBrowserSupport(): boolean {\n return (\n typeof Worker !== \"undefined\" &&\n typeof OffscreenCanvas !== \"undefined\" &&\n typeof createImageBitmap !== \"undefined\"\n );\n }\n\n private initializeWorkers(): void {\n for (let i = 0; i < this.poolSize; i++) {\n try {\n // Vite processes worker files - the URL should point to the actual worker file\n // When using ?worker_file&type=module, Vite will serve the processed worker\n const worker = new Worker(this.workerUrl, { type: \"module\" });\n \n // Test if worker is responding - cleanup handler after confirmation\n let testTimeout: number | null = null;\n let testHandler: ((event: MessageEvent) => void) | null = null;\n \n const cleanupTest = () => {\n if (testTimeout !== null) {\n clearTimeout(testTimeout);\n testTimeout = null;\n }\n if (testHandler !== null) {\n worker.removeEventListener(\"message\", testHandler);\n testHandler = null;\n }\n };\n \n testTimeout = window.setTimeout(() => {\n cleanupTest();\n }, WORKER_INIT_TEST_TIMEOUT_MS);\n \n testHandler = (event: MessageEvent) => {\n // Check if this is a test response (worker startup message)\n if (event.data && typeof event.data === \"string\" && event.data.includes(\"encoderWorker\")) {\n cleanupTest();\n }\n };\n worker.addEventListener(\"message\", testHandler);\n \n worker.onerror = (error) => {\n cleanupTest();\n console.error(`[WorkerPool] Worker ${i} error:`, {\n message: error.message,\n filename: error.filename,\n lineno: error.lineno,\n colno: error.colno,\n });\n };\n worker.onmessageerror = (error) => {\n console.error(`[WorkerPool] Worker ${i} message error:`, error);\n };\n this.workers.push(worker);\n this.availableWorkers.push(worker);\n } catch (error) {\n console.error(`[WorkerPool] Failed to create worker ${i}:`, error instanceof Error ? error.message : String(error));\n }\n }\n if (this.workers.length === 0) {\n console.error(`[WorkerPool] Failed to create any workers. URL: ${this.workerUrl}`);\n console.error(`[WorkerPool] Browser support check:`, {\n Worker: typeof Worker !== \"undefined\",\n OffscreenCanvas: typeof OffscreenCanvas !== \"undefined\",\n createImageBitmap: typeof createImageBitmap !== \"undefined\",\n });\n }\n }\n\n /**\n * Get the number of workers in the pool.\n */\n get workerCount(): number {\n return this.workers.length;\n }\n\n /**\n * Check if workers are available and initialized.\n */\n isAvailable(): boolean {\n return (\n this.hasBrowserSupport() &&\n this.workers.length > 0 &&\n !this.isTerminated\n );\n }\n\n /**\n * Execute a task using an available worker from the pool.\n */\n async execute<T>(task: (worker: Worker) => Promise<T>): Promise<T> {\n if (this.isTerminated) {\n throw new Error(\"WorkerPool has been terminated\");\n }\n\n // If workers aren't available, this will be handled by the caller's fallback\n if (!this.isAvailable()) {\n throw new Error(\"Workers not available\");\n }\n\n return new Promise<T>((resolve, reject) => {\n this.taskQueue.push({ \n resolve: resolve as (value: unknown) => void, \n reject, \n task \n });\n this.processQueue();\n });\n }\n\n private processQueue(): void {\n // Process tasks while we have available workers and queued tasks\n while (this.availableWorkers.length > 0 && this.taskQueue.length > 0) {\n const worker = this.availableWorkers.shift();\n const queuedTask = this.taskQueue.shift();\n \n if (!worker || !queuedTask) {\n // Safety check - should not happen but prevents crashes\n break;\n }\n\n const { resolve, reject, task } = queuedTask;\n\n // Execute the task\n task(worker)\n .then((result) => {\n resolve(result);\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n })\n .catch((error) => {\n reject(error instanceof Error ? error : new Error(String(error)));\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n });\n }\n }\n\n /**\n * Terminate all workers and clear the task queue.\n */\n terminate(): void {\n this.isTerminated = true;\n\n // Reject all pending tasks\n for (const { reject } of this.taskQueue) {\n reject(new Error(\"WorkerPool terminated\"));\n }\n this.taskQueue = [];\n\n // Terminate all workers\n for (const worker of this.workers) {\n worker.terminate();\n }\n this.workers = [];\n this.availableWorkers = [];\n }\n}\n\n/**\n * Helper function to encode a canvas using a worker.\n */\nexport async function encodeCanvasInWorker(\n worker: Worker,\n canvas: HTMLCanvasElement,\n preserveAlpha: boolean,\n): Promise<string> {\n return new Promise<string>((resolve, reject) => {\n const taskId = `task-${Date.now()}-${Math.random()}-${performance.now()}`;\n const startTime = performance.now();\n let timeoutId: number | null = null;\n\n const cleanup = () => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId);\n timeoutId = null;\n }\n worker.removeEventListener(\"message\", messageHandler);\n worker.removeEventListener(\"messageerror\", messageErrorHandler);\n };\n\n const messageHandler = (event: MessageEvent) => {\n const result = event.data as { taskId: string; dataUrl: string; error?: string };\n if (result.taskId === taskId) {\n cleanup();\n if (result.error) {\n reject(new Error(`Worker encoding failed: ${result.error}`));\n } else {\n resolve(result.dataUrl);\n }\n }\n };\n\n const messageErrorHandler = () => {\n cleanup();\n reject(new Error(\"Worker message error\"));\n };\n\n worker.addEventListener(\"message\", messageHandler);\n worker.addEventListener(\"messageerror\", messageErrorHandler);\n\n // Set timeout to detect if worker never responds\n timeoutId = window.setTimeout(() => {\n cleanup();\n reject(new Error(\"Worker task timed out\"));\n }, WORKER_TASK_TIMEOUT_MS);\n\n // Create ImageBitmap from canvas\n createImageBitmap(canvas)\n .then((bitmap) => {\n // Transfer bitmap to worker (zero-copy)\n worker.postMessage(\n {\n taskId,\n bitmap,\n preserveAlpha,\n },\n [bitmap],\n );\n })\n .catch((error) => {\n cleanup();\n reject(error instanceof Error ? error : new Error(String(error)));\n });\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { __toESM } from "../_virtual/rolldown_runtime.js";
2
+ import { require_react } from "../node_modules/react/index.js";
3
+ import { require_jsx_runtime } from "../node_modules/react/jsx-runtime.js";
4
+
5
+ //#region src/sandbox/PlaybackControls.tsx
6
+ var import_react = /* @__PURE__ */ __toESM(require_react());
7
+ var import_jsx_runtime = /* @__PURE__ */ __toESM(require_jsx_runtime());
8
+
9
+ //#endregion
10
+ //# sourceMappingURL=PlaybackControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PlaybackControls.js","names":[],"sources":["../../src/sandbox/PlaybackControls.tsx"],"sourcesContent":["import React, { useState, useEffect, useRef, useCallback } from \"react\";\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\n\nexport interface PlaybackControlsProps {\n /**\n * The timegroup element to control.\n * If null, controls are disabled.\n */\n timegroup: EFTimegroup | null;\n \n /**\n * Playback mode:\n * - \"auto\": Play/pause with continuous scrubbing\n * - \"step\": Discrete keyframe navigation\n */\n mode?: \"auto\" | \"step\";\n \n /**\n * Callback when mode changes.\n */\n onModeChange?: (mode: \"auto\" | \"step\") => void;\n \n /**\n * Keyframes for step mode (array of time values in ms).\n * If not provided, step mode divides duration into 10 equal steps.\n */\n keyframes?: number[];\n}\n\nfunction formatTime(ms: number): string {\n const totalSeconds = Math.floor(ms / 1000);\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n const centiseconds = Math.floor((ms % 1000) / 10);\n return `${minutes}:${seconds.toString().padStart(2, \"0\")}.${centiseconds.toString().padStart(2, \"0\")}`;\n}\n\nexport function PlaybackControls({\n timegroup,\n mode = \"auto\",\n onModeChange,\n keyframes,\n}: PlaybackControlsProps) {\n const [isPlaying, setIsPlaying] = useState(false);\n const [isLooping, setIsLooping] = useState(false);\n const [currentTimeMs, setCurrentTimeMs] = useState(0);\n const [durationMs, setDurationMs] = useState(0);\n const [isDragging, setIsDragging] = useState(false);\n const [currentStepIndex, setCurrentStepIndex] = useState(0);\n \n const scrubberRef = useRef<HTMLDivElement>(null);\n const updateIntervalRef = useRef<number | null>(null);\n\n // Calculate step keyframes\n const steps = React.useMemo(() => {\n if (keyframes && keyframes.length > 0) {\n return keyframes;\n }\n // Default: 11 steps (0%, 10%, 20%, ..., 100%)\n const stepCount = 11;\n const stepSize = durationMs / (stepCount - 1);\n return Array.from({ length: stepCount }, (_, i) => i * stepSize);\n }, [keyframes, durationMs]);\n\n // Update duration when timegroup changes\n useEffect(() => {\n if (timegroup) {\n setDurationMs(timegroup.durationMs);\n setCurrentTimeMs(timegroup.currentTimeMs);\n } else {\n setDurationMs(0);\n setCurrentTimeMs(0);\n }\n }, [timegroup]);\n\n // Update current time periodically\n useEffect(() => {\n if (!timegroup) return;\n\n const updateTime = () => {\n if (!isDragging) {\n setCurrentTimeMs(timegroup.currentTimeMs);\n setIsPlaying(timegroup.playbackController?.playing ?? false);\n }\n };\n\n updateIntervalRef.current = window.setInterval(updateTime, 50);\n\n return () => {\n if (updateIntervalRef.current) {\n window.clearInterval(updateIntervalRef.current);\n }\n };\n }, [timegroup, isDragging]);\n\n // Update step index when time changes in step mode\n useEffect(() => {\n if (mode === \"step\" && steps.length > 0) {\n // Find closest step\n let closestIndex = 0;\n const firstStep = steps[0];\n if (firstStep !== undefined) {\n let closestDiff = Math.abs(currentTimeMs - firstStep);\n for (let i = 1; i < steps.length; i++) {\n const step = steps[i];\n if (step !== undefined) {\n const diff = Math.abs(currentTimeMs - step);\n if (diff < closestDiff) {\n closestDiff = diff;\n closestIndex = i;\n }\n }\n }\n setCurrentStepIndex(closestIndex);\n }\n }\n }, [currentTimeMs, mode, steps]);\n\n const handlePlayPause = useCallback(async () => {\n if (!timegroup) return;\n \n if (isPlaying) {\n timegroup.pause();\n setIsPlaying(false);\n } else {\n // Handle AudioContext for mobile devices\n if (timegroup.playbackController) {\n try {\n const audioContext = new AudioContext({ latencyHint: \"playback\" });\n audioContext.resume();\n timegroup.playbackController.setPendingAudioContext(audioContext);\n } catch (error) {\n console.warn(\"Failed to create/resume AudioContext:\", error);\n }\n }\n await timegroup.play();\n setIsPlaying(true);\n }\n }, [timegroup, isPlaying]);\n\n const handleLoopToggle = useCallback(() => {\n if (!timegroup?.playbackController) return;\n \n const newLooping = !isLooping;\n // @ts-expect-error - loop property is read-only in types but writable at runtime\n timegroup.playbackController.loop = newLooping;\n setIsLooping(newLooping);\n }, [timegroup, isLooping]);\n\n const handleScrubStart = useCallback((e: React.MouseEvent | React.TouchEvent) => {\n if (!timegroup || !scrubberRef.current) return;\n \n setIsDragging(true);\n handleScrub(e);\n }, [timegroup]);\n\n const handleScrub = useCallback((e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent) => {\n if (!timegroup || !scrubberRef.current || !isDragging) return;\n \n const scrubber = scrubberRef.current;\n if (!scrubber) return;\n \n const rect = scrubber.getBoundingClientRect();\n const touches = \"touches\" in e ? e.touches : null;\n const clientX = touches && touches[0] \n ? touches[0].clientX \n : (e as MouseEvent).clientX;\n const x = clientX - rect.left;\n const percent = Math.max(0, Math.min(1, x / rect.width));\n const targetTimeMs = percent * durationMs;\n \n timegroup.currentTime = targetTimeMs / 1000;\n setCurrentTimeMs(targetTimeMs);\n }, [timegroup, durationMs, isDragging]);\n\n const handleScrubEnd = useCallback(() => {\n setIsDragging(false);\n }, []);\n\n // Global mouse/touch events for scrubbing\n useEffect(() => {\n if (isDragging) {\n const handleMove = (e: MouseEvent | TouchEvent) => handleScrub(e);\n const handleUp = () => handleScrubEnd();\n \n document.addEventListener(\"mousemove\", handleMove);\n document.addEventListener(\"mouseup\", handleUp);\n document.addEventListener(\"touchmove\", handleMove);\n document.addEventListener(\"touchend\", handleUp);\n \n return () => {\n document.removeEventListener(\"mousemove\", handleMove);\n document.removeEventListener(\"mouseup\", handleUp);\n document.removeEventListener(\"touchmove\", handleMove);\n document.removeEventListener(\"touchend\", handleUp);\n };\n }\n }, [isDragging, handleScrub, handleScrubEnd]);\n\n const handleStepPrevious = useCallback(() => {\n if (!timegroup || steps.length === 0) return;\n \n const newIndex = Math.max(0, currentStepIndex - 1);\n const step = steps[newIndex];\n if (step !== undefined) {\n timegroup.currentTime = step / 1000;\n setCurrentStepIndex(newIndex);\n setCurrentTimeMs(step);\n }\n }, [timegroup, steps, currentStepIndex]);\n\n const handleStepNext = useCallback(() => {\n if (!timegroup || steps.length === 0) return;\n \n const newIndex = Math.min(steps.length - 1, currentStepIndex + 1);\n const step = steps[newIndex];\n if (step !== undefined) {\n timegroup.currentTime = step / 1000;\n setCurrentStepIndex(newIndex);\n setCurrentTimeMs(step);\n }\n }, [timegroup, steps, currentStepIndex]);\n\n const progress = durationMs > 0 ? (currentTimeMs / durationMs) * 100 : 0;\n const isDisabled = !timegroup;\n\n return (\n <div style={styles.container}>\n {/* Mode Toggle */}\n <div style={styles.modeToggle}>\n <button\n style={{\n ...styles.modeButton,\n ...(mode === \"auto\" ? styles.modeButtonActive : {}),\n }}\n onClick={() => onModeChange?.(\"auto\")}\n disabled={isDisabled}\n >\n Auto\n </button>\n <button\n style={{\n ...styles.modeButton,\n ...(mode === \"step\" ? styles.modeButtonActive : {}),\n }}\n onClick={() => onModeChange?.(\"step\")}\n disabled={isDisabled}\n >\n Step\n </button>\n </div>\n\n {/* Controls */}\n <div style={styles.controls}>\n {mode === \"auto\" ? (\n <>\n {/* Play/Pause */}\n <button\n style={styles.button}\n onClick={handlePlayPause}\n disabled={isDisabled}\n title={isPlaying ? \"Pause\" : \"Play\"}\n >\n {isPlaying ? \"⏸\" : \"▶\"}\n </button>\n\n {/* Loop */}\n <button\n style={{\n ...styles.button,\n ...(isLooping ? styles.buttonActive : {}),\n }}\n onClick={handleLoopToggle}\n disabled={isDisabled}\n title=\"Loop\"\n >\n 🔁\n </button>\n </>\n ) : (\n <>\n {/* Previous Step */}\n <button\n style={styles.button}\n onClick={handleStepPrevious}\n disabled={isDisabled || currentStepIndex === 0}\n title=\"Previous Step\"\n >\n ⏮\n </button>\n\n {/* Step indicator */}\n <span style={styles.stepIndicator}>\n {currentStepIndex + 1} / {steps.length}\n </span>\n\n {/* Next Step */}\n <button\n style={styles.button}\n onClick={handleStepNext}\n disabled={isDisabled || currentStepIndex === steps.length - 1}\n title=\"Next Step\"\n >\n ⏭\n </button>\n </>\n )}\n </div>\n\n {/* Scrubber */}\n <div style={styles.scrubberContainer}>\n <span style={styles.time}>{formatTime(currentTimeMs)}</span>\n <div\n ref={scrubberRef}\n style={{\n ...styles.scrubberTrack,\n ...(isDragging ? styles.scrubberTrackDragging : {}),\n }}\n onMouseDown={handleScrubStart}\n onTouchStart={handleScrubStart}\n >\n <div\n style={{\n ...styles.scrubberProgress,\n width: `${progress}%`,\n }}\n >\n <div style={styles.scrubberHandle} />\n </div>\n </div>\n <span style={styles.time}>{formatTime(durationMs)}</span>\n </div>\n </div>\n );\n}\n\nconst styles: Record<string, React.CSSProperties> = {\n container: {\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"8px\",\n padding: \"8px\",\n background: \"#1f2937\",\n borderTop: \"1px solid #374151\",\n },\n modeToggle: {\n display: \"flex\",\n gap: \"4px\",\n justifyContent: \"center\",\n },\n modeButton: {\n padding: \"4px 12px\",\n fontSize: \"11px\",\n background: \"#374151\",\n border: \"1px solid #4b5563\",\n borderRadius: \"4px\",\n color: \"#d1d5db\",\n cursor: \"pointer\",\n },\n modeButtonActive: {\n background: \"#3b82f6\",\n borderColor: \"#3b82f6\",\n color: \"white\",\n },\n controls: {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: \"8px\",\n },\n button: {\n width: \"32px\",\n height: \"32px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n background: \"transparent\",\n border: \"none\",\n color: \"#e5e7eb\",\n fontSize: \"16px\",\n cursor: \"pointer\",\n borderRadius: \"4px\",\n },\n buttonActive: {\n color: \"#22c55e\",\n },\n stepIndicator: {\n fontSize: \"12px\",\n color: \"#9ca3af\",\n minWidth: \"50px\",\n textAlign: \"center\",\n },\n scrubberContainer: {\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n },\n time: {\n fontSize: \"11px\",\n color: \"#9ca3af\",\n fontFamily: \"'SF Mono', 'Monaco', 'Consolas', monospace\",\n minWidth: \"60px\",\n },\n scrubberTrack: {\n flex: 1,\n height: \"4px\",\n background: \"rgba(255, 255, 255, 0.2)\",\n borderRadius: \"2px\",\n cursor: \"pointer\",\n position: \"relative\",\n },\n scrubberTrackDragging: {\n cursor: \"grabbing\",\n },\n scrubberProgress: {\n height: \"100%\",\n background: \"#3b82f6\",\n borderRadius: \"2px\",\n position: \"relative\",\n transition: \"width 0.05s linear\",\n },\n scrubberHandle: {\n position: \"absolute\",\n right: \"-6px\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n width: \"12px\",\n height: \"12px\",\n background: \"white\",\n borderRadius: \"50%\",\n boxShadow: \"0 2px 4px rgba(0, 0, 0, 0.3)\",\n },\n};\n"],"mappings":""}
@@ -0,0 +1 @@
1
+ import { nothing } from "lit";
@@ -0,0 +1,2 @@
1
+ import "./PlaybackControls.js";
2
+ import "./ScenarioRunner.js";