@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.
- package/dist/EF_FRAMEGEN.d.ts +5 -0
- package/dist/EF_FRAMEGEN.js +20 -4
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_INTERACTIVE.js.map +1 -1
- package/dist/_virtual/rolldown_runtime.js +27 -0
- package/dist/canvas/EFCanvas.d.ts +311 -0
- package/dist/canvas/EFCanvas.js +1089 -0
- package/dist/canvas/EFCanvas.js.map +1 -0
- package/dist/canvas/EFCanvasItem.d.ts +55 -0
- package/dist/canvas/EFCanvasItem.js +72 -0
- package/dist/canvas/EFCanvasItem.js.map +1 -0
- package/dist/canvas/api/CanvasAPI.d.ts +115 -0
- package/dist/canvas/api/CanvasAPI.js +182 -0
- package/dist/canvas/api/CanvasAPI.js.map +1 -0
- package/dist/canvas/api/types.d.ts +42 -0
- package/dist/canvas/coordinateTransform.js +90 -0
- package/dist/canvas/coordinateTransform.js.map +1 -0
- package/dist/canvas/getElementBounds.js +40 -0
- package/dist/canvas/getElementBounds.js.map +1 -0
- package/dist/canvas/overlays/SelectionOverlay.js +265 -0
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -0
- package/dist/canvas/overlays/overlayState.js +153 -0
- package/dist/canvas/overlays/overlayState.js.map +1 -0
- package/dist/canvas/selection/SelectionController.js +105 -0
- package/dist/canvas/selection/SelectionController.js.map +1 -0
- package/dist/canvas/selection/SelectionModel.d.ts +98 -0
- package/dist/canvas/selection/SelectionModel.js +229 -0
- package/dist/canvas/selection/SelectionModel.js.map +1 -0
- package/dist/canvas/selection/selectionContext.d.ts +31 -0
- package/dist/canvas/selection/selectionContext.js +12 -0
- package/dist/canvas/selection/selectionContext.js.map +1 -0
- package/dist/elements/ContainerInfo.d.ts +29 -0
- package/dist/elements/ContainerInfo.js +30 -0
- package/dist/elements/ContainerInfo.js.map +1 -0
- package/dist/elements/EFAudio.d.ts +13 -3
- package/dist/elements/EFAudio.js +64 -10
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +18 -16
- package/dist/elements/EFCaptions.js +110 -19
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +16 -6
- package/dist/elements/EFImage.js +79 -9
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +51 -4
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +125 -52
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +24 -6
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +12 -8
- package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +46 -7
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +98 -73
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +18 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +8 -2
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +31 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +97 -72
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +25 -14
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +47 -16
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +37 -19
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +65 -21
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +8 -3
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +32 -9
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +33 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +23 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +34 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +31 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +31 -114
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +44 -8
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +18 -7
- package/dist/elements/EFMedia.js +23 -3
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +96 -0
- package/dist/elements/EFPanZoom.js +290 -0
- package/dist/elements/EFPanZoom.js.map +1 -0
- package/dist/elements/EFSourceMixin.js +7 -6
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +6 -6
- package/dist/elements/EFSurface.js +7 -2
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +2 -1
- package/dist/elements/EFTemporal.js +192 -71
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +5 -4
- package/dist/elements/EFText.js +102 -13
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +32 -6
- package/dist/elements/EFTextSegment.js +53 -15
- package/dist/elements/EFTextSegment.js.map +1 -1
- package/dist/elements/EFThumbnailStrip.d.ts +118 -56
- package/dist/elements/EFThumbnailStrip.js +522 -358
- package/dist/elements/EFThumbnailStrip.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +223 -27
- package/dist/elements/EFTimegroup.js +851 -148
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +42 -5
- package/dist/elements/EFVideo.js +165 -11
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +6 -6
- package/dist/elements/EFWaveform.js +2 -1
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.d.ts +35 -0
- package/dist/elements/ElementPositionInfo.js +49 -0
- package/dist/elements/ElementPositionInfo.js.map +1 -0
- package/dist/elements/FetchMixin.js +16 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/SessionThumbnailCache.js +152 -0
- package/dist/elements/SessionThumbnailCache.js.map +1 -0
- package/dist/elements/TargetController.js +3 -1
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js +9 -3
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/findRootTemporal.js +30 -0
- package/dist/elements/findRootTemporal.js.map +1 -0
- package/dist/elements/renderTemporalAudio.js +18 -5
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/updateAnimations.js +492 -109
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/gui/ContextMixin.js +4 -2
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js +74 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +50 -0
- package/dist/gui/EFActiveRootTemporal.js +94 -0
- package/dist/gui/EFActiveRootTemporal.js.map +1 -0
- package/dist/gui/EFConfiguration.d.ts +11 -5
- package/dist/gui/EFConfiguration.js.map +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +109 -13
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFFilmstrip.d.ts +11 -214
- package/dist/gui/EFFilmstrip.js +53 -1152
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.d.ts +3 -3
- package/dist/gui/EFFitScale.js +39 -12
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +4 -4
- package/dist/gui/EFOverlayItem.d.ts +48 -0
- package/dist/gui/EFOverlayItem.js +97 -0
- package/dist/gui/EFOverlayItem.js.map +1 -0
- package/dist/gui/EFOverlayLayer.d.ts +70 -0
- package/dist/gui/EFOverlayLayer.js +104 -0
- package/dist/gui/EFOverlayLayer.js.map +1 -0
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFPreview.d.ts +4 -4
- package/dist/gui/EFResizableBox.d.ts +12 -16
- package/dist/gui/EFResizableBox.js +109 -451
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +30 -5
- package/dist/gui/EFScrubber.js +224 -31
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.js +4 -1
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +71 -0
- package/dist/gui/EFTimelineRuler.js +320 -0
- package/dist/gui/EFTimelineRuler.js.map +1 -0
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTransformHandles.d.ts +91 -0
- package/dist/gui/EFTransformHandles.js +393 -0
- package/dist/gui/EFTransformHandles.js.map +1 -0
- package/dist/gui/EFWorkbench.d.ts +182 -4
- package/dist/gui/EFWorkbench.js +2067 -22
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.d.ts +31 -0
- package/dist/gui/FitScaleHelpers.js +41 -0
- package/dist/gui/FitScaleHelpers.js.map +1 -0
- package/dist/gui/PlaybackController.d.ts +2 -1
- package/dist/gui/PlaybackController.js +46 -15
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchy.d.ts +65 -0
- package/dist/gui/hierarchy/EFHierarchy.js +338 -0
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -0
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +118 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js +551 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -0
- package/dist/gui/hierarchy/hierarchyContext.d.ts +38 -0
- package/dist/gui/hierarchy/hierarchyContext.js +8 -0
- package/dist/gui/hierarchy/hierarchyContext.js.map +1 -0
- package/dist/gui/icons.js +34 -0
- package/dist/gui/icons.js.map +1 -0
- package/dist/gui/panZoomTransformContext.js +12 -0
- package/dist/gui/panZoomTransformContext.js.map +1 -0
- package/dist/gui/previewSettingsContext.js +12 -0
- package/dist/gui/previewSettingsContext.js.map +1 -0
- package/dist/gui/timeline/EFTimeline.d.ts +270 -0
- package/dist/gui/timeline/EFTimeline.js +1369 -0
- package/dist/gui/timeline/EFTimeline.js.map +1 -0
- package/dist/gui/timeline/EFTimelineRow.js +374 -0
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -0
- package/dist/gui/timeline/TrimHandles.d.ts +36 -0
- package/dist/gui/timeline/TrimHandles.js +204 -0
- package/dist/gui/timeline/TrimHandles.js.map +1 -0
- package/dist/gui/timeline/flattenHierarchy.js +31 -0
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -0
- package/dist/gui/timeline/timelineStateContext.d.ts +26 -0
- package/dist/gui/timeline/timelineStateContext.js +42 -0
- package/dist/gui/timeline/timelineStateContext.js.map +1 -0
- package/dist/gui/timeline/tracks/AudioTrack.js +264 -0
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js +595 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js +19 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ImageTrack.js +53 -0
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TextTrack.js +250 -0
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js +143 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TrackItem.js +269 -0
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -0
- package/dist/gui/timeline/tracks/VideoTrack.js +265 -0
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js +19 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ensureTrackItemInit.js +1 -0
- package/dist/gui/timeline/tracks/preloadTracks.js +9 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js +119 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -0
- package/dist/gui/timeline/tracks/waveformUtils.js +80 -0
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -0
- package/dist/gui/transformCalculations.js +217 -0
- package/dist/gui/transformCalculations.js.map +1 -0
- package/dist/gui/transformUtils.d.ts +37 -0
- package/dist/gui/transformUtils.js +77 -0
- package/dist/gui/transformUtils.js.map +1 -0
- package/dist/gui/tree/EFTree.d.ts +59 -0
- package/dist/gui/tree/EFTree.js +174 -0
- package/dist/gui/tree/EFTree.js.map +1 -0
- package/dist/gui/tree/EFTreeItem.d.ts +38 -0
- package/dist/gui/tree/EFTreeItem.js +146 -0
- package/dist/gui/tree/EFTreeItem.js.map +1 -0
- package/dist/gui/tree/treeContext.d.ts +60 -0
- package/dist/gui/tree/treeContext.js +23 -0
- package/dist/gui/tree/treeContext.js.map +1 -0
- package/dist/index.d.ts +32 -8
- package/dist/index.js +30 -6
- package/dist/index.js.map +1 -1
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +688 -0
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +1 -0
- package/dist/node_modules/react/cjs/react.development.js +1521 -0
- package/dist/node_modules/react/cjs/react.development.js.map +1 -0
- package/dist/node_modules/react/index.js +13 -0
- package/dist/node_modules/react/index.js.map +1 -0
- package/dist/node_modules/react/jsx-runtime.js +13 -0
- package/dist/node_modules/react/jsx-runtime.js.map +1 -0
- package/dist/preview/AdaptiveResolutionTracker.js +228 -0
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -0
- package/dist/preview/RenderProfiler.js +135 -0
- package/dist/preview/RenderProfiler.js.map +1 -0
- package/dist/preview/previewSettings.js +131 -0
- package/dist/preview/previewSettings.js.map +1 -0
- package/dist/preview/previewTypes.js +64 -0
- package/dist/preview/previewTypes.js.map +1 -0
- package/dist/preview/renderTimegroupPreview.js +656 -0
- package/dist/preview/renderTimegroupPreview.js.map +1 -0
- package/dist/preview/renderTimegroupToCanvas.d.ts +37 -0
- package/dist/preview/renderTimegroupToCanvas.js +840 -0
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -0
- package/dist/preview/renderTimegroupToVideo.d.ts +39 -0
- package/dist/preview/renderTimegroupToVideo.js +274 -0
- package/dist/preview/renderTimegroupToVideo.js.map +1 -0
- package/dist/preview/renderers.js +16 -0
- package/dist/preview/renderers.js.map +1 -0
- package/dist/preview/statsTrackingStrategy.js +201 -0
- package/dist/preview/statsTrackingStrategy.js.map +1 -0
- package/dist/preview/thumbnailCacheSettings.js +52 -0
- package/dist/preview/thumbnailCacheSettings.js.map +1 -0
- package/dist/preview/workers/WorkerPool.js +178 -0
- package/dist/preview/workers/WorkerPool.js.map +1 -0
- package/dist/sandbox/PlaybackControls.js +10 -0
- package/dist/sandbox/PlaybackControls.js.map +1 -0
- package/dist/sandbox/ScenarioRunner.js +1 -0
- package/dist/sandbox/index.js +2 -0
- package/dist/style.css +66 -69
- package/dist/transcoding/types/index.d.ts +2 -1
- package/dist/transcoding/utils/UrlGenerator.d.ts +6 -1
- package/dist/transcoding/utils/UrlGenerator.js +12 -3
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/dist/utils/LRUCache.js +1 -375
- package/dist/utils/LRUCache.js.map +1 -1
- package/dist/utils/frameTime.js +14 -0
- package/dist/utils/frameTime.js.map +1 -0
- package/package.json +3 -3
- package/test/profilingPlugin.ts +223 -0
- package/test/recordReplayProxyPlugin.js +22 -27
- package/test/thumbnail-performance-test.html +116 -0
- package/test/visualRegressionUtils.ts +286 -0
- package/types.json +1 -1
- package/dist/elements/TimegroupController.d.ts +0 -18
- package/dist/msToTimeCode.js +0 -17
- package/dist/msToTimeCode.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"updateAnimations.js","names":[],"sources":["../../src/elements/updateAnimations.ts"],"sourcesContent":["import {\n deepGetTemporalElements,\n type TemporalMixinInterface,\n} from \"./EFTemporal.ts\";\n\n// All animatable elements are temporal elements with HTMLElement interface\nexport type AnimatableElement = TemporalMixinInterface & HTMLElement;\n\n// Constants\nconst ANIMATION_PRECISION_OFFSET = 0.1; // Use 0.1ms to safely avoid completion threshold\nconst DEFAULT_ANIMATION_ITERATIONS = 1;\nconst PROGRESS_PROPERTY = \"--ef-progress\";\nconst DURATION_PROPERTY = \"--ef-duration\";\nconst TRANSITION_DURATION_PROPERTY = \"--ef-transition-duration\";\nconst TRANSITION_OUT_START_PROPERTY = \"--ef-transition-out-start\";\n\n/**\n * Represents the temporal state of an element relative to the timeline\n */\ninterface TemporalState {\n progress: number;\n isVisible: boolean;\n timelineTimeMs: number;\n}\n\n/**\n * Evaluates what the element's state should be based on the timeline\n */\nexport const evaluateTemporalState = (\n element: AnimatableElement,\n): TemporalState => {\n // Get timeline time from root timegroup, or use element's own time if it IS a timegroup\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n\n const progress =\n element.durationMs <= 0\n ? 1\n : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));\n\n // Root elements and elements aligned with composition end should remain visible at exact end time\n // Text segments should also use inclusive end since they're meant to be visible for full duration\n // Other elements use exclusive end for clean transitions\n const isRootElement = !(element as any).parentTimegroup;\n const isLastElementInComposition =\n element.endTimeMs === element.rootTimegroup?.endTimeMs;\n const isTextSegment = element.tagName === \"EF-TEXT-SEGMENT\";\n const useInclusiveEnd =\n isRootElement || isLastElementInComposition || isTextSegment;\n\n const isVisible =\n element.startTimeMs <= timelineTimeMs &&\n (useInclusiveEnd\n ? element.endTimeMs >= timelineTimeMs\n : element.endTimeMs > timelineTimeMs);\n\n return { progress, isVisible, timelineTimeMs };\n};\n\n/**\n * Evaluates element visibility specifically for animation coordination\n * Uses inclusive end boundaries to prevent animation jumps at exact boundaries\n */\nexport const evaluateTemporalStateForAnimation = (\n element: AnimatableElement,\n): TemporalState => {\n // Get timeline time from root timegroup, or use element's own time if it IS a timegroup\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n\n const progress =\n element.durationMs <= 0\n ? 1\n : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));\n\n // For animation coordination, use inclusive end for ALL elements to prevent visual jumps\n const isVisible =\n element.startTimeMs <= timelineTimeMs &&\n element.endTimeMs >= timelineTimeMs;\n\n return { progress, isVisible, timelineTimeMs };\n};\n\n/**\n * Updates the visual state (CSS + display) to match temporal state\n */\nconst updateVisualState = (\n element: AnimatableElement,\n state: TemporalState,\n): void => {\n // Always set progress (needed for many use cases)\n element.style.setProperty(PROGRESS_PROPERTY, `${state.progress * 100}%`);\n\n // Handle visibility\n if (!state.isVisible) {\n if (element.style.display !== \"none\") {\n element.style.display = \"none\";\n }\n return;\n }\n\n if (element.style.display === \"none\") {\n element.style.display = \"\";\n }\n\n // Set other CSS properties for visible elements only\n element.style.setProperty(DURATION_PROPERTY, `${element.durationMs}ms`);\n element.style.setProperty(\n TRANSITION_DURATION_PROPERTY,\n `${element.parentTimegroup?.overlapMs ?? 0}ms`,\n );\n element.style.setProperty(\n TRANSITION_OUT_START_PROPERTY,\n `${element.durationMs - (element.parentTimegroup?.overlapMs ?? 0)}ms`,\n );\n};\n\n/**\n * Coordinates animations for a single element and its subtree, using the element as the time source\n */\nconst coordinateAnimationsForSingleElement = (\n element: AnimatableElement,\n): void => {\n // Get animations on the element itself and its subtree\n // CSS animations created via the 'animation' property are included\n const animations = element.getAnimations({ subtree: true });\n\n for (const animation of animations) {\n // Ensure animation is in a playable state (not finished)\n // Finished animations can't be controlled, so reset them\n if (animation.playState === \"finished\") {\n animation.cancel();\n // Re-initialize the animation so it can be controlled\n animation.play();\n animation.pause();\n } else if (animation.playState === \"running\") {\n // Pause running animations so we can control them manually\n animation.pause();\n }\n\n const effect = animation.effect;\n if (!(effect && effect instanceof KeyframeEffect)) {\n continue;\n }\n\n const target = effect.target;\n if (!target) {\n continue;\n }\n\n // For animations in this element's subtree, always use this element as the time source\n // This handles both animations directly on the temporal element and on its non-temporal children\n const timing = effect.getTiming();\n // Duration and delay from getTiming() are already in milliseconds\n // They include CSS animation-duration and animation-delay values\n const duration = Number(timing.duration) || 0;\n let delay = Number(timing.delay) || 0;\n\n // For Web Animations API animations, getTiming().delay is always correct.\n // For CSS animations, we may need to read from computed styles.\n // Try to read delay from computed styles as a fallback/override for CSS animations\n if (target instanceof HTMLElement) {\n const computedStyle = window.getComputedStyle(target);\n const animationDelays = computedStyle.animationDelay\n .split(\", \")\n .map((s) => s.trim());\n\n // Parse CSS delay value\n const parseDelay = (delayStr: string): number => {\n if (delayStr === \"0s\" || delayStr === \"0ms\") {\n return 0;\n }\n const delayMatch = delayStr.match(/^([\\d.]+)(s|ms)$/);\n if (delayMatch?.[1] && delayMatch[2]) {\n const value = Number.parseFloat(delayMatch[1]);\n const unit = delayMatch[2];\n return unit === \"s\" ? value * 1000 : value;\n }\n return 0;\n };\n\n // Only override delay from computed styles if:\n // 1. We have a valid parsed delay value, OR\n // 2. The computed style explicitly says \"0s\" or \"0ms\" (meaning no CSS delay)\n // This ensures we don't override WAAPI delay with 0 from computed styles when there's no CSS animation\n if (animationDelays.length === 1 && animationDelays[0]) {\n const parsedDelay = parseDelay(animationDelays[0]);\n // Only override if we got a valid parse AND it's not just a default \"0s\" from computed styles\n // OR if it's explicitly \"0s\"/\"0ms\" and getTiming().delay is also 0 (CSS animation with no delay)\n if (parsedDelay > 0) {\n delay = parsedDelay;\n } else if (\n (animationDelays[0] === \"0s\" || animationDelays[0] === \"0ms\") &&\n delay === 0\n ) {\n // Both are 0, so keep 0\n delay = 0;\n }\n // Otherwise, keep getTiming().delay (for WAAPI animations)\n } else if (animationDelays.length > 1) {\n // Multiple animations: try to match by index\n const allAnimations = Array.from(target.getAnimations());\n const animationIndex = allAnimations.indexOf(animation);\n if (\n animationIndex >= 0 &&\n animationIndex < animationDelays.length &&\n animationDelays[animationIndex]\n ) {\n const parsedDelay = parseDelay(animationDelays[animationIndex]);\n if (parsedDelay > 0) {\n delay = parsedDelay;\n }\n // Otherwise, keep getTiming().delay\n }\n }\n // If no computed styles match, keep getTiming().delay (for WAAPI animations)\n }\n\n const iterations =\n Number(timing.iterations) || DEFAULT_ANIMATION_ITERATIONS;\n\n if (duration <= 0) {\n animation.currentTime = 0;\n continue;\n }\n\n // Use the element itself as the time source (it's guaranteed to be temporal)\n const currentTime = element.ownCurrentTimeMs ?? 0;\n\n // Special case for ef-text-segment: apply stagger offset for animation timing\n // This allows staggered animations while keeping visibility timing unchanged\n // We ADD the stagger offset to the delay, so animations start later for later segments\n let effectiveDelay = delay;\n if (\n element.tagName === \"EF-TEXT-SEGMENT\" &&\n (element as any).staggerOffsetMs !== undefined\n ) {\n const staggerOffsetMs = (element as any).staggerOffsetMs as number;\n effectiveDelay = delay + staggerOffsetMs;\n }\n\n // If before delay, show initial keyframe state (0% of animation)\n // Use strict < instead of <= so animations can start immediately when delay is reached\n if (currentTime < effectiveDelay) {\n // Set to 0 to show initial keyframe (animation time, not including delay)\n // When manually controlling animation.currentTime, 0 represents the start of the animation\n animation.currentTime = 0;\n continue;\n }\n\n const adjustedTime = currentTime - effectiveDelay;\n const currentIteration = Math.floor(adjustedTime / duration);\n let currentIterationTime = adjustedTime % duration;\n\n // Handle animation-direction\n const direction = timing.direction || \"normal\";\n const isAlternate =\n direction === \"alternate\" || direction === \"alternate-reverse\";\n const shouldReverse =\n direction === \"reverse\" ||\n (direction === \"alternate\" && currentIteration % 2 === 1) ||\n (direction === \"alternate-reverse\" && currentIteration % 2 === 0);\n\n if (shouldReverse) {\n currentIterationTime = duration - currentIterationTime;\n }\n\n if (currentIteration >= iterations) {\n // Animation would be complete - clamp to just before completion\n // This prevents the animation from being removed from the element\n // animation.currentTime is the time within the animation (not including delay)\n const maxSafeAnimationTime =\n duration * iterations - ANIMATION_PRECISION_OFFSET;\n\n // For alternate directions at completion, we need to set currentTime based on the final iteration\n // The final iteration for alternate is iteration (iterations - 1), which is forward if iterations is odd\n if (isAlternate) {\n const finalIteration = iterations - 1;\n const isFinalIterationReversed =\n (direction === \"alternate\" && finalIteration % 2 === 1) ||\n (direction === \"alternate-reverse\" && finalIteration % 2 === 0);\n if (isFinalIterationReversed) {\n // At end of reversed iteration, currentTime should be near 0 (but clamped)\n animation.currentTime = Math.min(\n duration - ANIMATION_PRECISION_OFFSET,\n maxSafeAnimationTime,\n );\n } else {\n // At end of forward iteration, currentTime should be near duration (but clamped)\n animation.currentTime = maxSafeAnimationTime;\n }\n } else {\n animation.currentTime = maxSafeAnimationTime;\n }\n } else {\n // Animation in progress\n // For alternate/alternate-reverse directions, currentTime should be set to the time within\n // the current iteration (after applying direction), not cumulative time.\n // However, when there's a delay, we need to use cumulative time (adjustedTime) instead.\n // For normal/reverse directions, currentTime is always cumulative time.\n if (isAlternate) {\n // For alternate directions without delay, use iteration time (after direction applied)\n // For alternate directions with delay:\n // - Iteration 0: use ownCurrentTimeMs (which equals adjustedTime + delay for iteration 0)\n // - Iteration 1+: use cumulative time (adjustedTime)\n if (effectiveDelay > 0) {\n if (currentIteration === 0) {\n // For iteration 0 with delay, use ownCurrentTimeMs (matches test expectations)\n animation.currentTime = currentTime;\n } else {\n // With delay and iteration > 0, use cumulative time\n const maxSafeAnimationTime =\n duration * iterations - ANIMATION_PRECISION_OFFSET;\n animation.currentTime = Math.min(\n adjustedTime,\n maxSafeAnimationTime,\n );\n }\n } else {\n // Without delay: use iteration time (after direction applied)\n animation.currentTime = currentIterationTime;\n }\n } else {\n // For normal/reverse directions, use cumulative time\n const timeWithinAnimation =\n currentIteration * duration + currentIterationTime;\n const maxSafeAnimationTime =\n duration * iterations - ANIMATION_PRECISION_OFFSET;\n animation.currentTime = Math.min(\n timeWithinAnimation,\n maxSafeAnimationTime,\n );\n }\n }\n }\n};\n\n/**\n * Main function: synchronizes DOM element with timeline\n */\nexport const updateAnimations = (element: AnimatableElement): void => {\n const temporalState = evaluateTemporalState(element);\n deepGetTemporalElements(element).forEach((temporalElement) => {\n const temporalState = evaluateTemporalState(temporalElement);\n updateVisualState(temporalElement, temporalState);\n });\n updateVisualState(element, temporalState);\n\n // Coordinate animations - use animation-specific visibility to prevent jumps at exact boundaries\n const animationState = evaluateTemporalStateForAnimation(element);\n if (animationState.isVisible) {\n coordinateAnimationsForSingleElement(element);\n }\n\n // Coordinate animations for child elements using animation-specific visibility\n deepGetTemporalElements(element).forEach((temporalElement) => {\n const childAnimationState =\n evaluateTemporalStateForAnimation(temporalElement);\n if (childAnimationState.isVisible) {\n coordinateAnimationsForSingleElement(temporalElement);\n }\n });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"updateAnimations.js","names":["rootTracked","currentAnimations: Animation[]","currentAnimations","timeSource: AnimatableElement","staggerElement: AnimatableElement"],"sources":["../../src/elements/updateAnimations.ts"],"sourcesContent":["import {\n deepGetTemporalElements,\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"./EFTemporal.ts\";\n\n// All animatable elements are temporal elements with HTMLElement interface\nexport type AnimatableElement = TemporalMixinInterface & HTMLElement;\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst ANIMATION_PRECISION_OFFSET = 0.1; // Use 0.1ms to safely avoid completion threshold\nconst DEFAULT_ANIMATION_ITERATIONS = 1;\nconst PROGRESS_PROPERTY = \"--ef-progress\";\nconst DURATION_PROPERTY = \"--ef-duration\";\nconst TRANSITION_DURATION_PROPERTY = \"--ef-transition-duration\";\nconst TRANSITION_OUT_START_PROPERTY = \"--ef-transition-out-start\";\n\n// ============================================================================\n// Animation Tracking\n// ============================================================================\n\n/**\n * Tracks animations per element to prevent them from being lost when they complete.\n * Once an animation reaches 100% completion, it's removed from getAnimations(),\n * but we keep a reference to it so we can continue controlling it.\n */\nconst animationTracker = new WeakMap<Element, Set<Animation>>();\n\n/**\n * Tracks whether DOM structure has changed for an element, requiring animation rediscovery.\n * For render clones (static DOM), this stays false after initial discovery.\n * For prime timeline (interactive), this is set to true when mutations occur.\n */\nconst domStructureChanged = new WeakMap<Element, boolean>();\n\n/**\n * Tracks the last known animation count for an element to detect new animations.\n * Used as a lightweight check before calling expensive getAnimations().\n */\nconst lastAnimationCount = new WeakMap<Element, number>();\n\n/**\n * Checks if an element is in a render clone (static DOM context).\n * Render clones are in containers with class \"ef-render-clone-container\".\n */\nconst isRenderClone = (element: Element): boolean => {\n return element.closest(\".ef-render-clone-container\") !== null;\n};\n\n/**\n * Validates that an animation is still valid and controllable.\n * Animations become invalid when:\n * - They've been cancelled (idle state and not in getAnimations())\n * - Their effect is null (animation was removed)\n * - Their target is no longer in the DOM\n */\nconst isAnimationValid = (\n animation: Animation,\n currentAnimations: Animation[],\n): boolean => {\n // Check if animation has been cancelled\n if (\n animation.playState === \"idle\" &&\n !currentAnimations.includes(animation)\n ) {\n return false;\n }\n\n // Check if animation effect is still valid\n const effect = animation.effect;\n if (!effect) {\n return false;\n }\n\n // Check if target is still in DOM\n if (effect instanceof KeyframeEffect) {\n const target = effect.target;\n if (target && target instanceof Element) {\n if (!target.isConnected) {\n return false;\n }\n }\n }\n\n return true;\n};\n\n/**\n * Discovers and tracks animations on an element and its subtree.\n * This ensures we have references to animations even after they complete.\n *\n * Tracks animations per element where they exist, not just on the root element.\n * This allows us to find animations on any element in the subtree.\n *\n * OPTIMIZATION: For render clones (static DOM), discovery happens once at creation.\n * For prime timeline (interactive), discovery is responsive to DOM changes.\n *\n * Also cleans up invalid animations (cancelled, removed from DOM, etc.)\n */\nconst discoverAndTrackAnimations = (\n element: AnimatableElement,\n): { tracked: Set<Animation>; current: Animation[] } => {\n const isClone = isRenderClone(element);\n const hasTrackedAnimations = animationTracker.has(element);\n const structureChanged = domStructureChanged.get(element) ?? true;\n \n // OPTIMIZATION: For render clones with already-tracked animations and no structure changes,\n // skip expensive getAnimations() call and reuse tracked animations.\n // We still need to validate tracked animations are still valid, but we can do that\n // without calling getAnimations({ subtree: true }) on every frame.\n if (isClone && hasTrackedAnimations && !structureChanged) {\n // Use tracked animations directly, but validate them are still valid\n const rootTracked = animationTracker.get(element)!;\n const currentAnimations: Animation[] = [];\n \n // Quick validation: check if any tracked animations are still in getAnimations()\n // We only check the root element's direct animations (cheaper than subtree)\n const rootDirectAnimations = element.getAnimations();\n for (const animation of rootTracked) {\n if (isAnimationValid(animation, rootDirectAnimations)) {\n currentAnimations.push(animation);\n }\n }\n \n // For render clones, animations don't change, so we can trust tracked set\n // Just return the tracked set (which should be complete)\n return { tracked: rootTracked, current: currentAnimations };\n }\n \n // For prime timeline or first discovery: get current animations from the browser (includes subtree)\n // CRITICAL: This is expensive, so we return it to avoid calling it again\n const currentAnimations = element.getAnimations({ subtree: true });\n \n // Mark structure as stable after discovery (for render clones, this stays stable)\n if (isClone) {\n domStructureChanged.set(element, false);\n }\n \n // Track animation count for lightweight change detection\n lastAnimationCount.set(element, currentAnimations.length);\n\n // Track animations on each element where they exist\n for (const animation of currentAnimations) {\n const effect = animation.effect;\n const target = effect && effect instanceof KeyframeEffect ? effect.target : null;\n if (target && target instanceof Element) {\n let tracked = animationTracker.get(target);\n if (!tracked) {\n tracked = new Set<Animation>();\n animationTracker.set(target, tracked);\n }\n tracked.add(animation);\n }\n }\n \n // Also maintain a set on the root element for coordination\n let rootTracked = animationTracker.get(element);\n if (!rootTracked) {\n rootTracked = new Set<Animation>();\n animationTracker.set(element, rootTracked);\n }\n\n // Update root set with all current animations\n for (const animation of currentAnimations) {\n rootTracked.add(animation);\n }\n\n // Clean up invalid animations from root set\n // This handles animations that were cancelled, removed from DOM, or had their effects removed\n for (const animation of rootTracked) {\n if (!isAnimationValid(animation, currentAnimations)) {\n rootTracked.delete(animation);\n }\n }\n\n // Also clean up invalid animations from per-element sets\n // We need to check all elements that might have tracked animations\n const allTargets = new Set<Element>();\n for (const animation of currentAnimations) {\n const effect = animation.effect;\n const target = effect && effect instanceof KeyframeEffect ? effect.target : null;\n if (target && target instanceof Element) {\n allTargets.add(target);\n }\n }\n\n // Check tracked sets for elements that might have invalid animations\n // We can't iterate WeakMap directly, but we can check elements we know about\n // and also check elements in the subtree\n const subtreeElements = element.querySelectorAll(\"*\");\n for (const el of subtreeElements) {\n const tracked = animationTracker.get(el);\n if (tracked) {\n for (const animation of tracked) {\n if (!isAnimationValid(animation, Array.from(el.getAnimations()))) {\n tracked.delete(animation);\n }\n }\n // Remove empty sets to free memory\n if (tracked.size === 0) {\n animationTracker.delete(el);\n }\n }\n }\n\n return { tracked: rootTracked, current: currentAnimations };\n};\n\n/**\n * Gets tracked animations for a specific element.\n * This allows external code to access animations even after they complete.\n * Automatically filters out invalid animations (cancelled, removed, etc.)\n */\nexport const getTrackedAnimations = (element: Element): Animation[] => {\n const tracked = animationTracker.get(element);\n if (!tracked) {\n return [];\n }\n\n const currentAnimations = element.getAnimations();\n\n // Filter out invalid animations\n return Array.from(tracked).filter((animation) =>\n isAnimationValid(animation, currentAnimations),\n );\n};\n\n/**\n * Cleans up tracked animations when an element is disconnected.\n * This prevents memory leaks.\n */\nexport const cleanupTrackedAnimations = (element: Element): void => {\n animationTracker.delete(element);\n domStructureChanged.delete(element);\n lastAnimationCount.delete(element);\n};\n\n/**\n * Marks that DOM structure has changed for an element, requiring animation rediscovery.\n * Should be called when elements are added/removed or CSS classes change that affect animations.\n * \n * For render clones, this should never be called (DOM is static).\n * For prime timeline, call this when mutations occur that might affect animations.\n */\nexport const markDomStructureChanged = (element: Element): void => {\n // Only mark changes for prime timeline (not render clones)\n if (!isRenderClone(element)) {\n domStructureChanged.set(element, true);\n }\n};\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Represents the phase an element is in relative to the timeline.\n * This is the primary concept that drives all visibility and animation decisions.\n */\nexport type ElementPhase =\n | \"before-start\"\n | \"active\"\n | \"at-end-boundary\"\n | \"after-end\";\n\n/**\n * Represents the temporal state of an element relative to the timeline\n */\ninterface TemporalState {\n progress: number;\n isVisible: boolean;\n timelineTimeMs: number;\n phase: ElementPhase;\n}\n\n/**\n * Context object that holds all evaluated state for an element update.\n * This groups related state together, reducing parameter passing and making\n * the data flow clearer.\n */\ninterface ElementUpdateContext {\n element: AnimatableElement;\n state: TemporalState;\n}\n\n/**\n * Animation timing information extracted from an animation effect.\n * Groups related timing properties together.\n */\ninterface AnimationTiming {\n duration: number;\n delay: number;\n iterations: number;\n direction: string;\n}\n\n/**\n * Capability interface for elements that support stagger offset.\n * This encapsulates the stagger behavior behind a capability check rather than\n * leaking tag name checks throughout the codebase.\n */\ninterface StaggerableElement extends AnimatableElement {\n staggerOffsetMs?: number;\n}\n\n// ============================================================================\n// Phase Determination\n// ============================================================================\n\n/**\n * Determines what phase an element is in relative to the timeline.\n *\n * WHY: Phase is the primary concept that drives all decisions. By explicitly\n * enumerating phases, we make the code's logic clear: phase determines visibility,\n * animation coordination, and visual state.\n *\n * Phases:\n * - before-start: Timeline is before element's start time\n * - active: Timeline is within element's active range (start to end, exclusive of end)\n * - at-end-boundary: Timeline is exactly at element's end time\n * - after-end: Timeline is after element's end time\n *\n * Note: We detect \"at-end-boundary\" by checking if timeline equals end time.\n * The boundary policy will then determine if this should be treated as visible/active\n * or not based on element characteristics.\n */\nconst determineElementPhase = (\n element: AnimatableElement,\n timelineTimeMs: number,\n): ElementPhase => {\n // Read endTimeMs once to avoid recalculation issues\n const endTimeMs = element.endTimeMs;\n const startTimeMs = element.startTimeMs;\n\n // Invalid range (end <= start) means element hasn't computed its duration yet,\n // or has no temporal children (e.g., timegroup with only static HTML).\n // Treat as always active - these elements should be visible at all times.\n if (endTimeMs <= startTimeMs) {\n return \"active\";\n }\n\n if (timelineTimeMs < startTimeMs) {\n return \"before-start\";\n }\n // Use epsilon to handle floating point precision issues\n const epsilon = 0.001;\n const diff = timelineTimeMs - endTimeMs;\n\n // If clearly after end (difference > epsilon), return 'after-end'\n if (diff > epsilon) {\n return \"after-end\";\n }\n // If at or very close to end boundary (within epsilon), return 'at-end-boundary'\n if (Math.abs(diff) <= epsilon) {\n return \"at-end-boundary\";\n }\n // Otherwise, we're before the end, so check if we're active\n return \"active\";\n};\n\n// ============================================================================\n// Boundary Policies\n// ============================================================================\n\n/**\n * Policy interface for determining behavior at boundaries.\n * Different policies apply different rules for when elements should be visible\n * or have animations coordinated at exact boundary times.\n */\ninterface BoundaryPolicy {\n /**\n * Determines if an element should be considered visible/active at the end boundary\n * based on the element's characteristics.\n */\n shouldIncludeEndBoundary(element: AnimatableElement): boolean;\n}\n\n/**\n * Visibility policy: determines when elements should be visible for display purposes.\n *\n * WHY: Root elements, elements aligned with composition end, and text segments\n * should remain visible at exact end time to prevent flicker and show final frames.\n * Other elements use exclusive end for clean transitions between elements.\n */\nclass VisibilityPolicy implements BoundaryPolicy {\n shouldIncludeEndBoundary(element: AnimatableElement): boolean {\n // Root elements should remain visible at exact end time to prevent flicker\n const isRootElement = !element.parentTimegroup;\n if (isRootElement) {\n return true;\n }\n\n // Elements aligned with composition end should remain visible at exact end time\n const isLastElementInComposition =\n element.endTimeMs === element.rootTimegroup?.endTimeMs;\n if (isLastElementInComposition) {\n return true;\n }\n\n // Text segments use inclusive end since they're meant to be visible for full duration\n if (this.isTextSegment(element)) {\n return true;\n }\n\n // Other elements use exclusive end for clean transitions\n return false;\n }\n\n /**\n * Checks if element is a text segment.\n * Encapsulates the tag name check to hide implementation detail.\n */\n protected isTextSegment(element: AnimatableElement): boolean {\n return element.tagName === \"EF-TEXT-SEGMENT\";\n }\n}\n\n/**\n * Animation policy: determines when animations should be coordinated.\n *\n * WHY: When an animation reaches exactly the end time of an element, using exclusive\n * end would make the element invisible, causing the animation to be removed from the\n * DOM and creating a visual jump. By using inclusive end, we ensure animations remain\n * coordinated even at exact boundary times, providing smooth visual transitions.\n */\nclass AnimationPolicy implements BoundaryPolicy {\n shouldIncludeEndBoundary(_element: AnimatableElement): boolean {\n return true;\n }\n}\n\n// Policy instances (singleton pattern for stateless policies)\nconst visibilityPolicy = new VisibilityPolicy();\nconst animationPolicy = new AnimationPolicy();\n\n/**\n * Determines if an element should be visible based on its phase and visibility policy.\n */\nconst shouldBeVisible = (\n phase: ElementPhase,\n element: AnimatableElement,\n): boolean => {\n if (phase === \"before-start\" || phase === \"after-end\") {\n return false;\n }\n if (phase === \"active\") {\n return true;\n }\n // phase === \"at-end-boundary\"\n return visibilityPolicy.shouldIncludeEndBoundary(element);\n};\n\n/**\n * Determines if animations should be coordinated based on element phase and animation policy.\n */\nconst shouldCoordinateAnimations = (\n phase: ElementPhase,\n element: AnimatableElement,\n): boolean => {\n if (phase === \"before-start\" || phase === \"after-end\") {\n return false;\n }\n if (phase === \"active\") {\n return true;\n }\n // phase === \"at-end-boundary\"\n return animationPolicy.shouldIncludeEndBoundary(element);\n};\n\n// ============================================================================\n// Temporal State Evaluation\n// ============================================================================\n\n/**\n * Evaluates what the element's state should be based on the timeline.\n *\n * WHY: This function determines the complete temporal state including phase,\n * which becomes the primary driver for all subsequent decisions.\n */\nexport const evaluateTemporalState = (\n element: AnimatableElement,\n): TemporalState => {\n // Get timeline time from root timegroup, or use element's own time if it IS a timegroup\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n\n const progress =\n element.durationMs <= 0\n ? 1\n : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));\n\n const phase = determineElementPhase(element, timelineTimeMs);\n const isVisible = shouldBeVisible(phase, element);\n\n return { progress, isVisible, timelineTimeMs, phase };\n};\n\n/**\n * Evaluates element visibility state specifically for animation coordination.\n * Uses inclusive end boundaries to prevent animation jumps at exact boundaries.\n *\n * This is exported for external use cases that need animation-specific visibility\n * evaluation without the full ElementUpdateContext.\n */\nexport const evaluateAnimationVisibilityState = (\n element: AnimatableElement,\n): TemporalState => {\n const state = evaluateTemporalState(element);\n // Override visibility based on animation policy\n const shouldCoordinate = shouldCoordinateAnimations(state.phase, element);\n return { ...state, isVisible: shouldCoordinate };\n};\n\n// ============================================================================\n// Animation Time Mapping\n// ============================================================================\n\n/**\n * Capability check: determines if an element supports stagger offset.\n * Encapsulates the knowledge of which element types support this feature.\n */\nconst supportsStaggerOffset = (\n element: AnimatableElement,\n): element is StaggerableElement => {\n // Currently only text segments support stagger offset\n return element.tagName === \"EF-TEXT-SEGMENT\";\n};\n\n/**\n * Calculates effective delay including stagger offset if applicable.\n *\n * Stagger offset allows elements (like text segments) to have their animations\n * start at different times while keeping their visibility timing unchanged.\n * This enables staggered animation effects within a single timegroup.\n */\nconst calculateEffectiveDelay = (\n delay: number,\n element: AnimatableElement,\n): number => {\n if (supportsStaggerOffset(element)) {\n // Read stagger offset - try property first (more reliable), then CSS variable\n // The staggerOffsetMs property is set directly on the element and is always available\n const segment = element as any;\n if (\n segment.staggerOffsetMs !== undefined &&\n segment.staggerOffsetMs !== null\n ) {\n return delay + segment.staggerOffsetMs;\n }\n\n // Fallback to CSS variable if property not available\n let cssValue = (element as HTMLElement).style\n .getPropertyValue(\"--ef-stagger-offset\")\n .trim();\n\n if (!cssValue) {\n cssValue = window\n .getComputedStyle(element)\n .getPropertyValue(\"--ef-stagger-offset\")\n .trim();\n }\n\n if (cssValue) {\n // Parse \"100ms\" format to milliseconds\n const match = cssValue.match(/(\\d+(?:\\.\\d+)?)\\s*ms?/);\n if (match) {\n const staggerOffset = parseFloat(match[1]!);\n if (!isNaN(staggerOffset)) {\n return delay + staggerOffset;\n }\n } else {\n // Try parsing as just a number\n const numValue = parseFloat(cssValue);\n if (!isNaN(numValue)) {\n return delay + numValue;\n }\n }\n }\n }\n return delay;\n};\n\n/**\n * Calculates maximum safe animation time to prevent completion.\n *\n * WHY: Once an animation reaches \"finished\" state, it can no longer be manually controlled\n * via currentTime. By clamping to just before completion (using ANIMATION_PRECISION_OFFSET),\n * we ensure the animation remains in a controllable state, allowing us to synchronize it\n * with the timeline even when it would naturally be complete.\n */\nconst calculateMaxSafeAnimationTime = (\n duration: number,\n iterations: number,\n): number => {\n return duration * iterations - ANIMATION_PRECISION_OFFSET;\n};\n\n/**\n * Determines if the current iteration should be reversed based on direction\n */\nconst shouldReverseIteration = (\n direction: string,\n currentIteration: number,\n): boolean => {\n return (\n direction === \"reverse\" ||\n (direction === \"alternate\" && currentIteration % 2 === 1) ||\n (direction === \"alternate-reverse\" && currentIteration % 2 === 0)\n );\n};\n\n/**\n * Applies direction to iteration time (reverses if needed)\n */\nconst applyDirectionToIterationTime = (\n currentIterationTime: number,\n duration: number,\n direction: string,\n currentIteration: number,\n): number => {\n if (shouldReverseIteration(direction, currentIteration)) {\n return duration - currentIterationTime;\n }\n return currentIterationTime;\n};\n\n/**\n * Maps element time to animation time for normal direction.\n * Uses cumulative time throughout the animation.\n * Note: elementTime should already be adjusted (elementTime - effectiveDelay).\n */\nconst mapNormalDirectionTime = (\n elementTime: number,\n duration: number,\n maxSafeTime: number,\n): number => {\n const currentIteration = Math.floor(elementTime / duration);\n const iterationTime = elementTime % duration;\n const cumulativeTime = currentIteration * duration + iterationTime;\n return Math.min(cumulativeTime, maxSafeTime);\n};\n\n/**\n * Maps element time to animation time for reverse direction.\n * Uses cumulative time with reversed iterations.\n * Note: elementTime should already be adjusted (elementTime - effectiveDelay).\n */\nconst mapReverseDirectionTime = (\n elementTime: number,\n duration: number,\n maxSafeTime: number,\n): number => {\n const currentIteration = Math.floor(elementTime / duration);\n const rawIterationTime = elementTime % duration;\n const reversedIterationTime = duration - rawIterationTime;\n const cumulativeTime = currentIteration * duration + reversedIterationTime;\n return Math.min(cumulativeTime, maxSafeTime);\n};\n\n/**\n * Maps element time to animation time for alternate/alternate-reverse directions.\n *\n * WHY SPECIAL HANDLING: Alternate directions oscillate between forward and reverse iterations.\n * Without delay, we use iteration time (0 to duration) because the animation naturally\n * resets each iteration. However, with delay, iteration 0 needs to account for the delay\n * offset (using ownCurrentTimeMs), and later iterations need cumulative time to properly\n * track progress across multiple iterations. This complexity requires a dedicated mapper\n * rather than trying to handle it in the general case.\n */\nconst mapAlternateDirectionTime = (\n elementTime: number,\n effectiveDelay: number,\n duration: number,\n direction: string,\n maxSafeTime: number,\n): number => {\n const adjustedTime = elementTime - effectiveDelay;\n\n if (effectiveDelay > 0) {\n // With delay: iteration 0 uses elementTime to include delay offset,\n // later iterations use cumulative time to track progress across iterations\n const currentIteration = Math.floor(adjustedTime / duration);\n if (currentIteration === 0) {\n return Math.min(elementTime, maxSafeTime);\n }\n return Math.min(adjustedTime, maxSafeTime);\n }\n\n // Without delay: use iteration time (after direction applied) since animation\n // naturally resets each iteration\n const currentIteration = Math.floor(elementTime / duration);\n const rawIterationTime = elementTime % duration;\n const iterationTime = applyDirectionToIterationTime(\n rawIterationTime,\n duration,\n direction,\n currentIteration,\n );\n return Math.min(iterationTime, maxSafeTime);\n};\n\n/**\n * Maps element time to animation time based on direction.\n *\n * WHY: This function explicitly transforms element time to animation time, making\n * the time mapping concept clear. Different directions require different transformations\n * to achieve the desired visual effect.\n */\nconst mapElementTimeToAnimationTime = (\n elementTime: number,\n timing: AnimationTiming,\n effectiveDelay: number,\n): number => {\n const { duration, iterations, direction } = timing;\n const maxSafeTime = calculateMaxSafeAnimationTime(duration, iterations);\n // Calculate adjusted time (element time minus delay) for normal/reverse directions\n const adjustedTime = elementTime - effectiveDelay;\n\n if (direction === \"reverse\") {\n return mapReverseDirectionTime(adjustedTime, duration, maxSafeTime);\n }\n if (direction === \"alternate\" || direction === \"alternate-reverse\") {\n return mapAlternateDirectionTime(\n elementTime,\n effectiveDelay,\n duration,\n direction,\n maxSafeTime,\n );\n }\n // normal direction - use adjustedTime to account for delay\n return mapNormalDirectionTime(adjustedTime, duration, maxSafeTime);\n};\n\n/**\n * Determines the animation time for a completed animation based on direction.\n */\nconst getCompletedAnimationTime = (\n timing: AnimationTiming,\n maxSafeTime: number,\n): number => {\n const { direction, iterations, duration } = timing;\n\n if (direction === \"alternate\" || direction === \"alternate-reverse\") {\n // For alternate directions, determine if final iteration is reversed\n const finalIteration = iterations - 1;\n const isFinalIterationReversed =\n (direction === \"alternate\" && finalIteration % 2 === 1) ||\n (direction === \"alternate-reverse\" && finalIteration % 2 === 0);\n\n if (isFinalIterationReversed) {\n // At end of reversed iteration, currentTime should be near 0 (but clamped)\n return Math.min(duration - ANIMATION_PRECISION_OFFSET, maxSafeTime);\n }\n }\n\n // For normal, reverse, or forward final iteration of alternate: use max safe time\n return maxSafeTime;\n};\n\n/**\n * Validates that animation effect is a KeyframeEffect with a target\n */\nconst validateAnimationEffect = (\n effect: AnimationEffect | null,\n): effect is KeyframeEffect & { target: Element } => {\n return (\n effect !== null &&\n effect instanceof KeyframeEffect &&\n effect.target !== null\n );\n};\n\n/**\n * Extracts timing information from an animation effect.\n * Duration and delay from getTiming() are already in milliseconds.\n * We use getTiming().delay directly from the animation object.\n */\nconst extractAnimationTiming = (effect: KeyframeEffect): AnimationTiming => {\n const timing = effect.getTiming();\n\n return {\n duration: Number(timing.duration) || 0,\n delay: Number(timing.delay) || 0,\n iterations: Number(timing.iterations) || DEFAULT_ANIMATION_ITERATIONS,\n direction: timing.direction || \"normal\",\n };\n};\n\n/**\n * Prepares animation for manual control by ensuring it's paused\n */\nconst prepareAnimation = (animation: Animation): void => {\n // Ensure animation is in a controllable state\n // Finished animations can't be controlled, so reset them\n if (animation.playState === \"finished\") {\n animation.cancel();\n // After cancel, animation is in idle state - we can set currentTime directly\n // No need to play/pause - we'll control it via currentTime\n } else if (animation.playState === \"running\") {\n // Pause running animations so we can control them manually\n animation.pause();\n }\n // For \"idle\" or \"paused\" state, we can set currentTime directly without play/pause\n // Setting currentTime on a paused animation will apply the keyframes\n // No initialization needed - we control everything via currentTime\n};\n\n/**\n * Maps element time to animation currentTime and sets it on the animation.\n *\n * WHY: This function explicitly performs the time mapping transformation,\n * making it clear that we're transforming element time to animation time.\n */\nconst mapAndSetAnimationTime = (\n animation: Animation,\n element: AnimatableElement,\n timing: AnimationTiming,\n effectiveDelay: number,\n): void => {\n // Use ownCurrentTimeMs for all elements (timegroups and other temporal elements)\n // This gives us time relative to when the element started, which ensures animations\n // on child elements are synchronized with their containing timegroup's timeline.\n // For timegroups, ownCurrentTimeMs is the time relative to when the timegroup started.\n // For other temporal elements, ownCurrentTimeMs is the time relative to their start.\n const elementTime = element.ownCurrentTimeMs ?? 0;\n\n // Ensure animation is paused before setting currentTime\n if (animation.playState === \"running\") {\n animation.pause();\n }\n\n // Calculate adjusted time (element time minus delay)\n const adjustedTime = elementTime - effectiveDelay;\n\n // If before delay, show initial keyframe state (0% of animation)\n if (adjustedTime < 0) {\n // Before delay: show initial keyframe state\n // For CSS animations with delay > 0, currentTime includes the delay, so set to elementTime\n // For CSS animations with delay = 0, currentTime is just animation progress, so set to 0\n if (timing.delay > 0) {\n animation.currentTime = elementTime;\n } else {\n animation.currentTime = 0;\n }\n return;\n }\n\n // At delay time (adjustedTime = 0) or after, the animation should be active\n const { duration, iterations } = timing;\n const currentIteration = Math.floor(adjustedTime / duration);\n\n if (currentIteration >= iterations) {\n // Animation is completed - use completed time mapping\n const maxSafeTime = calculateMaxSafeAnimationTime(duration, iterations);\n const completedAnimationTime = getCompletedAnimationTime(\n timing,\n maxSafeTime,\n );\n\n // CRITICAL: For CSS animations, currentTime behavior differs based on whether delay > 0:\n // - If timing.delay > 0: currentTime includes the delay (absolute timeline time)\n // - If timing.delay === 0: currentTime is just animation progress (0 to duration)\n if (timing.delay > 0) {\n // Completed: currentTime should be delay + completed animation time (absolute timeline time)\n animation.currentTime = effectiveDelay + completedAnimationTime;\n } else {\n // Completed: currentTime should be just the completed animation time (animation progress)\n animation.currentTime = completedAnimationTime;\n }\n } else {\n // Animation is in progress - map element time to animation time\n const animationTime = mapElementTimeToAnimationTime(\n elementTime,\n timing,\n effectiveDelay,\n );\n\n // CRITICAL: For CSS animations, currentTime behavior differs based on whether delay > 0:\n // - If timing.delay > 0: currentTime includes the delay (absolute timeline time)\n // - If timing.delay === 0: currentTime is just animation progress (0 to duration)\n // Stagger offset is handled via adjustedTime calculation, but doesn't affect currentTime format\n const { direction, delay } = timing;\n\n if (delay > 0) {\n // CSS animation with delay: currentTime is absolute timeline time\n const isAlternateWithDelay =\n (direction === \"alternate\" || direction === \"alternate-reverse\") &&\n effectiveDelay > 0;\n if (isAlternateWithDelay && currentIteration === 0) {\n // For alternate direction iteration 0 with delay, use elementTime directly\n animation.currentTime = elementTime;\n } else {\n // For other cases with delay, currentTime should be delay + animation time (absolute timeline time)\n animation.currentTime = effectiveDelay + animationTime;\n }\n } else {\n // CSS animation with delay = 0: currentTime is just animation progress\n // Stagger offset is already accounted for in adjustedTime, so animationTime is the progress\n animation.currentTime = animationTime;\n }\n }\n};\n\n/**\n * Synchronizes a single animation with the timeline using the element as the time source.\n *\n * For animations in this element's subtree, always use this element as the time source.\n * This handles both animations directly on the temporal element and on its non-temporal children.\n */\nconst synchronizeAnimation = (\n animation: Animation,\n element: AnimatableElement,\n): void => {\n const effect = animation.effect;\n if (!validateAnimationEffect(effect)) {\n return;\n }\n\n const timing = extractAnimationTiming(effect);\n\n if (timing.duration <= 0) {\n animation.currentTime = 0;\n return;\n }\n\n // Find the containing timegroup for the animation target.\n // Temporal elements are always synced to timegroups, so animations should use\n // the timegroup's timeline as the time source.\n const target = effect.target;\n let timeSource: AnimatableElement = element;\n\n if (target && target instanceof HTMLElement) {\n // Find the nearest timegroup in the DOM tree\n const nearestTimegroup = target.closest(\"ef-timegroup\");\n if (nearestTimegroup && isEFTemporal(nearestTimegroup)) {\n timeSource = nearestTimegroup as AnimatableElement;\n }\n }\n\n // For stagger offset, we need to find the actual text segment element.\n // CSS animations might be on the segment itself or on a child element.\n // If the target is not a text segment, try to find the parent text segment.\n let staggerElement: AnimatableElement = timeSource;\n if (target && target instanceof HTMLElement) {\n // Check if target is a text segment\n const targetAsAnimatable = target as AnimatableElement;\n if (supportsStaggerOffset(targetAsAnimatable)) {\n staggerElement = targetAsAnimatable;\n } else {\n // Target might be a child element - find the parent text segment\n const parentSegment = target.closest(\"ef-text-segment\");\n if (\n parentSegment &&\n supportsStaggerOffset(parentSegment as AnimatableElement)\n ) {\n staggerElement = parentSegment as AnimatableElement;\n }\n }\n }\n\n const effectiveDelay = calculateEffectiveDelay(timing.delay, staggerElement);\n mapAndSetAnimationTime(animation, timeSource, timing, effectiveDelay);\n};\n\n/**\n * Coordinates animations for a single element and its subtree, using the element as the time source.\n *\n * Uses tracked animations to ensure we can control animations even after they complete.\n * Both CSS animations (created via the 'animation' property) and WAAPI animations are included.\n *\n * CRITICAL: CSS animations are created asynchronously when classes are added. This function\n * discovers new animations on each call and tracks them in memory. Once animations complete,\n * they're removed from getAnimations(), but we keep references to them so we can continue\n * controlling them.\n */\nconst coordinateElementAnimations = (element: AnimatableElement): void => {\n // Discover and track animations (includes both current and previously completed ones)\n // Reuse the current animations array to avoid calling getAnimations() twice\n const { tracked: trackedAnimations, current: currentAnimations } = discoverAndTrackAnimations(element);\n\n for (const animation of trackedAnimations) {\n // Skip invalid animations (cancelled, removed from DOM, etc.)\n if (!isAnimationValid(animation, currentAnimations)) {\n continue;\n }\n\n prepareAnimation(animation);\n synchronizeAnimation(animation, element);\n }\n};\n\n// ============================================================================\n// Visual State Application\n// ============================================================================\n\n/**\n * Applies visual state (CSS + display) to match temporal state.\n *\n * WHY: This function applies visual state based on the element's phase and state.\n * Phase determines what should be visible, and this function applies that decision.\n */\nconst applyVisualState = (\n element: AnimatableElement,\n state: TemporalState,\n): void => {\n // Always set progress (needed for many use cases)\n element.style.setProperty(PROGRESS_PROPERTY, `${state.progress}`);\n\n // Handle visibility based on phase\n if (!state.isVisible) {\n element.style.setProperty(\"display\", \"none\");\n return;\n }\n element.style.removeProperty(\"display\");\n\n // Set other CSS properties for visible elements only\n element.style.setProperty(DURATION_PROPERTY, `${element.durationMs}ms`);\n element.style.setProperty(\n TRANSITION_DURATION_PROPERTY,\n `${element.parentTimegroup?.overlapMs ?? 0}ms`,\n );\n element.style.setProperty(\n TRANSITION_OUT_START_PROPERTY,\n `${element.durationMs - (element.parentTimegroup?.overlapMs ?? 0)}ms`,\n );\n};\n\n/**\n * Applies animation coordination if the element phase requires it.\n *\n * WHY: Animation coordination is driven by phase. If the element is in a phase\n * where animations should be coordinated, we coordinate them.\n */\nconst applyAnimationCoordination = (\n element: AnimatableElement,\n phase: ElementPhase,\n): void => {\n if (shouldCoordinateAnimations(phase, element)) {\n coordinateElementAnimations(element);\n }\n};\n\n// ============================================================================\n// Main Function\n// ============================================================================\n\n/**\n * Evaluates the complete state for an element update.\n * This separates evaluation (what should the state be?) from application (apply that state).\n */\nconst evaluateElementState = (\n element: AnimatableElement,\n): ElementUpdateContext => {\n return {\n element,\n state: evaluateTemporalState(element),\n };\n};\n\n/**\n * Main function: synchronizes DOM element with timeline.\n *\n * Orchestrates clear flow: Phase → Policy → Time Mapping → State Application\n *\n * WHY: This function makes the conceptual flow explicit:\n * 1. Determine phase (what phase is the element in?)\n * 2. Apply policies (should it be visible/coordinated based on phase?)\n * 3. Map time for animations (transform element time to animation time)\n * 4. Apply visual state (update CSS and display based on phase and policies)\n */\nexport const updateAnimations = (element: AnimatableElement): void => {\n // Evaluate all states\n const rootContext = evaluateElementState(element);\n const childContexts = deepGetTemporalElements(element).map(\n (temporalElement) => evaluateElementState(temporalElement),\n );\n\n // Apply visual state and animation coordination for root element\n applyVisualState(rootContext.element, rootContext.state);\n applyAnimationCoordination(rootContext.element, rootContext.state.phase);\n\n // Apply visual state and animation coordination for all temporal child elements\n childContexts.forEach((context) => {\n applyVisualState(context.element, context.state);\n applyAnimationCoordination(context.element, context.state.phase);\n });\n};\n"],"mappings":"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"}
|
package/dist/getRenderInfo.d.ts
CHANGED
|
@@ -22,9 +22,9 @@ declare const RenderInfo: z.ZodObject<{
|
|
|
22
22
|
}>;
|
|
23
23
|
}, "strip", z.ZodTypeAny, {
|
|
24
24
|
durationMs: number;
|
|
25
|
+
fps: number;
|
|
25
26
|
width: number;
|
|
26
27
|
height: number;
|
|
27
|
-
fps: number;
|
|
28
28
|
assets: {
|
|
29
29
|
efMedia: Record<string, any>;
|
|
30
30
|
efCaptions: string[];
|
|
@@ -32,9 +32,9 @@ declare const RenderInfo: z.ZodObject<{
|
|
|
32
32
|
};
|
|
33
33
|
}, {
|
|
34
34
|
durationMs: number;
|
|
35
|
+
fps: number;
|
|
35
36
|
width: number;
|
|
36
37
|
height: number;
|
|
37
|
-
fps: number;
|
|
38
38
|
assets: {
|
|
39
39
|
efMedia: Record<string, any>;
|
|
40
40
|
efCaptions: string[];
|
package/dist/gui/ContextMixin.js
CHANGED
|
@@ -31,7 +31,8 @@ function ContextMixin(superClass) {
|
|
|
31
31
|
this.fetch = async (url, init = {}) => {
|
|
32
32
|
init.headers ||= {};
|
|
33
33
|
Object.assign(init.headers, { "Content-Type": "application/json" });
|
|
34
|
-
|
|
34
|
+
const isLocalEndpoint = url.startsWith("/@ef-");
|
|
35
|
+
if (!EF_RENDERING() && this.signingURL && !isLocalEndpoint) {
|
|
35
36
|
const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);
|
|
36
37
|
const urlToken = await globalURLTokenDeduplicator.getToken(cacheKey, async () => {
|
|
37
38
|
try {
|
|
@@ -50,9 +51,10 @@ function ContextMixin(superClass) {
|
|
|
50
51
|
} else init.credentials = "include";
|
|
51
52
|
try {
|
|
52
53
|
return fetch(url, init).catch((error) => {
|
|
54
|
+
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
53
55
|
console.error("ContextMixin fetch error", url, error, window.location.href);
|
|
54
56
|
const enhancedError = new (error instanceof Error ? error.constructor : Error)(`Failed to fetch: ${url}. Original error: ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
-
if (error instanceof Error) {
|
|
57
|
+
if (error instanceof Error && !(error instanceof DOMException)) {
|
|
56
58
|
enhancedError.name = error.name;
|
|
57
59
|
enhancedError.stack = error.stack;
|
|
58
60
|
Object.assign(enhancedError, error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextMixin.js","names":["#getTokenCacheKey","#parseTokenExpiration","#apiHost","#targetTemporal","#onControllerUpdate","#controllerSubscribed","#targetTemporalProvider","#loop","#playingProvider","#loopProvider","#currentTimeMsProvider","#signingURL","#timegroupObserver"],"sources":["../../src/gui/ContextMixin.ts"],"sourcesContent":["import { ContextProvider, consume, createContext, provide } from \"@lit/context\";\nimport type { LitElement } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\nimport { EF_RENDERING } from \"../EF_RENDERING.ts\";\nimport {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../elements/EFTemporal.js\";\nimport { globalURLTokenDeduplicator } from \"../transcoding/cache/URLTokenDeduplicator.js\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport {\n type EFConfiguration,\n efConfigurationContext,\n} from \"./EFConfiguration.ts\";\nimport { efContext } from \"./efContext.js\";\nimport { fetchContext } from \"./fetchContext.js\";\nimport { type FocusContext, focusContext } from \"./focusContext.js\";\nimport { focusedElementContext } from \"./focusedElementContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\n\nexport const targetTemporalContext =\n createContext<TemporalMixinInterface | null>(Symbol(\"target-temporal\"));\n\nexport declare class ContextMixinInterface extends LitElement {\n signingURL?: string;\n apiHost?: string;\n rendering: boolean;\n playing: boolean;\n loop: boolean;\n currentTimeMs: number;\n focusedElement?: HTMLElement;\n targetTemporal: TemporalMixinInterface | null;\n play(): Promise<void>;\n pause(): void;\n}\n\nconst contextMixinSymbol = Symbol(\"contextMixin\");\n\nexport function isContextMixin(value: any): value is ContextMixinInterface {\n return (\n typeof value === \"object\" &&\n value !== null &&\n contextMixinSymbol in value.constructor\n );\n}\n\ntype Constructor<T = {}> = new (...args: any[]) => T;\nexport function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {\n class ContextElement extends superClass {\n static [contextMixinSymbol] = true;\n\n @consume({ context: efConfigurationContext, subscribe: true })\n efConfiguration: EFConfiguration | null = null;\n\n @provide({ context: focusContext })\n focusContext = this as FocusContext;\n\n @provide({ context: focusedElementContext })\n @state()\n focusedElement?: HTMLElement;\n\n #playingProvider!: ContextProvider<typeof playingContext>;\n #loopProvider!: ContextProvider<typeof loopContext>;\n #currentTimeMsProvider!: ContextProvider<typeof currentTimeContext>;\n #targetTemporalProvider!: ContextProvider<typeof targetTemporalContext>;\n\n #loop = false;\n\n #apiHost?: string;\n @property({ type: String, attribute: \"api-host\" })\n get apiHost() {\n return this.#apiHost ?? this.efConfiguration?.apiHost ?? \"\";\n }\n\n set apiHost(value: string) {\n this.#apiHost = value;\n }\n\n @provide({ context: efContext })\n efContext = this;\n\n #targetTemporal: TemporalMixinInterface | null = null;\n\n @state()\n get targetTemporal(): TemporalMixinInterface | null {\n return this.#targetTemporal;\n }\n #controllerSubscribed = false;\n\n /**\n * Find the first root temporal element (recursively searches through children)\n * Supports ef-timegroup, ef-video, ef-audio, and any other temporal elements\n * even when they're wrapped in non-temporal elements like divs\n */\n private findRootTemporal(): TemporalMixinInterface | null {\n const findRecursive = (\n element: Element,\n ): TemporalMixinInterface | null => {\n if (isEFTemporal(element)) {\n return element as TemporalMixinInterface & HTMLElement;\n }\n\n for (const child of element.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n };\n\n for (const child of this.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n }\n\n set targetTemporal(value: TemporalMixinInterface | null) {\n if (this.#targetTemporal === value) return;\n\n // Unsubscribe from old controller updates\n if (this.#targetTemporal?.playbackController) {\n this.#targetTemporal.playbackController.removeListener(\n this.#onControllerUpdate,\n );\n this.#controllerSubscribed = false;\n }\n\n this.#targetTemporal = value;\n this.#targetTemporalProvider?.setValue(value);\n\n // Sync all provided contexts\n this.requestUpdate(\"targetTemporal\");\n this.requestUpdate(\"playing\");\n this.requestUpdate(\"loop\");\n this.requestUpdate(\"currentTimeMs\");\n\n // If the new targetTemporal has a playbackController, apply stored loop value immediately\n if (value?.playbackController && this.#loop) {\n value.playbackController.setLoop(this.#loop);\n }\n\n // If the new targetTemporal doesn't have a playbackController yet,\n // wait for it to complete its updates (it might be initializing)\n if (value && !value.playbackController) {\n // Wait for the temporal element to initialize\n (value as any).updateComplete?.then(() => {\n if (value === this.#targetTemporal && !this.#controllerSubscribed) {\n this.requestUpdate();\n }\n });\n }\n }\n\n #onControllerUpdate = (\n event: import(\"./PlaybackController.js\").PlaybackControllerUpdateEvent,\n ) => {\n switch (event.property) {\n case \"playing\":\n this.#playingProvider.setValue(event.value as boolean);\n break;\n case \"loop\":\n this.#loopProvider.setValue(event.value as boolean);\n break;\n case \"currentTimeMs\":\n this.#currentTimeMsProvider.setValue(event.value as number);\n break;\n }\n };\n\n // Add reactive properties that depend on the targetTemporal\n @provide({ context: durationContext })\n @property({ type: Number })\n durationMs = 0;\n\n @property({ type: Number })\n endTimeMs = 0;\n\n @provide({ context: fetchContext })\n fetch = async (url: string, init: RequestInit = {}) => {\n init.headers ||= {};\n Object.assign(init.headers, {\n \"Content-Type\": \"application/json\",\n });\n\n if (!EF_RENDERING() && this.signingURL) {\n const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);\n\n // Use global token deduplicator to share tokens across all context providers\n const urlToken = await globalURLTokenDeduplicator.getToken(\n cacheKey,\n async () => {\n try {\n const response = await fetch(this.signingURL, {\n method: \"POST\",\n body: JSON.stringify(signingPayload),\n });\n\n if (response.ok) {\n const tokenData = await response.json();\n return tokenData.token;\n }\n throw new Error(\n `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,\n );\n } catch (error) {\n console.error(\"ContextMixin urlToken fetch error\", url, error);\n throw error;\n }\n },\n (token: string) => this.#parseTokenExpiration(token),\n );\n\n Object.assign(init.headers, {\n authorization: `Bearer ${urlToken}`,\n });\n } else {\n init.credentials = \"include\";\n }\n\n try {\n const fetchPromise = fetch(url, init);\n // Wrap the promise to catch rejections and log the URL\n // Return the promise chain so errors are logged but still propagate\n return fetchPromise.catch((error) => {\n console.error(\n \"ContextMixin fetch error\",\n url,\n error,\n window.location.href,\n );\n // Create a new error with the URL in the message, preserving the original error type\n const ErrorConstructor =\n error instanceof Error ? error.constructor : Error;\n const enhancedError = new (ErrorConstructor as typeof Error)(\n `Failed to fetch: ${url}. Original error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Preserve the original error's properties\n if (error instanceof Error) {\n enhancedError.name = error.name;\n enhancedError.stack = error.stack;\n // Copy any additional properties from the original error\n Object.assign(enhancedError, error);\n }\n throw enhancedError;\n });\n } catch (error) {\n console.error(\n \"ContextMixin fetch error (synchronous)\",\n url,\n error,\n window.location.href,\n );\n throw error;\n }\n };\n\n // Note: URL token caching is now handled globally via URLTokenDeduplicator\n // Keeping these for any potential backwards compatibility, but they're no longer used\n\n /**\n * Generate a cache key for URL token based on signing strategy\n *\n * Uses unified prefix + parameter matching approach:\n * - For transcode URLs: signs base \"/api/v1/transcode\" + params like {url: \"source.mp4\"}\n * - For regular URLs: signs full URL with empty params {}\n * - All validation uses prefix matching + exhaustive parameter matching\n * - Multiple transcode segments with same source share one token (reduces round-trips)\n */\n #getTokenCacheKey(url: string): {\n cacheKey: string;\n signingPayload: { url: string; params?: Record<string, string> };\n } {\n try {\n const urlObj = new URL(url);\n\n // Check if this is a transcode URL pattern\n if (urlObj.pathname.includes(\"/api/v1/transcode/\")) {\n const urlParam = urlObj.searchParams.get(\"url\");\n if (urlParam) {\n // For transcode URLs, sign the base path + url parameter\n const basePath = `${urlObj.origin}/api/v1/transcode`;\n const cacheKey = `${basePath}?url=${urlParam}`;\n return {\n cacheKey,\n signingPayload: { url: basePath, params: { url: urlParam } },\n };\n }\n }\n\n // For non-transcode URLs, use full URL (existing behavior)\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n } catch {\n // If URL parsing fails, fall back to full URL\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n }\n }\n\n /**\n * Parse JWT token to extract safe expiration time (with buffer)\n * @param token JWT token string\n * @returns Safe expiration timestamp in milliseconds (actual expiry minus buffer), or 0 if parsing fails\n */\n #parseTokenExpiration(token: string): number {\n try {\n // JWT has 3 parts separated by dots: header.payload.signature\n const parts = token.split(\".\");\n if (parts.length !== 3) return 0;\n\n // Decode the payload (second part)\n const payload = parts[1];\n if (!payload) return 0;\n\n const decoded = atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n const parsed = JSON.parse(decoded);\n\n // Extract timestamps (in seconds)\n const exp = parsed.exp;\n const iat = parsed.iat;\n if (!exp) return 0;\n\n // Calculate token lifetime and buffer\n const lifetimeSeconds = iat ? exp - iat : 3600; // Default to 1 hour if no iat\n const tenPercentBufferMs = lifetimeSeconds * 0.1 * 1000; // 10% of lifetime in ms\n const fiveMinutesMs = 5 * 60 * 1000; // 5 minutes in ms\n\n // Use whichever buffer is smaller (more conservative)\n const bufferMs = Math.min(fiveMinutesMs, tenPercentBufferMs);\n\n // Return expiration time minus buffer\n return exp * 1000 - bufferMs;\n } catch {\n return 0;\n }\n }\n\n #signingURL?: string;\n /**\n * A URL that will be used to generated signed tokens for accessing media files from the\n * editframe API. This is used to authenticate media requests per-user.\n */\n @property({ type: String, attribute: \"signing-url\" })\n get signingURL() {\n return this.#signingURL ?? this.efConfiguration?.signingURL ?? \"\";\n }\n set signingURL(value: string) {\n this.#signingURL = value;\n }\n\n @property({ type: Boolean, reflect: true })\n get playing(): boolean {\n return this.targetTemporal?.playbackController?.playing ?? false;\n }\n set playing(value: boolean) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setPlaying(value);\n }\n }\n\n @property({ type: Boolean, reflect: true, attribute: \"loop\" })\n get loop(): boolean {\n return this.targetTemporal?.playbackController?.loop ?? this.#loop;\n }\n set loop(value: boolean) {\n const oldValue = this.#loop;\n this.#loop = value;\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setLoop(value);\n }\n this.requestUpdate(\"loop\", oldValue);\n }\n\n @property({ type: Boolean })\n rendering = false;\n\n @property({ type: Number })\n get currentTimeMs(): number {\n return (\n this.targetTemporal?.playbackController?.currentTimeMs ?? Number.NaN\n );\n }\n set currentTimeMs(value: number) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setCurrentTimeMs(value);\n }\n }\n\n #timegroupObserver = new MutationObserver((mutations) => {\n let shouldUpdate = false;\n\n for (const mutation of mutations) {\n if (mutation.type === \"childList\") {\n const newTemporal = this.findRootTemporal();\n if (newTemporal !== this.targetTemporal) {\n this.targetTemporal = newTemporal;\n shouldUpdate = true;\n } else if (\n mutation.target instanceof Element &&\n isEFTemporal(mutation.target)\n ) {\n // Handle childList changes within existing temporal elements\n shouldUpdate = true;\n }\n } else if (mutation.type === \"attributes\") {\n // Watch for attribute changes that might affect duration\n const durationAffectingAttributes = [\n \"duration\",\n \"mode\",\n \"trimstart\",\n \"trimend\",\n \"sourcein\",\n \"sourceout\",\n ];\n\n if (\n durationAffectingAttributes.includes(\n mutation.attributeName || \"\",\n ) ||\n (mutation.target instanceof Element &&\n isEFTemporal(mutation.target))\n ) {\n shouldUpdate = true;\n }\n }\n }\n\n if (shouldUpdate) {\n // Trigger an update to ensure reactive properties recalculate\n // Use a microtask to ensure DOM updates are complete\n queueMicrotask(() => {\n // Recalculate duration and endTime when temporal element changes\n this.updateDurationProperties();\n this.requestUpdate();\n // Also ensure the targetTemporal updates its computed properties\n if (this.targetTemporal) {\n (this.targetTemporal as any).requestUpdate();\n }\n });\n }\n });\n\n /**\n * Update duration properties when temporal element changes\n */\n updateDurationProperties(): void {\n const newDuration = this.targetTemporal?.durationMs ?? 0;\n const newEndTime = this.targetTemporal?.endTimeMs ?? 0;\n\n if (this.durationMs !== newDuration) {\n this.durationMs = newDuration;\n }\n\n if (this.endTimeMs !== newEndTime) {\n this.endTimeMs = newEndTime;\n }\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Create manual context providers for playback state\n this.#playingProvider = new ContextProvider(this, {\n context: playingContext,\n initialValue: this.playing,\n });\n this.#loopProvider = new ContextProvider(this, {\n context: loopContext,\n initialValue: this.loop,\n });\n this.#currentTimeMsProvider = new ContextProvider(this, {\n context: currentTimeContext,\n initialValue: this.currentTimeMs,\n });\n this.#targetTemporalProvider = new ContextProvider(this, {\n context: targetTemporalContext,\n initialValue: this.targetTemporal,\n });\n\n // Initialize targetTemporal to first root temporal element\n this.targetTemporal = this.findRootTemporal();\n // Initialize duration properties\n this.updateDurationProperties();\n\n this.#timegroupObserver.observe(this, {\n childList: true,\n subtree: true,\n attributes: true,\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#timegroupObserver.disconnect();\n\n // Unsubscribe from controller\n if (this.#targetTemporal?.playbackController) {\n this.#targetTemporal.playbackController.removeListener(\n this.#onControllerUpdate,\n );\n this.#controllerSubscribed = false;\n }\n\n this.pause();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>) {\n super.updated?.(changedProperties);\n\n // Subscribe to controller when it becomes available\n if (\n !this.#controllerSubscribed &&\n this.#targetTemporal?.playbackController\n ) {\n this.#targetTemporal.playbackController.addListener(\n this.#onControllerUpdate,\n );\n this.#controllerSubscribed = true;\n\n // Apply stored loop value when playbackController becomes available\n if (this.#loop) {\n this.#targetTemporal.playbackController.setLoop(this.#loop);\n }\n\n // Trigger initial sync of context providers\n this.#playingProvider.setValue(this.playing);\n this.#loopProvider.setValue(this.loop);\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n }\n }\n\n async play() {\n // If targetTemporal is not set, try to find it now\n // This handles cases where the DOM may not have been fully ready during connectedCallback\n if (!this.targetTemporal) {\n // Wait for any temporal custom elements to be defined\n const potentialTemporalTags = Array.from(this.children)\n .map((el) => el.tagName.toLowerCase())\n .filter((tag) => tag.startsWith(\"ef-\"));\n\n await Promise.all(\n potentialTemporalTags.map((tag) =>\n customElements.whenDefined(tag).catch(() => {}),\n ),\n );\n\n const foundTemporal = this.findRootTemporal();\n if (foundTemporal) {\n this.targetTemporal = foundTemporal;\n // Wait for it to initialize\n await (foundTemporal as any).updateComplete;\n } else {\n console.warn(\"No temporal element found to play\");\n return;\n }\n }\n\n // If playbackController doesn't exist yet, wait for it\n if (!this.targetTemporal.playbackController) {\n await (this.targetTemporal as any).updateComplete;\n // After waiting, check again\n if (!this.targetTemporal.playbackController) {\n console.warn(\"PlaybackController not available for temporal element\");\n return;\n }\n }\n\n this.targetTemporal.playbackController.play();\n }\n\n pause() {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.pause();\n }\n }\n }\n\n return ContextElement as Constructor<ContextMixinInterface> & T;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ContextMixin.js","names":["#getTokenCacheKey","#parseTokenExpiration","#apiHost","#targetTemporal","#onControllerUpdate","#controllerSubscribed","#targetTemporalProvider","#loop","#playingProvider","#loopProvider","#currentTimeMsProvider","#signingURL","#timegroupObserver"],"sources":["../../src/gui/ContextMixin.ts"],"sourcesContent":["import { ContextProvider, consume, createContext, provide } from \"@lit/context\";\nimport type { LitElement } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\nimport { EF_RENDERING } from \"../EF_RENDERING.ts\";\nimport {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../elements/EFTemporal.js\";\nimport { globalURLTokenDeduplicator } from \"../transcoding/cache/URLTokenDeduplicator.js\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport {\n type EFConfiguration,\n efConfigurationContext,\n} from \"./EFConfiguration.ts\";\nimport { efContext } from \"./efContext.js\";\nimport { fetchContext } from \"./fetchContext.js\";\nimport { type FocusContext, focusContext } from \"./focusContext.js\";\nimport { focusedElementContext } from \"./focusedElementContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\n\nexport const targetTemporalContext =\n createContext<TemporalMixinInterface | null>(Symbol(\"target-temporal\"));\n\nexport declare class ContextMixinInterface extends LitElement {\n signingURL?: string;\n apiHost?: string;\n rendering: boolean;\n playing: boolean;\n loop: boolean;\n currentTimeMs: number;\n focusedElement?: HTMLElement;\n targetTemporal: TemporalMixinInterface | null;\n play(): Promise<void>;\n pause(): void;\n}\n\nconst contextMixinSymbol = Symbol(\"contextMixin\");\n\nexport function isContextMixin(value: any): value is ContextMixinInterface {\n return (\n typeof value === \"object\" &&\n value !== null &&\n contextMixinSymbol in value.constructor\n );\n}\n\ntype Constructor<T = {}> = new (...args: any[]) => T;\nexport function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {\n class ContextElement extends superClass {\n static [contextMixinSymbol] = true;\n\n @consume({ context: efConfigurationContext, subscribe: true })\n efConfiguration: EFConfiguration | null = null;\n\n @provide({ context: focusContext })\n focusContext = this as FocusContext;\n\n @provide({ context: focusedElementContext })\n @state()\n focusedElement?: HTMLElement;\n\n #playingProvider!: ContextProvider<typeof playingContext>;\n #loopProvider!: ContextProvider<typeof loopContext>;\n #currentTimeMsProvider!: ContextProvider<typeof currentTimeContext>;\n #targetTemporalProvider!: ContextProvider<typeof targetTemporalContext>;\n\n #loop = false;\n\n #apiHost?: string;\n @property({ type: String, attribute: \"api-host\" })\n get apiHost() {\n return this.#apiHost ?? this.efConfiguration?.apiHost ?? \"\";\n }\n\n set apiHost(value: string) {\n this.#apiHost = value;\n }\n\n @provide({ context: efContext })\n efContext = this;\n\n #targetTemporal: TemporalMixinInterface | null = null;\n\n @state()\n get targetTemporal(): TemporalMixinInterface | null {\n return this.#targetTemporal;\n }\n #controllerSubscribed = false;\n\n /**\n * Find the first root temporal element (recursively searches through children)\n * Supports ef-timegroup, ef-video, ef-audio, and any other temporal elements\n * even when they're wrapped in non-temporal elements like divs\n */\n private findRootTemporal(): TemporalMixinInterface | null {\n const findRecursive = (\n element: Element,\n ): TemporalMixinInterface | null => {\n if (isEFTemporal(element)) {\n return element as TemporalMixinInterface & HTMLElement;\n }\n\n for (const child of element.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n };\n\n for (const child of this.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n }\n\n set targetTemporal(value: TemporalMixinInterface | null) {\n if (this.#targetTemporal === value) return;\n\n // Unsubscribe from old controller updates\n if (this.#targetTemporal?.playbackController) {\n this.#targetTemporal.playbackController.removeListener(\n this.#onControllerUpdate,\n );\n this.#controllerSubscribed = false;\n }\n\n this.#targetTemporal = value;\n this.#targetTemporalProvider?.setValue(value);\n\n // Sync all provided contexts\n this.requestUpdate(\"targetTemporal\");\n this.requestUpdate(\"playing\");\n this.requestUpdate(\"loop\");\n this.requestUpdate(\"currentTimeMs\");\n\n // If the new targetTemporal has a playbackController, apply stored loop value immediately\n if (value?.playbackController && this.#loop) {\n value.playbackController.setLoop(this.#loop);\n }\n\n // If the new targetTemporal doesn't have a playbackController yet,\n // wait for it to complete its updates (it might be initializing)\n if (value && !value.playbackController) {\n // Wait for the temporal element to initialize\n (value as any).updateComplete?.then(() => {\n if (value === this.#targetTemporal && !this.#controllerSubscribed) {\n this.requestUpdate();\n }\n });\n }\n }\n\n #onControllerUpdate = (\n event: import(\"./PlaybackController.js\").PlaybackControllerUpdateEvent,\n ) => {\n switch (event.property) {\n case \"playing\":\n this.#playingProvider.setValue(event.value as boolean);\n break;\n case \"loop\":\n this.#loopProvider.setValue(event.value as boolean);\n break;\n case \"currentTimeMs\":\n this.#currentTimeMsProvider.setValue(event.value as number);\n break;\n }\n };\n\n // Add reactive properties that depend on the targetTemporal\n @provide({ context: durationContext })\n @property({ type: Number })\n durationMs = 0;\n\n @property({ type: Number })\n endTimeMs = 0;\n\n @provide({ context: fetchContext })\n fetch = async (url: string, init: RequestInit = {}) => {\n init.headers ||= {};\n Object.assign(init.headers, {\n \"Content-Type\": \"application/json\",\n });\n\n // Check if this is a local @ef-* endpoint that doesn't need authentication\n // These endpoints are handled by the Vite plugin locally and don't require signing\n const isLocalEndpoint = url.startsWith(\"/@ef-\");\n\n if (!EF_RENDERING() && this.signingURL && !isLocalEndpoint) {\n const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);\n\n // Use global token deduplicator to share tokens across all context providers\n const urlToken = await globalURLTokenDeduplicator.getToken(\n cacheKey,\n async () => {\n try {\n const response = await fetch(this.signingURL, {\n method: \"POST\",\n body: JSON.stringify(signingPayload),\n });\n\n if (response.ok) {\n const tokenData = await response.json();\n return tokenData.token;\n }\n throw new Error(\n `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,\n );\n } catch (error) {\n console.error(\"ContextMixin urlToken fetch error\", url, error);\n throw error;\n }\n },\n (token: string) => this.#parseTokenExpiration(token),\n );\n\n Object.assign(init.headers, {\n authorization: `Bearer ${urlToken}`,\n });\n } else {\n init.credentials = \"include\";\n }\n\n try {\n const fetchPromise = fetch(url, init);\n // Wrap the promise to catch rejections and log the URL\n // Return the promise chain so errors are logged but still propagate\n return fetchPromise.catch((error) => {\n // For AbortErrors, re-throw directly without modification\n // DOMException properties like 'name' are read-only\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n \n console.error(\n \"ContextMixin fetch error\",\n url,\n error,\n window.location.href,\n );\n // Create a new error with the URL in the message, preserving the original error type\n const ErrorConstructor =\n error instanceof Error ? error.constructor : Error;\n const enhancedError = new (ErrorConstructor as typeof Error)(\n `Failed to fetch: ${url}. Original error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Preserve the original error's properties (except for DOMException which has read-only properties)\n if (error instanceof Error && !(error instanceof DOMException)) {\n enhancedError.name = error.name;\n enhancedError.stack = error.stack;\n // Copy any additional properties from the original error\n Object.assign(enhancedError, error);\n }\n throw enhancedError;\n });\n } catch (error) {\n console.error(\n \"ContextMixin fetch error (synchronous)\",\n url,\n error,\n window.location.href,\n );\n throw error;\n }\n };\n\n // Note: URL token caching is now handled globally via URLTokenDeduplicator\n // Keeping these for any potential backwards compatibility, but they're no longer used\n\n /**\n * Generate a cache key for URL token based on signing strategy\n *\n * Uses unified prefix + parameter matching approach:\n * - For transcode URLs: signs base \"/api/v1/transcode\" + params like {url: \"source.mp4\"}\n * - For regular URLs: signs full URL with empty params {}\n * - All validation uses prefix matching + exhaustive parameter matching\n * - Multiple transcode segments with same source share one token (reduces round-trips)\n */\n #getTokenCacheKey(url: string): {\n cacheKey: string;\n signingPayload: { url: string; params?: Record<string, string> };\n } {\n try {\n const urlObj = new URL(url);\n\n // Check if this is a transcode URL pattern\n if (urlObj.pathname.includes(\"/api/v1/transcode/\")) {\n const urlParam = urlObj.searchParams.get(\"url\");\n if (urlParam) {\n // For transcode URLs, sign the base path + url parameter\n const basePath = `${urlObj.origin}/api/v1/transcode`;\n const cacheKey = `${basePath}?url=${urlParam}`;\n return {\n cacheKey,\n signingPayload: { url: basePath, params: { url: urlParam } },\n };\n }\n }\n\n // For non-transcode URLs, use full URL (existing behavior)\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n } catch {\n // If URL parsing fails, fall back to full URL\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n }\n }\n\n /**\n * Parse JWT token to extract safe expiration time (with buffer)\n * @param token JWT token string\n * @returns Safe expiration timestamp in milliseconds (actual expiry minus buffer), or 0 if parsing fails\n */\n #parseTokenExpiration(token: string): number {\n try {\n // JWT has 3 parts separated by dots: header.payload.signature\n const parts = token.split(\".\");\n if (parts.length !== 3) return 0;\n\n // Decode the payload (second part)\n const payload = parts[1];\n if (!payload) return 0;\n\n const decoded = atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n const parsed = JSON.parse(decoded);\n\n // Extract timestamps (in seconds)\n const exp = parsed.exp;\n const iat = parsed.iat;\n if (!exp) return 0;\n\n // Calculate token lifetime and buffer\n const lifetimeSeconds = iat ? exp - iat : 3600; // Default to 1 hour if no iat\n const tenPercentBufferMs = lifetimeSeconds * 0.1 * 1000; // 10% of lifetime in ms\n const fiveMinutesMs = 5 * 60 * 1000; // 5 minutes in ms\n\n // Use whichever buffer is smaller (more conservative)\n const bufferMs = Math.min(fiveMinutesMs, tenPercentBufferMs);\n\n // Return expiration time minus buffer\n return exp * 1000 - bufferMs;\n } catch {\n return 0;\n }\n }\n\n #signingURL?: string;\n /**\n * A URL that will be used to generated signed tokens for accessing media files from the\n * editframe API. This is used to authenticate media requests per-user.\n */\n @property({ type: String, attribute: \"signing-url\" })\n get signingURL() {\n return this.#signingURL ?? this.efConfiguration?.signingURL ?? \"\";\n }\n set signingURL(value: string) {\n this.#signingURL = value;\n }\n\n @property({ type: Boolean, reflect: true })\n get playing(): boolean {\n return this.targetTemporal?.playbackController?.playing ?? false;\n }\n set playing(value: boolean) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setPlaying(value);\n }\n }\n\n @property({ type: Boolean, reflect: true, attribute: \"loop\" })\n get loop(): boolean {\n return this.targetTemporal?.playbackController?.loop ?? this.#loop;\n }\n set loop(value: boolean) {\n const oldValue = this.#loop;\n this.#loop = value;\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setLoop(value);\n }\n this.requestUpdate(\"loop\", oldValue);\n }\n\n @property({ type: Boolean })\n rendering = false;\n\n @property({ type: Number })\n get currentTimeMs(): number {\n return (\n this.targetTemporal?.playbackController?.currentTimeMs ?? Number.NaN\n );\n }\n set currentTimeMs(value: number) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setCurrentTimeMs(value);\n }\n }\n\n #timegroupObserver = new MutationObserver((mutations) => {\n let shouldUpdate = false;\n\n for (const mutation of mutations) {\n if (mutation.type === \"childList\") {\n const newTemporal = this.findRootTemporal();\n if (newTemporal !== this.targetTemporal) {\n this.targetTemporal = newTemporal;\n shouldUpdate = true;\n } else if (\n mutation.target instanceof Element &&\n isEFTemporal(mutation.target)\n ) {\n // Handle childList changes within existing temporal elements\n shouldUpdate = true;\n }\n } else if (mutation.type === \"attributes\") {\n // Watch for attribute changes that might affect duration\n const durationAffectingAttributes = [\n \"duration\",\n \"mode\",\n \"trimstart\",\n \"trimend\",\n \"sourcein\",\n \"sourceout\",\n ];\n\n if (\n durationAffectingAttributes.includes(\n mutation.attributeName || \"\",\n ) ||\n (mutation.target instanceof Element &&\n isEFTemporal(mutation.target))\n ) {\n shouldUpdate = true;\n }\n }\n }\n\n if (shouldUpdate) {\n // Trigger an update to ensure reactive properties recalculate\n // Use a microtask to ensure DOM updates are complete\n queueMicrotask(() => {\n // Recalculate duration and endTime when temporal element changes\n this.updateDurationProperties();\n this.requestUpdate();\n // Also ensure the targetTemporal updates its computed properties\n if (this.targetTemporal) {\n (this.targetTemporal as any).requestUpdate();\n }\n });\n }\n });\n\n /**\n * Update duration properties when temporal element changes\n */\n updateDurationProperties(): void {\n const newDuration = this.targetTemporal?.durationMs ?? 0;\n const newEndTime = this.targetTemporal?.endTimeMs ?? 0;\n\n if (this.durationMs !== newDuration) {\n this.durationMs = newDuration;\n }\n\n if (this.endTimeMs !== newEndTime) {\n this.endTimeMs = newEndTime;\n }\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Create manual context providers for playback state\n this.#playingProvider = new ContextProvider(this, {\n context: playingContext,\n initialValue: this.playing,\n });\n this.#loopProvider = new ContextProvider(this, {\n context: loopContext,\n initialValue: this.loop,\n });\n this.#currentTimeMsProvider = new ContextProvider(this, {\n context: currentTimeContext,\n initialValue: this.currentTimeMs,\n });\n this.#targetTemporalProvider = new ContextProvider(this, {\n context: targetTemporalContext,\n initialValue: this.targetTemporal,\n });\n\n // Initialize targetTemporal to first root temporal element\n this.targetTemporal = this.findRootTemporal();\n // Initialize duration properties\n this.updateDurationProperties();\n\n this.#timegroupObserver.observe(this, {\n childList: true,\n subtree: true,\n attributes: true,\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#timegroupObserver.disconnect();\n\n // Unsubscribe from controller\n if (this.#targetTemporal?.playbackController) {\n this.#targetTemporal.playbackController.removeListener(\n this.#onControllerUpdate,\n );\n this.#controllerSubscribed = false;\n }\n\n this.pause();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>) {\n super.updated?.(changedProperties);\n\n // Subscribe to controller when it becomes available\n if (\n !this.#controllerSubscribed &&\n this.#targetTemporal?.playbackController\n ) {\n this.#targetTemporal.playbackController.addListener(\n this.#onControllerUpdate,\n );\n this.#controllerSubscribed = true;\n\n // Apply stored loop value when playbackController becomes available\n if (this.#loop) {\n this.#targetTemporal.playbackController.setLoop(this.#loop);\n }\n\n // Trigger initial sync of context providers\n this.#playingProvider.setValue(this.playing);\n this.#loopProvider.setValue(this.loop);\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n }\n }\n\n async play() {\n // If targetTemporal is not set, try to find it now\n // This handles cases where the DOM may not have been fully ready during connectedCallback\n if (!this.targetTemporal) {\n // Wait for any temporal custom elements to be defined\n const potentialTemporalTags = Array.from(this.children)\n .map((el) => el.tagName.toLowerCase())\n .filter((tag) => tag.startsWith(\"ef-\"));\n\n await Promise.all(\n potentialTemporalTags.map((tag) =>\n customElements.whenDefined(tag).catch(() => {}),\n ),\n );\n\n const foundTemporal = this.findRootTemporal();\n if (foundTemporal) {\n this.targetTemporal = foundTemporal;\n // Wait for it to initialize\n await (foundTemporal as any).updateComplete;\n } else {\n console.warn(\"No temporal element found to play\");\n return;\n }\n }\n\n // If playbackController doesn't exist yet, wait for it\n if (!this.targetTemporal.playbackController) {\n await (this.targetTemporal as any).updateComplete;\n // After waiting, check again\n if (!this.targetTemporal.playbackController) {\n console.warn(\"PlaybackController not available for temporal element\");\n return;\n }\n }\n\n this.targetTemporal.playbackController.play();\n }\n\n pause() {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.pause();\n }\n }\n }\n\n return ContextElement as Constructor<ContextMixinInterface> & T;\n}\n"],"mappings":"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"}
|
package/dist/gui/Controllable.js
CHANGED
|
@@ -8,7 +8,80 @@ function isControllable(value) {
|
|
|
8
8
|
if (isEFTemporal(value)) return value.playbackController !== void 0;
|
|
9
9
|
return false;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Determines the type of controllable target for subscription purposes.
|
|
13
|
+
*
|
|
14
|
+
* - "context-provider": Target is a ContextMixin (like EFPreview) that provides contexts
|
|
15
|
+
* - "direct-temporal": Target is a root temporal element with its own playbackController
|
|
16
|
+
* - "none": Target is not controllable (null, undefined, or nested temporal)
|
|
17
|
+
*/
|
|
18
|
+
function determineTargetType(target) {
|
|
19
|
+
if (!target) return "none";
|
|
20
|
+
if (isContextMixin(target)) return "context-provider";
|
|
21
|
+
if (isEFTemporal(target)) {
|
|
22
|
+
if (target.playbackController) return "direct-temporal";
|
|
23
|
+
}
|
|
24
|
+
return "none";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Creates a subscription to a direct temporal element's playback controller.
|
|
28
|
+
* Used when EFControls targets a temporal element directly (not wrapped in EFPreview).
|
|
29
|
+
*/
|
|
30
|
+
function createDirectTemporalSubscription(target, callbacks) {
|
|
31
|
+
const controller = target.playbackController;
|
|
32
|
+
callbacks.onPlayingChange(controller.playing);
|
|
33
|
+
callbacks.onLoopChange(controller.loop);
|
|
34
|
+
callbacks.onCurrentTimeMsChange(controller.currentTimeMs);
|
|
35
|
+
callbacks.onDurationMsChange(target.durationMs);
|
|
36
|
+
callbacks.onTargetTemporalChange(target);
|
|
37
|
+
const listener = (event) => {
|
|
38
|
+
switch (event.property) {
|
|
39
|
+
case "playing":
|
|
40
|
+
callbacks.onPlayingChange(event.value);
|
|
41
|
+
break;
|
|
42
|
+
case "loop":
|
|
43
|
+
callbacks.onLoopChange(event.value);
|
|
44
|
+
break;
|
|
45
|
+
case "currentTimeMs":
|
|
46
|
+
callbacks.onCurrentTimeMsChange(event.value);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
controller.addListener(listener);
|
|
51
|
+
const durationObserver = new MutationObserver(() => {
|
|
52
|
+
callbacks.onDurationMsChange(target.durationMs);
|
|
53
|
+
});
|
|
54
|
+
durationObserver.observe(target, {
|
|
55
|
+
attributes: true,
|
|
56
|
+
attributeFilter: [
|
|
57
|
+
"duration",
|
|
58
|
+
"trimstart",
|
|
59
|
+
"trimend",
|
|
60
|
+
"sourcein",
|
|
61
|
+
"sourceout"
|
|
62
|
+
],
|
|
63
|
+
subtree: true
|
|
64
|
+
});
|
|
65
|
+
let lastKnownDuration = target.durationMs;
|
|
66
|
+
let durationPollInterval = null;
|
|
67
|
+
if (lastKnownDuration === 0) durationPollInterval = setInterval(() => {
|
|
68
|
+
const currentDuration = target.durationMs;
|
|
69
|
+
if (currentDuration !== lastKnownDuration) {
|
|
70
|
+
lastKnownDuration = currentDuration;
|
|
71
|
+
callbacks.onDurationMsChange(currentDuration);
|
|
72
|
+
if (currentDuration > 0 && durationPollInterval) {
|
|
73
|
+
clearInterval(durationPollInterval);
|
|
74
|
+
durationPollInterval = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}, 100);
|
|
78
|
+
return { unsubscribe: () => {
|
|
79
|
+
controller.removeListener(listener);
|
|
80
|
+
durationObserver.disconnect();
|
|
81
|
+
if (durationPollInterval) clearInterval(durationPollInterval);
|
|
82
|
+
} };
|
|
83
|
+
}
|
|
11
84
|
|
|
12
85
|
//#endregion
|
|
13
|
-
export { isControllable };
|
|
86
|
+
export { createDirectTemporalSubscription, determineTargetType, isControllable };
|
|
14
87
|
//# sourceMappingURL=Controllable.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Controllable.js","names":[],"sources":["../../src/gui/Controllable.ts"],"sourcesContent":["import type { LitElement } from \"lit\";\n\nimport {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../elements/EFTemporal.js\";\nimport { type ContextMixinInterface, isContextMixin } from \"./ContextMixin.js\";\n\nexport declare class ControllableInterface extends LitElement {\n playing: boolean;\n loop: boolean;\n currentTimeMs: number;\n durationMs: number;\n play(): void | Promise<void>;\n pause(): void;\n}\n\nexport function isControllable(value: any): value is ControllableInterface {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n if (isContextMixin(value)) {\n return true;\n }\n\n if (isEFTemporal(value)) {\n const temporal = value as TemporalMixinInterface;\n return temporal.playbackController !== undefined;\n }\n\n return false;\n}\n\nexport type ControllableElement =\n | ContextMixinInterface\n | (TemporalMixinInterface & {\n playbackController: NonNullable<\n TemporalMixinInterface[\"playbackController\"]\n >;\n });\n"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"Controllable.js","names":["durationPollInterval: ReturnType<typeof setInterval> | null"],"sources":["../../src/gui/Controllable.ts"],"sourcesContent":["import type { LitElement } from \"lit\";\n\nimport {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../elements/EFTemporal.js\";\nimport { type ContextMixinInterface, isContextMixin } from \"./ContextMixin.js\";\nimport type { PlaybackControllerUpdateEvent } from \"./PlaybackController.js\";\n\nexport declare class ControllableInterface extends LitElement {\n playing: boolean;\n loop: boolean;\n currentTimeMs: number;\n durationMs: number;\n play(): void | Promise<void>;\n pause(): void;\n}\n\nexport function isControllable(value: any): value is ControllableInterface {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n if (isContextMixin(value)) {\n return true;\n }\n\n if (isEFTemporal(value)) {\n const temporal = value as TemporalMixinInterface;\n return temporal.playbackController !== undefined;\n }\n\n return false;\n}\n\nexport type ControllableElement =\n | ContextMixinInterface\n | (TemporalMixinInterface & {\n playbackController: NonNullable<\n TemporalMixinInterface[\"playbackController\"]\n >;\n });\n\n// ============================================================================\n// Core Concept: Controllable Target Type\n// ============================================================================\n// A controllable target is either a context-providing wrapper (EFPreview)\n// OR a direct temporal element with its own playback controller.\n// This enumeration makes the mental model explicit.\n// ============================================================================\n\nexport type ControllableTargetType = \"context-provider\" | \"direct-temporal\" | \"none\";\n\n/**\n * Determines the type of controllable target for subscription purposes.\n * \n * - \"context-provider\": Target is a ContextMixin (like EFPreview) that provides contexts\n * - \"direct-temporal\": Target is a root temporal element with its own playbackController\n * - \"none\": Target is not controllable (null, undefined, or nested temporal)\n */\nexport function determineTargetType(target: unknown): ControllableTargetType {\n if (!target) return \"none\";\n \n if (isContextMixin(target)) {\n return \"context-provider\";\n }\n \n if (isEFTemporal(target)) {\n const temporal = target as TemporalMixinInterface;\n // Only root temporal elements have playbackController\n // Nested elements delegate to their root\n if (temporal.playbackController) {\n return \"direct-temporal\";\n }\n }\n \n return \"none\";\n}\n\n// ============================================================================\n// Subscription Interface\n// ============================================================================\n// Abstracts the mechanism of subscribing to playback state updates.\n// Different target types use different mechanisms (context vs direct listener).\n// ============================================================================\n\nexport interface SubscriptionCallbacks {\n onPlayingChange(value: boolean): void;\n onLoopChange(value: boolean): void;\n onCurrentTimeMsChange(value: number): void;\n onDurationMsChange(value: number): void;\n onTargetTemporalChange(value: TemporalMixinInterface | null): void;\n onFocusedElementChange?(value: HTMLElement | undefined): void;\n}\n\nexport interface ControllableSubscription {\n unsubscribe(): void;\n}\n\n/**\n * Creates a subscription to a direct temporal element's playback controller.\n * Used when EFControls targets a temporal element directly (not wrapped in EFPreview).\n */\nexport function createDirectTemporalSubscription(\n target: TemporalMixinInterface & HTMLElement,\n callbacks: SubscriptionCallbacks,\n): ControllableSubscription {\n const controller = target.playbackController!;\n \n // Initial sync - propagate current state immediately\n callbacks.onPlayingChange(controller.playing);\n callbacks.onLoopChange(controller.loop);\n callbacks.onCurrentTimeMsChange(controller.currentTimeMs);\n callbacks.onDurationMsChange(target.durationMs);\n callbacks.onTargetTemporalChange(target);\n \n // Subscribe to playback controller updates\n const listener = (event: PlaybackControllerUpdateEvent) => {\n switch (event.property) {\n case \"playing\":\n callbacks.onPlayingChange(event.value as boolean);\n break;\n case \"loop\":\n callbacks.onLoopChange(event.value as boolean);\n break;\n case \"currentTimeMs\":\n callbacks.onCurrentTimeMsChange(event.value as number);\n break;\n }\n };\n controller.addListener(listener);\n \n // Watch for duration changes via MutationObserver on duration-affecting attributes\n const durationObserver = new MutationObserver(() => {\n callbacks.onDurationMsChange(target.durationMs);\n });\n durationObserver.observe(target, {\n attributes: true,\n attributeFilter: [\"duration\", \"trimstart\", \"trimend\", \"sourcein\", \"sourceout\"],\n subtree: true,\n });\n \n // For media elements (ef-video, ef-audio), also watch for intrinsic duration changes\n // The intrinsicDurationMs comes from mediaEngineTask which loads asynchronously\n let lastKnownDuration = target.durationMs;\n let durationPollInterval: ReturnType<typeof setInterval> | null = null;\n \n // If duration is currently 0, poll until it becomes available\n // This handles the case where media hasn't loaded yet\n if (lastKnownDuration === 0) {\n durationPollInterval = setInterval(() => {\n const currentDuration = target.durationMs;\n if (currentDuration !== lastKnownDuration) {\n lastKnownDuration = currentDuration;\n callbacks.onDurationMsChange(currentDuration);\n // Once we have a non-zero duration, stop polling\n if (currentDuration > 0 && durationPollInterval) {\n clearInterval(durationPollInterval);\n durationPollInterval = null;\n }\n }\n }, 100); // Check every 100ms\n }\n \n return {\n unsubscribe: () => {\n controller.removeListener(listener);\n durationObserver.disconnect();\n if (durationPollInterval) {\n clearInterval(durationPollInterval);\n }\n },\n };\n}\n"],"mappings":";;;;AAkBA,SAAgB,eAAe,OAA4C;AACzE,KAAI,CAAC,SAAS,OAAO,UAAU,SAC7B,QAAO;AAGT,KAAI,eAAe,MAAM,CACvB,QAAO;AAGT,KAAI,aAAa,MAAM,CAErB,QADiB,MACD,uBAAuB;AAGzC,QAAO;;;;;;;;;AA4BT,SAAgB,oBAAoB,QAAyC;AAC3E,KAAI,CAAC,OAAQ,QAAO;AAEpB,KAAI,eAAe,OAAO,CACxB,QAAO;AAGT,KAAI,aAAa,OAAO,EAItB;MAHiB,OAGJ,mBACX,QAAO;;AAIX,QAAO;;;;;;AA2BT,SAAgB,iCACd,QACA,WAC0B;CAC1B,MAAM,aAAa,OAAO;AAG1B,WAAU,gBAAgB,WAAW,QAAQ;AAC7C,WAAU,aAAa,WAAW,KAAK;AACvC,WAAU,sBAAsB,WAAW,cAAc;AACzD,WAAU,mBAAmB,OAAO,WAAW;AAC/C,WAAU,uBAAuB,OAAO;CAGxC,MAAM,YAAY,UAAyC;AACzD,UAAQ,MAAM,UAAd;GACE,KAAK;AACH,cAAU,gBAAgB,MAAM,MAAiB;AACjD;GACF,KAAK;AACH,cAAU,aAAa,MAAM,MAAiB;AAC9C;GACF,KAAK;AACH,cAAU,sBAAsB,MAAM,MAAgB;AACtD;;;AAGN,YAAW,YAAY,SAAS;CAGhC,MAAM,mBAAmB,IAAI,uBAAuB;AAClD,YAAU,mBAAmB,OAAO,WAAW;GAC/C;AACF,kBAAiB,QAAQ,QAAQ;EAC/B,YAAY;EACZ,iBAAiB;GAAC;GAAY;GAAa;GAAW;GAAY;GAAY;EAC9E,SAAS;EACV,CAAC;CAIF,IAAI,oBAAoB,OAAO;CAC/B,IAAIA,uBAA8D;AAIlE,KAAI,sBAAsB,EACxB,wBAAuB,kBAAkB;EACvC,MAAM,kBAAkB,OAAO;AAC/B,MAAI,oBAAoB,mBAAmB;AACzC,uBAAoB;AACpB,aAAU,mBAAmB,gBAAgB;AAE7C,OAAI,kBAAkB,KAAK,sBAAsB;AAC/C,kBAAc,qBAAqB;AACnC,2BAAuB;;;IAG1B,IAAI;AAGT,QAAO,EACL,mBAAmB;AACjB,aAAW,eAAe,SAAS;AACnC,mBAAiB,YAAY;AAC7B,MAAI,qBACF,eAAc,qBAAqB;IAGxC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as lit24 from "lit";
|
|
2
|
+
import { LitElement } from "lit";
|
|
3
|
+
import * as lit_html24 from "lit-html";
|
|
4
|
+
|
|
5
|
+
//#region src/gui/EFActiveRootTemporal.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Displays the ID of the active root temporal element from a canvas.
|
|
9
|
+
* Automatically updates when selection changes.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```html
|
|
13
|
+
* <ef-active-root-temporal canvas="canvas"></ef-active-root-temporal>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
declare class EFActiveRootTemporal extends LitElement {
|
|
17
|
+
static styles: lit24.CSSResult;
|
|
18
|
+
/**
|
|
19
|
+
* Canvas element ID or selector to bind to.
|
|
20
|
+
* If not specified, will search for the nearest ef-canvas ancestor.
|
|
21
|
+
*/
|
|
22
|
+
canvas: string;
|
|
23
|
+
private activeRootTemporal;
|
|
24
|
+
private canvasElement;
|
|
25
|
+
private activeroottemporalchangeHandler?;
|
|
26
|
+
connectedCallback(): void;
|
|
27
|
+
disconnectedCallback(): void;
|
|
28
|
+
protected updated(changedProperties: Map<string | number | symbol, unknown>): void;
|
|
29
|
+
/**
|
|
30
|
+
* Find the canvas element to bind to.
|
|
31
|
+
*/
|
|
32
|
+
private findCanvas;
|
|
33
|
+
/**
|
|
34
|
+
* Setup listener for activeroottemporalchange events.
|
|
35
|
+
*/
|
|
36
|
+
private setupListener;
|
|
37
|
+
/**
|
|
38
|
+
* Remove event listener.
|
|
39
|
+
*/
|
|
40
|
+
private removeListener;
|
|
41
|
+
render(): lit_html24.TemplateResult<1>;
|
|
42
|
+
}
|
|
43
|
+
declare global {
|
|
44
|
+
interface HTMLElementTagNameMap {
|
|
45
|
+
"ef-active-root-temporal": EFActiveRootTemporal;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { EFActiveRootTemporal };
|
|
50
|
+
//# sourceMappingURL=EFActiveRootTemporal.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.94.0/helpers/decorate.js";
|
|
2
|
+
import { LitElement, css, html } from "lit";
|
|
3
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
4
|
+
|
|
5
|
+
//#region src/gui/EFActiveRootTemporal.ts
|
|
6
|
+
let EFActiveRootTemporal = class EFActiveRootTemporal$1 extends LitElement {
|
|
7
|
+
constructor(..._args) {
|
|
8
|
+
super(..._args);
|
|
9
|
+
this.canvas = "";
|
|
10
|
+
this.activeRootTemporal = null;
|
|
11
|
+
this.canvasElement = null;
|
|
12
|
+
}
|
|
13
|
+
static {
|
|
14
|
+
this.styles = css`
|
|
15
|
+
:host {
|
|
16
|
+
display: inline-block;
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
connectedCallback() {
|
|
21
|
+
super.connectedCallback();
|
|
22
|
+
this.findCanvas();
|
|
23
|
+
this.setupListener();
|
|
24
|
+
}
|
|
25
|
+
disconnectedCallback() {
|
|
26
|
+
super.disconnectedCallback();
|
|
27
|
+
this.removeListener();
|
|
28
|
+
}
|
|
29
|
+
updated(changedProperties) {
|
|
30
|
+
if (changedProperties.has("canvas")) {
|
|
31
|
+
this.findCanvas();
|
|
32
|
+
this.setupListener();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Find the canvas element to bind to.
|
|
37
|
+
*/
|
|
38
|
+
findCanvas() {
|
|
39
|
+
if (this.canvas) {
|
|
40
|
+
const byId = document.getElementById(this.canvas);
|
|
41
|
+
if (byId && byId.tagName === "EF-CANVAS") {
|
|
42
|
+
this.canvasElement = byId;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const bySelector = document.querySelector(this.canvas);
|
|
47
|
+
if (bySelector && bySelector.tagName === "EF-CANVAS") {
|
|
48
|
+
this.canvasElement = bySelector;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
const ancestor = this.closest("ef-canvas");
|
|
54
|
+
if (ancestor) {
|
|
55
|
+
this.canvasElement = ancestor;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.canvasElement = null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Setup listener for activeroottemporalchange events.
|
|
62
|
+
*/
|
|
63
|
+
setupListener() {
|
|
64
|
+
this.removeListener();
|
|
65
|
+
if (!this.canvasElement) {
|
|
66
|
+
this.activeRootTemporal = null;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.activeRootTemporal = this.canvasElement.activeRootTemporal || null;
|
|
70
|
+
this.activeroottemporalchangeHandler = () => {
|
|
71
|
+
this.activeRootTemporal = this.canvasElement.activeRootTemporal || null;
|
|
72
|
+
};
|
|
73
|
+
this.canvasElement.addEventListener("activeroottemporalchange", this.activeroottemporalchangeHandler);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Remove event listener.
|
|
77
|
+
*/
|
|
78
|
+
removeListener() {
|
|
79
|
+
if (this.canvasElement && this.activeroottemporalchangeHandler) {
|
|
80
|
+
this.canvasElement.removeEventListener("activeroottemporalchange", this.activeroottemporalchangeHandler);
|
|
81
|
+
this.activeroottemporalchangeHandler = void 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
render() {
|
|
85
|
+
return html`<span>${this.activeRootTemporal?.id || this.textContent || "None"}</span>`;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
__decorate([property({ type: String })], EFActiveRootTemporal.prototype, "canvas", void 0);
|
|
89
|
+
__decorate([state()], EFActiveRootTemporal.prototype, "activeRootTemporal", void 0);
|
|
90
|
+
EFActiveRootTemporal = __decorate([customElement("ef-active-root-temporal")], EFActiveRootTemporal);
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
export { EFActiveRootTemporal };
|
|
94
|
+
//# sourceMappingURL=EFActiveRootTemporal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EFActiveRootTemporal.js","names":["EFActiveRootTemporal"],"sources":["../../src/gui/EFActiveRootTemporal.ts"],"sourcesContent":["import { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport type { EFCanvas } from \"../canvas/EFCanvas.js\";\nimport type { TemporalMixinInterface } from \"../elements/EFTemporal.js\";\n\n/**\n * Displays the ID of the active root temporal element from a canvas.\n * Automatically updates when selection changes.\n * \n * @example\n * ```html\n * <ef-active-root-temporal canvas=\"canvas\"></ef-active-root-temporal>\n * ```\n */\n@customElement(\"ef-active-root-temporal\")\nexport class EFActiveRootTemporal extends LitElement {\n static styles = css`\n :host {\n display: inline-block;\n }\n `;\n\n /**\n * Canvas element ID or selector to bind to.\n * If not specified, will search for the nearest ef-canvas ancestor.\n */\n @property({ type: String })\n canvas = \"\";\n\n @state()\n private activeRootTemporal: (TemporalMixinInterface & HTMLElement) | null =\n null;\n\n private canvasElement: EFCanvas | null = null;\n private activeroottemporalchangeHandler?: () => void;\n\n connectedCallback(): void {\n super.connectedCallback();\n this.findCanvas();\n this.setupListener();\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.removeListener();\n }\n\n protected updated(changedProperties: Map<string | number | symbol, unknown>): void {\n if (changedProperties.has(\"canvas\")) {\n this.findCanvas();\n this.setupListener();\n }\n }\n\n /**\n * Find the canvas element to bind to.\n */\n private findCanvas(): void {\n // If canvas attribute is set, use it\n if (this.canvas) {\n const byId = document.getElementById(this.canvas);\n if (byId && byId.tagName === \"EF-CANVAS\") {\n this.canvasElement = byId as EFCanvas;\n return;\n }\n\n // Try as selector\n try {\n const bySelector = document.querySelector(this.canvas) as EFCanvas | null;\n if (bySelector && bySelector.tagName === \"EF-CANVAS\") {\n this.canvasElement = bySelector;\n return;\n }\n } catch {\n // Invalid selector, ignore\n }\n }\n\n // Fall back to nearest ancestor\n const ancestor = this.closest(\"ef-canvas\") as EFCanvas | null;\n if (ancestor) {\n this.canvasElement = ancestor;\n return;\n }\n\n this.canvasElement = null;\n }\n\n /**\n * Setup listener for activeroottemporalchange events.\n */\n private setupListener(): void {\n this.removeListener();\n\n if (!this.canvasElement) {\n this.activeRootTemporal = null;\n return;\n }\n\n // Get initial value\n const canvasEl = this.canvasElement as any;\n this.activeRootTemporal = canvasEl.activeRootTemporal || null;\n\n // Listen for changes\n this.activeroottemporalchangeHandler = () => {\n const canvasEl = this.canvasElement as any;\n this.activeRootTemporal = canvasEl.activeRootTemporal || null;\n };\n\n this.canvasElement.addEventListener(\n \"activeroottemporalchange\",\n this.activeroottemporalchangeHandler,\n );\n }\n\n /**\n * Remove event listener.\n */\n private removeListener(): void {\n if (this.canvasElement && this.activeroottemporalchangeHandler) {\n this.canvasElement.removeEventListener(\n \"activeroottemporalchange\",\n this.activeroottemporalchangeHandler,\n );\n this.activeroottemporalchangeHandler = undefined;\n }\n }\n\n render() {\n const displayText =\n this.activeRootTemporal?.id || this.textContent || \"None\";\n return html`<span>${displayText}</span>`;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-active-root-temporal\": EFActiveRootTemporal;\n }\n}\n\n"],"mappings":";;;;;AAeO,iCAAMA,+BAA6B,WAAW;;;gBAY1C;4BAIP;uBAEuC;;;gBAjBzB,GAAG;;;;;;CAoBnB,oBAA0B;AACxB,QAAM,mBAAmB;AACzB,OAAK,YAAY;AACjB,OAAK,eAAe;;CAGtB,uBAA6B;AAC3B,QAAM,sBAAsB;AAC5B,OAAK,gBAAgB;;CAGvB,AAAU,QAAQ,mBAAiE;AACjF,MAAI,kBAAkB,IAAI,SAAS,EAAE;AACnC,QAAK,YAAY;AACjB,QAAK,eAAe;;;;;;CAOxB,AAAQ,aAAmB;AAEzB,MAAI,KAAK,QAAQ;GACf,MAAM,OAAO,SAAS,eAAe,KAAK,OAAO;AACjD,OAAI,QAAQ,KAAK,YAAY,aAAa;AACxC,SAAK,gBAAgB;AACrB;;AAIF,OAAI;IACF,MAAM,aAAa,SAAS,cAAc,KAAK,OAAO;AACtD,QAAI,cAAc,WAAW,YAAY,aAAa;AACpD,UAAK,gBAAgB;AACrB;;WAEI;;EAMV,MAAM,WAAW,KAAK,QAAQ,YAAY;AAC1C,MAAI,UAAU;AACZ,QAAK,gBAAgB;AACrB;;AAGF,OAAK,gBAAgB;;;;;CAMvB,AAAQ,gBAAsB;AAC5B,OAAK,gBAAgB;AAErB,MAAI,CAAC,KAAK,eAAe;AACvB,QAAK,qBAAqB;AAC1B;;AAKF,OAAK,qBADY,KAAK,cACa,sBAAsB;AAGzD,OAAK,wCAAwC;AAE3C,QAAK,qBADY,KAAK,cACa,sBAAsB;;AAG3D,OAAK,cAAc,iBACjB,4BACA,KAAK,gCACN;;;;;CAMH,AAAQ,iBAAuB;AAC7B,MAAI,KAAK,iBAAiB,KAAK,iCAAiC;AAC9D,QAAK,cAAc,oBACjB,4BACA,KAAK,gCACN;AACD,QAAK,kCAAkC;;;CAI3C,SAAS;AAGP,SAAO,IAAI,SADT,KAAK,oBAAoB,MAAM,KAAK,eAAe,OACrB;;;YAzGjC,SAAS,EAAE,MAAM,QAAQ,CAAC;YAG1B,OAAO;mCAfT,cAAc,0BAA0B"}
|