@editframe/elements 0.37.2-beta → 0.38.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.js +17 -14
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_RENDERING.js.map +1 -1
- package/dist/canvas/EFCanvas.d.ts +9 -2
- package/dist/canvas/EFCanvas.js +14 -4
- package/dist/canvas/EFCanvas.js.map +1 -1
- package/dist/canvas/EFCanvasItem.d.ts +4 -4
- package/dist/canvas/overlays/SelectionOverlay.d.ts +10 -2
- package/dist/canvas/overlays/SelectionOverlay.js +5 -12
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
- package/dist/canvas/overlays/overlayState.js.map +1 -1
- package/dist/canvas/selection/SelectionController.js.map +1 -1
- package/dist/elements/EFAudio.d.ts +1 -11
- package/dist/elements/EFAudio.js +2 -10
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +5 -9
- package/dist/elements/EFCaptions.js +34 -11
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +10 -8
- package/dist/elements/EFImage.js +117 -32
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +2 -2
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +15 -92
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js +10 -11
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/{AssetIdMediaEngine.js → FileMediaEngine.js} +44 -24
- package/dist/elements/EFMedia/FileMediaEngine.js.map +1 -0
- package/dist/elements/EFMedia/JitMediaEngine.js +14 -13
- package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -3
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +12 -7
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/shared/timeoutUtils.js +44 -0
- package/dist/elements/EFMedia/shared/timeoutUtils.js.map +1 -0
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +4 -4
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +14 -8
- package/dist/elements/EFMedia.js +52 -19
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +2 -2
- package/dist/elements/EFPanZoom.js +1 -1
- package/dist/elements/EFPanZoom.js.map +1 -1
- package/dist/elements/EFSourceMixin.js +16 -8
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +7 -10
- package/dist/elements/EFSurface.js +4 -43
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +33 -8
- package/dist/elements/EFTemporal.js +92 -40
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +3 -0
- package/dist/elements/EFText.js +54 -21
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.js +8 -4
- package/dist/elements/EFTextSegment.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +26 -43
- package/dist/elements/EFTimegroup.js +295 -314
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +44 -42
- package/dist/elements/EFVideo.js +259 -172
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +3 -8
- package/dist/elements/EFWaveform.js +18 -13
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.js.map +1 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/TargetController.d.ts +0 -3
- package/dist/elements/TargetController.js +12 -35
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/cloneFactoryRegistry.d.ts +14 -0
- package/dist/elements/cloneFactoryRegistry.js +15 -0
- package/dist/elements/cloneFactoryRegistry.js.map +1 -0
- package/dist/elements/renderTemporalAudio.js +8 -6
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/setupTemporalHierarchy.js +62 -0
- package/dist/elements/setupTemporalHierarchy.js.map +1 -0
- package/dist/elements/updateAnimations.js +62 -87
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.d.ts +3 -2
- package/dist/getRenderInfo.js +20 -4
- package/dist/getRenderInfo.js.map +1 -1
- package/dist/gui/ContextMixin.js +68 -12
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js +1 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
- package/dist/gui/EFActiveRootTemporal.js.map +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +2 -2
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFDial.js +12 -9
- package/dist/gui/EFDial.js.map +1 -1
- package/dist/gui/EFFilmstrip.d.ts +2 -0
- package/dist/gui/EFFilmstrip.js +18 -10
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.d.ts +28 -4
- package/dist/gui/EFFitScale.js +88 -26
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +4 -4
- package/dist/gui/EFFocusOverlay.js +3 -3
- package/dist/gui/EFFocusOverlay.js.map +1 -1
- package/dist/gui/EFOverlayItem.d.ts +4 -4
- package/dist/gui/EFOverlayLayer.d.ts +4 -4
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPause.js +1 -1
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFPlay.js +1 -1
- package/dist/gui/EFPreview.js +1 -1
- package/dist/gui/EFResizableBox.d.ts +4 -4
- package/dist/gui/EFResizableBox.js +5 -5
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +4 -4
- package/dist/gui/EFScrubber.js +8 -13
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +8 -4
- package/dist/gui/EFTimeDisplay.js +25 -7
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +4 -4
- package/dist/gui/EFTimelineRuler.js +3 -3
- package/dist/gui/EFTimelineRuler.js.map +1 -1
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFToggleLoop.js +1 -1
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTogglePlay.js +1 -1
- package/dist/gui/EFTransformHandles.d.ts +4 -4
- package/dist/gui/EFTransformHandles.js +6 -6
- package/dist/gui/EFTransformHandles.js.map +1 -1
- package/dist/gui/EFWorkbench.d.ts +40 -36
- package/dist/gui/EFWorkbench.js +436 -822
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.js.map +1 -1
- package/dist/gui/PlaybackController.d.ts +3 -8
- package/dist/gui/PlaybackController.js +59 -56
- 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/TargetOrContextMixin.js +43 -6
- package/dist/gui/TargetOrContextMixin.js.map +1 -1
- package/dist/gui/ef-theme.css +136 -0
- package/dist/gui/hierarchy/EFHierarchy.d.ts +2 -2
- package/dist/gui/hierarchy/EFHierarchy.js +14 -24
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +3 -3
- package/dist/gui/hierarchy/EFHierarchyItem.js +22 -10
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
- package/dist/gui/icons.js.map +1 -1
- package/dist/gui/previewSettingsContext.d.ts +18 -0
- package/dist/gui/previewSettingsContext.js.map +1 -1
- package/dist/gui/theme.js +34 -0
- package/dist/gui/theme.js.map +1 -0
- package/dist/gui/timeline/EFTimeline.d.ts +2 -2
- package/dist/gui/timeline/EFTimeline.js +70 -52
- package/dist/gui/timeline/EFTimeline.js.map +1 -1
- package/dist/gui/timeline/EFTimelineRow.d.ts +3 -1
- package/dist/gui/timeline/EFTimelineRow.js +55 -32
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
- package/dist/gui/timeline/TrimHandles.d.ts +23 -9
- package/dist/gui/timeline/TrimHandles.js +224 -51
- package/dist/gui/timeline/TrimHandles.js.map +1 -1
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
- package/dist/gui/timeline/timelineEditingContext.d.ts +34 -0
- package/dist/gui/timeline/timelineEditingContext.js +24 -0
- package/dist/gui/timeline/timelineEditingContext.js.map +1 -0
- package/dist/gui/timeline/timelineStateContext.js.map +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.js +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/CaptionsTrack.d.ts +2 -3
- package/dist/gui/timeline/tracks/CaptionsTrack.js +17 -75
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +52 -0
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js +596 -0
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TextTrack.d.ts +3 -2
- package/dist/gui/timeline/tracks/TextTrack.js +17 -43
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +5 -6
- package/dist/gui/timeline/tracks/TimegroupTrack.js +33 -23
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TrackItem.d.ts +7 -9
- package/dist/gui/timeline/tracks/TrackItem.js +18 -17
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.d.ts +3 -3
- package/dist/gui/timeline/tracks/VideoTrack.js +11 -14
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
- package/dist/gui/tree/EFTree.d.ts +4 -4
- package/dist/gui/tree/EFTree.js +8 -14
- package/dist/gui/tree/EFTree.js.map +1 -1
- package/dist/gui/tree/EFTreeItem.d.ts +4 -4
- package/dist/gui/tree/EFTreeItem.js +3 -3
- package/dist/gui/tree/EFTreeItem.js.map +1 -1
- package/dist/gui/tree/treeContext.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.js +2 -2
- package/dist/preview/AdaptiveResolutionTracker.js +3 -3
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
- package/dist/preview/FrameController.d.ts +2 -17
- package/dist/preview/FrameController.js +40 -63
- package/dist/preview/FrameController.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.d.ts +76 -0
- package/dist/preview/QualityUpgradeScheduler.js +158 -0
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -0
- package/dist/preview/RenderContext.d.ts +119 -1
- package/dist/preview/RenderContext.js +21 -3
- package/dist/preview/RenderContext.js.map +1 -1
- package/dist/preview/RenderProfiler.js.map +1 -1
- package/dist/preview/RenderStats.js +85 -0
- package/dist/preview/RenderStats.js.map +1 -0
- package/dist/preview/encoding/canvasEncoder.js +2 -52
- package/dist/preview/encoding/canvasEncoder.js.map +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
- package/dist/preview/encoding/workerEncoder.js.map +1 -1
- package/dist/preview/logger.js.map +1 -1
- package/dist/preview/previewSettings.d.ts +34 -0
- package/dist/preview/previewSettings.js +29 -17
- package/dist/preview/previewSettings.js.map +1 -1
- package/dist/preview/previewTypes.js +4 -4
- package/dist/preview/previewTypes.js.map +1 -1
- package/dist/preview/renderElementToCanvas.d.ts +44 -0
- package/dist/preview/renderElementToCanvas.js +72 -0
- package/dist/preview/renderElementToCanvas.js.map +1 -0
- package/dist/preview/renderTimegroupToCanvas.js +267 -145
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.types.d.ts +30 -0
- package/dist/preview/renderTimegroupToVideo.js +85 -105
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/{renderTimegroupToVideo.d.ts → renderTimegroupToVideo.types.d.ts} +9 -9
- package/dist/preview/renderVideoToVideo.js +286 -0
- package/dist/preview/renderVideoToVideo.js.map +1 -0
- package/dist/preview/renderers.js.map +1 -1
- package/dist/preview/rendering/ScaleConfig.js +74 -0
- package/dist/preview/rendering/ScaleConfig.js.map +1 -0
- package/dist/preview/rendering/inlineImages.js +1 -44
- package/dist/preview/rendering/inlineImages.js.map +1 -1
- package/dist/preview/rendering/loadImage.js +22 -0
- package/dist/preview/rendering/loadImage.js.map +1 -0
- package/dist/preview/rendering/renderToImageNative.js +3 -3
- package/dist/preview/rendering/renderToImageNative.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +224 -68
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
- package/dist/preview/statsTrackingStrategy.js +1 -101
- package/dist/preview/statsTrackingStrategy.js.map +1 -1
- package/dist/preview/workers/WorkerPool.js +0 -1
- package/dist/preview/workers/WorkerPool.js.map +1 -1
- package/dist/preview/workers/encoderWorkerInline.js +21 -54
- package/dist/preview/workers/encoderWorkerInline.js.map +1 -1
- package/dist/render/EFRenderAPI.d.ts +2 -1
- package/dist/render/EFRenderAPI.js +12 -36
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/render/getRenderData.js +4 -4
- package/dist/render/getRenderData.js.map +1 -1
- package/dist/style.css +114 -163
- package/dist/transcoding/cache/RequestDeduplicator.js +1 -0
- package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
- package/dist/transcoding/types/index.d.ts +1 -1
- package/dist/transcoding/utils/UrlGenerator.js +10 -3
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/dist/utils/LRUCache.js +1 -0
- package/dist/utils/LRUCache.js.map +1 -1
- package/dist/utils/frameTime.js +23 -1
- package/dist/utils/frameTime.js.map +1 -1
- package/package.json +21 -8
- package/scripts/build-css.js +8 -1
- package/test/setup.ts +0 -1
- package/test/useAssetMSW.ts +50 -0
- package/test/visualRegressionUtils.ts +23 -9
- package/dist/_virtual/rolldown_runtime.js +0 -27
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +0 -1
- package/dist/elements/EFThumbnailStrip.d.ts +0 -167
- package/dist/elements/EFThumbnailStrip.js +0 -731
- package/dist/elements/EFThumbnailStrip.js.map +0 -1
- package/dist/elements/SessionThumbnailCache.js +0 -154
- package/dist/elements/SessionThumbnailCache.js.map +0 -1
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +0 -688
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +0 -1
- package/dist/node_modules/react/cjs/react.development.js +0 -1521
- package/dist/node_modules/react/cjs/react.development.js.map +0 -1
- package/dist/node_modules/react/index.js +0 -13
- package/dist/node_modules/react/index.js.map +0 -1
- package/dist/node_modules/react/jsx-runtime.js +0 -13
- package/dist/node_modules/react/jsx-runtime.js.map +0 -1
- package/dist/preview/encoding/types.d.ts +0 -1
- package/dist/preview/renderTimegroupPreview.js +0 -686
- package/dist/preview/renderTimegroupPreview.js.map +0 -1
- package/dist/preview/renderTimegroupToCanvas.d.ts +0 -42
- package/dist/preview/rendering/renderToImage.d.ts +0 -2
- package/dist/preview/rendering/renderToImage.js +0 -95
- package/dist/preview/rendering/renderToImage.js.map +0 -1
- package/dist/preview/rendering/renderToImageForeignObject.js +0 -163
- package/dist/preview/rendering/renderToImageForeignObject.js.map +0 -1
- package/dist/preview/rendering/renderToImageNative.d.ts +0 -1
- package/dist/preview/rendering/svgSerializer.js +0 -43
- package/dist/preview/rendering/svgSerializer.js.map +0 -1
- package/dist/preview/rendering/types.d.ts +0 -2
- package/dist/preview/thumbnailCacheSettings.js +0 -52
- package/dist/preview/thumbnailCacheSettings.js.map +0 -1
- package/dist/sandbox/PlaybackControls.d.ts +0 -1
- package/dist/sandbox/PlaybackControls.js +0 -10
- package/dist/sandbox/PlaybackControls.js.map +0 -1
- package/dist/sandbox/ScenarioRunner.d.ts +0 -1
- package/dist/sandbox/ScenarioRunner.js +0 -1
- package/dist/sandbox/defineSandbox.d.ts +0 -1
- package/dist/sandbox/index.d.ts +0 -3
- package/dist/sandbox/index.js +0 -2
- package/test/EFVideo.framegen.browsertest.ts +0 -80
- package/test/thumbnail-performance-test.html +0 -116
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { __decorate } from "../../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
|
|
2
1
|
import { TWMixin } from "../TWMixin2.js";
|
|
2
|
+
import { __decorate } from "../../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
|
|
3
|
+
import { timelineEditingContext } from "./timelineEditingContext.js";
|
|
4
|
+
import { consume } from "@lit/context";
|
|
3
5
|
import { LitElement, css, html, nothing } from "lit";
|
|
4
6
|
import { customElement, property, state } from "lit/decorators.js";
|
|
5
7
|
import { styleMap } from "lit/directives/style-map.js";
|
|
@@ -8,36 +10,48 @@ import { styleMap } from "lit/directives/style-map.js";
|
|
|
8
10
|
let EFTrimHandles = class EFTrimHandles$1 extends TWMixin(LitElement) {
|
|
9
11
|
constructor(..._args) {
|
|
10
12
|
super(..._args);
|
|
13
|
+
this.mode = "standalone";
|
|
11
14
|
this.elementId = "";
|
|
12
|
-
this.pixelsPerMs =
|
|
13
|
-
this.
|
|
14
|
-
|
|
15
|
+
this.pixelsPerMs = null;
|
|
16
|
+
this.value = {
|
|
17
|
+
startMs: 0,
|
|
18
|
+
endMs: 0
|
|
19
|
+
};
|
|
15
20
|
this.intrinsicDurationMs = 0;
|
|
16
21
|
this.showOverlays = true;
|
|
22
|
+
this.seekTarget = "";
|
|
17
23
|
this.draggingHandle = null;
|
|
18
24
|
this.dragStartX = 0;
|
|
19
25
|
this.dragStartValue = 0;
|
|
20
26
|
this.handlePointerMove = (e) => {
|
|
21
27
|
if (!this.draggingHandle) return;
|
|
22
|
-
const
|
|
28
|
+
const pxPerMs = this.#effectivePixelsPerMs;
|
|
29
|
+
const deltaMs = (e.clientX - this.dragStartX) / pxPerMs;
|
|
30
|
+
if (this.draggingHandle === "region") {
|
|
31
|
+
const clampedDelta = Math.max(-this.#regionDragStartTrimStart, Math.min(this.#regionDragStartTrimEnd, deltaMs));
|
|
32
|
+
const newValue = {
|
|
33
|
+
startMs: this.#regionDragStartTrimStart + clampedDelta,
|
|
34
|
+
endMs: this.#regionDragStartTrimEnd - clampedDelta
|
|
35
|
+
};
|
|
36
|
+
this.#emitChange("region", newValue);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
23
39
|
let newValueMs;
|
|
24
40
|
if (this.draggingHandle === "start") {
|
|
25
41
|
newValueMs = Math.max(0, this.dragStartValue + deltaMs);
|
|
26
42
|
newValueMs = Math.min(newValueMs, this.intrinsicDurationMs - (this.trimEndMs || 0));
|
|
43
|
+
this.#emitChange("start", {
|
|
44
|
+
startMs: newValueMs,
|
|
45
|
+
endMs: this.trimEndMs
|
|
46
|
+
});
|
|
27
47
|
} else {
|
|
28
48
|
newValueMs = Math.max(0, this.dragStartValue - deltaMs);
|
|
29
49
|
newValueMs = Math.min(newValueMs, this.intrinsicDurationMs - this.trimStartMs);
|
|
50
|
+
this.#emitChange("end", {
|
|
51
|
+
startMs: this.trimStartMs,
|
|
52
|
+
endMs: newValueMs
|
|
53
|
+
});
|
|
30
54
|
}
|
|
31
|
-
this.dispatchEvent(new CustomEvent("trim-change", {
|
|
32
|
-
detail: {
|
|
33
|
-
elementId: this.elementId,
|
|
34
|
-
type: this.draggingHandle,
|
|
35
|
-
deltaMs,
|
|
36
|
-
newValueMs
|
|
37
|
-
},
|
|
38
|
-
bubbles: true,
|
|
39
|
-
composed: true
|
|
40
|
-
}));
|
|
41
55
|
};
|
|
42
56
|
this.handlePointerUp = (e) => {
|
|
43
57
|
const target = e.currentTarget;
|
|
@@ -54,6 +68,7 @@ let EFTrimHandles = class EFTrimHandles$1 extends TWMixin(LitElement) {
|
|
|
54
68
|
composed: true
|
|
55
69
|
}));
|
|
56
70
|
this.draggingHandle = null;
|
|
71
|
+
if (this.editingContext) this.editingContext.setState({ mode: "idle" });
|
|
57
72
|
};
|
|
58
73
|
}
|
|
59
74
|
static {
|
|
@@ -72,45 +87,44 @@ let EFTrimHandles = class EFTrimHandles$1 extends TWMixin(LitElement) {
|
|
|
72
87
|
position: absolute;
|
|
73
88
|
top: 0;
|
|
74
89
|
bottom: 0;
|
|
75
|
-
width: 8px;
|
|
90
|
+
width: var(--trim-handle-width, 8px);
|
|
76
91
|
cursor: ew-resize;
|
|
77
92
|
pointer-events: auto;
|
|
78
93
|
z-index: 10;
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
justify-content: center;
|
|
79
97
|
}
|
|
80
98
|
|
|
81
|
-
.handle
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
height: 60%;
|
|
88
|
-
min-height: 12px;
|
|
89
|
-
max-height: 24px;
|
|
99
|
+
.handle-inner {
|
|
100
|
+
width: 100%;
|
|
101
|
+
height: 100%;
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
justify-content: center;
|
|
90
105
|
background: var(--trim-handle-color, rgba(255, 255, 255, 0.7));
|
|
91
|
-
border-radius: 2px;
|
|
92
106
|
transition: background 0.15s ease;
|
|
93
107
|
}
|
|
94
108
|
|
|
95
|
-
.handle:hover
|
|
96
|
-
.handle.dragging
|
|
109
|
+
.handle:hover .handle-inner,
|
|
110
|
+
.handle.dragging .handle-inner {
|
|
97
111
|
background: var(--trim-handle-active-color, #3b82f6);
|
|
98
112
|
}
|
|
99
113
|
|
|
100
|
-
.handle-start {
|
|
101
|
-
|
|
114
|
+
.handle-start .handle-inner {
|
|
115
|
+
border-radius: var(--trim-handle-border-radius-start, 2px 0 0 2px);
|
|
102
116
|
}
|
|
103
117
|
|
|
104
|
-
.handle-
|
|
105
|
-
|
|
118
|
+
.handle-end .handle-inner {
|
|
119
|
+
border-radius: var(--trim-handle-border-radius-end, 0 2px 2px 0);
|
|
106
120
|
}
|
|
107
121
|
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
/* Track mode: handles pinned at container edges */
|
|
123
|
+
:host([mode="track"]) .handle-start {
|
|
124
|
+
left: -4px;
|
|
110
125
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
right: 2px;
|
|
126
|
+
:host([mode="track"]) .handle-end {
|
|
127
|
+
right: -4px;
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
.handle.dragging {
|
|
@@ -132,14 +146,132 @@ let EFTrimHandles = class EFTrimHandles$1 extends TWMixin(LitElement) {
|
|
|
132
146
|
.trim-overlay-end {
|
|
133
147
|
right: 0;
|
|
134
148
|
}
|
|
149
|
+
|
|
150
|
+
.region {
|
|
151
|
+
position: absolute;
|
|
152
|
+
top: 0;
|
|
153
|
+
bottom: 0;
|
|
154
|
+
cursor: grab;
|
|
155
|
+
pointer-events: auto;
|
|
156
|
+
z-index: 5;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.region.dragging {
|
|
160
|
+
cursor: grabbing;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.selected-border {
|
|
164
|
+
position: absolute;
|
|
165
|
+
left: 0;
|
|
166
|
+
right: 0;
|
|
167
|
+
height: var(--trim-selected-border-width, 0px);
|
|
168
|
+
background: var(--trim-selected-border-color, transparent);
|
|
169
|
+
pointer-events: none;
|
|
170
|
+
z-index: 15;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.selected-border-top {
|
|
174
|
+
top: 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.selected-border-bottom {
|
|
178
|
+
bottom: 0;
|
|
179
|
+
}
|
|
135
180
|
`];
|
|
136
181
|
}
|
|
182
|
+
get trimStartMs() {
|
|
183
|
+
return this.value.startMs;
|
|
184
|
+
}
|
|
185
|
+
set trimStartMs(v) {
|
|
186
|
+
this.value = {
|
|
187
|
+
...this.value,
|
|
188
|
+
startMs: v
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
get trimEndMs() {
|
|
192
|
+
return this.value.endMs;
|
|
193
|
+
}
|
|
194
|
+
set trimEndMs(v) {
|
|
195
|
+
this.value = {
|
|
196
|
+
...this.value,
|
|
197
|
+
endMs: v
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
#regionDragStartTrimStart = 0;
|
|
201
|
+
#regionDragStartTrimEnd = 0;
|
|
202
|
+
#resizeObserver = null;
|
|
203
|
+
#hostWidth = 0;
|
|
204
|
+
#emitChange(type, value) {
|
|
205
|
+
this.dispatchEvent(new CustomEvent("trim-change", {
|
|
206
|
+
detail: {
|
|
207
|
+
elementId: this.elementId,
|
|
208
|
+
type,
|
|
209
|
+
value
|
|
210
|
+
},
|
|
211
|
+
bubbles: true,
|
|
212
|
+
composed: true
|
|
213
|
+
}));
|
|
214
|
+
this.#seekToTarget(type, value);
|
|
215
|
+
}
|
|
216
|
+
#seekToTarget(type, value) {
|
|
217
|
+
if (!this.seekTarget) return;
|
|
218
|
+
const target = this.getRootNode().getElementById(this.seekTarget);
|
|
219
|
+
if (!target || !("currentTimeMs" in target)) return;
|
|
220
|
+
if (type === "end") target.currentTimeMs = this.intrinsicDurationMs - value.startMs - value.endMs;
|
|
221
|
+
else target.currentTimeMs = 0;
|
|
222
|
+
}
|
|
223
|
+
get #effectivePixelsPerMs() {
|
|
224
|
+
if (this.pixelsPerMs != null) return this.pixelsPerMs;
|
|
225
|
+
if (this.#hostWidth > 0 && this.intrinsicDurationMs > 0) return this.#hostWidth / this.intrinsicDurationMs;
|
|
226
|
+
return .04;
|
|
227
|
+
}
|
|
228
|
+
connectedCallback() {
|
|
229
|
+
super.connectedCallback();
|
|
230
|
+
this.#resizeObserver = new ResizeObserver((entries) => {
|
|
231
|
+
const entry = entries[0];
|
|
232
|
+
if (!entry) return;
|
|
233
|
+
const width = entry.contentRect.width;
|
|
234
|
+
if (width !== this.#hostWidth) {
|
|
235
|
+
this.#hostWidth = width;
|
|
236
|
+
this.requestUpdate();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
this.#resizeObserver.observe(this);
|
|
240
|
+
}
|
|
241
|
+
disconnectedCallback() {
|
|
242
|
+
super.disconnectedCallback();
|
|
243
|
+
this.#resizeObserver?.disconnect();
|
|
244
|
+
this.#resizeObserver = null;
|
|
245
|
+
}
|
|
137
246
|
handlePointerDown(e, type) {
|
|
138
247
|
e.preventDefault();
|
|
139
248
|
e.stopPropagation();
|
|
140
249
|
this.draggingHandle = type;
|
|
141
250
|
this.dragStartX = e.clientX;
|
|
142
251
|
this.dragStartValue = type === "start" ? this.trimStartMs : this.trimEndMs;
|
|
252
|
+
if (this.editingContext) this.editingContext.setState({
|
|
253
|
+
mode: "trimming",
|
|
254
|
+
elementId: this.elementId,
|
|
255
|
+
handle: type
|
|
256
|
+
});
|
|
257
|
+
const target = e.currentTarget;
|
|
258
|
+
target.setPointerCapture(e.pointerId);
|
|
259
|
+
target.addEventListener("pointermove", this.handlePointerMove);
|
|
260
|
+
target.addEventListener("pointerup", this.handlePointerUp);
|
|
261
|
+
target.addEventListener("pointercancel", this.handlePointerUp);
|
|
262
|
+
}
|
|
263
|
+
handleRegionPointerDown(e) {
|
|
264
|
+
e.preventDefault();
|
|
265
|
+
e.stopPropagation();
|
|
266
|
+
this.draggingHandle = "region";
|
|
267
|
+
this.dragStartX = e.clientX;
|
|
268
|
+
this.#regionDragStartTrimStart = this.trimStartMs;
|
|
269
|
+
this.#regionDragStartTrimEnd = this.trimEndMs;
|
|
270
|
+
if (this.editingContext) this.editingContext.setState({
|
|
271
|
+
mode: "trimming",
|
|
272
|
+
elementId: this.elementId,
|
|
273
|
+
handle: "start"
|
|
274
|
+
});
|
|
143
275
|
const target = e.currentTarget;
|
|
144
276
|
target.setPointerCapture(e.pointerId);
|
|
145
277
|
target.addEventListener("pointermove", this.handlePointerMove);
|
|
@@ -147,29 +279,69 @@ let EFTrimHandles = class EFTrimHandles$1 extends TWMixin(LitElement) {
|
|
|
147
279
|
target.addEventListener("pointercancel", this.handlePointerUp);
|
|
148
280
|
}
|
|
149
281
|
render() {
|
|
150
|
-
const
|
|
151
|
-
const
|
|
282
|
+
const pxPerMs = this.#effectivePixelsPerMs;
|
|
283
|
+
const trimStartPx = this.trimStartMs * pxPerMs;
|
|
284
|
+
const trimEndPx = (this.trimEndMs || 0) * pxPerMs;
|
|
285
|
+
const isStandalone = this.mode === "standalone";
|
|
286
|
+
const handleWidthPx = parseFloat(getComputedStyle(this).getPropertyValue("--trim-handle-width")) || 8;
|
|
152
287
|
return html`
|
|
153
288
|
${this.showOverlays && this.trimStartMs > 0 ? html`<div
|
|
154
289
|
class="trim-overlay trim-overlay-start"
|
|
155
|
-
style=${styleMap({ width: `${
|
|
290
|
+
style=${styleMap({ width: `${trimStartPx}px` })}
|
|
156
291
|
></div>` : nothing}
|
|
157
292
|
${this.showOverlays && this.trimEndMs > 0 ? html`<div
|
|
158
293
|
class="trim-overlay trim-overlay-end"
|
|
159
|
-
style=${styleMap({ width: `${
|
|
294
|
+
style=${styleMap({ width: `${trimEndPx}px` })}
|
|
160
295
|
></div>` : nothing}
|
|
161
296
|
|
|
297
|
+
${isStandalone ? html`
|
|
298
|
+
<div
|
|
299
|
+
class="region ${this.draggingHandle === "region" ? "dragging" : ""}"
|
|
300
|
+
style=${styleMap({
|
|
301
|
+
left: `${trimStartPx + handleWidthPx}px`,
|
|
302
|
+
right: `${trimEndPx + handleWidthPx}px`
|
|
303
|
+
})}
|
|
304
|
+
@pointerdown=${(e) => this.handleRegionPointerDown(e)}
|
|
305
|
+
></div>
|
|
306
|
+
<div class="selected-border selected-border-top"
|
|
307
|
+
style=${styleMap({
|
|
308
|
+
left: `${trimStartPx}px`,
|
|
309
|
+
right: `${trimEndPx}px`
|
|
310
|
+
})}
|
|
311
|
+
></div>
|
|
312
|
+
<div class="selected-border selected-border-bottom"
|
|
313
|
+
style=${styleMap({
|
|
314
|
+
left: `${trimStartPx}px`,
|
|
315
|
+
right: `${trimEndPx}px`
|
|
316
|
+
})}
|
|
317
|
+
></div>
|
|
318
|
+
` : nothing}
|
|
319
|
+
|
|
162
320
|
<div
|
|
163
321
|
class="handle handle-start ${this.draggingHandle === "start" ? "dragging" : ""}"
|
|
322
|
+
style=${isStandalone ? styleMap({ left: `${trimStartPx}px` }) : nothing}
|
|
164
323
|
@pointerdown=${(e) => this.handlePointerDown(e, "start")}
|
|
165
|
-
|
|
324
|
+
>
|
|
325
|
+
<div class="handle-inner">
|
|
326
|
+
<slot name="handle-start"></slot>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
166
329
|
<div
|
|
167
330
|
class="handle handle-end ${this.draggingHandle === "end" ? "dragging" : ""}"
|
|
331
|
+
style=${isStandalone ? styleMap({ right: `${trimEndPx}px` }) : nothing}
|
|
168
332
|
@pointerdown=${(e) => this.handlePointerDown(e, "end")}
|
|
169
|
-
|
|
333
|
+
>
|
|
334
|
+
<div class="handle-inner">
|
|
335
|
+
<slot name="handle-end"></slot>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
170
338
|
`;
|
|
171
339
|
}
|
|
172
340
|
};
|
|
341
|
+
__decorate([property({
|
|
342
|
+
type: String,
|
|
343
|
+
reflect: true
|
|
344
|
+
})], EFTrimHandles.prototype, "mode", void 0);
|
|
173
345
|
__decorate([property({
|
|
174
346
|
type: String,
|
|
175
347
|
attribute: "element-id"
|
|
@@ -178,14 +350,7 @@ __decorate([property({
|
|
|
178
350
|
type: Number,
|
|
179
351
|
attribute: "pixels-per-ms"
|
|
180
352
|
})], EFTrimHandles.prototype, "pixelsPerMs", void 0);
|
|
181
|
-
__decorate([property({
|
|
182
|
-
type: Number,
|
|
183
|
-
attribute: "trim-start-ms"
|
|
184
|
-
})], EFTrimHandles.prototype, "trimStartMs", void 0);
|
|
185
|
-
__decorate([property({
|
|
186
|
-
type: Number,
|
|
187
|
-
attribute: "trim-end-ms"
|
|
188
|
-
})], EFTrimHandles.prototype, "trimEndMs", void 0);
|
|
353
|
+
__decorate([property({ attribute: false })], EFTrimHandles.prototype, "value", void 0);
|
|
189
354
|
__decorate([property({
|
|
190
355
|
type: Number,
|
|
191
356
|
attribute: "intrinsic-duration-ms"
|
|
@@ -194,6 +359,14 @@ __decorate([property({
|
|
|
194
359
|
type: Boolean,
|
|
195
360
|
attribute: "show-overlays"
|
|
196
361
|
})], EFTrimHandles.prototype, "showOverlays", void 0);
|
|
362
|
+
__decorate([property({
|
|
363
|
+
type: String,
|
|
364
|
+
attribute: "seek-target"
|
|
365
|
+
})], EFTrimHandles.prototype, "seekTarget", void 0);
|
|
366
|
+
__decorate([consume({
|
|
367
|
+
context: timelineEditingContext,
|
|
368
|
+
subscribe: true
|
|
369
|
+
})], EFTrimHandles.prototype, "editingContext", void 0);
|
|
197
370
|
__decorate([state()], EFTrimHandles.prototype, "draggingHandle", void 0);
|
|
198
371
|
__decorate([state()], EFTrimHandles.prototype, "dragStartX", void 0);
|
|
199
372
|
__decorate([state()], EFTrimHandles.prototype, "dragStartValue", void 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TrimHandles.js","names":["EFTrimHandles","newValueMs: number"],"sources":["../../../src/gui/timeline/TrimHandles.ts"],"sourcesContent":["import { css, html, LitElement, nothing } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\nimport { TWMixin } from \"../TWMixin.js\";\n\nexport interface TrimChangeDetail {\n elementId: string;\n type: \"start\" | \"end\";\n deltaMs: number;\n newValueMs: number;\n}\n\n@customElement(\"ef-trim-handles\")\nexport class EFTrimHandles extends TWMixin(LitElement) {\n static styles = [\n css`\n :host {\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n }\n\n .handle {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 8px;\n cursor: ew-resize;\n pointer-events: auto;\n z-index: 10;\n }\n\n .handle::before {\n content: \"\";\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n width: 4px;\n height: 60%;\n min-height: 12px;\n max-height: 24px;\n background: var(--trim-handle-color, rgba(255, 255, 255, 0.7));\n border-radius: 2px;\n transition: background 0.15s ease;\n }\n\n .handle:hover::before,\n .handle.dragging::before {\n background: var(--trim-handle-active-color, #3b82f6);\n }\n\n .handle-start {\n left: -4px;\n }\n\n .handle-start::before {\n left: 2px;\n }\n\n .handle-end {\n right: -4px;\n }\n\n .handle-end::before {\n right: 2px;\n }\n\n .handle.dragging {\n cursor: grabbing;\n }\n\n .trim-overlay {\n position: absolute;\n top: 0;\n bottom: 0;\n background: var(--trim-overlay-color, rgba(0, 0, 0, 0.4));\n pointer-events: none;\n }\n\n .trim-overlay-start {\n left: 0;\n }\n\n .trim-overlay-end {\n right: 0;\n }\n `,\n ];\n\n @property({ type: String, attribute: \"element-id\" })\n elementId = \"\";\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs = 0.04;\n\n @property({ type: Number, attribute: \"trim-start-ms\" })\n trimStartMs = 0;\n\n @property({ type: Number, attribute: \"trim-end-ms\" })\n trimEndMs = 0;\n\n @property({ type: Number, attribute: \"intrinsic-duration-ms\" })\n intrinsicDurationMs = 0;\n\n @property({ type: Boolean, attribute: \"show-overlays\" })\n showOverlays = true;\n\n @state()\n private draggingHandle: \"start\" | \"end\" | null = null;\n\n @state()\n private dragStartX = 0;\n\n @state()\n private dragStartValue = 0;\n\n private handlePointerDown(e: PointerEvent, type: \"start\" | \"end\"): void {\n e.preventDefault();\n e.stopPropagation();\n\n this.draggingHandle = type;\n this.dragStartX = e.clientX;\n this.dragStartValue = type === \"start\" ? this.trimStartMs : this.trimEndMs;\n\n const target = e.currentTarget as HTMLElement;\n target.setPointerCapture(e.pointerId);\n\n target.addEventListener(\"pointermove\", this.handlePointerMove);\n target.addEventListener(\"pointerup\", this.handlePointerUp);\n target.addEventListener(\"pointercancel\", this.handlePointerUp);\n }\n\n private handlePointerMove = (e: PointerEvent): void => {\n if (!this.draggingHandle) return;\n\n const deltaX = e.clientX - this.dragStartX;\n const deltaMs = deltaX / this.pixelsPerMs;\n\n let newValueMs: number;\n\n if (this.draggingHandle === \"start\") {\n newValueMs = Math.max(0, this.dragStartValue + deltaMs);\n newValueMs = Math.min(\n newValueMs,\n this.intrinsicDurationMs - (this.trimEndMs || 0),\n );\n } else {\n newValueMs = Math.max(0, this.dragStartValue - deltaMs);\n newValueMs = Math.min(\n newValueMs,\n this.intrinsicDurationMs - this.trimStartMs,\n );\n }\n\n this.dispatchEvent(\n new CustomEvent<TrimChangeDetail>(\"trim-change\", {\n detail: {\n elementId: this.elementId,\n type: this.draggingHandle,\n deltaMs,\n newValueMs,\n },\n bubbles: true,\n composed: true,\n }),\n );\n };\n\n private handlePointerUp = (e: PointerEvent): void => {\n const target = e.currentTarget as HTMLElement;\n target.releasePointerCapture(e.pointerId);\n target.removeEventListener(\"pointermove\", this.handlePointerMove);\n target.removeEventListener(\"pointerup\", this.handlePointerUp);\n target.removeEventListener(\"pointercancel\", this.handlePointerUp);\n\n if (this.draggingHandle) {\n this.dispatchEvent(\n new CustomEvent(\"trim-change-end\", {\n detail: {\n elementId: this.elementId,\n type: this.draggingHandle,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n this.draggingHandle = null;\n };\n\n render() {\n const trimStartWidth = this.trimStartMs * this.pixelsPerMs;\n const trimEndWidth = (this.trimEndMs || 0) * this.pixelsPerMs;\n\n return html`\n ${\n this.showOverlays && this.trimStartMs > 0\n ? html`<div\n class=\"trim-overlay trim-overlay-start\"\n style=${styleMap({ width: `${trimStartWidth}px` })}\n ></div>`\n : nothing\n }\n ${\n this.showOverlays && this.trimEndMs > 0\n ? html`<div\n class=\"trim-overlay trim-overlay-end\"\n style=${styleMap({ width: `${trimEndWidth}px` })}\n ></div>`\n : nothing\n }\n\n <div\n class=\"handle handle-start ${this.draggingHandle === \"start\" ? \"dragging\" : \"\"}\"\n @pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, \"start\")}\n ></div>\n <div\n class=\"handle handle-end ${this.draggingHandle === \"end\" ? \"dragging\" : \"\"}\"\n @pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, \"end\")}\n ></div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-trim-handles\": EFTrimHandles;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TrimHandles.js","names":["EFTrimHandles","#effectivePixelsPerMs","#regionDragStartTrimStart","#regionDragStartTrimEnd","newValue: TrimValue","#emitChange","newValueMs: number","#seekToTarget","#hostWidth","#resizeObserver"],"sources":["../../../src/gui/timeline/TrimHandles.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement, nothing } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\nimport { TWMixin } from \"../TWMixin.js\";\nimport {\n timelineEditingContext,\n type TimelineEditingContext,\n} from \"./timelineEditingContext.js\";\n\nexport interface TrimValue {\n startMs: number;\n endMs: number;\n}\n\nexport interface TrimChangeDetail {\n elementId: string;\n type: \"start\" | \"end\" | \"region\";\n value: TrimValue;\n}\n\n@customElement(\"ef-trim-handles\")\nexport class EFTrimHandles extends TWMixin(LitElement) {\n static styles = [\n css`\n :host {\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n }\n\n .handle {\n position: absolute;\n top: 0;\n bottom: 0;\n width: var(--trim-handle-width, 8px);\n cursor: ew-resize;\n pointer-events: auto;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .handle-inner {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--trim-handle-color, rgba(255, 255, 255, 0.7));\n transition: background 0.15s ease;\n }\n\n .handle:hover .handle-inner,\n .handle.dragging .handle-inner {\n background: var(--trim-handle-active-color, #3b82f6);\n }\n\n .handle-start .handle-inner {\n border-radius: var(--trim-handle-border-radius-start, 2px 0 0 2px);\n }\n\n .handle-end .handle-inner {\n border-radius: var(--trim-handle-border-radius-end, 0 2px 2px 0);\n }\n\n /* Track mode: handles pinned at container edges */\n :host([mode=\"track\"]) .handle-start {\n left: -4px;\n }\n :host([mode=\"track\"]) .handle-end {\n right: -4px;\n }\n\n .handle.dragging {\n cursor: grabbing;\n }\n\n .trim-overlay {\n position: absolute;\n top: 0;\n bottom: 0;\n background: var(--trim-overlay-color, rgba(0, 0, 0, 0.4));\n pointer-events: none;\n }\n\n .trim-overlay-start {\n left: 0;\n }\n\n .trim-overlay-end {\n right: 0;\n }\n\n .region {\n position: absolute;\n top: 0;\n bottom: 0;\n cursor: grab;\n pointer-events: auto;\n z-index: 5;\n }\n\n .region.dragging {\n cursor: grabbing;\n }\n\n .selected-border {\n position: absolute;\n left: 0;\n right: 0;\n height: var(--trim-selected-border-width, 0px);\n background: var(--trim-selected-border-color, transparent);\n pointer-events: none;\n z-index: 15;\n }\n\n .selected-border-top {\n top: 0;\n }\n\n .selected-border-bottom {\n bottom: 0;\n }\n `,\n ];\n\n @property({ type: String, reflect: true })\n mode: \"standalone\" | \"track\" = \"standalone\";\n\n @property({ type: String, attribute: \"element-id\" })\n elementId = \"\";\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs: number | null = null;\n\n @property({ attribute: false })\n value: TrimValue = { startMs: 0, endMs: 0 };\n\n @property({ type: Number, attribute: \"intrinsic-duration-ms\" })\n intrinsicDurationMs = 0;\n\n get trimStartMs(): number {\n return this.value.startMs;\n }\n set trimStartMs(v: number) {\n this.value = { ...this.value, startMs: v };\n }\n\n get trimEndMs(): number {\n return this.value.endMs;\n }\n set trimEndMs(v: number) {\n this.value = { ...this.value, endMs: v };\n }\n\n @property({ type: Boolean, attribute: \"show-overlays\" })\n showOverlays = true;\n\n @property({ type: String, attribute: \"seek-target\" })\n seekTarget = \"\";\n\n @consume({ context: timelineEditingContext, subscribe: true })\n editingContext?: TimelineEditingContext;\n\n @state()\n private draggingHandle: \"start\" | \"end\" | \"region\" | null = null;\n\n @state()\n private dragStartX = 0;\n\n @state()\n private dragStartValue = 0;\n\n #regionDragStartTrimStart = 0;\n #regionDragStartTrimEnd = 0;\n #resizeObserver: ResizeObserver | null = null;\n #hostWidth = 0;\n\n #emitChange(type: \"start\" | \"end\" | \"region\", value: TrimValue): void {\n this.dispatchEvent(\n new CustomEvent<TrimChangeDetail>(\"trim-change\", {\n detail: { elementId: this.elementId, type, value },\n bubbles: true,\n composed: true,\n }),\n );\n this.#seekToTarget(type, value);\n }\n\n #seekToTarget(type: \"start\" | \"end\" | \"region\", value: TrimValue): void {\n if (!this.seekTarget) return;\n const target = (this.getRootNode() as Document | ShadowRoot).getElementById(\n this.seekTarget,\n ) as any;\n if (!target || !(\"currentTimeMs\" in target)) return;\n\n if (type === \"end\") {\n target.currentTimeMs =\n this.intrinsicDurationMs - value.startMs - value.endMs;\n } else {\n target.currentTimeMs = 0;\n }\n }\n\n get #effectivePixelsPerMs(): number {\n if (this.pixelsPerMs != null) {\n return this.pixelsPerMs;\n }\n if (this.#hostWidth > 0 && this.intrinsicDurationMs > 0) {\n return this.#hostWidth / this.intrinsicDurationMs;\n }\n return 0.04;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n this.#resizeObserver = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (!entry) return;\n const width = entry.contentRect.width;\n if (width !== this.#hostWidth) {\n this.#hostWidth = width;\n this.requestUpdate();\n }\n });\n this.#resizeObserver.observe(this);\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n }\n\n private handlePointerDown(e: PointerEvent, type: \"start\" | \"end\"): void {\n e.preventDefault();\n e.stopPropagation();\n\n this.draggingHandle = type;\n this.dragStartX = e.clientX;\n this.dragStartValue = type === \"start\" ? this.trimStartMs : this.trimEndMs;\n\n if (this.editingContext) {\n this.editingContext.setState({\n mode: \"trimming\",\n elementId: this.elementId,\n handle: type,\n });\n }\n\n const target = e.currentTarget as HTMLElement;\n target.setPointerCapture(e.pointerId);\n\n target.addEventListener(\"pointermove\", this.handlePointerMove);\n target.addEventListener(\"pointerup\", this.handlePointerUp);\n target.addEventListener(\"pointercancel\", this.handlePointerUp);\n }\n\n private handleRegionPointerDown(e: PointerEvent): void {\n e.preventDefault();\n e.stopPropagation();\n\n this.draggingHandle = \"region\";\n this.dragStartX = e.clientX;\n this.#regionDragStartTrimStart = this.trimStartMs;\n this.#regionDragStartTrimEnd = this.trimEndMs;\n\n if (this.editingContext) {\n this.editingContext.setState({\n mode: \"trimming\",\n elementId: this.elementId,\n handle: \"start\",\n });\n }\n\n const target = e.currentTarget as HTMLElement;\n target.setPointerCapture(e.pointerId);\n\n target.addEventListener(\"pointermove\", this.handlePointerMove);\n target.addEventListener(\"pointerup\", this.handlePointerUp);\n target.addEventListener(\"pointercancel\", this.handlePointerUp);\n }\n\n private handlePointerMove = (e: PointerEvent): void => {\n if (!this.draggingHandle) return;\n\n const pxPerMs = this.#effectivePixelsPerMs;\n const deltaX = e.clientX - this.dragStartX;\n const deltaMs = deltaX / pxPerMs;\n\n if (this.draggingHandle === \"region\") {\n const clampedDelta = Math.max(\n -this.#regionDragStartTrimStart,\n Math.min(this.#regionDragStartTrimEnd, deltaMs),\n );\n\n const newValue: TrimValue = {\n startMs: this.#regionDragStartTrimStart + clampedDelta,\n endMs: this.#regionDragStartTrimEnd - clampedDelta,\n };\n\n this.#emitChange(\"region\", newValue);\n return;\n }\n\n let newValueMs: number;\n\n if (this.draggingHandle === \"start\") {\n newValueMs = Math.max(0, this.dragStartValue + deltaMs);\n newValueMs = Math.min(\n newValueMs,\n this.intrinsicDurationMs - (this.trimEndMs || 0),\n );\n\n this.#emitChange(\"start\", { startMs: newValueMs, endMs: this.trimEndMs });\n } else {\n newValueMs = Math.max(0, this.dragStartValue - deltaMs);\n newValueMs = Math.min(\n newValueMs,\n this.intrinsicDurationMs - this.trimStartMs,\n );\n\n this.#emitChange(\"end\", { startMs: this.trimStartMs, endMs: newValueMs });\n }\n };\n\n private handlePointerUp = (e: PointerEvent): void => {\n const target = e.currentTarget as HTMLElement;\n target.releasePointerCapture(e.pointerId);\n target.removeEventListener(\"pointermove\", this.handlePointerMove);\n target.removeEventListener(\"pointerup\", this.handlePointerUp);\n target.removeEventListener(\"pointercancel\", this.handlePointerUp);\n\n if (this.draggingHandle) {\n this.dispatchEvent(\n new CustomEvent(\"trim-change-end\", {\n detail: {\n elementId: this.elementId,\n type: this.draggingHandle,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n this.draggingHandle = null;\n\n if (this.editingContext) {\n this.editingContext.setState({ mode: \"idle\" });\n }\n };\n\n render() {\n const pxPerMs = this.#effectivePixelsPerMs;\n const trimStartPx = this.trimStartMs * pxPerMs;\n const trimEndPx = (this.trimEndMs || 0) * pxPerMs;\n const isStandalone = this.mode === \"standalone\";\n const handleWidthPx =\n parseFloat(\n getComputedStyle(this).getPropertyValue(\"--trim-handle-width\"),\n ) || 8;\n\n return html`\n ${\n this.showOverlays && this.trimStartMs > 0\n ? html`<div\n class=\"trim-overlay trim-overlay-start\"\n style=${styleMap({ width: `${trimStartPx}px` })}\n ></div>`\n : nothing\n }\n ${\n this.showOverlays && this.trimEndMs > 0\n ? html`<div\n class=\"trim-overlay trim-overlay-end\"\n style=${styleMap({ width: `${trimEndPx}px` })}\n ></div>`\n : nothing\n }\n\n ${\n isStandalone\n ? html`\n <div\n class=\"region ${this.draggingHandle === \"region\" ? \"dragging\" : \"\"}\"\n style=${styleMap({\n left: `${trimStartPx + handleWidthPx}px`,\n right: `${trimEndPx + handleWidthPx}px`,\n })}\n @pointerdown=${(e: PointerEvent) => this.handleRegionPointerDown(e)}\n ></div>\n <div class=\"selected-border selected-border-top\"\n style=${styleMap({\n left: `${trimStartPx}px`,\n right: `${trimEndPx}px`,\n })}\n ></div>\n <div class=\"selected-border selected-border-bottom\"\n style=${styleMap({\n left: `${trimStartPx}px`,\n right: `${trimEndPx}px`,\n })}\n ></div>\n `\n : nothing\n }\n\n <div\n class=\"handle handle-start ${this.draggingHandle === \"start\" ? \"dragging\" : \"\"}\"\n style=${isStandalone ? styleMap({ left: `${trimStartPx}px` }) : nothing}\n @pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, \"start\")}\n >\n <div class=\"handle-inner\">\n <slot name=\"handle-start\"></slot>\n </div>\n </div>\n <div\n class=\"handle handle-end ${this.draggingHandle === \"end\" ? \"dragging\" : \"\"}\"\n style=${isStandalone ? styleMap({ right: `${trimEndPx}px` }) : nothing}\n @pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, \"end\")}\n >\n <div class=\"handle-inner\">\n <slot name=\"handle-end\"></slot>\n </div>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-trim-handles\": EFTrimHandles;\n }\n}\n"],"mappings":";;;;;;;;;AAsBO,0BAAMA,wBAAsB,QAAQ,WAAW,CAAC;;;cA+GtB;mBAGnB;qBAGiB;eAGV;GAAE,SAAS;GAAG,OAAO;GAAG;6BAGrB;sBAiBP;oBAGF;wBAM+C;oBAGvC;wBAGI;4BAgHI,MAA0B;AACrD,OAAI,CAAC,KAAK,eAAgB;GAE1B,MAAM,UAAU,MAAKC;GAErB,MAAM,WADS,EAAE,UAAU,KAAK,cACP;AAEzB,OAAI,KAAK,mBAAmB,UAAU;IACpC,MAAM,eAAe,KAAK,IACxB,CAAC,MAAKC,0BACN,KAAK,IAAI,MAAKC,wBAAyB,QAAQ,CAChD;IAED,MAAMC,WAAsB;KAC1B,SAAS,MAAKF,2BAA4B;KAC1C,OAAO,MAAKC,yBAA0B;KACvC;AAED,UAAKE,WAAY,UAAU,SAAS;AACpC;;GAGF,IAAIC;AAEJ,OAAI,KAAK,mBAAmB,SAAS;AACnC,iBAAa,KAAK,IAAI,GAAG,KAAK,iBAAiB,QAAQ;AACvD,iBAAa,KAAK,IAChB,YACA,KAAK,uBAAuB,KAAK,aAAa,GAC/C;AAED,UAAKD,WAAY,SAAS;KAAE,SAAS;KAAY,OAAO,KAAK;KAAW,CAAC;UACpE;AACL,iBAAa,KAAK,IAAI,GAAG,KAAK,iBAAiB,QAAQ;AACvD,iBAAa,KAAK,IAChB,YACA,KAAK,sBAAsB,KAAK,YACjC;AAED,UAAKA,WAAY,OAAO;KAAE,SAAS,KAAK;KAAa,OAAO;KAAY,CAAC;;;0BAIlD,MAA0B;GACnD,MAAM,SAAS,EAAE;AACjB,UAAO,sBAAsB,EAAE,UAAU;AACzC,UAAO,oBAAoB,eAAe,KAAK,kBAAkB;AACjE,UAAO,oBAAoB,aAAa,KAAK,gBAAgB;AAC7D,UAAO,oBAAoB,iBAAiB,KAAK,gBAAgB;AAEjE,OAAI,KAAK,eACP,MAAK,cACH,IAAI,YAAY,mBAAmB;IACjC,QAAQ;KACN,WAAW,KAAK;KAChB,MAAM,KAAK;KACZ;IACD,SAAS;IACT,UAAU;IACX,CAAC,CACH;AAGH,QAAK,iBAAiB;AAEtB,OAAI,KAAK,eACP,MAAK,eAAe,SAAS,EAAE,MAAM,QAAQ,CAAC;;;;gBA5UlC,CACd,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA0GJ;;CAiBD,IAAI,cAAsB;AACxB,SAAO,KAAK,MAAM;;CAEpB,IAAI,YAAY,GAAW;AACzB,OAAK,QAAQ;GAAE,GAAG,KAAK;GAAO,SAAS;GAAG;;CAG5C,IAAI,YAAoB;AACtB,SAAO,KAAK,MAAM;;CAEpB,IAAI,UAAU,GAAW;AACvB,OAAK,QAAQ;GAAE,GAAG,KAAK;GAAO,OAAO;GAAG;;CAqB1C,4BAA4B;CAC5B,0BAA0B;CAC1B,kBAAyC;CACzC,aAAa;CAEb,YAAY,MAAkC,OAAwB;AACpE,OAAK,cACH,IAAI,YAA8B,eAAe;GAC/C,QAAQ;IAAE,WAAW,KAAK;IAAW;IAAM;IAAO;GAClD,SAAS;GACT,UAAU;GACX,CAAC,CACH;AACD,QAAKE,aAAc,MAAM,MAAM;;CAGjC,cAAc,MAAkC,OAAwB;AACtE,MAAI,CAAC,KAAK,WAAY;EACtB,MAAM,SAAU,KAAK,aAAa,CAA2B,eAC3D,KAAK,WACN;AACD,MAAI,CAAC,UAAU,EAAE,mBAAmB,QAAS;AAE7C,MAAI,SAAS,MACX,QAAO,gBACL,KAAK,sBAAsB,MAAM,UAAU,MAAM;MAEnD,QAAO,gBAAgB;;CAI3B,KAAIN,uBAAgC;AAClC,MAAI,KAAK,eAAe,KACtB,QAAO,KAAK;AAEd,MAAI,MAAKO,YAAa,KAAK,KAAK,sBAAsB,EACpD,QAAO,MAAKA,YAAa,KAAK;AAEhC,SAAO;;CAGT,oBAA0B;AACxB,QAAM,mBAAmB;AACzB,QAAKC,iBAAkB,IAAI,gBAAgB,YAAY;GACrD,MAAM,QAAQ,QAAQ;AACtB,OAAI,CAAC,MAAO;GACZ,MAAM,QAAQ,MAAM,YAAY;AAChC,OAAI,UAAU,MAAKD,WAAY;AAC7B,UAAKA,YAAa;AAClB,SAAK,eAAe;;IAEtB;AACF,QAAKC,eAAgB,QAAQ,KAAK;;CAGpC,uBAA6B;AAC3B,QAAM,sBAAsB;AAC5B,QAAKA,gBAAiB,YAAY;AAClC,QAAKA,iBAAkB;;CAGzB,AAAQ,kBAAkB,GAAiB,MAA6B;AACtE,IAAE,gBAAgB;AAClB,IAAE,iBAAiB;AAEnB,OAAK,iBAAiB;AACtB,OAAK,aAAa,EAAE;AACpB,OAAK,iBAAiB,SAAS,UAAU,KAAK,cAAc,KAAK;AAEjE,MAAI,KAAK,eACP,MAAK,eAAe,SAAS;GAC3B,MAAM;GACN,WAAW,KAAK;GAChB,QAAQ;GACT,CAAC;EAGJ,MAAM,SAAS,EAAE;AACjB,SAAO,kBAAkB,EAAE,UAAU;AAErC,SAAO,iBAAiB,eAAe,KAAK,kBAAkB;AAC9D,SAAO,iBAAiB,aAAa,KAAK,gBAAgB;AAC1D,SAAO,iBAAiB,iBAAiB,KAAK,gBAAgB;;CAGhE,AAAQ,wBAAwB,GAAuB;AACrD,IAAE,gBAAgB;AAClB,IAAE,iBAAiB;AAEnB,OAAK,iBAAiB;AACtB,OAAK,aAAa,EAAE;AACpB,QAAKP,2BAA4B,KAAK;AACtC,QAAKC,yBAA0B,KAAK;AAEpC,MAAI,KAAK,eACP,MAAK,eAAe,SAAS;GAC3B,MAAM;GACN,WAAW,KAAK;GAChB,QAAQ;GACT,CAAC;EAGJ,MAAM,SAAS,EAAE;AACjB,SAAO,kBAAkB,EAAE,UAAU;AAErC,SAAO,iBAAiB,eAAe,KAAK,kBAAkB;AAC9D,SAAO,iBAAiB,aAAa,KAAK,gBAAgB;AAC1D,SAAO,iBAAiB,iBAAiB,KAAK,gBAAgB;;CAyEhE,SAAS;EACP,MAAM,UAAU,MAAKF;EACrB,MAAM,cAAc,KAAK,cAAc;EACvC,MAAM,aAAa,KAAK,aAAa,KAAK;EAC1C,MAAM,eAAe,KAAK,SAAS;EACnC,MAAM,gBACJ,WACE,iBAAiB,KAAK,CAAC,iBAAiB,sBAAsB,CAC/D,IAAI;AAEP,SAAO,IAAI;QAEP,KAAK,gBAAgB,KAAK,cAAc,IACpC,IAAI;;oBAEI,SAAS,EAAE,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC;qBAEhD,QACL;QAEC,KAAK,gBAAgB,KAAK,YAAY,IAClC,IAAI;;oBAEI,SAAS,EAAE,OAAO,GAAG,UAAU,KAAK,CAAC,CAAC;qBAE9C,QACL;;QAGC,eACI,IAAI;;gCAEgB,KAAK,mBAAmB,WAAW,aAAa,GAAG;wBAC3D,SAAS;GACf,MAAM,GAAG,cAAc,cAAc;GACrC,OAAO,GAAG,YAAY,cAAc;GACrC,CAAC,CAAC;gCACa,MAAoB,KAAK,wBAAwB,EAAE,CAAC;;;wBAG5D,SAAS;GACf,MAAM,GAAG,YAAY;GACrB,OAAO,GAAG,UAAU;GACrB,CAAC,CAAC;;;wBAGK,SAAS;GACf,MAAM,GAAG,YAAY;GACrB,OAAO,GAAG,UAAU;GACrB,CAAC,CAAC;;gBAGP,QACL;;;qCAG8B,KAAK,mBAAmB,UAAU,aAAa,GAAG;gBACvE,eAAe,SAAS,EAAE,MAAM,GAAG,YAAY,KAAK,CAAC,GAAG,QAAQ;wBACxD,MAAoB,KAAK,kBAAkB,GAAG,QAAQ,CAAC;;;;;;;mCAO5C,KAAK,mBAAmB,QAAQ,aAAa,GAAG;gBACnE,eAAe,SAAS,EAAE,OAAO,GAAG,UAAU,KAAK,CAAC,GAAG,QAAQ;wBACvD,MAAoB,KAAK,kBAAkB,GAAG,MAAM,CAAC;;;;;;;;;YAtS1E,SAAS;CAAE,MAAM;CAAQ,SAAS;CAAM,CAAC;YAGzC,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAc,CAAC;YAGnD,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAiB,CAAC;YAGtD,SAAS,EAAE,WAAW,OAAO,CAAC;YAG9B,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAyB,CAAC;YAiB9D,SAAS;CAAE,MAAM;CAAS,WAAW;CAAiB,CAAC;YAGvD,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAe,CAAC;YAGpD,QAAQ;CAAE,SAAS;CAAwB,WAAW;CAAM,CAAC;YAG7D,OAAO;YAGP,OAAO;YAGP,OAAO;4BA3JT,cAAc,kBAAkB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flattenHierarchy.js","names":["rows: TimelineRowModel[]"],"sources":["../../../src/gui/timeline/flattenHierarchy.ts"],"sourcesContent":["import {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../../elements/EFTemporal.js\";\nimport { EFTimegroup } from \"../../elements/EFTimegroup.js\";\n\nexport interface TimelineRowModel {\n element: TemporalMixinInterface & Element;\n depth: number;\n}\n\n/**\n * Flattens a hierarchical temporal element tree into a flat array of rows.\n * Each row contains the element and its depth in the hierarchy.\n *\n * @param root - The root temporal element to flatten\n * @param startDepth - Starting depth (default 0)\n * @returns Array of {element, depth} in depth-first order\n */\nexport function flattenHierarchy(\n root: TemporalMixinInterface & Element,\n startDepth = 0,\n): TimelineRowModel[] {\n const rows: TimelineRowModel[] = [{ element: root, depth: startDepth }];\n\n if (root instanceof EFTimegroup) {\n for (const child of root.children) {\n if (isEFTemporal(child)) {\n // Skip child elements that are consolidated into their parent track\n const tagName = (child as Element).tagName?.toUpperCase();\n
|
|
1
|
+
{"version":3,"file":"flattenHierarchy.js","names":["rows: TimelineRowModel[]"],"sources":["../../../src/gui/timeline/flattenHierarchy.ts"],"sourcesContent":["import {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../../elements/EFTemporal.js\";\nimport { EFTimegroup } from \"../../elements/EFTimegroup.js\";\n\nexport interface TimelineRowModel {\n element: TemporalMixinInterface & Element;\n depth: number;\n}\n\n/**\n * Flattens a hierarchical temporal element tree into a flat array of rows.\n * Each row contains the element and its depth in the hierarchy.\n *\n * @param root - The root temporal element to flatten\n * @param startDepth - Starting depth (default 0)\n * @returns Array of {element, depth} in depth-first order\n */\nexport function flattenHierarchy(\n root: TemporalMixinInterface & Element,\n startDepth = 0,\n): TimelineRowModel[] {\n const rows: TimelineRowModel[] = [{ element: root, depth: startDepth }];\n\n if (root instanceof EFTimegroup) {\n for (const child of root.children) {\n if (isEFTemporal(child)) {\n // Skip child elements that are consolidated into their parent track\n const tagName = (child as Element).tagName?.toUpperCase();\n\n // Skip captions child elements - they're shown inline in the captions track\n if (\n tagName === \"EF-CAPTIONS-ACTIVE-WORD\" ||\n tagName === \"EF-CAPTIONS-SEGMENT\" ||\n tagName === \"EF-CAPTIONS-BEFORE-ACTIVE-WORD\" ||\n tagName === \"EF-CAPTIONS-AFTER-ACTIVE-WORD\"\n ) {\n continue;\n }\n\n // Skip text segments - they're shown inline in the text track\n if (tagName === \"EF-TEXT-SEGMENT\") {\n continue;\n }\n\n rows.push(\n ...flattenHierarchy(\n child as TemporalMixinInterface & Element,\n startDepth + 1,\n ),\n );\n }\n }\n }\n\n return rows;\n}\n"],"mappings":";;;;;;;;;;;;AAmBA,SAAgB,iBACd,MACA,aAAa,GACO;CACpB,MAAMA,OAA2B,CAAC;EAAE,SAAS;EAAM,OAAO;EAAY,CAAC;AAEvE,KAAI,gBAAgB,aAClB;OAAK,MAAM,SAAS,KAAK,SACvB,KAAI,aAAa,MAAM,EAAE;GAEvB,MAAM,UAAW,MAAkB,SAAS,aAAa;AAGzD,OACE,YAAY,6BACZ,YAAY,yBACZ,YAAY,oCACZ,YAAY,gCAEZ;AAIF,OAAI,YAAY,kBACd;AAGF,QAAK,KACH,GAAG,iBACD,OACA,aAAa,EACd,CACF;;;AAKP,QAAO"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/gui/timeline/timelineEditingContext.d.ts
|
|
2
|
+
type TimelineEditingState = {
|
|
3
|
+
mode: "idle";
|
|
4
|
+
} | {
|
|
5
|
+
mode: "scrubbing";
|
|
6
|
+
startTimeMs: number;
|
|
7
|
+
} | {
|
|
8
|
+
mode: "trimming";
|
|
9
|
+
elementId: string;
|
|
10
|
+
handle: "start" | "end";
|
|
11
|
+
} | {
|
|
12
|
+
mode: "dragging";
|
|
13
|
+
elementIds: string[];
|
|
14
|
+
startPositions: Map<string, number>;
|
|
15
|
+
} | {
|
|
16
|
+
mode: "selecting";
|
|
17
|
+
box: DOMRect;
|
|
18
|
+
};
|
|
19
|
+
interface TimelineEditingContext {
|
|
20
|
+
state: TimelineEditingState;
|
|
21
|
+
setState: (state: TimelineEditingState) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Returns true if any editing operation is in progress (not idle).
|
|
24
|
+
*/
|
|
25
|
+
isEditing: () => boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Returns true if hover and other passive interactions should be allowed.
|
|
28
|
+
* False when an active editing operation would be disrupted by hover feedback.
|
|
29
|
+
*/
|
|
30
|
+
canInteract: () => boolean;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { TimelineEditingContext };
|
|
34
|
+
//# sourceMappingURL=timelineEditingContext.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createContext } from "@lit/context";
|
|
2
|
+
|
|
3
|
+
//#region src/gui/timeline/timelineEditingContext.ts
|
|
4
|
+
const timelineEditingContext = createContext(Symbol("timelineEditingContext"));
|
|
5
|
+
/**
|
|
6
|
+
* Create a timeline editing context with helper methods.
|
|
7
|
+
*/
|
|
8
|
+
function createTimelineEditingContext() {
|
|
9
|
+
const context = {
|
|
10
|
+
state: { mode: "idle" },
|
|
11
|
+
setState: (state) => {
|
|
12
|
+
context.state = state;
|
|
13
|
+
},
|
|
14
|
+
isEditing: () => context.state.mode !== "idle",
|
|
15
|
+
canInteract: () => {
|
|
16
|
+
return context.state.mode === "idle";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
return context;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { createTimelineEditingContext, timelineEditingContext };
|
|
24
|
+
//# sourceMappingURL=timelineEditingContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timelineEditingContext.js","names":["context: TimelineEditingContext"],"sources":["../../../src/gui/timeline/timelineEditingContext.ts"],"sourcesContent":["import { createContext } from \"@lit/context\";\n\nexport type TimelineEditingState =\n | { mode: \"idle\" }\n | { mode: \"scrubbing\"; startTimeMs: number }\n | { mode: \"trimming\"; elementId: string; handle: \"start\" | \"end\" }\n | {\n mode: \"dragging\";\n elementIds: string[];\n startPositions: Map<string, number>;\n }\n | { mode: \"selecting\"; box: DOMRect };\n\nexport interface TimelineEditingContext {\n state: TimelineEditingState;\n setState: (state: TimelineEditingState) => void;\n\n /**\n * Returns true if any editing operation is in progress (not idle).\n */\n isEditing: () => boolean;\n\n /**\n * Returns true if hover and other passive interactions should be allowed.\n * False when an active editing operation would be disrupted by hover feedback.\n */\n canInteract: () => boolean;\n}\n\nexport const timelineEditingContext = createContext<TimelineEditingContext>(\n Symbol(\"timelineEditingContext\"),\n);\n\n/**\n * Create a timeline editing context with helper methods.\n */\nexport function createTimelineEditingContext(): TimelineEditingContext {\n const context: TimelineEditingContext = {\n state: { mode: \"idle\" },\n setState: (state: TimelineEditingState) => {\n context.state = state;\n },\n isEditing: () => context.state.mode !== \"idle\",\n canInteract: () => {\n // Block interactions during active drag operations\n return context.state.mode === \"idle\";\n },\n };\n return context;\n}\n"],"mappings":";;;AA6BA,MAAa,yBAAyB,cACpC,OAAO,yBAAyB,CACjC;;;;AAKD,SAAgB,+BAAuD;CACrE,MAAMA,UAAkC;EACtC,OAAO,EAAE,MAAM,QAAQ;EACvB,WAAW,UAAgC;AACzC,WAAQ,QAAQ;;EAElB,iBAAiB,QAAQ,MAAM,SAAS;EACxC,mBAAmB;AAEjB,UAAO,QAAQ,MAAM,SAAS;;EAEjC;AACD,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timelineStateContext.js","names":[],"sources":["../../../src/gui/timeline/timelineStateContext.ts"],"sourcesContent":["import { createContext } from \"@lit/context\";\n\n/**\n * The core invariant of the timeline system.\n * Everything else (ruler positions, track positions, playhead position) derives from this.\n */\nexport interface TimelineState {\n /** Pixels per millisecond - the single zoom value */\n pixelsPerMs: number;\n /** Current playhead position in milliseconds */\n currentTimeMs: number;\n /** Total duration in milliseconds */\n durationMs: number;\n /** Viewport scroll position in pixels - single source of truth for visible time range */\n viewportScrollLeft: number;\n /** Viewport width in pixels - for calculating visible time range */\n viewportWidth: number;\n /** Seek to a specific time */\n seek: (timeMs: number) => void;\n /** Zoom in */\n zoomIn: () => void;\n /** Zoom out */\n zoomOut: () => void;\n}\n\nexport const timelineStateContext =\n createContext<TimelineState>(\"timeline-state\");\n\n/**\n * Convert time to pixel position\n */\nexport function timeToPx(timeMs: number, pixelsPerMs: number): number {\n return timeMs * pixelsPerMs;\n}\n\n/**\n * Convert pixel position to time\n */\nexport function pxToTime(px: number, pixelsPerMs: number): number {\n return px / pixelsPerMs;\n}\n\n/**\n * Default pixels per ms at 100% zoom (100 pixels per second)\n */\nexport const DEFAULT_PIXELS_PER_MS = 0.1;\n\n/**\n * Timeline row height in pixels - must match --timeline-row-height CSS variable\n */\nexport const TIMELINE_ROW_HEIGHT = 28;\n\n/**\n * Timeline track content height in pixels - must match --timeline-track-height CSS variable\n */\nexport const TIMELINE_TRACK_HEIGHT = 22;\n\n/**\n * Vertical padding within a row (row height - track height) / 2\n */\nexport const TIMELINE_ROW_PADDING
|
|
1
|
+
{"version":3,"file":"timelineStateContext.js","names":[],"sources":["../../../src/gui/timeline/timelineStateContext.ts"],"sourcesContent":["import { createContext } from \"@lit/context\";\n\n/**\n * The core invariant of the timeline system.\n * Everything else (ruler positions, track positions, playhead position) derives from this.\n */\nexport interface TimelineState {\n /** Pixels per millisecond - the single zoom value */\n pixelsPerMs: number;\n /** Current playhead position in milliseconds */\n currentTimeMs: number;\n /** Total duration in milliseconds */\n durationMs: number;\n /** Viewport scroll position in pixels - single source of truth for visible time range */\n viewportScrollLeft: number;\n /** Viewport width in pixels - for calculating visible time range */\n viewportWidth: number;\n /** Seek to a specific time */\n seek: (timeMs: number) => void;\n /** Zoom in */\n zoomIn: () => void;\n /** Zoom out */\n zoomOut: () => void;\n}\n\nexport const timelineStateContext =\n createContext<TimelineState>(\"timeline-state\");\n\n/**\n * Convert time to pixel position\n */\nexport function timeToPx(timeMs: number, pixelsPerMs: number): number {\n return timeMs * pixelsPerMs;\n}\n\n/**\n * Convert pixel position to time\n */\nexport function pxToTime(px: number, pixelsPerMs: number): number {\n return px / pixelsPerMs;\n}\n\n/**\n * Default pixels per ms at 100% zoom (100 pixels per second)\n */\nexport const DEFAULT_PIXELS_PER_MS = 0.1;\n\n/**\n * Timeline row height in pixels - must match --timeline-row-height CSS variable\n */\nexport const TIMELINE_ROW_HEIGHT = 28;\n\n/**\n * Timeline track content height in pixels - must match --timeline-track-height CSS variable\n */\nexport const TIMELINE_TRACK_HEIGHT = 22;\n\n/**\n * Vertical padding within a row (row height - track height) / 2\n */\nexport const TIMELINE_ROW_PADDING =\n (TIMELINE_ROW_HEIGHT - TIMELINE_TRACK_HEIGHT) / 2;\n\n/**\n * Calculate pixels per ms from a zoom scale\n */\nexport function zoomToPixelsPerMs(zoomScale: number): number {\n return DEFAULT_PIXELS_PER_MS * zoomScale;\n}\n\n/**\n * Calculate zoom scale from pixels per ms\n */\nexport function pixelsPerMsToZoom(pixelsPerMs: number): number {\n return pixelsPerMs / DEFAULT_PIXELS_PER_MS;\n}\n"],"mappings":";;;AAyBA,MAAa,uBACX,cAA6B,iBAAiB;;;;AAKhD,SAAgB,SAAS,QAAgB,aAA6B;AACpE,QAAO,SAAS;;;;;AAMlB,SAAgB,SAAS,IAAY,aAA6B;AAChE,QAAO,KAAK;;;;;AAMd,MAAa,wBAAwB;;;;AAKrC,MAAa,sBAAsB;;;;AAKnC,MAAa,wBAAwB;;;;AAKrC,MAAa,wBACV,sBAAsB,yBAAyB;;;;AAYlD,SAAgB,kBAAkB,aAA6B;AAC7D,QAAO,cAAc"}
|
|
@@ -94,7 +94,7 @@ let EFAudioTrack = class EFAudioTrack$1 extends TrackItem {
|
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
96
|
* Render the waveform to canvas with virtual rendering.
|
|
97
|
-
*
|
|
97
|
+
*
|
|
98
98
|
* The approach:
|
|
99
99
|
* 1. Calculate the visible portion of the track (intersection of track and viewport)
|
|
100
100
|
* 2. Position the canvas at that visible portion within the track
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioTrack.js","names":["EFAudioTrack","#loadWaveformData","#lastSrc","#abortController","#scheduleRender","#renderRequested","#renderWaveform","#getTrackPositionInfo","#hostHeight","#drawWaveformRegion","#resizeObserver","#renderPlaceholder"],"sources":["../../../../src/gui/timeline/tracks/AudioTrack.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, nothing } from \"lit\";\nimport { customElement, state } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport { EFAudio } from \"../../../elements/EFAudio.js\";\nimport { TrackItem } from \"./TrackItem.js\";\nimport {\n extractWaveformData,\n type WaveformData,\n} from \"./waveformUtils.js\";\nimport {\n timelineStateContext,\n type TimelineState,\n} from \"../timelineStateContext.js\";\n\n/** Padding in pixels to render beyond visible area (for smooth scrolling) */\nconst VIRTUAL_RENDER_PADDING_PX = 100;\n\n@customElement(\"ef-audio-track\")\nexport class EFAudioTrack extends TrackItem {\n static styles = [\n ...TrackItem.styles,\n css`\n .waveform-host {\n position: absolute;\n left: 0;\n top: 2px;\n right: 0;\n bottom: 2px;\n overflow: hidden;\n }\n .waveform-canvas {\n display: block;\n position: absolute;\n top: 0;\n height: 100%;\n pointer-events: none;\n }\n `,\n ];\n\n canvasRef = createRef<HTMLCanvasElement>();\n\n /** Timeline state context for viewport info */\n @consume({ context: timelineStateContext, subscribe: true })\n @state()\n private _timelineState?: TimelineState;\n\n @state()\n private _waveformData: WaveformData | null = null;\n\n @state()\n private _isLoading = false;\n\n #lastSrc: string | null = null;\n #abortController: AbortController | null = null;\n #resizeObserver?: ResizeObserver;\n #renderRequested = false;\n #hostHeight = 0;\n\n /**\n * Load waveform data when the audio source changes\n */\n async #loadWaveformData(): Promise<void> {\n const audio = this.element as EFAudio;\n const src = audio?.src;\n\n // Skip if no source or same source already loaded\n if (!src || src === this.#lastSrc) {\n return;\n }\n\n this.#lastSrc = src;\n\n // Cancel any in-progress load\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n\n this._isLoading = true;\n\n try {\n const waveformData = await extractWaveformData(\n src,\n this.#abortController.signal,\n );\n\n if (waveformData) {\n this._waveformData = waveformData;\n this.#scheduleRender();\n }\n } catch (error) {\n if (!(error instanceof DOMException && error.name === \"AbortError\")) {\n console.warn(\"Failed to load waveform data:\", error);\n }\n } finally {\n this._isLoading = false;\n }\n }\n\n /**\n * Schedule a canvas render on the next animation frame\n */\n #scheduleRender(): void {\n if (this.#renderRequested) return;\n this.#renderRequested = true;\n\n requestAnimationFrame(() => {\n this.#renderRequested = false;\n this.#renderWaveform();\n });\n }\n\n /**\n * Get the track's position info relative to timeline scroll\n */\n #getTrackPositionInfo(): {\n trackStartPx: number;\n trackWidthPx: number;\n viewportScrollLeft: number;\n viewportWidth: number;\n pixelsPerMs: number;\n } | null {\n const audio = this.element as EFAudio;\n const durationMs = audio.durationMs ?? 0;\n if (durationMs === 0) return null;\n\n const pixelsPerMs = this._timelineState?.pixelsPerMs ?? this.pixelsPerMs;\n const trackWidthPx = durationMs * pixelsPerMs;\n\n // Get track's absolute position from startTimeMs\n const trackStartMs = audio.startTimeMs ?? 0;\n const trackStartPx = trackStartMs * pixelsPerMs;\n\n // Get viewport info from context\n const viewportScrollLeft = this._timelineState?.viewportScrollLeft ?? 0;\n const viewportWidth = this._timelineState?.viewportWidth ?? 800;\n\n return {\n trackStartPx,\n trackWidthPx,\n viewportScrollLeft,\n viewportWidth,\n pixelsPerMs,\n };\n }\n\n /**\n * Render the waveform to canvas with virtual rendering.\n * \n * The approach:\n * 1. Calculate the visible portion of the track (intersection of track and viewport)\n * 2. Position the canvas at that visible portion within the track\n * 3. Draw only the waveform data for that visible time range\n * 4. Update position and content as scroll/zoom changes\n */\n #renderWaveform(): void {\n const canvas = this.canvasRef.value;\n const waveformData = this._waveformData;\n\n if (!canvas || !waveformData) return;\n\n const positionInfo = this.#getTrackPositionInfo();\n if (!positionInfo) return;\n\n const { trackStartPx, trackWidthPx, viewportScrollLeft, viewportWidth, pixelsPerMs } =\n positionInfo;\n\n // Calculate visible region in absolute pixels (with padding for smooth scrolling)\n const visibleLeftPx = viewportScrollLeft - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx = viewportScrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n\n // Track boundaries in absolute pixels\n const trackEndPx = trackStartPx + trackWidthPx;\n\n // Check if track is visible at all\n if (trackEndPx < visibleLeftPx || trackStartPx > visibleRightPx) {\n // Track not visible, hide canvas\n canvas.style.display = \"none\";\n return;\n }\n canvas.style.display = \"block\";\n\n // Calculate the intersection: what part of the track is visible\n // All coordinates are now relative to the track's left edge (0 = track start)\n const visibleStartInTrack = Math.max(0, visibleLeftPx - trackStartPx);\n const visibleEndInTrack = Math.min(trackWidthPx, visibleRightPx - trackStartPx);\n const visibleWidthPx = visibleEndInTrack - visibleStartInTrack;\n\n if (visibleWidthPx <= 0) return;\n\n const height = this.#hostHeight || 18;\n\n // Set canvas size with DPR\n const dpr = window.devicePixelRatio || 1;\n const targetWidth = Math.ceil(visibleWidthPx * dpr);\n const targetHeight = Math.ceil(height * dpr);\n\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n }\n \n // Position canvas at the visible portion within the track\n canvas.style.left = `${visibleStartInTrack}px`;\n canvas.style.width = `${visibleWidthPx}px`;\n canvas.style.height = `${height}px`;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, visibleWidthPx, height);\n\n // Calculate what time range to render\n const audio = this.element as EFAudio;\n const sourceInMs = audio.sourceStartMs ?? 0;\n \n // Convert visible pixel range to time range\n const timeStartMs = sourceInMs + visibleStartInTrack / pixelsPerMs;\n const timeEndMs = sourceInMs + visibleEndInTrack / pixelsPerMs;\n\n // Draw the waveform for the visible portion\n this.#drawWaveformRegion(\n ctx,\n waveformData,\n 0, // Start drawing at x=0 of canvas (canvas is already positioned)\n visibleWidthPx,\n height,\n timeStartMs,\n timeEndMs,\n );\n }\n\n /**\n * Draw a region of the waveform to canvas\n */\n #drawWaveformRegion(\n ctx: CanvasRenderingContext2D,\n waveformData: WaveformData,\n x: number,\n width: number,\n height: number,\n startMs: number,\n endMs: number,\n ): void {\n const { peaks, samplesPerSecond } = waveformData;\n\n // Calculate sample range\n const startSample = Math.floor((startMs / 1000) * samplesPerSecond);\n const endSample = Math.ceil((endMs / 1000) * samplesPerSecond);\n const sampleCount = endSample - startSample;\n\n if (sampleCount <= 0 || width <= 0) return;\n\n const centerY = height / 2;\n const halfHeight = (height / 2) - 2; // Leave 2px padding top/bottom\n const color = this.getElementTypeColor();\n\n ctx.fillStyle = color;\n ctx.globalAlpha = 0.8;\n ctx.beginPath();\n\n // Draw top half (max values) left to right\n const pixelsPerSample = width / sampleCount;\n\n for (let i = 0; i <= sampleCount; i++) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n\n // Clamp to valid range\n if (peakIndex + 1 >= peaks.length) break;\n\n const maxValue = peaks[peakIndex + 1] ?? 0;\n const px = x + i * pixelsPerSample;\n const py = centerY - maxValue * halfHeight;\n\n if (i === 0) {\n ctx.moveTo(px, py);\n } else {\n ctx.lineTo(px, py);\n }\n }\n\n // Draw bottom half (min values) right to left\n for (let i = sampleCount; i >= 0; i--) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n\n // Clamp to valid range\n if (peakIndex >= peaks.length) continue;\n\n const minValue = peaks[peakIndex] ?? 0;\n const px = x + i * pixelsPerSample;\n const py = centerY - minValue * halfHeight;\n\n ctx.lineTo(px, py);\n }\n\n ctx.closePath();\n ctx.fill();\n\n // Draw center line\n ctx.globalAlpha = 0.3;\n ctx.strokeStyle = color;\n ctx.lineWidth = 1;\n ctx.beginPath();\n ctx.moveTo(x, centerY);\n ctx.lineTo(x + width, centerY);\n ctx.stroke();\n\n ctx.globalAlpha = 1;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Start loading waveform data\n this.#loadWaveformData();\n\n // Observe size changes\n this.#resizeObserver = new ResizeObserver((entries) => {\n for (const entry of entries) {\n this.#hostHeight =\n entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;\n this.#scheduleRender();\n }\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#abortController?.abort();\n this.#resizeObserver?.disconnect();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>): void {\n super.updated(changedProperties);\n\n // Check if we need to reload waveform data\n const audio = this.element as EFAudio;\n if (audio?.src !== this.#lastSrc) {\n this.#loadWaveformData();\n }\n\n // Re-render when timeline state changes (scroll, zoom)\n if (changedProperties.has(\"_timelineState\")) {\n this.#scheduleRender();\n }\n\n // Attach resize observer to track container once rendered\n if (this.canvasRef.value && this.#resizeObserver) {\n const container = this.canvasRef.value.parentElement;\n if (container) {\n this.#resizeObserver.disconnect();\n this.#resizeObserver.observe(container);\n }\n }\n\n // Always schedule render after update to catch any changes\n this.#scheduleRender();\n }\n\n contents() {\n const audio = this.element as EFAudio;\n if (!(audio instanceof EFAudio)) {\n return nothing;\n }\n\n const durationMs = audio.durationMs ?? 0;\n if (durationMs === 0) {\n return nothing;\n }\n\n // Show loading placeholder if no waveform data yet\n if (!this._waveformData) {\n return this.#renderPlaceholder();\n }\n\n // The host fills the track container, canvas is positioned within it\n return html`\n <div class=\"waveform-host\">\n <canvas ${ref(this.canvasRef)} class=\"waveform-canvas\"></canvas>\n </div>\n `;\n }\n\n /**\n * Render placeholder while loading\n */\n #renderPlaceholder() {\n return html`\n <div\n style=\"\n position: absolute;\n left: 0;\n top: 2px;\n bottom: 2px;\n right: 0;\n background: linear-gradient(90deg, \n ${this.getElementTypeColor()}22 0%, \n ${this.getElementTypeColor()}44 50%,\n ${this.getElementTypeColor()}22 100%\n );\n background-size: 200% 100%;\n animation: ${this._isLoading ? \"shimmer 1.5s infinite\" : \"none\"};\n border-radius: 2px;\n \"\n ></div>\n <style>\n @keyframes shimmer {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n }\n </style>\n `;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAgBA,MAAM,4BAA4B;AAG3B,yBAAMA,uBAAqB,UAAU;;;mBAsB9B,WAA8B;uBAQG;oBAGxB;;;gBAhCL,CACd,GAAG,UAAU,QACb,GAAG;;;;;;;;;;;;;;;;MAiBJ;;CAeD,WAA0B;CAC1B,mBAA2C;CAC3C;CACA,mBAAmB;CACnB,cAAc;;;;CAKd,OAAMC,mBAAmC;EAEvC,MAAM,MADQ,KAAK,SACA;AAGnB,MAAI,CAAC,OAAO,QAAQ,MAAKC,QACvB;AAGF,QAAKA,UAAW;AAGhB,QAAKC,iBAAkB,OAAO;AAC9B,QAAKA,kBAAmB,IAAI,iBAAiB;AAE7C,OAAK,aAAa;AAElB,MAAI;GACF,MAAM,eAAe,MAAM,oBACzB,KACA,MAAKA,gBAAiB,OACvB;AAED,OAAI,cAAc;AAChB,SAAK,gBAAgB;AACrB,UAAKC,gBAAiB;;WAEjB,OAAO;AACd,OAAI,EAAE,iBAAiB,gBAAgB,MAAM,SAAS,cACpD,SAAQ,KAAK,iCAAiC,MAAM;YAE9C;AACR,QAAK,aAAa;;;;;;CAOtB,kBAAwB;AACtB,MAAI,MAAKC,gBAAkB;AAC3B,QAAKA,kBAAmB;AAExB,8BAA4B;AAC1B,SAAKA,kBAAmB;AACxB,SAAKC,gBAAiB;IACtB;;;;;CAMJ,wBAMS;EACP,MAAM,QAAQ,KAAK;EACnB,MAAM,aAAa,MAAM,cAAc;AACvC,MAAI,eAAe,EAAG,QAAO;EAE7B,MAAM,cAAc,KAAK,gBAAgB,eAAe,KAAK;EAC7D,MAAM,eAAe,aAAa;AAUlC,SAAO;GACL,eARmB,MAAM,eAAe,KACN;GAQlC;GACA,oBANyB,KAAK,gBAAgB,sBAAsB;GAOpE,eANoB,KAAK,gBAAgB,iBAAiB;GAO1D;GACD;;;;;;;;;;;CAYH,kBAAwB;EACtB,MAAM,SAAS,KAAK,UAAU;EAC9B,MAAM,eAAe,KAAK;AAE1B,MAAI,CAAC,UAAU,CAAC,aAAc;EAE9B,MAAM,eAAe,MAAKC,sBAAuB;AACjD,MAAI,CAAC,aAAc;EAEnB,MAAM,EAAE,cAAc,cAAc,oBAAoB,eAAe,gBACrE;EAGF,MAAM,gBAAgB,qBAAqB;EAC3C,MAAM,iBAAiB,qBAAqB,gBAAgB;AAM5D,MAHmB,eAAe,eAGjB,iBAAiB,eAAe,gBAAgB;AAE/D,UAAO,MAAM,UAAU;AACvB;;AAEF,SAAO,MAAM,UAAU;EAIvB,MAAM,sBAAsB,KAAK,IAAI,GAAG,gBAAgB,aAAa;EACrE,MAAM,oBAAoB,KAAK,IAAI,cAAc,iBAAiB,aAAa;EAC/E,MAAM,iBAAiB,oBAAoB;AAE3C,MAAI,kBAAkB,EAAG;EAEzB,MAAM,SAAS,MAAKC,cAAe;EAGnC,MAAM,MAAM,OAAO,oBAAoB;EACvC,MAAM,cAAc,KAAK,KAAK,iBAAiB,IAAI;EACnD,MAAM,eAAe,KAAK,KAAK,SAAS,IAAI;AAE5C,MAAI,OAAO,UAAU,eAAe,OAAO,WAAW,cAAc;AAClE,UAAO,QAAQ;AACf,UAAO,SAAS;;AAIlB,SAAO,MAAM,OAAO,GAAG,oBAAoB;AAC3C,SAAO,MAAM,QAAQ,GAAG,eAAe;AACvC,SAAO,MAAM,SAAS,GAAG,OAAO;EAEhC,MAAM,MAAM,OAAO,WAAW,KAAK;AACnC,MAAI,CAAC,IAAK;AAEV,MAAI,aAAa,KAAK,GAAG,GAAG,KAAK,GAAG,EAAE;AACtC,MAAI,UAAU,GAAG,GAAG,gBAAgB,OAAO;EAI3C,MAAM,aADQ,KAAK,QACM,iBAAiB;EAG1C,MAAM,cAAc,aAAa,sBAAsB;EACvD,MAAM,YAAY,aAAa,oBAAoB;AAGnD,QAAKC,mBACH,KACA,cACA,GACA,gBACA,QACA,aACA,UACD;;;;;CAMH,oBACE,KACA,cACA,GACA,OACA,QACA,SACA,OACM;EACN,MAAM,EAAE,OAAO,qBAAqB;EAGpC,MAAM,cAAc,KAAK,MAAO,UAAU,MAAQ,iBAAiB;EAEnE,MAAM,cADY,KAAK,KAAM,QAAQ,MAAQ,iBAAiB,GAC9B;AAEhC,MAAI,eAAe,KAAK,SAAS,EAAG;EAEpC,MAAM,UAAU,SAAS;EACzB,MAAM,aAAc,SAAS,IAAK;EAClC,MAAM,QAAQ,KAAK,qBAAqB;AAExC,MAAI,YAAY;AAChB,MAAI,cAAc;AAClB,MAAI,WAAW;EAGf,MAAM,kBAAkB,QAAQ;AAEhC,OAAK,IAAI,IAAI,GAAG,KAAK,aAAa,KAAK;GAErC,MAAM,aADc,cAAc,KACF;AAGhC,OAAI,YAAY,KAAK,MAAM,OAAQ;GAEnC,MAAM,WAAW,MAAM,YAAY,MAAM;GACzC,MAAM,KAAK,IAAI,IAAI;GACnB,MAAM,KAAK,UAAU,WAAW;AAEhC,OAAI,MAAM,EACR,KAAI,OAAO,IAAI,GAAG;OAElB,KAAI,OAAO,IAAI,GAAG;;AAKtB,OAAK,IAAI,IAAI,aAAa,KAAK,GAAG,KAAK;GAErC,MAAM,aADc,cAAc,KACF;AAGhC,OAAI,aAAa,MAAM,OAAQ;GAE/B,MAAM,WAAW,MAAM,cAAc;GACrC,MAAM,KAAK,IAAI,IAAI;GACnB,MAAM,KAAK,UAAU,WAAW;AAEhC,OAAI,OAAO,IAAI,GAAG;;AAGpB,MAAI,WAAW;AACf,MAAI,MAAM;AAGV,MAAI,cAAc;AAClB,MAAI,cAAc;AAClB,MAAI,YAAY;AAChB,MAAI,WAAW;AACf,MAAI,OAAO,GAAG,QAAQ;AACtB,MAAI,OAAO,IAAI,OAAO,QAAQ;AAC9B,MAAI,QAAQ;AAEZ,MAAI,cAAc;;CAGpB,oBAA0B;AACxB,QAAM,mBAAmB;AAGzB,QAAKR,kBAAmB;AAGxB,QAAKS,iBAAkB,IAAI,gBAAgB,YAAY;AACrD,QAAK,MAAM,SAAS,SAAS;AAC3B,UAAKF,aACH,MAAM,gBAAgB,IAAI,aAAa,MAAM,YAAY;AAC3D,UAAKJ,gBAAiB;;IAExB;;CAGJ,uBAA6B;AAC3B,QAAM,sBAAsB;AAC5B,QAAKD,iBAAkB,OAAO;AAC9B,QAAKO,gBAAiB,YAAY;;CAGpC,QAAQ,mBAAiE;AACvE,QAAM,QAAQ,kBAAkB;AAIhC,MADc,KAAK,SACR,QAAQ,MAAKR,QACtB,OAAKD,kBAAmB;AAI1B,MAAI,kBAAkB,IAAI,iBAAiB,CACzC,OAAKG,gBAAiB;AAIxB,MAAI,KAAK,UAAU,SAAS,MAAKM,gBAAiB;GAChD,MAAM,YAAY,KAAK,UAAU,MAAM;AACvC,OAAI,WAAW;AACb,UAAKA,eAAgB,YAAY;AACjC,UAAKA,eAAgB,QAAQ,UAAU;;;AAK3C,QAAKN,gBAAiB;;CAGxB,WAAW;EACT,MAAM,QAAQ,KAAK;AACnB,MAAI,EAAE,iBAAiB,SACrB,QAAO;AAIT,OADmB,MAAM,cAAc,OACpB,EACjB,QAAO;AAIT,MAAI,CAAC,KAAK,cACR,QAAO,MAAKO,mBAAoB;AAIlC,SAAO,IAAI;;kBAEG,IAAI,KAAK,UAAU,CAAC;;;;;;;CAQpC,qBAAqB;AACnB,SAAO,IAAI;;;;;;;;;cASD,KAAK,qBAAqB,CAAC;cAC3B,KAAK,qBAAqB,CAAC;cAC3B,KAAK,qBAAqB,CAAC;;;uBAGlB,KAAK,aAAa,0BAA0B,OAAO;;;;;;;;;;;;;YAxWvE,QAAQ;CAAE,SAAS;CAAsB,WAAW;CAAM,CAAC,EAC3D,OAAO;YAGP,OAAO;YAGP,OAAO;2BAjCT,cAAc,iBAAiB"}
|
|
1
|
+
{"version":3,"file":"AudioTrack.js","names":["EFAudioTrack","#loadWaveformData","#lastSrc","#abortController","#scheduleRender","#renderRequested","#renderWaveform","#getTrackPositionInfo","#hostHeight","#drawWaveformRegion","#resizeObserver","#renderPlaceholder"],"sources":["../../../../src/gui/timeline/tracks/AudioTrack.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, nothing } from \"lit\";\nimport { customElement, state } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport { EFAudio } from \"../../../elements/EFAudio.js\";\nimport { TrackItem } from \"./TrackItem.js\";\nimport { extractWaveformData, type WaveformData } from \"./waveformUtils.js\";\nimport {\n timelineStateContext,\n type TimelineState,\n} from \"../timelineStateContext.js\";\n\n/** Padding in pixels to render beyond visible area (for smooth scrolling) */\nconst VIRTUAL_RENDER_PADDING_PX = 100;\n\n@customElement(\"ef-audio-track\")\nexport class EFAudioTrack extends TrackItem {\n static styles = [\n ...TrackItem.styles,\n css`\n .waveform-host {\n position: absolute;\n left: 0;\n top: 2px;\n right: 0;\n bottom: 2px;\n overflow: hidden;\n }\n .waveform-canvas {\n display: block;\n position: absolute;\n top: 0;\n height: 100%;\n pointer-events: none;\n }\n `,\n ];\n\n canvasRef = createRef<HTMLCanvasElement>();\n\n /** Timeline state context for viewport info */\n @consume({ context: timelineStateContext, subscribe: true })\n @state()\n private _timelineState?: TimelineState;\n\n @state()\n private _waveformData: WaveformData | null = null;\n\n @state()\n private _isLoading = false;\n\n #lastSrc: string | null = null;\n #abortController: AbortController | null = null;\n #resizeObserver?: ResizeObserver;\n #renderRequested = false;\n #hostHeight = 0;\n\n /**\n * Load waveform data when the audio source changes\n */\n async #loadWaveformData(): Promise<void> {\n const audio = this.element as EFAudio;\n const src = audio?.src;\n\n // Skip if no source or same source already loaded\n if (!src || src === this.#lastSrc) {\n return;\n }\n\n this.#lastSrc = src;\n\n // Cancel any in-progress load\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n\n this._isLoading = true;\n\n try {\n const waveformData = await extractWaveformData(\n src,\n this.#abortController.signal,\n );\n\n if (waveformData) {\n this._waveformData = waveformData;\n this.#scheduleRender();\n }\n } catch (error) {\n if (!(error instanceof DOMException && error.name === \"AbortError\")) {\n console.warn(\"Failed to load waveform data:\", error);\n }\n } finally {\n this._isLoading = false;\n }\n }\n\n /**\n * Schedule a canvas render on the next animation frame\n */\n #scheduleRender(): void {\n if (this.#renderRequested) return;\n this.#renderRequested = true;\n\n requestAnimationFrame(() => {\n this.#renderRequested = false;\n this.#renderWaveform();\n });\n }\n\n /**\n * Get the track's position info relative to timeline scroll\n */\n #getTrackPositionInfo(): {\n trackStartPx: number;\n trackWidthPx: number;\n viewportScrollLeft: number;\n viewportWidth: number;\n pixelsPerMs: number;\n } | null {\n const audio = this.element as EFAudio;\n const durationMs = audio.durationMs ?? 0;\n if (durationMs === 0) return null;\n\n const pixelsPerMs = this._timelineState?.pixelsPerMs ?? this.pixelsPerMs;\n const trackWidthPx = durationMs * pixelsPerMs;\n\n // Get track's absolute position from startTimeMs\n const trackStartMs = audio.startTimeMs ?? 0;\n const trackStartPx = trackStartMs * pixelsPerMs;\n\n // Get viewport info from context\n const viewportScrollLeft = this._timelineState?.viewportScrollLeft ?? 0;\n const viewportWidth = this._timelineState?.viewportWidth ?? 800;\n\n return {\n trackStartPx,\n trackWidthPx,\n viewportScrollLeft,\n viewportWidth,\n pixelsPerMs,\n };\n }\n\n /**\n * Render the waveform to canvas with virtual rendering.\n *\n * The approach:\n * 1. Calculate the visible portion of the track (intersection of track and viewport)\n * 2. Position the canvas at that visible portion within the track\n * 3. Draw only the waveform data for that visible time range\n * 4. Update position and content as scroll/zoom changes\n */\n #renderWaveform(): void {\n const canvas = this.canvasRef.value;\n const waveformData = this._waveformData;\n\n if (!canvas || !waveformData) return;\n\n const positionInfo = this.#getTrackPositionInfo();\n if (!positionInfo) return;\n\n const {\n trackStartPx,\n trackWidthPx,\n viewportScrollLeft,\n viewportWidth,\n pixelsPerMs,\n } = positionInfo;\n\n // Calculate visible region in absolute pixels (with padding for smooth scrolling)\n const visibleLeftPx = viewportScrollLeft - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx =\n viewportScrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n\n // Track boundaries in absolute pixels\n const trackEndPx = trackStartPx + trackWidthPx;\n\n // Check if track is visible at all\n if (trackEndPx < visibleLeftPx || trackStartPx > visibleRightPx) {\n // Track not visible, hide canvas\n canvas.style.display = \"none\";\n return;\n }\n canvas.style.display = \"block\";\n\n // Calculate the intersection: what part of the track is visible\n // All coordinates are now relative to the track's left edge (0 = track start)\n const visibleStartInTrack = Math.max(0, visibleLeftPx - trackStartPx);\n const visibleEndInTrack = Math.min(\n trackWidthPx,\n visibleRightPx - trackStartPx,\n );\n const visibleWidthPx = visibleEndInTrack - visibleStartInTrack;\n\n if (visibleWidthPx <= 0) return;\n\n const height = this.#hostHeight || 18;\n\n // Set canvas size with DPR\n const dpr = window.devicePixelRatio || 1;\n const targetWidth = Math.ceil(visibleWidthPx * dpr);\n const targetHeight = Math.ceil(height * dpr);\n\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n }\n\n // Position canvas at the visible portion within the track\n canvas.style.left = `${visibleStartInTrack}px`;\n canvas.style.width = `${visibleWidthPx}px`;\n canvas.style.height = `${height}px`;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, visibleWidthPx, height);\n\n // Calculate what time range to render\n const audio = this.element as EFAudio;\n const sourceInMs = audio.sourceStartMs ?? 0;\n\n // Convert visible pixel range to time range\n const timeStartMs = sourceInMs + visibleStartInTrack / pixelsPerMs;\n const timeEndMs = sourceInMs + visibleEndInTrack / pixelsPerMs;\n\n // Draw the waveform for the visible portion\n this.#drawWaveformRegion(\n ctx,\n waveformData,\n 0, // Start drawing at x=0 of canvas (canvas is already positioned)\n visibleWidthPx,\n height,\n timeStartMs,\n timeEndMs,\n );\n }\n\n /**\n * Draw a region of the waveform to canvas\n */\n #drawWaveformRegion(\n ctx: CanvasRenderingContext2D,\n waveformData: WaveformData,\n x: number,\n width: number,\n height: number,\n startMs: number,\n endMs: number,\n ): void {\n const { peaks, samplesPerSecond } = waveformData;\n\n // Calculate sample range\n const startSample = Math.floor((startMs / 1000) * samplesPerSecond);\n const endSample = Math.ceil((endMs / 1000) * samplesPerSecond);\n const sampleCount = endSample - startSample;\n\n if (sampleCount <= 0 || width <= 0) return;\n\n const centerY = height / 2;\n const halfHeight = height / 2 - 2; // Leave 2px padding top/bottom\n const color = this.getElementTypeColor();\n\n ctx.fillStyle = color;\n ctx.globalAlpha = 0.8;\n ctx.beginPath();\n\n // Draw top half (max values) left to right\n const pixelsPerSample = width / sampleCount;\n\n for (let i = 0; i <= sampleCount; i++) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n\n // Clamp to valid range\n if (peakIndex + 1 >= peaks.length) break;\n\n const maxValue = peaks[peakIndex + 1] ?? 0;\n const px = x + i * pixelsPerSample;\n const py = centerY - maxValue * halfHeight;\n\n if (i === 0) {\n ctx.moveTo(px, py);\n } else {\n ctx.lineTo(px, py);\n }\n }\n\n // Draw bottom half (min values) right to left\n for (let i = sampleCount; i >= 0; i--) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n\n // Clamp to valid range\n if (peakIndex >= peaks.length) continue;\n\n const minValue = peaks[peakIndex] ?? 0;\n const px = x + i * pixelsPerSample;\n const py = centerY - minValue * halfHeight;\n\n ctx.lineTo(px, py);\n }\n\n ctx.closePath();\n ctx.fill();\n\n // Draw center line\n ctx.globalAlpha = 0.3;\n ctx.strokeStyle = color;\n ctx.lineWidth = 1;\n ctx.beginPath();\n ctx.moveTo(x, centerY);\n ctx.lineTo(x + width, centerY);\n ctx.stroke();\n\n ctx.globalAlpha = 1;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Start loading waveform data\n this.#loadWaveformData();\n\n // Observe size changes\n this.#resizeObserver = new ResizeObserver((entries) => {\n for (const entry of entries) {\n this.#hostHeight =\n entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;\n this.#scheduleRender();\n }\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#abortController?.abort();\n this.#resizeObserver?.disconnect();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>): void {\n super.updated(changedProperties);\n\n // Check if we need to reload waveform data\n const audio = this.element as EFAudio;\n if (audio?.src !== this.#lastSrc) {\n this.#loadWaveformData();\n }\n\n // Re-render when timeline state changes (scroll, zoom)\n if (changedProperties.has(\"_timelineState\")) {\n this.#scheduleRender();\n }\n\n // Attach resize observer to track container once rendered\n if (this.canvasRef.value && this.#resizeObserver) {\n const container = this.canvasRef.value.parentElement;\n if (container) {\n this.#resizeObserver.disconnect();\n this.#resizeObserver.observe(container);\n }\n }\n\n // Always schedule render after update to catch any changes\n this.#scheduleRender();\n }\n\n contents() {\n const audio = this.element as EFAudio;\n if (!(audio instanceof EFAudio)) {\n return nothing;\n }\n\n const durationMs = audio.durationMs ?? 0;\n if (durationMs === 0) {\n return nothing;\n }\n\n // Show loading placeholder if no waveform data yet\n if (!this._waveformData) {\n return this.#renderPlaceholder();\n }\n\n // The host fills the track container, canvas is positioned within it\n return html`\n <div class=\"waveform-host\">\n <canvas ${ref(this.canvasRef)} class=\"waveform-canvas\"></canvas>\n </div>\n `;\n }\n\n /**\n * Render placeholder while loading\n */\n #renderPlaceholder() {\n return html`\n <div\n style=\"\n position: absolute;\n left: 0;\n top: 2px;\n bottom: 2px;\n right: 0;\n background: linear-gradient(90deg, \n ${this.getElementTypeColor()}22 0%, \n ${this.getElementTypeColor()}44 50%,\n ${this.getElementTypeColor()}22 100%\n );\n background-size: 200% 100%;\n animation: ${this._isLoading ? \"shimmer 1.5s infinite\" : \"none\"};\n border-radius: 2px;\n \"\n ></div>\n <style>\n @keyframes shimmer {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n }\n </style>\n `;\n }\n}\n"],"mappings":"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"}
|