@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,119 @@
1
+ import { EFAudio } from "../../../elements/EFAudio.js";
2
+ import { EFVideo } from "../../../elements/EFVideo.js";
3
+ import { EFCaptions, EFCaptionsActiveWord } from "../../../elements/EFCaptions.js";
4
+ import { EFImage } from "../../../elements/EFImage.js";
5
+ import { EFText } from "../../../elements/EFText.js";
6
+ import { EFTextSegment } from "../../../elements/EFTextSegment.js";
7
+ import { EFWaveform } from "../../../elements/EFWaveform.js";
8
+ import { shouldRenderElement } from "../../hierarchy/EFHierarchyItem.js";
9
+ import { EFTimegroup } from "../../../elements/EFTimegroup.js";
10
+ import { html, nothing } from "lit";
11
+
12
+ //#region src/gui/timeline/tracks/renderTrackChildren.ts
13
+ function renderTrackChildren(children, pixelsPerMs, hideSelectors, showSelectors, skipRootFiltering = false, enableTrim = false, useAbsolutePosition = false) {
14
+ return children.map((child) => {
15
+ if (!skipRootFiltering && !shouldRenderElement(child, hideSelectors, showSelectors)) return nothing;
16
+ if (child instanceof EFTimegroup) return html`<ef-timegroup-track
17
+ .element=${child}
18
+ pixels-per-ms=${pixelsPerMs}
19
+ ?enable-trim=${enableTrim}
20
+ ?use-absolute-position=${useAbsolutePosition}
21
+ .hideSelectors=${hideSelectors}
22
+ .showSelectors=${showSelectors}
23
+ >
24
+ </ef-timegroup-track>`;
25
+ if (child instanceof EFImage) return html`<ef-image-track
26
+ .element=${child}
27
+ pixels-per-ms=${pixelsPerMs}
28
+ ?enable-trim=${enableTrim}
29
+ ?use-absolute-position=${useAbsolutePosition}
30
+ .hideSelectors=${hideSelectors}
31
+ .showSelectors=${showSelectors}
32
+ ></ef-image-track>`;
33
+ if (child instanceof EFAudio) return html`<ef-audio-track
34
+ .element=${child}
35
+ pixels-per-ms=${pixelsPerMs}
36
+ ?enable-trim=${enableTrim}
37
+ ?use-absolute-position=${useAbsolutePosition}
38
+ .hideSelectors=${hideSelectors}
39
+ .showSelectors=${showSelectors}
40
+ ></ef-audio-track>`;
41
+ if (child instanceof EFVideo) return html`<ef-video-track
42
+ .element=${child}
43
+ pixels-per-ms=${pixelsPerMs}
44
+ ?enable-trim=${enableTrim}
45
+ ?use-absolute-position=${useAbsolutePosition}
46
+ .hideSelectors=${hideSelectors}
47
+ .showSelectors=${showSelectors}
48
+ ></ef-video-track>`;
49
+ if (child instanceof EFCaptions) return html`<ef-captions-track
50
+ .element=${child}
51
+ pixels-per-ms=${pixelsPerMs}
52
+ ?enable-trim=${enableTrim}
53
+ ?use-absolute-position=${useAbsolutePosition}
54
+ .hideSelectors=${hideSelectors}
55
+ .showSelectors=${showSelectors}
56
+ ></ef-captions-track>`;
57
+ if (child instanceof EFCaptionsActiveWord) return html`<ef-captions-active-word-track
58
+ .element=${child}
59
+ pixels-per-ms=${pixelsPerMs}
60
+ ?enable-trim=${enableTrim}
61
+ ?use-absolute-position=${useAbsolutePosition}
62
+ .hideSelectors=${hideSelectors}
63
+ .showSelectors=${showSelectors}
64
+ ></ef-captions-active-word-track>`;
65
+ if (child instanceof EFText) return html`<ef-text-track
66
+ .element=${child}
67
+ pixels-per-ms=${pixelsPerMs}
68
+ ?enable-trim=${enableTrim}
69
+ ?use-absolute-position=${useAbsolutePosition}
70
+ .hideSelectors=${hideSelectors}
71
+ .showSelectors=${showSelectors}
72
+ ></ef-text-track>`;
73
+ if (child instanceof EFTextSegment) return html`<ef-text-segment-track
74
+ .element=${child}
75
+ pixels-per-ms=${pixelsPerMs}
76
+ ?enable-trim=${enableTrim}
77
+ ?use-absolute-position=${useAbsolutePosition}
78
+ .hideSelectors=${hideSelectors}
79
+ .showSelectors=${showSelectors}
80
+ ></ef-text-segment-track>`;
81
+ if (child.tagName === "EF-CAPTIONS-SEGMENT") return html`<ef-captions-segment-track
82
+ .element=${child}
83
+ pixels-per-ms=${pixelsPerMs}
84
+ ?enable-trim=${enableTrim}
85
+ ?use-absolute-position=${useAbsolutePosition}
86
+ .hideSelectors=${hideSelectors}
87
+ .showSelectors=${showSelectors}
88
+ ></ef-captions-segment-track>`;
89
+ if (child.tagName === "EF-CAPTIONS-BEFORE-ACTIVE-WORD") return html`<ef-captions-before-word-track
90
+ .element=${child}
91
+ pixels-per-ms=${pixelsPerMs}
92
+ ?enable-trim=${enableTrim}
93
+ ?use-absolute-position=${useAbsolutePosition}
94
+ .hideSelectors=${hideSelectors}
95
+ .showSelectors=${showSelectors}
96
+ ></ef-captions-before-word-track>`;
97
+ if (child.tagName === "EF-CAPTIONS-AFTER-ACTIVE-WORD") return html`<ef-captions-after-word-track
98
+ .element=${child}
99
+ pixels-per-ms=${pixelsPerMs}
100
+ ?enable-trim=${enableTrim}
101
+ ?use-absolute-position=${useAbsolutePosition}
102
+ .hideSelectors=${hideSelectors}
103
+ .showSelectors=${showSelectors}
104
+ ></ef-captions-after-word-track>`;
105
+ if (child instanceof EFWaveform) return html`<ef-waveform-track
106
+ .element=${child}
107
+ pixels-per-ms=${pixelsPerMs}
108
+ ?enable-trim=${enableTrim}
109
+ ?use-absolute-position=${useAbsolutePosition}
110
+ .hideSelectors=${hideSelectors}
111
+ .showSelectors=${showSelectors}
112
+ ></ef-waveform-track>`;
113
+ return nothing;
114
+ });
115
+ }
116
+
117
+ //#endregion
118
+ export { renderTrackChildren };
119
+ //# sourceMappingURL=renderTrackChildren.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderTrackChildren.js","names":[],"sources":["../../../../src/gui/timeline/tracks/renderTrackChildren.ts"],"sourcesContent":["import { html, nothing, type TemplateResult } from \"lit\";\nimport { EFAudio } from \"../../../elements/EFAudio.js\";\nimport {\n EFCaptions,\n EFCaptionsActiveWord,\n} from \"../../../elements/EFCaptions.js\";\nimport { EFImage } from \"../../../elements/EFImage.js\";\nimport { EFText } from \"../../../elements/EFText.js\";\nimport { EFTextSegment } from \"../../../elements/EFTextSegment.js\";\nimport { EFTimegroup } from \"../../../elements/EFTimegroup.js\";\nimport { EFVideo } from \"../../../elements/EFVideo.js\";\nimport { EFWaveform } from \"../../../elements/EFWaveform.js\";\nimport { shouldRenderElement } from \"../../hierarchy/EFHierarchyItem.js\";\n\n// NOTE: Track components are NOT imported here to avoid circular dependencies.\n// They must be pre-loaded elsewhere before this module is used.\n// The custom elements (ef-audio-track, ef-video-track, etc.) are rendered\n// via Lit templates which will work as long as the elements are registered.\n// See preloadTracks.ts for the track component initialization.\n\nexport function renderTrackChildren(\n children: Element[],\n pixelsPerMs: number,\n hideSelectors?: string[],\n showSelectors?: string[],\n skipRootFiltering = false,\n enableTrim = false,\n useAbsolutePosition = false,\n): Array<TemplateResult<1> | typeof nothing> {\n return children.map((child) => {\n if (\n !skipRootFiltering &&\n !shouldRenderElement(child, hideSelectors, showSelectors)\n ) {\n return nothing;\n }\n\n if (child instanceof EFTimegroup) {\n return html`<ef-timegroup-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n >\n </ef-timegroup-track>`;\n }\n if (child instanceof EFImage) {\n return html`<ef-image-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-image-track>`;\n }\n if (child instanceof EFAudio) {\n return html`<ef-audio-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-audio-track>`;\n }\n if (child instanceof EFVideo) {\n return html`<ef-video-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-video-track>`;\n }\n if (child instanceof EFCaptions) {\n return html`<ef-captions-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-track>`;\n }\n if (child instanceof EFCaptionsActiveWord) {\n return html`<ef-captions-active-word-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-active-word-track>`;\n }\n if (child instanceof EFText) {\n return html`<ef-text-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-text-track>`;\n }\n if (child instanceof EFTextSegment) {\n return html`<ef-text-segment-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-text-segment-track>`;\n }\n if (child.tagName === \"EF-CAPTIONS-SEGMENT\") {\n return html`<ef-captions-segment-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-segment-track>`;\n }\n if (child.tagName === \"EF-CAPTIONS-BEFORE-ACTIVE-WORD\") {\n return html`<ef-captions-before-word-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-before-word-track>`;\n }\n if (child.tagName === \"EF-CAPTIONS-AFTER-ACTIVE-WORD\") {\n return html`<ef-captions-after-word-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-after-word-track>`;\n }\n if (child instanceof EFWaveform) {\n return html`<ef-waveform-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-waveform-track>`;\n }\n return nothing;\n });\n}\n\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,oBACd,UACA,aACA,eACA,eACA,oBAAoB,OACpB,aAAa,OACb,sBAAsB,OACqB;AAC3C,QAAO,SAAS,KAAK,UAAU;AAC7B,MACE,CAAC,qBACD,CAAC,oBAAoB,OAAO,eAAe,cAAc,CAEzD,QAAO;AAGT,MAAI,iBAAiB,YACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;;AAInC,MAAI,iBAAiB,QACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,QACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,QACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,WACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,qBACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,OACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,cACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,MAAM,YAAY,sBACpB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,MAAM,YAAY,iCACpB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,MAAM,YAAY,gCACpB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,WACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,SAAO;GACP"}
@@ -0,0 +1,80 @@
1
+ //#region src/gui/timeline/tracks/waveformUtils.ts
2
+ /**
3
+ * Waveform extraction utilities for DAW-style audio visualization.
4
+ *
5
+ * Extracts min/max peak pairs from audio data at a given resolution.
6
+ * Designed for timeline visualization where we need to see amplitude
7
+ * overview across the entire audio duration.
8
+ */
9
+ /** Samples per second for waveform data - balances resolution vs. data size */
10
+ const WAVEFORM_SAMPLES_PER_SECOND = 100;
11
+ /** Simple cache for waveform data keyed by audio URL */
12
+ const waveformCache = /* @__PURE__ */ new Map();
13
+ /**
14
+ * Extract waveform peak data from an audio URL.
15
+ * Uses Web Audio API to decode and analyze the audio.
16
+ * Results are cached by URL.
17
+ */
18
+ async function extractWaveformData(audioUrl, signal) {
19
+ const cached = waveformCache.get(audioUrl);
20
+ if (cached) return cached;
21
+ try {
22
+ const response = await fetch(audioUrl, { signal });
23
+ if (!response.ok) {
24
+ console.warn(`Failed to fetch audio for waveform: ${response.status}`);
25
+ return null;
26
+ }
27
+ const arrayBuffer = await response.arrayBuffer();
28
+ signal?.throwIfAborted();
29
+ const audioContext = new OfflineAudioContext(1, 1, 44100);
30
+ let audioBuffer;
31
+ try {
32
+ audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
33
+ } catch (decodeError) {
34
+ console.warn("Failed to decode audio for waveform:", decodeError);
35
+ return null;
36
+ }
37
+ signal?.throwIfAborted();
38
+ const waveformData = {
39
+ peaks: extractPeaksFromBuffer(audioBuffer, WAVEFORM_SAMPLES_PER_SECOND),
40
+ durationMs: audioBuffer.duration * 1e3,
41
+ samplesPerSecond: WAVEFORM_SAMPLES_PER_SECOND
42
+ };
43
+ waveformCache.set(audioUrl, waveformData);
44
+ return waveformData;
45
+ } catch (error) {
46
+ if (error instanceof DOMException && error.name === "AbortError") throw error;
47
+ console.warn("Error extracting waveform data:", error);
48
+ return null;
49
+ }
50
+ }
51
+ /**
52
+ * Extract min/max peaks from an AudioBuffer.
53
+ * Returns Float32Array with alternating [min, max, min, max, ...] values.
54
+ */
55
+ function extractPeaksFromBuffer(buffer, samplesPerSecond) {
56
+ const channelData = buffer.getChannelData(0);
57
+ const sampleRate = buffer.sampleRate;
58
+ const duration = buffer.duration;
59
+ const outputSamples = Math.ceil(duration * samplesPerSecond);
60
+ const peaks = new Float32Array(outputSamples * 2);
61
+ const samplesPerWindow = Math.floor(sampleRate / samplesPerSecond);
62
+ for (let i = 0; i < outputSamples; i++) {
63
+ const startSample = i * samplesPerWindow;
64
+ const endSample = Math.min(startSample + samplesPerWindow, channelData.length);
65
+ let min = 0;
66
+ let max = 0;
67
+ for (let j = startSample; j < endSample; j++) {
68
+ const sample = channelData[j] ?? 0;
69
+ if (sample < min) min = sample;
70
+ if (sample > max) max = sample;
71
+ }
72
+ peaks[i * 2] = min;
73
+ peaks[i * 2 + 1] = max;
74
+ }
75
+ return peaks;
76
+ }
77
+
78
+ //#endregion
79
+ export { extractWaveformData };
80
+ //# sourceMappingURL=waveformUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"waveformUtils.js","names":["audioBuffer: AudioBuffer","waveformData: WaveformData"],"sources":["../../../../src/gui/timeline/tracks/waveformUtils.ts"],"sourcesContent":["/**\n * Waveform extraction utilities for DAW-style audio visualization.\n * \n * Extracts min/max peak pairs from audio data at a given resolution.\n * Designed for timeline visualization where we need to see amplitude\n * overview across the entire audio duration.\n */\n\n/** Samples per second for waveform data - balances resolution vs. data size */\nexport const WAVEFORM_SAMPLES_PER_SECOND = 100;\n\n/** Waveform peak data: alternating min/max values normalized to [-1, 1] */\nexport interface WaveformData {\n /** Peak data: [min0, max0, min1, max1, ...] normalized to [-1, 1] */\n peaks: Float32Array;\n /** Duration of the audio in milliseconds */\n durationMs: number;\n /** Samples per second (for interpreting peaks array) */\n samplesPerSecond: number;\n}\n\n/** Simple cache for waveform data keyed by audio URL */\nconst waveformCache = new Map<string, WaveformData>();\n\n/**\n * Extract waveform peak data from an audio URL.\n * Uses Web Audio API to decode and analyze the audio.\n * Results are cached by URL.\n */\nexport async function extractWaveformData(\n audioUrl: string,\n signal?: AbortSignal,\n): Promise<WaveformData | null> {\n // Check cache first\n const cached = waveformCache.get(audioUrl);\n if (cached) {\n return cached;\n }\n\n try {\n // Fetch the audio file\n const response = await fetch(audioUrl, { signal });\n if (!response.ok) {\n console.warn(`Failed to fetch audio for waveform: ${response.status}`);\n return null;\n }\n\n const arrayBuffer = await response.arrayBuffer();\n signal?.throwIfAborted();\n\n // Decode audio data\n const audioContext = new OfflineAudioContext(1, 1, 44100);\n let audioBuffer: AudioBuffer;\n \n try {\n audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n } catch (decodeError) {\n console.warn(\"Failed to decode audio for waveform:\", decodeError);\n return null;\n }\n\n signal?.throwIfAborted();\n\n // Extract peaks from the decoded audio\n const peaks = extractPeaksFromBuffer(audioBuffer, WAVEFORM_SAMPLES_PER_SECOND);\n const durationMs = audioBuffer.duration * 1000;\n\n const waveformData: WaveformData = {\n peaks,\n durationMs,\n samplesPerSecond: WAVEFORM_SAMPLES_PER_SECOND,\n };\n\n // Cache the result\n waveformCache.set(audioUrl, waveformData);\n\n return waveformData;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.warn(\"Error extracting waveform data:\", error);\n return null;\n }\n}\n\n/**\n * Extract min/max peaks from an AudioBuffer.\n * Returns Float32Array with alternating [min, max, min, max, ...] values.\n */\nfunction extractPeaksFromBuffer(\n buffer: AudioBuffer,\n samplesPerSecond: number,\n): Float32Array {\n const channelData = buffer.getChannelData(0); // Use first channel\n const sampleRate = buffer.sampleRate;\n const duration = buffer.duration;\n \n // Calculate how many samples to output\n const outputSamples = Math.ceil(duration * samplesPerSecond);\n \n // Each output sample has min and max\n const peaks = new Float32Array(outputSamples * 2);\n \n // Samples per output window\n const samplesPerWindow = Math.floor(sampleRate / samplesPerSecond);\n \n for (let i = 0; i < outputSamples; i++) {\n const startSample = i * samplesPerWindow;\n const endSample = Math.min(startSample + samplesPerWindow, channelData.length);\n \n let min = 0;\n let max = 0;\n \n for (let j = startSample; j < endSample; j++) {\n const sample = channelData[j] ?? 0;\n if (sample < min) min = sample;\n if (sample > max) max = sample;\n }\n \n // Store as alternating min/max pairs\n peaks[i * 2] = min;\n peaks[i * 2 + 1] = max;\n }\n \n return peaks;\n}\n\n/**\n * Render waveform data to a canvas context.\n * Draws a filled waveform path centered vertically.\n */\nexport function renderWaveformToCanvas(\n ctx: CanvasRenderingContext2D,\n waveformData: WaveformData,\n x: number,\n y: number,\n width: number,\n height: number,\n color: string,\n startMs: number = 0,\n endMs?: number,\n): void {\n const { peaks, durationMs, samplesPerSecond } = waveformData;\n const actualEndMs = endMs ?? durationMs;\n \n // Calculate which samples to render\n const startSample = Math.floor((startMs / 1000) * samplesPerSecond);\n const endSample = Math.ceil((actualEndMs / 1000) * samplesPerSecond);\n const sampleCount = endSample - startSample;\n \n if (sampleCount <= 0) return;\n \n const centerY = y + height / 2;\n const halfHeight = height / 2;\n const pixelsPerSample = width / sampleCount;\n \n ctx.fillStyle = color;\n ctx.beginPath();\n \n // Draw top half (max values) left to right\n for (let i = 0; i < sampleCount; i++) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n const maxValue = peaks[peakIndex + 1] ?? 0;\n \n const px = x + i * pixelsPerSample;\n const py = centerY - maxValue * halfHeight;\n \n if (i === 0) {\n ctx.moveTo(px, py);\n } else {\n ctx.lineTo(px, py);\n }\n }\n \n // Draw bottom half (min values) right to left\n for (let i = sampleCount - 1; i >= 0; i--) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n const minValue = peaks[peakIndex] ?? 0;\n \n const px = x + i * pixelsPerSample;\n const py = centerY - minValue * halfHeight;\n \n ctx.lineTo(px, py);\n }\n \n ctx.closePath();\n ctx.fill();\n}\n\n/**\n * Clear waveform cache (useful for testing or memory management)\n */\nexport function clearWaveformCache(): void {\n waveformCache.clear();\n}\n"],"mappings":";;;;;;;;;AASA,MAAa,8BAA8B;;AAa3C,MAAM,gCAAgB,IAAI,KAA2B;;;;;;AAOrD,eAAsB,oBACpB,UACA,QAC8B;CAE9B,MAAM,SAAS,cAAc,IAAI,SAAS;AAC1C,KAAI,OACF,QAAO;AAGT,KAAI;EAEF,MAAM,WAAW,MAAM,MAAM,UAAU,EAAE,QAAQ,CAAC;AAClD,MAAI,CAAC,SAAS,IAAI;AAChB,WAAQ,KAAK,uCAAuC,SAAS,SAAS;AACtE,UAAO;;EAGT,MAAM,cAAc,MAAM,SAAS,aAAa;AAChD,UAAQ,gBAAgB;EAGxB,MAAM,eAAe,IAAI,oBAAoB,GAAG,GAAG,MAAM;EACzD,IAAIA;AAEJ,MAAI;AACF,iBAAc,MAAM,aAAa,gBAAgB,YAAY;WACtD,aAAa;AACpB,WAAQ,KAAK,wCAAwC,YAAY;AACjE,UAAO;;AAGT,UAAQ,gBAAgB;EAMxB,MAAMC,eAA6B;GACjC,OAJY,uBAAuB,aAAa,4BAA4B;GAK5E,YAJiB,YAAY,WAAW;GAKxC,kBAAkB;GACnB;AAGD,gBAAc,IAAI,UAAU,aAAa;AAEzC,SAAO;UACA,OAAO;AACd,MAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAER,UAAQ,KAAK,mCAAmC,MAAM;AACtD,SAAO;;;;;;;AAQX,SAAS,uBACP,QACA,kBACc;CACd,MAAM,cAAc,OAAO,eAAe,EAAE;CAC5C,MAAM,aAAa,OAAO;CAC1B,MAAM,WAAW,OAAO;CAGxB,MAAM,gBAAgB,KAAK,KAAK,WAAW,iBAAiB;CAG5D,MAAM,QAAQ,IAAI,aAAa,gBAAgB,EAAE;CAGjD,MAAM,mBAAmB,KAAK,MAAM,aAAa,iBAAiB;AAElE,MAAK,IAAI,IAAI,GAAG,IAAI,eAAe,KAAK;EACtC,MAAM,cAAc,IAAI;EACxB,MAAM,YAAY,KAAK,IAAI,cAAc,kBAAkB,YAAY,OAAO;EAE9E,IAAI,MAAM;EACV,IAAI,MAAM;AAEV,OAAK,IAAI,IAAI,aAAa,IAAI,WAAW,KAAK;GAC5C,MAAM,SAAS,YAAY,MAAM;AACjC,OAAI,SAAS,IAAK,OAAM;AACxB,OAAI,SAAS,IAAK,OAAM;;AAI1B,QAAM,IAAI,KAAK;AACf,QAAM,IAAI,IAAI,KAAK;;AAGrB,QAAO"}
@@ -0,0 +1,217 @@
1
+ import { getCornerPoint, getOppositeCorner } from "./transformUtils.js";
2
+
3
+ //#region src/gui/transformCalculations.ts
4
+ /**
5
+ * Calculate the axis-aligned bounding box of a rotated rectangle.
6
+ * Given the element's position (top-left), size, and rotation,
7
+ * returns the min/max x/y that fully contains the rotated rectangle.
8
+ */
9
+ function getRotatedBoundingBox(x, y, width, height, rotationDegrees) {
10
+ if (rotationDegrees === 0) return {
11
+ minX: x,
12
+ minY: y,
13
+ maxX: x + width,
14
+ maxY: y + height
15
+ };
16
+ const rotationRadians = rotationDegrees * Math.PI / 180;
17
+ const cos = Math.cos(rotationRadians);
18
+ const sin = Math.sin(rotationRadians);
19
+ const centerX = x + width / 2;
20
+ const centerY = y + height / 2;
21
+ const halfW = width / 2;
22
+ const halfH = height / 2;
23
+ const corners = [
24
+ {
25
+ x: -halfW,
26
+ y: -halfH
27
+ },
28
+ {
29
+ x: halfW,
30
+ y: -halfH
31
+ },
32
+ {
33
+ x: halfW,
34
+ y: halfH
35
+ },
36
+ {
37
+ x: -halfW,
38
+ y: halfH
39
+ }
40
+ ];
41
+ let minX = Infinity;
42
+ let minY = Infinity;
43
+ let maxX = -Infinity;
44
+ let maxY = -Infinity;
45
+ for (const corner of corners) {
46
+ const rotatedX = corner.x * cos - corner.y * sin + centerX;
47
+ const rotatedY = corner.x * sin + corner.y * cos + centerY;
48
+ minX = Math.min(minX, rotatedX);
49
+ minY = Math.min(minY, rotatedY);
50
+ maxX = Math.max(maxX, rotatedX);
51
+ maxY = Math.max(maxY, rotatedY);
52
+ }
53
+ return {
54
+ minX,
55
+ minY,
56
+ maxX,
57
+ maxY
58
+ };
59
+ }
60
+ /**
61
+ * Get the cursor type for a resize handle based on rotation.
62
+ * The cursor should reflect the actual direction the handle will resize in screen space.
63
+ *
64
+ * @param handle - The resize handle identifier
65
+ * @param rotationDegrees - Current rotation in degrees (0-360)
66
+ * @returns CSS cursor value
67
+ */
68
+ function getResizeHandleCursor(handle, rotationDegrees) {
69
+ const effectiveAngle = ({
70
+ n: 0,
71
+ ne: 45,
72
+ e: 90,
73
+ se: 135,
74
+ s: 180,
75
+ sw: 225,
76
+ w: 270,
77
+ nw: 315
78
+ }[handle] + rotationDegrees) % 360;
79
+ const normalizedAngle = effectiveAngle < 0 ? effectiveAngle + 360 : effectiveAngle;
80
+ if (normalizedAngle >= 337.5 || normalizedAngle < 22.5) return "n-resize";
81
+ else if (normalizedAngle >= 22.5 && normalizedAngle < 67.5) return "ne-resize";
82
+ else if (normalizedAngle >= 67.5 && normalizedAngle < 112.5) return "e-resize";
83
+ else if (normalizedAngle >= 112.5 && normalizedAngle < 157.5) return "se-resize";
84
+ else if (normalizedAngle >= 157.5 && normalizedAngle < 202.5) return "s-resize";
85
+ else if (normalizedAngle >= 202.5 && normalizedAngle < 247.5) return "sw-resize";
86
+ else if (normalizedAngle >= 247.5 && normalizedAngle < 292.5) return "w-resize";
87
+ else return "nw-resize";
88
+ }
89
+ /**
90
+ * Calculate new bounds for drag operation in canvas coordinates.
91
+ * Pure function - no side effects.
92
+ *
93
+ * Works in canvas coordinate space with zoom as a parameter.
94
+ * Converts screen deltas to canvas deltas.
95
+ *
96
+ * @param startPosition - Starting position in canvas coordinates
97
+ * @param screenDeltaX - Mouse movement delta in screen pixels
98
+ * @param screenDeltaY - Mouse movement delta in screen pixels
99
+ * @param zoomScale - Canvas zoom scale (1.0 = no zoom, 2.0 = 2x zoom, etc.)
100
+ * @returns New bounds with updated position (in canvas coordinates)
101
+ */
102
+ function calculateDragBounds(startPosition, screenDeltaX, screenDeltaY, zoomScale = 1) {
103
+ if (zoomScale <= 0) throw new Error("Zoom scale must be greater than 0");
104
+ const canvasDeltaX = screenDeltaX / zoomScale;
105
+ const canvasDeltaY = screenDeltaY / zoomScale;
106
+ return {
107
+ x: startPosition.x + canvasDeltaX,
108
+ y: startPosition.y + canvasDeltaY
109
+ };
110
+ }
111
+ /**
112
+ * Calculate new bounds for resize operation in canvas coordinates.
113
+ * Pure function - no side effects.
114
+ *
115
+ * Works in canvas coordinate space with zoom as a parameter.
116
+ * Converts screen deltas to canvas deltas, calculates new bounds in canvas coordinates.
117
+ *
118
+ * @param startSize - Starting size in canvas coordinates
119
+ * @param startPosition - Starting position in canvas coordinates
120
+ * @param startCorner - Starting corner position in canvas coordinates
121
+ * @param handle - Resize handle being dragged
122
+ * @param screenDeltaX - Mouse movement delta in screen pixels
123
+ * @param screenDeltaY - Mouse movement delta in screen pixels
124
+ * @param rotationDegrees - Current rotation in degrees
125
+ * @param minSize - Minimum size constraint in canvas coordinates
126
+ * @param zoomScale - Canvas zoom scale (1.0 = no zoom, 2.0 = 2x zoom, etc.)
127
+ * @param options - Optional resize modifiers (lockAspectRatio, resizeFromCenter)
128
+ * @returns New bounds with updated size and position (in canvas coordinates)
129
+ */
130
+ function calculateResizeBounds(startSize, startPosition, startCorner, handle, screenDeltaX, screenDeltaY, rotationDegrees, minSize, zoomScale = 1, options = {}) {
131
+ if (zoomScale <= 0) throw new Error("Zoom scale must be greater than 0");
132
+ const { lockAspectRatio = false, resizeFromCenter = false } = options;
133
+ const initialAspectRatio = startSize.width / startSize.height;
134
+ const canvasDeltaX = screenDeltaX / zoomScale;
135
+ const canvasDeltaY = screenDeltaY / zoomScale;
136
+ const rotationRadians = rotationDegrees * Math.PI / 180;
137
+ const oppositeCorner = getOppositeCorner(handle);
138
+ const cos = Math.cos(-rotationRadians);
139
+ const sin = Math.sin(-rotationRadians);
140
+ const rotatedDeltaX = cos * canvasDeltaX - sin * canvasDeltaY;
141
+ const rotatedDeltaY = sin * canvasDeltaX + cos * canvasDeltaY;
142
+ const deltaMultiplier = resizeFromCenter ? 2 : 1;
143
+ let newWidth = startSize.width;
144
+ let newHeight = startSize.height;
145
+ if (handle.includes("e")) newWidth = startSize.width + rotatedDeltaX * deltaMultiplier;
146
+ else if (handle.includes("w")) newWidth = startSize.width - rotatedDeltaX * deltaMultiplier;
147
+ if (handle.includes("s")) newHeight = startSize.height + rotatedDeltaY * deltaMultiplier;
148
+ else if (handle.includes("n")) newHeight = startSize.height - rotatedDeltaY * deltaMultiplier;
149
+ if (lockAspectRatio) {
150
+ const isCornerHandle = handle.length === 2;
151
+ const isHorizontalOnly = handle === "e" || handle === "w";
152
+ const isVerticalOnly = handle === "n" || handle === "s";
153
+ if (isCornerHandle) {
154
+ const widthScale = newWidth / startSize.width;
155
+ const heightScale = newHeight / startSize.height;
156
+ const uniformScale = Math.abs(widthScale - 1) > Math.abs(heightScale - 1) ? widthScale : heightScale;
157
+ newWidth = startSize.width * uniformScale;
158
+ newHeight = startSize.height * uniformScale;
159
+ } else if (isHorizontalOnly) newHeight = newWidth / initialAspectRatio;
160
+ else if (isVerticalOnly) newWidth = newHeight * initialAspectRatio;
161
+ }
162
+ newWidth = Math.max(minSize, newWidth);
163
+ newHeight = Math.max(minSize, newHeight);
164
+ if (lockAspectRatio && (newWidth === minSize || newHeight === minSize)) if (newWidth === minSize) newHeight = Math.max(minSize, minSize / initialAspectRatio);
165
+ else newWidth = Math.max(minSize, minSize * initialAspectRatio);
166
+ let newX;
167
+ let newY;
168
+ if (resizeFromCenter) {
169
+ const centerX = startPosition.x + startSize.width / 2;
170
+ const centerY = startPosition.y + startSize.height / 2;
171
+ newX = centerX - newWidth / 2;
172
+ newY = centerY - newHeight / 2;
173
+ } else {
174
+ const newOppositeCorner = getCornerPoint(startPosition.x, startPosition.y, newWidth, newHeight, rotationRadians, oppositeCorner.x, oppositeCorner.y);
175
+ const offsetX = startCorner.x - newOppositeCorner.x;
176
+ const offsetY = startCorner.y - newOppositeCorner.y;
177
+ newX = startPosition.x + offsetX;
178
+ newY = startPosition.y + offsetY;
179
+ }
180
+ return {
181
+ x: newX,
182
+ y: newY,
183
+ width: newWidth,
184
+ height: newHeight
185
+ };
186
+ }
187
+ /**
188
+ * Parse rotation angle from CSS transform.
189
+ * Handles both rotate() syntax and matrix() transforms.
190
+ * Pure function - no side effects.
191
+ *
192
+ * @param transform - CSS transform string (e.g., "rotate(45deg)" or "matrix(a, b, c, d, e, f)")
193
+ * @returns Rotation angle in degrees
194
+ */
195
+ function parseRotationFromTransform(transform) {
196
+ if (!transform || transform === "none") return 0;
197
+ const rotateMatch = transform.match(/rotate\(([^)]+)\)/);
198
+ if (rotateMatch?.[1]) {
199
+ const value = rotateMatch[1].trim();
200
+ const numValue = parseFloat(value);
201
+ const unit = value.replace(String(numValue), "").trim();
202
+ if (unit === "rad" || unit === "radians") return numValue * 180 / Math.PI;
203
+ return numValue;
204
+ }
205
+ const matrixMatch = transform.match(/matrix\(([^)]+)\)/);
206
+ if (!matrixMatch?.[1]) return 0;
207
+ const values = matrixMatch[1].split(",").map((v) => parseFloat(v.trim()));
208
+ if (values.length < 2) return 0;
209
+ const a = values[0];
210
+ const b = values[1];
211
+ if (a === void 0 || b === void 0 || isNaN(a) || isNaN(b)) return 0;
212
+ return Math.atan2(b, a) * (180 / Math.PI);
213
+ }
214
+
215
+ //#endregion
216
+ export { calculateDragBounds, calculateResizeBounds, getResizeHandleCursor, getRotatedBoundingBox, parseRotationFromTransform };
217
+ //# sourceMappingURL=transformCalculations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transformCalculations.js","names":["newX: number","newY: number"],"sources":["../../src/gui/transformCalculations.ts"],"sourcesContent":["import { getCornerPoint, getOppositeCorner } from \"./transformUtils.js\";\nimport type { TransformBounds } from \"./EFTransformHandles.js\";\n\nexport type ResizeHandle = \"nw\" | \"n\" | \"ne\" | \"e\" | \"se\" | \"s\" | \"sw\" | \"w\";\n\n/**\n * Calculate the axis-aligned bounding box of a rotated rectangle.\n * Given the element's position (top-left), size, and rotation,\n * returns the min/max x/y that fully contains the rotated rectangle.\n */\nexport function getRotatedBoundingBox(\n x: number,\n y: number,\n width: number,\n height: number,\n rotationDegrees: number,\n): { minX: number; minY: number; maxX: number; maxY: number } {\n // If no rotation, simple case\n if (rotationDegrees === 0) {\n return { minX: x, minY: y, maxX: x + width, maxY: y + height };\n }\n\n const rotationRadians = (rotationDegrees * Math.PI) / 180;\n const cos = Math.cos(rotationRadians);\n const sin = Math.sin(rotationRadians);\n\n // Center of the rectangle\n const centerX = x + width / 2;\n const centerY = y + height / 2;\n\n // Half dimensions\n const halfW = width / 2;\n const halfH = height / 2;\n\n // Four corners relative to center (before rotation)\n const corners = [\n { x: -halfW, y: -halfH }, // top-left\n { x: halfW, y: -halfH }, // top-right\n { x: halfW, y: halfH }, // bottom-right\n { x: -halfW, y: halfH }, // bottom-left\n ];\n\n // Rotate each corner and find bounds\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n\n for (const corner of corners) {\n // Rotate corner around center\n const rotatedX = corner.x * cos - corner.y * sin + centerX;\n const rotatedY = corner.x * sin + corner.y * cos + centerY;\n\n minX = Math.min(minX, rotatedX);\n minY = Math.min(minY, rotatedY);\n maxX = Math.max(maxX, rotatedX);\n maxY = Math.max(maxY, rotatedY);\n }\n\n return { minX, minY, maxX, maxY };\n}\n\ntype CursorType =\n | \"n-resize\"\n | \"e-resize\"\n | \"s-resize\"\n | \"w-resize\"\n | \"ne-resize\"\n | \"nw-resize\"\n | \"se-resize\"\n | \"sw-resize\";\n\n/**\n * Get the cursor type for a resize handle based on rotation.\n * The cursor should reflect the actual direction the handle will resize in screen space.\n *\n * @param handle - The resize handle identifier\n * @param rotationDegrees - Current rotation in degrees (0-360)\n * @returns CSS cursor value\n */\nexport function getResizeHandleCursor(\n handle: ResizeHandle,\n rotationDegrees: number,\n): CursorType {\n // Map handles to their base angles (in degrees, where 0° is north, clockwise)\n const handleAngles: Record<ResizeHandle, number> = {\n n: 0,\n ne: 45,\n e: 90,\n se: 135,\n s: 180,\n sw: 225,\n w: 270,\n nw: 315,\n };\n\n // Calculate the effective angle after rotation\n const baseAngle = handleAngles[handle];\n const effectiveAngle = (baseAngle + rotationDegrees) % 360;\n const normalizedAngle =\n effectiveAngle < 0 ? effectiveAngle + 360 : effectiveAngle;\n\n // Map angle back to cursor\n // Edge handles (n, e, s, w) map to cardinal directions\n // Corner handles (ne, nw, se, sw) map to diagonal directions\n if (normalizedAngle >= 337.5 || normalizedAngle < 22.5) {\n return \"n-resize\";\n } else if (normalizedAngle >= 22.5 && normalizedAngle < 67.5) {\n return \"ne-resize\";\n } else if (normalizedAngle >= 67.5 && normalizedAngle < 112.5) {\n return \"e-resize\";\n } else if (normalizedAngle >= 112.5 && normalizedAngle < 157.5) {\n return \"se-resize\";\n } else if (normalizedAngle >= 157.5 && normalizedAngle < 202.5) {\n return \"s-resize\";\n } else if (normalizedAngle >= 202.5 && normalizedAngle < 247.5) {\n return \"sw-resize\";\n } else if (normalizedAngle >= 247.5 && normalizedAngle < 292.5) {\n return \"w-resize\";\n } else {\n // 292.5 to 337.5\n return \"nw-resize\";\n }\n}\n\n/**\n * Convert screen coordinate delta to canvas coordinate delta.\n * @param screenDeltaX - Screen pixel delta X\n * @param screenDeltaY - Screen pixel delta Y\n * @param canvasScale - Canvas zoom scale (must be > 0)\n * @returns Canvas coordinate delta\n */\nexport function screenToCanvasDelta(\n screenDeltaX: number,\n screenDeltaY: number,\n canvasScale: number,\n): { x: number; y: number } {\n if (canvasScale <= 0) {\n throw new Error(\"Canvas scale must be greater than 0\");\n }\n return {\n x: screenDeltaX / canvasScale,\n y: screenDeltaY / canvasScale,\n };\n}\n\n/**\n * Convert canvas coordinate delta to screen coordinate delta.\n * @param canvasDeltaX - Canvas coordinate delta X\n * @param canvasDeltaY - Canvas coordinate delta Y\n * @param canvasScale - Canvas zoom scale (must be > 0)\n * @returns Screen pixel delta\n */\nexport function canvasToScreenDelta(\n canvasDeltaX: number,\n canvasDeltaY: number,\n canvasScale: number,\n): { x: number; y: number } {\n if (canvasScale <= 0) {\n throw new Error(\"Canvas scale must be greater than 0\");\n }\n return {\n x: canvasDeltaX * canvasScale,\n y: canvasDeltaY * canvasScale,\n };\n}\n\n/**\n * Calculate new bounds for drag operation in canvas coordinates.\n * Pure function - no side effects.\n *\n * Works in canvas coordinate space with zoom as a parameter.\n * Converts screen deltas to canvas deltas.\n *\n * @param startPosition - Starting position in canvas coordinates\n * @param screenDeltaX - Mouse movement delta in screen pixels\n * @param screenDeltaY - Mouse movement delta in screen pixels\n * @param zoomScale - Canvas zoom scale (1.0 = no zoom, 2.0 = 2x zoom, etc.)\n * @returns New bounds with updated position (in canvas coordinates)\n */\nexport function calculateDragBounds(\n startPosition: { x: number; y: number },\n screenDeltaX: number,\n screenDeltaY: number,\n zoomScale: number = 1,\n): { x: number; y: number } {\n if (zoomScale <= 0) {\n throw new Error(\"Zoom scale must be greater than 0\");\n }\n\n // Convert screen deltas to canvas deltas\n const canvasDeltaX = screenDeltaX / zoomScale;\n const canvasDeltaY = screenDeltaY / zoomScale;\n\n return {\n x: startPosition.x + canvasDeltaX,\n y: startPosition.y + canvasDeltaY,\n };\n}\n\n/**\n * Options for resize calculation.\n * Modifier keys and constraints.\n */\nexport interface ResizeOptions {\n /** Lock aspect ratio (Shift key or multi-selection) */\n lockAspectRatio?: boolean;\n /** Resize from center instead of opposite corner (Ctrl key) */\n resizeFromCenter?: boolean;\n}\n\n/**\n * Calculate new bounds for resize operation in canvas coordinates.\n * Pure function - no side effects.\n *\n * Works in canvas coordinate space with zoom as a parameter.\n * Converts screen deltas to canvas deltas, calculates new bounds in canvas coordinates.\n *\n * @param startSize - Starting size in canvas coordinates\n * @param startPosition - Starting position in canvas coordinates\n * @param startCorner - Starting corner position in canvas coordinates\n * @param handle - Resize handle being dragged\n * @param screenDeltaX - Mouse movement delta in screen pixels\n * @param screenDeltaY - Mouse movement delta in screen pixels\n * @param rotationDegrees - Current rotation in degrees\n * @param minSize - Minimum size constraint in canvas coordinates\n * @param zoomScale - Canvas zoom scale (1.0 = no zoom, 2.0 = 2x zoom, etc.)\n * @param options - Optional resize modifiers (lockAspectRatio, resizeFromCenter)\n * @returns New bounds with updated size and position (in canvas coordinates)\n */\nexport function calculateResizeBounds(\n startSize: { width: number; height: number },\n startPosition: { x: number; y: number },\n startCorner: { x: number; y: number },\n handle: ResizeHandle,\n screenDeltaX: number,\n screenDeltaY: number,\n rotationDegrees: number,\n minSize: number,\n zoomScale: number = 1,\n options: ResizeOptions = {},\n): TransformBounds {\n if (zoomScale <= 0) {\n throw new Error(\"Zoom scale must be greater than 0\");\n }\n\n const { lockAspectRatio = false, resizeFromCenter = false } = options;\n const initialAspectRatio = startSize.width / startSize.height;\n\n // Convert screen deltas to canvas deltas\n const canvasDeltaX = screenDeltaX / zoomScale;\n const canvasDeltaY = screenDeltaY / zoomScale;\n\n const rotationRadians = (rotationDegrees * Math.PI) / 180;\n const oppositeCorner = getOppositeCorner(handle);\n\n // Rotate canvas deltas to align with element's local coordinate system\n const cos = Math.cos(-rotationRadians);\n const sin = Math.sin(-rotationRadians);\n const rotatedDeltaX = cos * canvasDeltaX - sin * canvasDeltaY;\n const rotatedDeltaY = sin * canvasDeltaX + cos * canvasDeltaY;\n\n // For center resize, delta applies to both sides (double effect)\n const deltaMultiplier = resizeFromCenter ? 2 : 1;\n\n // Calculate new size in canvas coordinates\n let newWidth = startSize.width;\n let newHeight = startSize.height;\n\n if (handle.includes(\"e\")) {\n newWidth = startSize.width + rotatedDeltaX * deltaMultiplier;\n } else if (handle.includes(\"w\")) {\n newWidth = startSize.width - rotatedDeltaX * deltaMultiplier;\n }\n\n if (handle.includes(\"s\")) {\n newHeight = startSize.height + rotatedDeltaY * deltaMultiplier;\n } else if (handle.includes(\"n\")) {\n newHeight = startSize.height - rotatedDeltaY * deltaMultiplier;\n }\n\n // Apply aspect ratio constraint if enabled\n if (lockAspectRatio) {\n const isCornerHandle = handle.length === 2; // \"ne\", \"nw\", \"se\", \"sw\"\n const isHorizontalOnly = handle === \"e\" || handle === \"w\";\n const isVerticalOnly = handle === \"n\" || handle === \"s\";\n\n if (isCornerHandle) {\n // For corners: use the dimension with larger change\n const widthScale = newWidth / startSize.width;\n const heightScale = newHeight / startSize.height;\n const uniformScale =\n Math.abs(widthScale - 1) > Math.abs(heightScale - 1)\n ? widthScale\n : heightScale;\n newWidth = startSize.width * uniformScale;\n newHeight = startSize.height * uniformScale;\n } else if (isHorizontalOnly) {\n // Horizontal handle: adjust height to match aspect ratio\n newHeight = newWidth / initialAspectRatio;\n } else if (isVerticalOnly) {\n // Vertical handle: adjust width to match aspect ratio\n newWidth = newHeight * initialAspectRatio;\n }\n }\n\n // Apply min size constraint (in canvas coordinates)\n newWidth = Math.max(minSize, newWidth);\n newHeight = Math.max(minSize, newHeight);\n\n // Re-apply aspect ratio after min size if needed\n if (lockAspectRatio && (newWidth === minSize || newHeight === minSize)) {\n if (newWidth === minSize) {\n newHeight = Math.max(minSize, minSize / initialAspectRatio);\n } else {\n newWidth = Math.max(minSize, minSize * initialAspectRatio);\n }\n }\n\n // Calculate new position based on resize mode\n let newX: number;\n let newY: number;\n\n if (resizeFromCenter) {\n // Keep center fixed\n const centerX = startPosition.x + startSize.width / 2;\n const centerY = startPosition.y + startSize.height / 2;\n newX = centerX - newWidth / 2;\n newY = centerY - newHeight / 2;\n } else {\n // Keep opposite corner fixed\n const newOppositeCorner = getCornerPoint(\n startPosition.x,\n startPosition.y,\n newWidth,\n newHeight,\n rotationRadians,\n oppositeCorner.x,\n oppositeCorner.y,\n );\n\n const offsetX = startCorner.x - newOppositeCorner.x;\n const offsetY = startCorner.y - newOppositeCorner.y;\n newX = startPosition.x + offsetX;\n newY = startPosition.y + offsetY;\n }\n\n return {\n x: newX,\n y: newY,\n width: newWidth,\n height: newHeight,\n };\n}\n\n/**\n * Calculate new rotation angle.\n * Pure function - no side effects.\n *\n * @param startAngle - Starting angle in degrees (0-360)\n * @param startRotation - Starting rotation value in degrees\n * @param currentMouseX - Current mouse X in screen pixels\n * @param currentMouseY - Current mouse Y in screen pixels\n * @param centerX - Element center X in screen pixels\n * @param centerY - Element center Y in screen pixels\n * @param rotationStep - Optional rotation step for snapping (in degrees)\n * @returns New rotation angle in degrees\n */\nexport function calculateRotation(\n startAngle: number,\n startRotation: number,\n currentMouseX: number,\n currentMouseY: number,\n centerX: number,\n centerY: number,\n rotationStep?: number,\n): number {\n const dx = currentMouseX - centerX;\n const dy = currentMouseY - centerY;\n const radians = Math.atan2(dy, dx);\n const currentAngle = radians * (180 / Math.PI) + 90;\n\n // Normalize angle difference to [-180, 180] to avoid wrapping issues\n let deltaAngle = currentAngle - startAngle;\n while (deltaAngle > 180) deltaAngle -= 360;\n while (deltaAngle < -180) deltaAngle += 360;\n\n let newRotation = startRotation + deltaAngle;\n\n if (rotationStep !== undefined && rotationStep > 0) {\n newRotation = Math.round(newRotation / rotationStep) * rotationStep;\n }\n\n return newRotation;\n}\n\n/**\n * Parse rotation angle from CSS transform.\n * Handles both rotate() syntax and matrix() transforms.\n * Pure function - no side effects.\n *\n * @param transform - CSS transform string (e.g., \"rotate(45deg)\" or \"matrix(a, b, c, d, e, f)\")\n * @returns Rotation angle in degrees\n */\nexport function parseRotationFromTransform(transform: string): number {\n if (!transform || transform === \"none\") return 0;\n\n // Try rotate() syntax first (e.g., \"rotate(45deg)\", \"rotate(0.5rad)\")\n const rotateMatch = transform.match(/rotate\\(([^)]+)\\)/);\n if (rotateMatch?.[1]) {\n const value = rotateMatch[1].trim();\n const numValue = parseFloat(value);\n const unit = value.replace(String(numValue), \"\").trim();\n if (unit === \"rad\" || unit === \"radians\") {\n return (numValue * 180) / Math.PI;\n }\n return numValue; // degrees (default)\n }\n\n // Fall back to matrix transform: matrix(a, b, c, d, tx, ty)\n // For rotation: a = cos(θ), b = sin(θ)\n const matrixMatch = transform.match(/matrix\\(([^)]+)\\)/);\n if (!matrixMatch?.[1]) return 0;\n\n const values = matrixMatch[1].split(\",\").map((v) => parseFloat(v.trim()));\n if (values.length < 2) return 0;\n\n const a = values[0];\n const b = values[1];\n if (a === undefined || b === undefined || isNaN(a) || isNaN(b)) {\n return 0;\n }\n\n return Math.atan2(b, a) * (180 / Math.PI);\n}\n"],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ //#region src/gui/transformUtils.d.ts
2
+ /**
3
+ * Pure utility functions for transform calculations.
4
+ * Extracted from motion designer TransformHandles component.
5
+ */
6
+ /**
7
+ * Rotate a point around a center point by given radians.
8
+ */
9
+ declare function rotatePoint(cx: number, cy: number, x: number, y: number, radians: number): {
10
+ x: number;
11
+ y: number;
12
+ };
13
+ /**
14
+ * Calculate corner point in canvas coordinates for a rotated element.
15
+ * @param x - Element x position
16
+ * @param y - Element y position
17
+ * @param width - Element width
18
+ * @param height - Element height
19
+ * @param rotationRadians - Rotation in radians
20
+ * @param xMagnitude - 0 = left, 0.5 = center, 1 = right
21
+ * @param yMagnitude - 0 = top, 0.5 = center, 1 = bottom
22
+ */
23
+ declare function getCornerPoint(x: number, y: number, width: number, height: number, rotationRadians: number, xMagnitude: number, yMagnitude: number): {
24
+ x: number;
25
+ y: number;
26
+ };
27
+ /**
28
+ * Get opposite corner magnitudes for a handle.
29
+ * Used to determine which corner stays fixed during resize.
30
+ */
31
+ declare function getOppositeCorner(handle: string): {
32
+ x: number;
33
+ y: number;
34
+ };
35
+ //#endregion
36
+ export { getCornerPoint, getOppositeCorner, rotatePoint };
37
+ //# sourceMappingURL=transformUtils.d.ts.map
@@ -0,0 +1,77 @@
1
+ //#region src/gui/transformUtils.ts
2
+ /**
3
+ * Pure utility functions for transform calculations.
4
+ * Extracted from motion designer TransformHandles component.
5
+ */
6
+ /**
7
+ * Rotate a point around a center point by given radians.
8
+ */
9
+ function rotatePoint(cx, cy, x, y, radians) {
10
+ const cos = Math.cos(radians);
11
+ const sin = Math.sin(radians);
12
+ return {
13
+ x: cos * (x - cx) - sin * (y - cy) + cx,
14
+ y: sin * (x - cx) + cos * (y - cy) + cy
15
+ };
16
+ }
17
+ /**
18
+ * Calculate corner point in canvas coordinates for a rotated element.
19
+ * @param x - Element x position
20
+ * @param y - Element y position
21
+ * @param width - Element width
22
+ * @param height - Element height
23
+ * @param rotationRadians - Rotation in radians
24
+ * @param xMagnitude - 0 = left, 0.5 = center, 1 = right
25
+ * @param yMagnitude - 0 = top, 0.5 = center, 1 = bottom
26
+ */
27
+ function getCornerPoint(x, y, width, height, rotationRadians, xMagnitude, yMagnitude) {
28
+ return rotatePoint(x + width / 2, y + height / 2, x + xMagnitude * width, y + yMagnitude * height, rotationRadians);
29
+ }
30
+ /**
31
+ * Get opposite corner magnitudes for a handle.
32
+ * Used to determine which corner stays fixed during resize.
33
+ */
34
+ function getOppositeCorner(handle) {
35
+ switch (handle) {
36
+ case "nw": return {
37
+ x: 1,
38
+ y: 1
39
+ };
40
+ case "n": return {
41
+ x: .5,
42
+ y: 1
43
+ };
44
+ case "ne": return {
45
+ x: 0,
46
+ y: 1
47
+ };
48
+ case "e": return {
49
+ x: 0,
50
+ y: .5
51
+ };
52
+ case "se": return {
53
+ x: 0,
54
+ y: 0
55
+ };
56
+ case "s": return {
57
+ x: .5,
58
+ y: 0
59
+ };
60
+ case "sw": return {
61
+ x: 1,
62
+ y: 0
63
+ };
64
+ case "w": return {
65
+ x: 1,
66
+ y: .5
67
+ };
68
+ default: return {
69
+ x: .5,
70
+ y: .5
71
+ };
72
+ }
73
+ }
74
+
75
+ //#endregion
76
+ export { getCornerPoint, getOppositeCorner, rotatePoint };
77
+ //# sourceMappingURL=transformUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transformUtils.js","names":[],"sources":["../../src/gui/transformUtils.ts"],"sourcesContent":["/**\n * Pure utility functions for transform calculations.\n * Extracted from motion designer TransformHandles component.\n */\n\n/**\n * Rotate a point around a center point by given radians.\n */\nexport function rotatePoint(\n cx: number,\n cy: number,\n x: number,\n y: number,\n radians: number,\n): { x: number; y: number } {\n const cos = Math.cos(radians);\n const sin = Math.sin(radians);\n const nx = cos * (x - cx) - sin * (y - cy) + cx;\n const ny = sin * (x - cx) + cos * (y - cy) + cy;\n return { x: nx, y: ny };\n}\n\n/**\n * Calculate corner point in canvas coordinates for a rotated element.\n * @param x - Element x position\n * @param y - Element y position\n * @param width - Element width\n * @param height - Element height\n * @param rotationRadians - Rotation in radians\n * @param xMagnitude - 0 = left, 0.5 = center, 1 = right\n * @param yMagnitude - 0 = top, 0.5 = center, 1 = bottom\n */\nexport function getCornerPoint(\n x: number,\n y: number,\n width: number,\n height: number,\n rotationRadians: number,\n xMagnitude: number,\n yMagnitude: number,\n): { x: number; y: number } {\n const centerX = x + width / 2;\n const centerY = y + height / 2;\n const localCornerX = x + xMagnitude * width;\n const localCornerY = y + yMagnitude * height;\n return rotatePoint(\n centerX,\n centerY,\n localCornerX,\n localCornerY,\n rotationRadians,\n );\n}\n\n/**\n * Get opposite corner magnitudes for a handle.\n * Used to determine which corner stays fixed during resize.\n */\nexport function getOppositeCorner(handle: string): { x: number; y: number } {\n switch (handle) {\n case \"nw\":\n return { x: 1, y: 1 }; // se corner\n case \"n\":\n return { x: 0.5, y: 1 }; // s corner\n case \"ne\":\n return { x: 0, y: 1 }; // sw corner\n case \"e\":\n return { x: 0, y: 0.5 }; // w corner\n case \"se\":\n return { x: 0, y: 0 }; // nw corner\n case \"s\":\n return { x: 0.5, y: 0 }; // n corner\n case \"sw\":\n return { x: 1, y: 0 }; // ne corner\n case \"w\":\n return { x: 1, y: 0.5 }; // e corner\n default:\n return { x: 0.5, y: 0.5 };\n }\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,YACd,IACA,IACA,GACA,GACA,SAC0B;CAC1B,MAAM,MAAM,KAAK,IAAI,QAAQ;CAC7B,MAAM,MAAM,KAAK,IAAI,QAAQ;AAG7B,QAAO;EAAE,GAFE,OAAO,IAAI,MAAM,OAAO,IAAI,MAAM;EAE7B,GADL,OAAO,IAAI,MAAM,OAAO,IAAI,MAAM;EACtB;;;;;;;;;;;;AAazB,SAAgB,eACd,GACA,GACA,OACA,QACA,iBACA,YACA,YAC0B;AAK1B,QAAO,YAJS,IAAI,QAAQ,GACZ,IAAI,SAAS,GACR,IAAI,aAAa,OACjB,IAAI,aAAa,QAMpC,gBACD;;;;;;AAOH,SAAgB,kBAAkB,QAA0C;AAC1E,SAAQ,QAAR;EACE,KAAK,KACH,QAAO;GAAE,GAAG;GAAG,GAAG;GAAG;EACvB,KAAK,IACH,QAAO;GAAE,GAAG;GAAK,GAAG;GAAG;EACzB,KAAK,KACH,QAAO;GAAE,GAAG;GAAG,GAAG;GAAG;EACvB,KAAK,IACH,QAAO;GAAE,GAAG;GAAG,GAAG;GAAK;EACzB,KAAK,KACH,QAAO;GAAE,GAAG;GAAG,GAAG;GAAG;EACvB,KAAK,IACH,QAAO;GAAE,GAAG;GAAK,GAAG;GAAG;EACzB,KAAK,KACH,QAAO;GAAE,GAAG;GAAG,GAAG;GAAG;EACvB,KAAK,IACH,QAAO;GAAE,GAAG;GAAG,GAAG;GAAK;EACzB,QACE,QAAO;GAAE,GAAG;GAAK,GAAG;GAAK"}