@editframe/elements 0.42.8 → 0.44.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/README.md +7 -26
- package/dist/elements/EFCaptions.d.ts +4 -4
- package/dist/elements/EFImage.d.ts +4 -4
- package/dist/elements/EFMedia.d.ts +2 -2
- package/dist/elements/EFText.d.ts +4 -4
- package/dist/elements/EFTextSegment.d.ts +4 -4
- package/dist/elements/EFWaveform.d.ts +4 -4
- package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
- package/dist/gui/EFConfiguration.d.ts +4 -4
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFFilmstrip.d.ts +4 -4
- 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/EFPlay.d.ts +4 -4
- package/dist/gui/EFPreview.d.ts +4 -4
- package/dist/gui/EFScrubber.d.ts +5 -4
- package/dist/gui/EFScrubber.js +35 -20
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimelineRuler.d.ts +4 -4
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFWorkbench.d.ts +4 -4
- package/dist/gui/PlaybackController.js +4 -2
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +2 -2
- package/dist/gui/timeline/EFTimeline.d.ts +2 -2
- package/dist/gui/timeline/TrimHandles.d.ts +4 -4
- package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +4 -4
- package/dist/gui/tree/EFTree.d.ts +4 -4
- package/dist/gui/tree/EFTreeItem.d.ts +4 -4
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,30 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @editframe/elements
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Web components SDK for building video compositions with Editframe.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- `src/task-system/Task.ts` - Core Task class with lifecycle management, hierarchy, and event handling
|
|
7
|
-
- `src/task-system/Task.test.ts` - Comprehensive unit tests for Task class
|
|
8
|
-
- `src/task-system/TaskManager.ts` - Core task orchestration with dependency tracking, resolution, and priority scheduling
|
|
9
|
-
- `src/task-system/TaskManager.test.ts` - Comprehensive unit tests for TaskManager with dependency scenarios
|
|
10
|
-
- `src/task-system/TaskRegistry.ts` - Global registry with automatic cleanup, memory management, and monitoring
|
|
11
|
-
- `src/task-system/TaskRegistry.test.ts` - Comprehensive unit tests for TaskRegistry covering all cleanup scenarios
|
|
12
|
-
- `src/task-system/index.ts` - Public API exports with convenience functions for common task patterns
|
|
13
|
-
- `src/integrations/lit-html-task-mixin.ts` - Lit-html component integration mixin
|
|
14
|
-
- `src/integrations/lit-html-task-mixin.test.ts` - Unit tests for lit-html integration
|
|
15
|
-
- `src/utils/task-debug.ts` - Developer debugging utilities and task graph visualization
|
|
16
|
-
- `src/utils/task-debug.test.ts` - Unit tests for debugging utilities
|
|
5
|
+
## Install
|
|
17
6
|
|
|
18
|
-
|
|
7
|
+
```sh
|
|
8
|
+
npm install @editframe/elements
|
|
9
|
+
```
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
- Use `npx tsx` to execute TypeScript files for testing
|
|
22
|
-
- The system should be domain-agnostic and reusable across different use cases
|
|
23
|
-
|
|
24
|
-
## Tasks
|
|
25
|
-
|
|
26
|
-
- [x] 1.0 Design and implement core task abstraction and lifecycle management
|
|
27
|
-
- [x] 2.0 Build hierarchical dependency tracking and resolution system
|
|
28
|
-
- [x] 3.0 Create task registry and cleanup mechanisms
|
|
29
|
-
- [ ] 4.0 Implement lit-html component integration layer
|
|
30
|
-
- [ ] 5.0 Add debugging and observability features
|
|
11
|
+
See the [documentation](https://editframe.com/docs) for usage.
|
|
@@ -5,9 +5,9 @@ import { FetchMixinInterface } from "./FetchMixin.js";
|
|
|
5
5
|
import { AsyncValue } from "./EFMedia.js";
|
|
6
6
|
import { EFAudio } from "./EFAudio.js";
|
|
7
7
|
import { EFVideo } from "./EFVideo.js";
|
|
8
|
-
import * as
|
|
8
|
+
import * as lit1 from "lit";
|
|
9
9
|
import { LitElement, PropertyValueMap } from "lit";
|
|
10
|
-
import * as
|
|
10
|
+
import * as lit_html0 from "lit-html";
|
|
11
11
|
|
|
12
12
|
//#region src/elements/EFCaptions.d.ts
|
|
13
13
|
interface WordSegment {
|
|
@@ -61,7 +61,7 @@ declare class EFCaptionsAfterActiveWord extends EFCaptionsSegment {
|
|
|
61
61
|
declare const EFCaptions_base: (new (...args: any[]) => EFSourceMixinInterface) & (new (...args: any[]) => TemporalMixinInterface) & (new (...args: any[]) => FetchMixinInterface) & typeof LitElement;
|
|
62
62
|
declare class EFCaptions extends EFCaptions_base implements FrameRenderable {
|
|
63
63
|
#private;
|
|
64
|
-
static styles:
|
|
64
|
+
static styles: lit1.CSSResult[];
|
|
65
65
|
targetSelector: string;
|
|
66
66
|
set target(value: string);
|
|
67
67
|
wordStyle: string;
|
|
@@ -84,7 +84,7 @@ declare class EFCaptions extends EFCaptions_base implements FrameRenderable {
|
|
|
84
84
|
segmentContainers: HTMLCollectionOf<EFCaptionsSegment>;
|
|
85
85
|
beforeActiveWordContainers: HTMLCollectionOf<EFCaptionsBeforeActiveWord>;
|
|
86
86
|
afterActiveWordContainers: HTMLCollectionOf<EFCaptionsAfterActiveWord>;
|
|
87
|
-
render():
|
|
87
|
+
render(): lit_html0.TemplateResult<1>;
|
|
88
88
|
transcriptionsPath(): string | null;
|
|
89
89
|
captionsPath(): string | null;
|
|
90
90
|
/**
|
|
@@ -2,16 +2,16 @@ import { FrameRenderable, FrameState } from "../preview/FrameController.js";
|
|
|
2
2
|
import { EFSourceMixinInterface } from "./EFSourceMixin.js";
|
|
3
3
|
import { TemporalMixinInterface } from "./EFTemporal.js";
|
|
4
4
|
import { FetchMixinInterface } from "./FetchMixin.js";
|
|
5
|
-
import * as
|
|
5
|
+
import * as lit2 from "lit";
|
|
6
6
|
import { LitElement, PropertyValueMap } from "lit";
|
|
7
|
-
import * as
|
|
7
|
+
import * as lit_html1 from "lit-html";
|
|
8
8
|
import * as lit_html_directives_ref_js0 from "lit-html/directives/ref.js";
|
|
9
9
|
|
|
10
10
|
//#region src/elements/EFImage.d.ts
|
|
11
11
|
declare const EFImage_base: (new (...args: any[]) => TemporalMixinInterface) & (new (...args: any[]) => EFSourceMixinInterface) & (new (...args: any[]) => FetchMixinInterface) & typeof LitElement;
|
|
12
12
|
declare class EFImage extends EFImage_base implements FrameRenderable {
|
|
13
13
|
#private;
|
|
14
|
-
static styles:
|
|
14
|
+
static styles: lit2.CSSResult[];
|
|
15
15
|
static get observedAttributes(): string[];
|
|
16
16
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
17
17
|
imageRef: lit_html_directives_ref_js0.Ref<HTMLImageElement>;
|
|
@@ -33,7 +33,7 @@ declare class EFImage extends EFImage_base implements FrameRenderable {
|
|
|
33
33
|
/** @deprecated Use fileId instead */
|
|
34
34
|
get assetId(): string | null;
|
|
35
35
|
set assetId(value: string | null);
|
|
36
|
-
render():
|
|
36
|
+
render(): lit_html1.TemplateResult<1>;
|
|
37
37
|
private isDirectUrl;
|
|
38
38
|
assetPath(): string;
|
|
39
39
|
get hasOwnDuration(): boolean;
|
|
@@ -6,7 +6,7 @@ import { ControllableInterface } from "../gui/Controllable.js";
|
|
|
6
6
|
import { UrlGenerator } from "../transcoding/utils/UrlGenerator.js";
|
|
7
7
|
import { MediaEngine } from "./EFMedia/MediaEngine.js";
|
|
8
8
|
import { AudioSpan } from "../transcoding/types/index.js";
|
|
9
|
-
import * as
|
|
9
|
+
import * as lit0 from "lit";
|
|
10
10
|
import { LitElement, PropertyValueMap } from "lit";
|
|
11
11
|
|
|
12
12
|
//#region src/elements/EFMedia.d.ts
|
|
@@ -59,7 +59,7 @@ declare class EFMedia extends EFMedia_base {
|
|
|
59
59
|
*/
|
|
60
60
|
get requiredTracks(): "audio" | "video" | "both";
|
|
61
61
|
static get observedAttributes(): string[];
|
|
62
|
-
static styles:
|
|
62
|
+
static styles: lit0.CSSResult[];
|
|
63
63
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
64
64
|
/**
|
|
65
65
|
* Duration in milliseconds for audio buffering ahead of current time
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { TemporalMixinInterface } from "./EFTemporal.js";
|
|
2
2
|
import { EFTextSegment } from "./EFTextSegment.js";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit4 from "lit";
|
|
4
4
|
import { LitElement, PropertyValueMap } from "lit";
|
|
5
|
-
import * as
|
|
5
|
+
import * as lit_html4 from "lit-html";
|
|
6
6
|
|
|
7
7
|
//#region src/elements/EFText.d.ts
|
|
8
8
|
type SplitMode = "line" | "word" | "char";
|
|
9
9
|
declare const EFText_base: (new (...args: any[]) => TemporalMixinInterface) & typeof LitElement;
|
|
10
10
|
declare class EFText extends EFText_base {
|
|
11
11
|
#private;
|
|
12
|
-
static styles:
|
|
12
|
+
static styles: lit4.CSSResult[];
|
|
13
13
|
split: SplitMode;
|
|
14
14
|
private validateSplit;
|
|
15
15
|
staggerMs?: number;
|
|
@@ -21,7 +21,7 @@ declare class EFText extends EFText_base {
|
|
|
21
21
|
private _textContent;
|
|
22
22
|
private _templateElement;
|
|
23
23
|
private _segmentsReadyResolvers;
|
|
24
|
-
render():
|
|
24
|
+
render(): lit_html4.TemplateResult<1>;
|
|
25
25
|
set textContent(value: string | null);
|
|
26
26
|
get textContent(): string;
|
|
27
27
|
/**
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { TemporalMixinInterface } from "./EFTemporal.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit5 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html5 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/elements/EFTextSegment.d.ts
|
|
7
7
|
declare const EFTextSegment_base: (new (...args: any[]) => TemporalMixinInterface) & typeof LitElement;
|
|
8
8
|
declare class EFTextSegment extends EFTextSegment_base {
|
|
9
|
-
static styles:
|
|
10
|
-
render():
|
|
9
|
+
static styles: lit5.CSSResult[];
|
|
10
|
+
render(): lit_html5.TemplateResult<1>;
|
|
11
11
|
private setCSSVariables;
|
|
12
12
|
protected firstUpdated(): void;
|
|
13
13
|
protected updated(): void;
|
|
@@ -3,16 +3,16 @@ import { TemporalMixinInterface } from "./EFTemporal.js";
|
|
|
3
3
|
import { EFAudio } from "./EFAudio.js";
|
|
4
4
|
import { EFVideo } from "./EFVideo.js";
|
|
5
5
|
import { TargetController } from "./TargetController.js";
|
|
6
|
-
import * as
|
|
6
|
+
import * as lit6 from "lit";
|
|
7
7
|
import { LitElement, PropertyValueMap } from "lit";
|
|
8
8
|
import { Ref } from "lit/directives/ref.js";
|
|
9
|
-
import * as
|
|
9
|
+
import * as lit_html6 from "lit-html";
|
|
10
10
|
|
|
11
11
|
//#region src/elements/EFWaveform.d.ts
|
|
12
12
|
declare const EFWaveform_base: (new (...args: any[]) => TemporalMixinInterface) & typeof LitElement;
|
|
13
13
|
declare class EFWaveform extends EFWaveform_base implements FrameRenderable {
|
|
14
14
|
#private;
|
|
15
|
-
static styles:
|
|
15
|
+
static styles: lit6.CSSResult;
|
|
16
16
|
canvasRef: Ref<HTMLCanvasElement>;
|
|
17
17
|
private ctx;
|
|
18
18
|
private styleObserver;
|
|
@@ -24,7 +24,7 @@ declare class EFWaveform extends EFWaveform_base implements FrameRenderable {
|
|
|
24
24
|
* @public
|
|
25
25
|
*/
|
|
26
26
|
get renderVersion(): number;
|
|
27
|
-
render():
|
|
27
|
+
render(): lit_html6.TemplateResult<1>;
|
|
28
28
|
mode: "roundBars" | "bars" | "bricks" | "line" | "curve" | "pixel" | "wave" | "spikes";
|
|
29
29
|
color: string;
|
|
30
30
|
target: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as lit21 from "lit";
|
|
2
2
|
import { LitElement } from "lit";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit_html20 from "lit-html";
|
|
4
4
|
|
|
5
5
|
//#region src/gui/EFActiveRootTemporal.d.ts
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ import * as lit_html21 from "lit-html";
|
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
declare class EFActiveRootTemporal extends LitElement {
|
|
17
|
-
static styles:
|
|
17
|
+
static styles: lit21.CSSResult;
|
|
18
18
|
/**
|
|
19
19
|
* Canvas element ID or selector to bind to.
|
|
20
20
|
* If not specified, will search for the nearest ef-canvas ancestor.
|
|
@@ -38,7 +38,7 @@ declare class EFActiveRootTemporal extends LitElement {
|
|
|
38
38
|
* Remove event listener.
|
|
39
39
|
*/
|
|
40
40
|
private removeListener;
|
|
41
|
-
render():
|
|
41
|
+
render(): lit_html20.TemplateResult<1>;
|
|
42
42
|
}
|
|
43
43
|
declare global {
|
|
44
44
|
interface HTMLElementTagNameMap {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as lit7 from "lit";
|
|
2
2
|
import { LitElement } from "lit";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit_html7 from "lit-html";
|
|
4
4
|
|
|
5
5
|
//#region src/gui/EFConfiguration.d.ts
|
|
6
6
|
declare class EFConfiguration extends LitElement {
|
|
7
|
-
static styles:
|
|
7
|
+
static styles: lit7.CSSResult[];
|
|
8
8
|
efConfiguration: this;
|
|
9
9
|
apiHost?: string;
|
|
10
10
|
signingURL: string;
|
|
11
|
-
render():
|
|
11
|
+
render(): lit_html7.TemplateResult<1>;
|
|
12
12
|
}
|
|
13
13
|
declare global {
|
|
14
14
|
interface HTMLElementTagNameMap {
|
package/dist/gui/EFDial.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as lit22 from "lit";
|
|
2
2
|
import { LitElement } from "lit";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit_html21 from "lit-html";
|
|
4
4
|
|
|
5
5
|
//#region src/gui/EFDial.d.ts
|
|
6
6
|
interface DialChangeDetail {
|
|
@@ -13,12 +13,12 @@ declare class EFDial extends LitElement {
|
|
|
13
13
|
private isDragging;
|
|
14
14
|
private dragStartAngle;
|
|
15
15
|
private dragStartValue;
|
|
16
|
-
static styles:
|
|
16
|
+
static styles: lit22.CSSResult;
|
|
17
17
|
private getAngleFromPoint;
|
|
18
18
|
private handlePointerDown;
|
|
19
19
|
private handlePointerMove;
|
|
20
20
|
private handlePointerUp;
|
|
21
|
-
render():
|
|
21
|
+
render(): lit_html21.TemplateResult<1>;
|
|
22
22
|
}
|
|
23
23
|
//#endregion
|
|
24
24
|
export { DialChangeDetail, EFDial };
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { TemporalMixinInterface } from "../elements/EFTemporal.js";
|
|
2
2
|
import "./timeline/EFTimeline.js";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit10 from "lit";
|
|
4
4
|
import { LitElement } from "lit";
|
|
5
|
-
import * as
|
|
5
|
+
import * as lit_html10 from "lit-html";
|
|
6
6
|
import * as lit_html_directives_ref0 from "lit-html/directives/ref";
|
|
7
7
|
|
|
8
8
|
//#region src/gui/EFFilmstrip.d.ts
|
|
9
9
|
declare const EFFilmstrip_base: typeof LitElement;
|
|
10
10
|
declare class EFFilmstrip extends EFFilmstrip_base {
|
|
11
11
|
#private;
|
|
12
|
-
static styles:
|
|
12
|
+
static styles: lit10.CSSResult[];
|
|
13
13
|
target: string;
|
|
14
14
|
pixelsPerMs: number;
|
|
15
15
|
hidePlayhead: boolean;
|
|
@@ -24,7 +24,7 @@ declare class EFFilmstrip extends EFFilmstrip_base {
|
|
|
24
24
|
timelineRef: lit_html_directives_ref0.Ref<HTMLElement>;
|
|
25
25
|
connectedCallback(): void;
|
|
26
26
|
protected willUpdate(changedProperties: Map<string | number | symbol, unknown>): void;
|
|
27
|
-
render():
|
|
27
|
+
render(): lit_html10.TemplateResult<1>;
|
|
28
28
|
}
|
|
29
29
|
declare global {
|
|
30
30
|
interface HTMLElementTagNameMap {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as lit32 from "lit";
|
|
2
2
|
import { LitElement } from "lit";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit_html30 from "lit-html";
|
|
4
4
|
|
|
5
5
|
//#region src/gui/EFOverlayItem.d.ts
|
|
6
6
|
/**
|
|
@@ -23,7 +23,7 @@ interface OverlayItemPosition {
|
|
|
23
23
|
* ensures transforms are applied before positions are read.
|
|
24
24
|
*/
|
|
25
25
|
declare class EFOverlayItem extends LitElement {
|
|
26
|
-
static styles:
|
|
26
|
+
static styles: lit32.CSSResult[];
|
|
27
27
|
elementId?: string;
|
|
28
28
|
target?: HTMLElement | string;
|
|
29
29
|
private currentPosition;
|
|
@@ -36,7 +36,7 @@ declare class EFOverlayItem extends LitElement {
|
|
|
36
36
|
updatePosition(): void;
|
|
37
37
|
connectedCallback(): void;
|
|
38
38
|
disconnectedCallback(): void;
|
|
39
|
-
render():
|
|
39
|
+
render(): lit_html30.TemplateResult<1>;
|
|
40
40
|
}
|
|
41
41
|
declare global {
|
|
42
42
|
interface HTMLElementTagNameMap {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { PanZoomTransform } from "../elements/EFPanZoom.js";
|
|
2
2
|
import { EFOverlayItem } from "./EFOverlayItem.js";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit31 from "lit";
|
|
4
4
|
import { LitElement } from "lit";
|
|
5
|
-
import * as
|
|
5
|
+
import * as lit_html29 from "lit-html";
|
|
6
6
|
|
|
7
7
|
//#region src/gui/EFOverlayLayer.d.ts
|
|
8
8
|
|
|
@@ -26,7 +26,7 @@ import * as lit_html30 from "lit-html";
|
|
|
26
26
|
* 2. EFOverlayItem can use this rect for coordinate calculations
|
|
27
27
|
*/
|
|
28
28
|
declare class EFOverlayLayer extends LitElement {
|
|
29
|
-
static styles:
|
|
29
|
+
static styles: lit31.CSSResult[];
|
|
30
30
|
panZoomTransformFromContext?: PanZoomTransform;
|
|
31
31
|
/**
|
|
32
32
|
* Pan/zoom transform as fallback for when context or sibling PanZoom is not available.
|
|
@@ -58,7 +58,7 @@ declare class EFOverlayLayer extends LitElement {
|
|
|
58
58
|
connectedCallback(): void;
|
|
59
59
|
disconnectedCallback(): void;
|
|
60
60
|
updated(): void;
|
|
61
|
-
render():
|
|
61
|
+
render(): lit_html29.TemplateResult<1>;
|
|
62
62
|
}
|
|
63
63
|
declare global {
|
|
64
64
|
interface HTMLElementTagNameMap {
|
package/dist/gui/EFPause.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ControllableInterface } from "./Controllable.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit17 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html16 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFPause.d.ts
|
|
7
7
|
declare const EFPause_base: (new (...args: any[]) => {
|
|
@@ -10,13 +10,13 @@ declare const EFPause_base: (new (...args: any[]) => {
|
|
|
10
10
|
effectiveContext: ControllableInterface | null;
|
|
11
11
|
}) & typeof LitElement;
|
|
12
12
|
declare class EFPause extends EFPause_base {
|
|
13
|
-
static styles:
|
|
13
|
+
static styles: lit17.CSSResult[];
|
|
14
14
|
playing: boolean;
|
|
15
15
|
get efContext(): ControllableInterface | null;
|
|
16
16
|
connectedCallback(): void;
|
|
17
17
|
disconnectedCallback(): void;
|
|
18
18
|
updated(changedProperties: Map<string | number | symbol, unknown>): void;
|
|
19
|
-
render():
|
|
19
|
+
render(): lit_html16.TemplateResult<1>;
|
|
20
20
|
handleClick: () => void;
|
|
21
21
|
}
|
|
22
22
|
declare global {
|
package/dist/gui/EFPlay.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ControllableInterface } from "./Controllable.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit16 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html15 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFPlay.d.ts
|
|
7
7
|
declare const EFPlay_base: (new (...args: any[]) => {
|
|
@@ -10,13 +10,13 @@ declare const EFPlay_base: (new (...args: any[]) => {
|
|
|
10
10
|
effectiveContext: ControllableInterface | null;
|
|
11
11
|
}) & typeof LitElement;
|
|
12
12
|
declare class EFPlay extends EFPlay_base {
|
|
13
|
-
static styles:
|
|
13
|
+
static styles: lit16.CSSResult[];
|
|
14
14
|
playing: boolean;
|
|
15
15
|
get efContext(): ControllableInterface | null;
|
|
16
16
|
connectedCallback(): void;
|
|
17
17
|
disconnectedCallback(): void;
|
|
18
18
|
updated(changedProperties: Map<string | number | symbol, unknown>): void;
|
|
19
|
-
render():
|
|
19
|
+
render(): lit_html15.TemplateResult<1>;
|
|
20
20
|
handleClick: () => void;
|
|
21
21
|
}
|
|
22
22
|
declare global {
|
package/dist/gui/EFPreview.d.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { ContextMixinInterface } from "./ContextMixin.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit9 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html9 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFPreview.d.ts
|
|
7
7
|
declare const EFPreview_base: (new (...args: any[]) => ContextMixinInterface) & typeof LitElement;
|
|
8
8
|
declare class EFPreview extends EFPreview_base {
|
|
9
|
-
static styles:
|
|
9
|
+
static styles: lit9.CSSResult[];
|
|
10
10
|
focusedElement?: HTMLElement;
|
|
11
11
|
/**
|
|
12
12
|
* Find the closest temporal element (timegroup, video, audio, etc.)
|
|
13
13
|
*/
|
|
14
14
|
private findClosestTemporal;
|
|
15
15
|
constructor();
|
|
16
|
-
render():
|
|
16
|
+
render(): lit_html9.TemplateResult<1>;
|
|
17
17
|
}
|
|
18
18
|
declare global {
|
|
19
19
|
interface HTMLElementTagNameMap {
|
package/dist/gui/EFScrubber.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ControllableInterface } from "./Controllable.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit19 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html18 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFScrubber.d.ts
|
|
7
7
|
declare const EFScrubber_base: (new (...args: any[]) => {
|
|
@@ -10,7 +10,7 @@ declare const EFScrubber_base: (new (...args: any[]) => {
|
|
|
10
10
|
effectiveContext: ControllableInterface | null;
|
|
11
11
|
}) & typeof LitElement;
|
|
12
12
|
declare class EFScrubber extends EFScrubber_base {
|
|
13
|
-
static styles:
|
|
13
|
+
static styles: lit19.CSSResult[];
|
|
14
14
|
playing: boolean;
|
|
15
15
|
contextCurrentTimeMs: number;
|
|
16
16
|
contextDurationMs: number;
|
|
@@ -44,13 +44,14 @@ declare class EFScrubber extends EFScrubber_base {
|
|
|
44
44
|
private scrubberRef;
|
|
45
45
|
private _scrubberElement?;
|
|
46
46
|
private capturedPointerId;
|
|
47
|
+
private _wasPlayingBeforeScrub;
|
|
47
48
|
private updateProgress;
|
|
48
49
|
private handlePointerDown;
|
|
49
50
|
private boundHandlePointerMove;
|
|
50
51
|
private boundHandlePointerUp;
|
|
51
52
|
private boundHandlePointerCancel;
|
|
52
53
|
private boundHandleContextMenu;
|
|
53
|
-
render():
|
|
54
|
+
render(): lit_html18.TemplateResult<1>;
|
|
54
55
|
connectedCallback(): void;
|
|
55
56
|
disconnectedCallback(): void;
|
|
56
57
|
}
|
package/dist/gui/EFScrubber.js
CHANGED
|
@@ -7,7 +7,7 @@ import { quantizeToFrameTimeMs } from "./EFTimelineRuler.js";
|
|
|
7
7
|
import { TargetOrContextMixin } from "./TargetOrContextMixin.js";
|
|
8
8
|
import { consume } from "@lit/context";
|
|
9
9
|
import { LitElement, css, html } from "lit";
|
|
10
|
-
import { customElement,
|
|
10
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
11
11
|
import { createRef, ref } from "lit/directives/ref.js";
|
|
12
12
|
|
|
13
13
|
//#region src/gui/EFScrubber.ts
|
|
@@ -36,6 +36,26 @@ let EFScrubber = class EFScrubber$1 extends TargetOrContextMixin(LitElement, efC
|
|
|
36
36
|
this.isMoving = false;
|
|
37
37
|
this.scrubberRef = createRef();
|
|
38
38
|
this.capturedPointerId = null;
|
|
39
|
+
this._wasPlayingBeforeScrub = false;
|
|
40
|
+
this.handlePointerDown = (e) => {
|
|
41
|
+
const scrubberEl = this.scrubberRef.value || this._scrubberElement;
|
|
42
|
+
if (!scrubberEl) return;
|
|
43
|
+
this.isMoving = true;
|
|
44
|
+
if (this.isScrubbingRef) this.isScrubbingRef.current = true;
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
e.stopPropagation();
|
|
47
|
+
this.capturedPointerId = e.pointerId;
|
|
48
|
+
try {
|
|
49
|
+
scrubberEl.setPointerCapture(e.pointerId);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.warn("Failed to set pointer capture:", err);
|
|
52
|
+
}
|
|
53
|
+
if (this.playing && this.context) {
|
|
54
|
+
this._wasPlayingBeforeScrub = true;
|
|
55
|
+
this.context.pause();
|
|
56
|
+
}
|
|
57
|
+
this.updateProgress(e);
|
|
58
|
+
};
|
|
39
59
|
this.boundHandlePointerMove = (e) => {
|
|
40
60
|
if (this.isMoving && e.pointerId === this.capturedPointerId) {
|
|
41
61
|
e.preventDefault();
|
|
@@ -54,6 +74,10 @@ let EFScrubber = class EFScrubber$1 extends TargetOrContextMixin(LitElement, efC
|
|
|
54
74
|
this.capturedPointerId = null;
|
|
55
75
|
this.isMoving = false;
|
|
56
76
|
if (this.isScrubbingRef) this.isScrubbingRef.current = false;
|
|
77
|
+
if (this._wasPlayingBeforeScrub) {
|
|
78
|
+
this._wasPlayingBeforeScrub = false;
|
|
79
|
+
this.context?.play();
|
|
80
|
+
}
|
|
57
81
|
}
|
|
58
82
|
};
|
|
59
83
|
this.boundHandlePointerCancel = (e) => {
|
|
@@ -65,6 +89,10 @@ let EFScrubber = class EFScrubber$1 extends TargetOrContextMixin(LitElement, efC
|
|
|
65
89
|
this.capturedPointerId = null;
|
|
66
90
|
this.isMoving = false;
|
|
67
91
|
if (this.isScrubbingRef) this.isScrubbingRef.current = false;
|
|
92
|
+
if (this._wasPlayingBeforeScrub) {
|
|
93
|
+
this._wasPlayingBeforeScrub = false;
|
|
94
|
+
this.context?.play();
|
|
95
|
+
}
|
|
68
96
|
}
|
|
69
97
|
};
|
|
70
98
|
this.boundHandleContextMenu = (e) => {
|
|
@@ -241,21 +269,6 @@ let EFScrubber = class EFScrubber$1 extends TargetOrContextMixin(LitElement, efC
|
|
|
241
269
|
}
|
|
242
270
|
}
|
|
243
271
|
}
|
|
244
|
-
handlePointerDown(e) {
|
|
245
|
-
const scrubberEl = this.scrubberRef.value || this._scrubberElement;
|
|
246
|
-
if (!scrubberEl) return;
|
|
247
|
-
this.isMoving = true;
|
|
248
|
-
if (this.isScrubbingRef) this.isScrubbingRef.current = true;
|
|
249
|
-
e.preventDefault();
|
|
250
|
-
e.stopPropagation();
|
|
251
|
-
this.capturedPointerId = e.pointerId;
|
|
252
|
-
try {
|
|
253
|
-
scrubberEl.setPointerCapture(e.pointerId);
|
|
254
|
-
} catch (err) {
|
|
255
|
-
console.warn("Failed to set pointer capture:", err);
|
|
256
|
-
}
|
|
257
|
-
this.updateProgress(e);
|
|
258
|
-
}
|
|
259
272
|
render() {
|
|
260
273
|
const duration = this.effectiveDurationMs;
|
|
261
274
|
const currentTime = this.effectiveCurrentTimeMs;
|
|
@@ -310,6 +323,7 @@ let EFScrubber = class EFScrubber$1 extends TargetOrContextMixin(LitElement, efC
|
|
|
310
323
|
window.addEventListener("pointermove", this.boundHandlePointerMove, { passive: false });
|
|
311
324
|
window.addEventListener("pointercancel", this.boundHandlePointerCancel, { passive: false });
|
|
312
325
|
this.addEventListener("contextmenu", this.boundHandleContextMenu, { passive: false });
|
|
326
|
+
this.addEventListener("pointerdown", this.handlePointerDown, { passive: false });
|
|
313
327
|
}
|
|
314
328
|
disconnectedCallback() {
|
|
315
329
|
super.disconnectedCallback();
|
|
@@ -317,6 +331,11 @@ let EFScrubber = class EFScrubber$1 extends TargetOrContextMixin(LitElement, efC
|
|
|
317
331
|
window.removeEventListener("pointermove", this.boundHandlePointerMove);
|
|
318
332
|
window.removeEventListener("pointercancel", this.boundHandlePointerCancel);
|
|
319
333
|
this.removeEventListener("contextmenu", this.boundHandleContextMenu);
|
|
334
|
+
this.removeEventListener("pointerdown", this.handlePointerDown);
|
|
335
|
+
if (this._wasPlayingBeforeScrub) {
|
|
336
|
+
this._wasPlayingBeforeScrub = false;
|
|
337
|
+
if (this.context?.isConnected) this.context.play();
|
|
338
|
+
}
|
|
320
339
|
}
|
|
321
340
|
};
|
|
322
341
|
__decorate([consume({
|
|
@@ -365,10 +384,6 @@ __decorate([property({ attribute: false })], EFScrubber.prototype, "onSeek", voi
|
|
|
365
384
|
__decorate([property({ attribute: false })], EFScrubber.prototype, "isScrubbingRef", void 0);
|
|
366
385
|
__decorate([state()], EFScrubber.prototype, "scrubProgress", void 0);
|
|
367
386
|
__decorate([state()], EFScrubber.prototype, "isMoving", void 0);
|
|
368
|
-
__decorate([eventOptions({
|
|
369
|
-
passive: false,
|
|
370
|
-
capture: false
|
|
371
|
-
})], EFScrubber.prototype, "handlePointerDown", null);
|
|
372
387
|
EFScrubber = __decorate([customElement("ef-scrubber")], EFScrubber);
|
|
373
388
|
|
|
374
389
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFScrubber.js","names":["EFScrubber"],"sources":["../../src/gui/EFScrubber.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement } from \"lit\";\nimport {\n customElement,\n eventOptions,\n property,\n state,\n} from \"lit/decorators.js\";\n\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport type { ControllableInterface } from \"./Controllable.js\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport { efContext } from \"./efContext.js\";\nimport { playingContext } from \"./playingContext.js\";\nimport { TargetOrContextMixin } from \"./TargetOrContextMixin.js\";\nimport { quantizeToFrameTimeMs } from \"./EFTimelineRuler.js\";\n\nconst BASE_PIXELS_PER_SECOND = 100;\n\nfunction timeToPixels(\n timeMs: number,\n durationMs: number,\n _containerWidth: number,\n zoomScale: number,\n): number {\n if (durationMs <= 0) return 0;\n const pixelsPerSecond = BASE_PIXELS_PER_SECOND * zoomScale;\n return (timeMs / 1000) * pixelsPerSecond;\n}\n\nfunction pixelsToTime(\n pixels: number,\n durationMs: number,\n _containerWidth: number,\n zoomScale: number,\n): number {\n if (durationMs <= 0) return 0;\n const pixelsPerSecond = BASE_PIXELS_PER_SECOND * zoomScale;\n return (pixels / pixelsPerSecond) * 1000;\n}\n\n@customElement(\"ef-scrubber\")\nexport class EFScrubber extends TargetOrContextMixin(LitElement, efContext) {\n static styles = [\n css`\n :host {\n --ef-scrubber-height: 4px;\n --ef-scrubber-background: var(--ef-color-border, rgba(255, 255, 255, 0.2));\n --ef-scrubber-progress-color: var(--ef-color-primary, #fff);\n --ef-scrubber-handle-size: 12px;\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n \n :host([orientation=\"vertical\"]) {\n width: 100%;\n height: 100%;\n position: absolute;\n inset: 0;\n pointer-events: auto;\n }\n\n .scrubber {\n width: 100%;\n height: var(--ef-scrubber-height);\n background: var(--ef-scrubber-background);\n position: relative;\n cursor: pointer;\n border-radius: 2px;\n touch-action: none;\n user-select: none;\n }\n\n :host([orientation=\"vertical\"]) .scrubber {\n width: 100%;\n height: 100%;\n background: transparent;\n cursor: ew-resize;\n }\n\n .progress {\n position: absolute;\n height: 100%;\n background: var(--ef-scrubber-progress-color);\n border-radius: 2px;\n }\n\n :host([orientation=\"vertical\"]) .progress {\n display: none;\n }\n\n .handle {\n position: absolute;\n width: var(--ef-scrubber-handle-size);\n height: var(--ef-scrubber-handle-size);\n background: var(--ef-scrubber-progress-color);\n border-radius: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n cursor: grab;\n }\n\n :host([orientation=\"vertical\"]) .handle {\n display: none;\n }\n\n .playhead {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 2px;\n background: var(--ef-scrubber-progress-color);\n pointer-events: auto;\n cursor: ew-resize;\n z-index: 30;\n }\n\n ::part(playhead) {\n z-index: 30;\n }\n\n .playhead-handle {\n position: absolute;\n top: 0;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 12px;\n height: 12px;\n background: var(--ef-scrubber-progress-color);\n border-radius: 50%;\n }\n\n .raw-preview {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 2px;\n background: color-mix(in srgb, var(--ef-color-primary) 20%, transparent);\n pointer-events: none;\n z-index: 20;\n }\n\n /* Add CSS Shadow Parts */\n ::part(scrubber) { }\n ::part(progress) { }\n ::part(handle) { }\n `,\n ];\n\n @consume({ context: playingContext, subscribe: true })\n playing = false;\n\n @consume({ context: currentTimeContext, subscribe: true })\n contextCurrentTimeMs = Number.NaN;\n\n @consume({ context: durationContext, subscribe: true })\n contextDurationMs = 0;\n\n @property({ type: String, attribute: \"orientation\" })\n orientation: \"horizontal\" | \"vertical\" = \"horizontal\";\n\n @property({ type: Number, attribute: \"current-time-ms\" })\n currentTimeMs = Number.NaN;\n\n @property({ type: Number, attribute: \"duration-ms\" })\n durationMs = 0;\n\n @property({ type: Number, attribute: \"zoom-scale\" })\n zoomScale = 1.0;\n\n @property({ type: Number, attribute: \"container-width\" })\n containerWidth = 0;\n\n @property({ type: Number, attribute: \"fps\" })\n fps?: number;\n\n @property({ type: Number, attribute: \"raw-scrub-time-ms\" })\n rawScrubTimeMs?: number | null;\n\n @property({ attribute: false })\n scrollContainerRef?: { current: HTMLElement | null };\n\n /**\n * Reference to the element that represents the actual track content area.\n * Used to calculate the offset between the scroll container and where tracks begin.\n */\n @property({ attribute: false })\n trackContentRef?: { current: HTMLElement | null };\n\n @property({ attribute: false })\n onSeek?: (time: number) => void;\n\n @property({ attribute: false })\n isScrubbingRef?: { current: boolean };\n\n get context(): ControllableInterface | null {\n return this.effectiveContext;\n }\n\n get effectiveCurrentTimeMs(): number {\n if (!Number.isNaN(this.currentTimeMs)) {\n return this.currentTimeMs;\n }\n if (!Number.isNaN(this.contextCurrentTimeMs)) {\n return this.contextCurrentTimeMs;\n }\n return 0;\n }\n\n get effectiveDurationMs(): number {\n return this.durationMs || this.contextDurationMs || 0;\n }\n\n get isTimelineMode(): boolean {\n return this.orientation === \"vertical\" && this.zoomScale > 0;\n }\n\n @state()\n private scrubProgress = 0;\n\n @state()\n private isMoving = false;\n\n private scrubberRef = createRef<HTMLElement>();\n private _scrubberElement?: HTMLElement;\n private capturedPointerId: number | null = null;\n\n private updateProgress(e: PointerEvent) {\n const scrubberEl = this.scrubberRef.value || this._scrubberElement;\n if (!scrubberEl) return;\n\n const duration = this.effectiveDurationMs;\n if (duration <= 0) return;\n\n if (this.isTimelineMode) {\n // Timeline mode: use pixel-based positioning with zoom\n const scrollContainer =\n this.scrollContainerRef?.current || scrubberEl.parentElement;\n if (!scrollContainer) return;\n\n const scrollContainerRect = scrollContainer.getBoundingClientRect();\n const scrollLeft = scrollContainer.scrollLeft || 0;\n\n // Calculate pixel offset dynamically from the track content element\n // This accounts for any hierarchy panel or other elements before the tracks\n let pixelOffset = 0;\n if (this.trackContentRef?.current) {\n const trackRect = this.trackContentRef.current.getBoundingClientRect();\n pixelOffset =\n trackRect.left -\n scrollContainerRect.left +\n scrollContainer.scrollLeft;\n }\n\n const x = e.clientX - scrollContainerRect.left - pixelOffset;\n const pixelPosition = scrollLeft + x;\n const effectiveWidth =\n this.containerWidth > 0\n ? this.containerWidth\n : scrollContainerRect.width;\n if (effectiveWidth <= 0) return;\n\n let rawTime = pixelsToTime(\n pixelPosition,\n duration,\n effectiveWidth,\n this.zoomScale,\n );\n rawTime = Math.max(0, Math.min(rawTime, duration));\n\n // Quantize to frame boundaries if FPS is provided, then clamp to duration\n let quantizedTime =\n this.fps && this.fps > 0\n ? quantizeToFrameTimeMs(rawTime, this.fps)\n : rawTime;\n quantizedTime = Math.max(0, Math.min(quantizedTime, duration));\n\n this.scrubProgress = quantizedTime / duration;\n\n if (this.onSeek) {\n this.onSeek(quantizedTime);\n } else {\n // Emit seek event for event listeners\n this.dispatchEvent(\n new CustomEvent(\"seek\", {\n detail: quantizedTime,\n bubbles: true,\n composed: true,\n }),\n );\n if (this.context) {\n this.context.currentTimeMs = quantizedTime;\n }\n }\n } else {\n // Horizontal mode: simple progress calculation\n const rect = scrubberEl.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const progress = Math.max(0, Math.min(1, x / rect.width));\n\n this.scrubProgress = progress;\n const timeMs = progress * duration;\n\n if (this.onSeek) {\n this.onSeek(timeMs);\n } else {\n // Emit seek event for event listeners\n this.dispatchEvent(\n new CustomEvent(\"seek\", {\n detail: timeMs,\n bubbles: true,\n composed: true,\n }),\n );\n if (this.context) {\n this.context.currentTimeMs = timeMs;\n }\n }\n }\n }\n\n @eventOptions({ passive: false, capture: false })\n private handlePointerDown(e: PointerEvent) {\n const scrubberEl = this.scrubberRef.value || this._scrubberElement;\n if (!scrubberEl) return;\n\n this.isMoving = true;\n if (this.isScrubbingRef) {\n this.isScrubbingRef.current = true;\n }\n e.preventDefault();\n e.stopPropagation();\n this.capturedPointerId = e.pointerId;\n try {\n scrubberEl.setPointerCapture(e.pointerId);\n } catch (err) {\n // setPointerCapture may fail in some cases, continue anyway\n console.warn(\"Failed to set pointer capture:\", err);\n }\n this.updateProgress(e);\n }\n\n private boundHandlePointerMove = (e: PointerEvent) => {\n if (this.isMoving && e.pointerId === this.capturedPointerId) {\n e.preventDefault();\n e.stopPropagation();\n this.updateProgress(e);\n }\n };\n\n private boundHandlePointerUp = (e: PointerEvent) => {\n const scrubberEl = this.scrubberRef.value || this._scrubberElement;\n if (e.pointerId === this.capturedPointerId && scrubberEl) {\n e.preventDefault();\n e.stopPropagation();\n try {\n scrubberEl.releasePointerCapture(e.pointerId);\n } catch (_err) {\n // releasePointerCapture may fail if capture was already lost\n }\n this.capturedPointerId = null;\n this.isMoving = false;\n if (this.isScrubbingRef) {\n this.isScrubbingRef.current = false;\n }\n }\n };\n\n private boundHandlePointerCancel = (e: PointerEvent) => {\n const scrubberEl = this.scrubberRef.value || this._scrubberElement;\n if (e.pointerId === this.capturedPointerId && scrubberEl) {\n try {\n scrubberEl.releasePointerCapture(e.pointerId);\n } catch (_err) {\n // releasePointerCapture may fail if capture was already lost\n }\n this.capturedPointerId = null;\n this.isMoving = false;\n if (this.isScrubbingRef) {\n this.isScrubbingRef.current = false;\n }\n }\n };\n\n private boundHandleContextMenu = (e: Event) => {\n if (this.isMoving) {\n e.preventDefault();\n e.stopPropagation();\n }\n };\n\n render() {\n const duration = this.effectiveDurationMs;\n const currentTime = this.effectiveCurrentTimeMs;\n\n if (duration <= 0) {\n return html``;\n }\n\n if (this.isTimelineMode) {\n // Vertical timeline mode: render playhead line\n const scrubberEl = this.scrubberRef.value || this._scrubberElement;\n const effectiveWidth =\n this.containerWidth > 0\n ? this.containerWidth\n : scrubberEl?.parentElement?.getBoundingClientRect().width || 0;\n\n const positionPixels =\n effectiveWidth > 0\n ? timeToPixels(currentTime, duration, effectiveWidth, this.zoomScale)\n : 0;\n\n const rawScrubPositionPixels =\n this.rawScrubTimeMs !== null &&\n this.rawScrubTimeMs !== undefined &&\n effectiveWidth > 0\n ? timeToPixels(\n this.rawScrubTimeMs,\n duration,\n effectiveWidth,\n this.zoomScale,\n )\n : null;\n\n return html`\n ${\n rawScrubPositionPixels !== null &&\n rawScrubPositionPixels !== positionPixels\n ? html`<div\n class=\"raw-preview\"\n style=\"left: ${rawScrubPositionPixels}px\"\n ></div>`\n : html``\n }\n <div\n ${ref(this.scrubberRef)}\n part=\"scrubber\"\n class=\"scrubber\"\n @pointerdown=${this.handlePointerDown}\n @contextmenu=${this.boundHandleContextMenu}\n >\n <div\n part=\"playhead\"\n class=\"playhead\"\n style=\"left: ${positionPixels}px\"\n @pointerdown=${this.handlePointerDown}\n >\n <div class=\"playhead-handle\"></div>\n </div>\n </div>\n `;\n } else {\n // Horizontal mode: render progress bar\n const currentProgress = duration > 0 ? currentTime / duration : 0;\n const displayProgress = this.isMoving\n ? this.scrubProgress\n : currentProgress;\n\n return html`\n <div\n ${ref(this.scrubberRef)}\n part=\"scrubber\"\n class=\"scrubber\"\n @pointerdown=${this.handlePointerDown}\n @contextmenu=${this.boundHandleContextMenu}\n >\n <div part=\"progress\" class=\"progress\" style=\"width: ${displayProgress * 100}%\"></div>\n <div part=\"handle\" class=\"handle\" style=\"left: ${displayProgress * 100}%\"></div>\n </div>\n `;\n }\n }\n\n connectedCallback() {\n super.connectedCallback();\n window.addEventListener(\n \"pointerup\",\n this.boundHandlePointerUp as EventListener,\n { passive: false },\n );\n window.addEventListener(\"pointermove\", this.boundHandlePointerMove, {\n passive: false,\n });\n window.addEventListener(\n \"pointercancel\",\n this.boundHandlePointerCancel as EventListener,\n { passive: false },\n );\n this.addEventListener(\"contextmenu\", this.boundHandleContextMenu, {\n passive: false,\n });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n window.removeEventListener(\n \"pointerup\",\n this.boundHandlePointerUp as EventListener,\n );\n window.removeEventListener(\"pointermove\", this.boundHandlePointerMove);\n window.removeEventListener(\n \"pointercancel\",\n this.boundHandlePointerCancel as EventListener,\n );\n this.removeEventListener(\"contextmenu\", this.boundHandleContextMenu);\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-scrubber\": EFScrubber;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EFScrubber.js","names":["EFScrubber"],"sources":["../../src/gui/EFScrubber.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\n\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport type { ControllableInterface } from \"./Controllable.js\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport { efContext } from \"./efContext.js\";\nimport { playingContext } from \"./playingContext.js\";\nimport { TargetOrContextMixin } from \"./TargetOrContextMixin.js\";\nimport { quantizeToFrameTimeMs } from \"./EFTimelineRuler.js\";\n\nconst BASE_PIXELS_PER_SECOND = 100;\n\nfunction timeToPixels(\n timeMs: number,\n durationMs: number,\n _containerWidth: number,\n zoomScale: number,\n): number {\n if (durationMs <= 0) return 0;\n const pixelsPerSecond = BASE_PIXELS_PER_SECOND * zoomScale;\n return (timeMs / 1000) * pixelsPerSecond;\n}\n\nfunction pixelsToTime(\n pixels: number,\n durationMs: number,\n _containerWidth: number,\n zoomScale: number,\n): number {\n if (durationMs <= 0) return 0;\n const pixelsPerSecond = BASE_PIXELS_PER_SECOND * zoomScale;\n return (pixels / pixelsPerSecond) * 1000;\n}\n\n@customElement(\"ef-scrubber\")\nexport class EFScrubber extends TargetOrContextMixin(LitElement, efContext) {\n static styles = [\n css`\n :host {\n --ef-scrubber-height: 4px;\n --ef-scrubber-background: var(--ef-color-border, rgba(255, 255, 255, 0.2));\n --ef-scrubber-progress-color: var(--ef-color-primary, #fff);\n --ef-scrubber-handle-size: 12px;\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n \n :host([orientation=\"vertical\"]) {\n width: 100%;\n height: 100%;\n position: absolute;\n inset: 0;\n pointer-events: auto;\n }\n\n .scrubber {\n width: 100%;\n height: var(--ef-scrubber-height);\n background: var(--ef-scrubber-background);\n position: relative;\n cursor: pointer;\n border-radius: 2px;\n touch-action: none;\n user-select: none;\n }\n\n :host([orientation=\"vertical\"]) .scrubber {\n width: 100%;\n height: 100%;\n background: transparent;\n cursor: ew-resize;\n }\n\n .progress {\n position: absolute;\n height: 100%;\n background: var(--ef-scrubber-progress-color);\n border-radius: 2px;\n }\n\n :host([orientation=\"vertical\"]) .progress {\n display: none;\n }\n\n .handle {\n position: absolute;\n width: var(--ef-scrubber-handle-size);\n height: var(--ef-scrubber-handle-size);\n background: var(--ef-scrubber-progress-color);\n border-radius: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n cursor: grab;\n }\n\n :host([orientation=\"vertical\"]) .handle {\n display: none;\n }\n\n .playhead {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 2px;\n background: var(--ef-scrubber-progress-color);\n pointer-events: auto;\n cursor: ew-resize;\n z-index: 30;\n }\n\n ::part(playhead) {\n z-index: 30;\n }\n\n .playhead-handle {\n position: absolute;\n top: 0;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 12px;\n height: 12px;\n background: var(--ef-scrubber-progress-color);\n border-radius: 50%;\n }\n\n .raw-preview {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 2px;\n background: color-mix(in srgb, var(--ef-color-primary) 20%, transparent);\n pointer-events: none;\n z-index: 20;\n }\n\n /* Add CSS Shadow Parts */\n ::part(scrubber) { }\n ::part(progress) { }\n ::part(handle) { }\n `,\n ];\n\n @consume({ context: playingContext, subscribe: true })\n playing = false;\n\n @consume({ context: currentTimeContext, subscribe: true })\n contextCurrentTimeMs = Number.NaN;\n\n @consume({ context: durationContext, subscribe: true })\n contextDurationMs = 0;\n\n @property({ type: String, attribute: \"orientation\" })\n orientation: \"horizontal\" | \"vertical\" = \"horizontal\";\n\n @property({ type: Number, attribute: \"current-time-ms\" })\n currentTimeMs = Number.NaN;\n\n @property({ type: Number, attribute: \"duration-ms\" })\n durationMs = 0;\n\n @property({ type: Number, attribute: \"zoom-scale\" })\n zoomScale = 1.0;\n\n @property({ type: Number, attribute: \"container-width\" })\n containerWidth = 0;\n\n @property({ type: Number, attribute: \"fps\" })\n fps?: number;\n\n @property({ type: Number, attribute: \"raw-scrub-time-ms\" })\n rawScrubTimeMs?: number | null;\n\n @property({ attribute: false })\n scrollContainerRef?: { current: HTMLElement | null };\n\n /**\n * Reference to the element that represents the actual track content area.\n * Used to calculate the offset between the scroll container and where tracks begin.\n */\n @property({ attribute: false })\n trackContentRef?: { current: HTMLElement | null };\n\n @property({ attribute: false })\n onSeek?: (time: number) => void;\n\n @property({ attribute: false })\n isScrubbingRef?: { current: boolean };\n\n get context(): ControllableInterface | null {\n return this.effectiveContext;\n }\n\n get effectiveCurrentTimeMs(): number {\n if (!Number.isNaN(this.currentTimeMs)) {\n return this.currentTimeMs;\n }\n if (!Number.isNaN(this.contextCurrentTimeMs)) {\n return this.contextCurrentTimeMs;\n }\n return 0;\n }\n\n get effectiveDurationMs(): number {\n return this.durationMs || this.contextDurationMs || 0;\n }\n\n get isTimelineMode(): boolean {\n return this.orientation === \"vertical\" && this.zoomScale > 0;\n }\n\n @state()\n private scrubProgress = 0;\n\n @state()\n private isMoving = false;\n\n private scrubberRef = createRef<HTMLElement>();\n private _scrubberElement?: HTMLElement;\n private capturedPointerId: number | null = null;\n private _wasPlayingBeforeScrub = false;\n\n private updateProgress(e: PointerEvent) {\n const scrubberEl = this.scrubberRef.value || this._scrubberElement;\n if (!scrubberEl) return;\n\n const duration = this.effectiveDurationMs;\n if (duration <= 0) return;\n\n if (this.isTimelineMode) {\n // Timeline mode: use pixel-based positioning with zoom\n const scrollContainer =\n this.scrollContainerRef?.current || scrubberEl.parentElement;\n if (!scrollContainer) return;\n\n const scrollContainerRect = scrollContainer.getBoundingClientRect();\n const scrollLeft = scrollContainer.scrollLeft || 0;\n\n // Calculate pixel offset dynamically from the track content element\n // This accounts for any hierarchy panel or other elements before the tracks\n let pixelOffset = 0;\n if (this.trackContentRef?.current) {\n const trackRect = this.trackContentRef.current.getBoundingClientRect();\n pixelOffset =\n trackRect.left -\n scrollContainerRect.left +\n scrollContainer.scrollLeft;\n }\n\n const x = e.clientX - scrollContainerRect.left - pixelOffset;\n const pixelPosition = scrollLeft + x;\n const effectiveWidth =\n this.containerWidth > 0\n ? this.containerWidth\n : scrollContainerRect.width;\n if (effectiveWidth <= 0) return;\n\n let rawTime = pixelsToTime(\n pixelPosition,\n duration,\n effectiveWidth,\n this.zoomScale,\n );\n rawTime = Math.max(0, Math.min(rawTime, duration));\n\n // Quantize to frame boundaries if FPS is provided, then clamp to duration\n let quantizedTime =\n this.fps && this.fps > 0\n ? quantizeToFrameTimeMs(rawTime, this.fps)\n : rawTime;\n quantizedTime = Math.max(0, Math.min(quantizedTime, duration));\n\n this.scrubProgress = quantizedTime / duration;\n\n if (this.onSeek) {\n this.onSeek(quantizedTime);\n } else {\n // Emit seek event for event listeners\n this.dispatchEvent(\n new CustomEvent(\"seek\", {\n detail: quantizedTime,\n bubbles: true,\n composed: true,\n }),\n );\n if (this.context) {\n this.context.currentTimeMs = quantizedTime;\n }\n }\n } else {\n // Horizontal mode: simple progress calculation\n const rect = scrubberEl.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const progress = Math.max(0, Math.min(1, x / rect.width));\n\n this.scrubProgress = progress;\n const timeMs = progress * duration;\n\n if (this.onSeek) {\n this.onSeek(timeMs);\n } else {\n // Emit seek event for event listeners\n this.dispatchEvent(\n new CustomEvent(\"seek\", {\n detail: timeMs,\n bubbles: true,\n composed: true,\n }),\n );\n if (this.context) {\n this.context.currentTimeMs = timeMs;\n }\n }\n }\n }\n\n private handlePointerDown = (e: PointerEvent) => {\n const scrubberEl = this.scrubberRef.value || this._scrubberElement;\n if (!scrubberEl) return;\n\n this.isMoving = true;\n if (this.isScrubbingRef) {\n this.isScrubbingRef.current = true;\n }\n e.preventDefault();\n e.stopPropagation();\n this.capturedPointerId = e.pointerId;\n try {\n scrubberEl.setPointerCapture(e.pointerId);\n } catch (err) {\n // setPointerCapture may fail in some cases, continue anyway\n console.warn(\"Failed to set pointer capture:\", err);\n }\n\n if (this.playing && this.context) {\n this._wasPlayingBeforeScrub = true;\n this.context.pause();\n }\n\n this.updateProgress(e);\n };\n\n private boundHandlePointerMove = (e: PointerEvent) => {\n if (this.isMoving && e.pointerId === this.capturedPointerId) {\n e.preventDefault();\n e.stopPropagation();\n this.updateProgress(e);\n }\n };\n\n private boundHandlePointerUp = (e: PointerEvent) => {\n const scrubberEl = this.scrubberRef.value || this._scrubberElement;\n if (e.pointerId === this.capturedPointerId && scrubberEl) {\n e.preventDefault();\n e.stopPropagation();\n try {\n scrubberEl.releasePointerCapture(e.pointerId);\n } catch (_err) {\n // releasePointerCapture may fail if capture was already lost\n }\n this.capturedPointerId = null;\n this.isMoving = false;\n if (this.isScrubbingRef) {\n this.isScrubbingRef.current = false;\n }\n if (this._wasPlayingBeforeScrub) {\n this._wasPlayingBeforeScrub = false;\n this.context?.play();\n }\n }\n };\n\n private boundHandlePointerCancel = (e: PointerEvent) => {\n const scrubberEl = this.scrubberRef.value || this._scrubberElement;\n if (e.pointerId === this.capturedPointerId && scrubberEl) {\n try {\n scrubberEl.releasePointerCapture(e.pointerId);\n } catch (_err) {\n // releasePointerCapture may fail if capture was already lost\n }\n this.capturedPointerId = null;\n this.isMoving = false;\n if (this.isScrubbingRef) {\n this.isScrubbingRef.current = false;\n }\n if (this._wasPlayingBeforeScrub) {\n this._wasPlayingBeforeScrub = false;\n this.context?.play();\n }\n }\n };\n\n private boundHandleContextMenu = (e: Event) => {\n if (this.isMoving) {\n e.preventDefault();\n e.stopPropagation();\n }\n };\n\n render() {\n const duration = this.effectiveDurationMs;\n const currentTime = this.effectiveCurrentTimeMs;\n\n if (duration <= 0) {\n return html``;\n }\n\n if (this.isTimelineMode) {\n // Vertical timeline mode: render playhead line\n const scrubberEl = this.scrubberRef.value || this._scrubberElement;\n const effectiveWidth =\n this.containerWidth > 0\n ? this.containerWidth\n : scrubberEl?.parentElement?.getBoundingClientRect().width || 0;\n\n const positionPixels =\n effectiveWidth > 0\n ? timeToPixels(currentTime, duration, effectiveWidth, this.zoomScale)\n : 0;\n\n const rawScrubPositionPixels =\n this.rawScrubTimeMs !== null &&\n this.rawScrubTimeMs !== undefined &&\n effectiveWidth > 0\n ? timeToPixels(\n this.rawScrubTimeMs,\n duration,\n effectiveWidth,\n this.zoomScale,\n )\n : null;\n\n return html`\n ${\n rawScrubPositionPixels !== null &&\n rawScrubPositionPixels !== positionPixels\n ? html`<div\n class=\"raw-preview\"\n style=\"left: ${rawScrubPositionPixels}px\"\n ></div>`\n : html``\n }\n <div\n ${ref(this.scrubberRef)}\n part=\"scrubber\"\n class=\"scrubber\"\n @pointerdown=${this.handlePointerDown}\n @contextmenu=${this.boundHandleContextMenu}\n >\n <div\n part=\"playhead\"\n class=\"playhead\"\n style=\"left: ${positionPixels}px\"\n @pointerdown=${this.handlePointerDown}\n >\n <div class=\"playhead-handle\"></div>\n </div>\n </div>\n `;\n } else {\n // Horizontal mode: render progress bar\n const currentProgress = duration > 0 ? currentTime / duration : 0;\n const displayProgress = this.isMoving\n ? this.scrubProgress\n : currentProgress;\n\n return html`\n <div\n ${ref(this.scrubberRef)}\n part=\"scrubber\"\n class=\"scrubber\"\n @pointerdown=${this.handlePointerDown}\n @contextmenu=${this.boundHandleContextMenu}\n >\n <div part=\"progress\" class=\"progress\" style=\"width: ${displayProgress * 100}%\"></div>\n <div part=\"handle\" class=\"handle\" style=\"left: ${displayProgress * 100}%\"></div>\n </div>\n `;\n }\n }\n\n connectedCallback() {\n super.connectedCallback();\n window.addEventListener(\n \"pointerup\",\n this.boundHandlePointerUp as EventListener,\n { passive: false },\n );\n window.addEventListener(\"pointermove\", this.boundHandlePointerMove, {\n passive: false,\n });\n window.addEventListener(\n \"pointercancel\",\n this.boundHandlePointerCancel as EventListener,\n { passive: false },\n );\n this.addEventListener(\"contextmenu\", this.boundHandleContextMenu, {\n passive: false,\n });\n this.addEventListener(\n \"pointerdown\",\n this.handlePointerDown as EventListener,\n { passive: false },\n );\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n window.removeEventListener(\n \"pointerup\",\n this.boundHandlePointerUp as EventListener,\n );\n window.removeEventListener(\"pointermove\", this.boundHandlePointerMove);\n window.removeEventListener(\n \"pointercancel\",\n this.boundHandlePointerCancel as EventListener,\n );\n this.removeEventListener(\"contextmenu\", this.boundHandleContextMenu);\n this.removeEventListener(\n \"pointerdown\",\n this.handlePointerDown as EventListener,\n );\n if (this._wasPlayingBeforeScrub) {\n this._wasPlayingBeforeScrub = false;\n if ((this.context as unknown as Element | null)?.isConnected) {\n this.context!.play();\n }\n }\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-scrubber\": EFScrubber;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ControllableInterface } from "./Controllable.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit20 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html19 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFTimeDisplay.d.ts
|
|
7
7
|
declare const EFTimeDisplay_base: (new (...args: any[]) => {
|
|
@@ -10,7 +10,7 @@ declare const EFTimeDisplay_base: (new (...args: any[]) => {
|
|
|
10
10
|
effectiveContext: ControllableInterface | null;
|
|
11
11
|
}) & typeof LitElement;
|
|
12
12
|
declare class EFTimeDisplay extends EFTimeDisplay_base {
|
|
13
|
-
static styles:
|
|
13
|
+
static styles: lit20.CSSResult;
|
|
14
14
|
currentTimeMs: number;
|
|
15
15
|
durationMs: number;
|
|
16
16
|
contextCurrentTimeMs: number;
|
|
@@ -18,7 +18,7 @@ declare class EFTimeDisplay extends EFTimeDisplay_base {
|
|
|
18
18
|
get effectiveCurrentTimeMs(): number;
|
|
19
19
|
get effectiveDurationMs(): number;
|
|
20
20
|
private formatTime;
|
|
21
|
-
render():
|
|
21
|
+
render(): lit_html19.TemplateResult<1>;
|
|
22
22
|
}
|
|
23
23
|
declare global {
|
|
24
24
|
interface HTMLElementTagNameMap {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TimelineState } from "./timeline/timelineStateContext.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit33 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html31 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFTimelineRuler.d.ts
|
|
7
7
|
/**
|
|
@@ -25,7 +25,7 @@ declare function calculatePixelsPerFrame(frameIntervalMs: number, pixelsPerMs: n
|
|
|
25
25
|
*/
|
|
26
26
|
declare function shouldShowFrameMarkers(pixelsPerFrame: number, minSpacing?: number): boolean;
|
|
27
27
|
declare class EFTimelineRuler extends LitElement {
|
|
28
|
-
static styles:
|
|
28
|
+
static styles: lit33.CSSResult[];
|
|
29
29
|
durationMs: number;
|
|
30
30
|
contextDurationMs: number;
|
|
31
31
|
timelineState?: TimelineState;
|
|
@@ -59,7 +59,7 @@ declare class EFTimelineRuler extends LitElement {
|
|
|
59
59
|
private calculateLabelInterval;
|
|
60
60
|
private getVisibleLabels;
|
|
61
61
|
private renderCanvas;
|
|
62
|
-
render():
|
|
62
|
+
render(): lit_html31.TemplateResult<1>;
|
|
63
63
|
}
|
|
64
64
|
declare global {
|
|
65
65
|
interface HTMLElementTagNameMap {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ControllableInterface } from "./Controllable.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit18 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html17 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFToggleLoop.d.ts
|
|
7
7
|
declare const EFToggleLoop_base: (new (...args: any[]) => {
|
|
@@ -10,9 +10,9 @@ declare const EFToggleLoop_base: (new (...args: any[]) => {
|
|
|
10
10
|
effectiveContext: ControllableInterface | null;
|
|
11
11
|
}) & typeof LitElement;
|
|
12
12
|
declare class EFToggleLoop extends EFToggleLoop_base {
|
|
13
|
-
static styles:
|
|
13
|
+
static styles: lit18.CSSResult[];
|
|
14
14
|
get context(): ControllableInterface | null;
|
|
15
|
-
render():
|
|
15
|
+
render(): lit_html17.TemplateResult<1>;
|
|
16
16
|
}
|
|
17
17
|
declare global {
|
|
18
18
|
interface HTMLElementTagNameMap {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ControllableInterface } from "./Controllable.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit15 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html14 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFTogglePlay.d.ts
|
|
7
7
|
declare const EFTogglePlay_base: (new (...args: any[]) => {
|
|
@@ -10,12 +10,12 @@ declare const EFTogglePlay_base: (new (...args: any[]) => {
|
|
|
10
10
|
effectiveContext: ControllableInterface | null;
|
|
11
11
|
}) & typeof LitElement;
|
|
12
12
|
declare class EFTogglePlay extends EFTogglePlay_base {
|
|
13
|
-
static styles:
|
|
13
|
+
static styles: lit15.CSSResult[];
|
|
14
14
|
playing: boolean;
|
|
15
15
|
get efContext(): ControllableInterface | null;
|
|
16
16
|
connectedCallback(): void;
|
|
17
17
|
disconnectedCallback(): void;
|
|
18
|
-
render():
|
|
18
|
+
render(): lit_html14.TemplateResult<1>;
|
|
19
19
|
togglePlay: () => void;
|
|
20
20
|
private getPlaybackController;
|
|
21
21
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { ContextMixinInterface } from "./ContextMixin.js";
|
|
2
2
|
import { RenderToVideoOptions } from "../preview/renderTimegroupToVideo.types.js";
|
|
3
3
|
import "./EFFitScale.js";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit8 from "lit";
|
|
5
5
|
import { LitElement, PropertyValueMap } from "lit";
|
|
6
|
-
import * as
|
|
6
|
+
import * as lit_html8 from "lit-html";
|
|
7
7
|
import * as lit_html_directives_ref_js2 from "lit-html/directives/ref.js";
|
|
8
8
|
|
|
9
9
|
//#region src/gui/EFWorkbench.d.ts
|
|
10
10
|
declare const EFWorkbench_base: (new (...args: any[]) => ContextMixinInterface) & typeof LitElement;
|
|
11
11
|
declare class EFWorkbench extends EFWorkbench_base {
|
|
12
12
|
#private;
|
|
13
|
-
static styles:
|
|
13
|
+
static styles: lit8.CSSResult[];
|
|
14
14
|
rendering: boolean;
|
|
15
15
|
private panZoomTransform;
|
|
16
16
|
private isExporting;
|
|
@@ -200,7 +200,7 @@ declare class EFWorkbench extends EFWorkbench_base {
|
|
|
200
200
|
updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void;
|
|
201
201
|
drawOverlays: () => void;
|
|
202
202
|
private renderPlaybackStats;
|
|
203
|
-
render():
|
|
203
|
+
render(): lit_html8.TemplateResult<1>;
|
|
204
204
|
}
|
|
205
205
|
declare global {
|
|
206
206
|
interface HTMLElementTagNameMap {
|
|
@@ -322,12 +322,14 @@ var PlaybackController = class {
|
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
324
|
async stopPlayback() {
|
|
325
|
+
if (this.#playbackAnimationFrameRequest) {
|
|
326
|
+
cancelAnimationFrame(this.#playbackAnimationFrameRequest);
|
|
327
|
+
this.#playbackAnimationFrameRequest = null;
|
|
328
|
+
}
|
|
325
329
|
if (this.#playbackAudioContext) {
|
|
326
330
|
if (this.#playbackAudioContext.state !== "closed") await this.#playbackAudioContext.close();
|
|
327
331
|
}
|
|
328
|
-
if (this.#playbackAnimationFrameRequest) cancelAnimationFrame(this.#playbackAnimationFrameRequest);
|
|
329
332
|
this.#playbackAudioContext = null;
|
|
330
|
-
this.#playbackAnimationFrameRequest = null;
|
|
331
333
|
this.#pendingAudioContext = null;
|
|
332
334
|
}
|
|
333
335
|
async startPlayback() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlaybackController.js","names":["#FPS","#host","#playingProvider","#playing","#loopProvider","#loop","#currentTimeMsProvider","#durationMsProvider","#currentTime","#processingPendingSeek","#pendingSeekTime","#seekInProgress","#runSeek","#seekAbortController","#notifyListeners","#hasConnected","#removed","#initializeTime","#selfRenderSuspended","#selfRenderAbortController","#selfRenderPromise","#selfRenderDirty","#startSelfRender","#listeners","#pendingAudioContext","#playbackAudioContext","rawTimeMs: number","#playbackWrapTimeSeconds","#loopingPlayback","#MS_PER_FRAME","#updatePlaybackTime","#playbackAnimationFrameRequest","#syncPlayheadToAudioContext","#AUDIO_PLAYBACK_SLICE_MS"],"sources":["../../src/gui/PlaybackController.ts"],"sourcesContent":["import { ContextProvider } from \"@lit/context\";\nimport type { ReactiveController, ReactiveControllerHost } from \"lit\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\nimport {\n updateAnimations,\n type AnimatableElement,\n} from \"../elements/updateAnimations.js\";\nimport type {\n RenderFrameOptions,\n FrameRenderable,\n} from \"../preview/FrameController.js\";\n\ninterface PlaybackHost extends HTMLElement, ReactiveControllerHost {\n currentTimeMs: number;\n durationMs: number;\n endTimeMs: number;\n /** Centralized frame controller (present on EFTimegroup) */\n frameController?: {\n renderFrame(timeMs: number, options?: RenderFrameOptions): Promise<void>;\n abort(): void;\n };\n renderAudio?(fromMs: number, toMs: number): Promise<AudioBuffer>;\n waitForMediaDurations?(signal?: AbortSignal): Promise<void>;\n saveTimeToLocalStorage?(time: number): void;\n loadTimeFromLocalStorage?(): number | undefined;\n requestUpdate(property?: string): void;\n updateComplete: Promise<boolean>;\n playing: boolean;\n loop: boolean;\n play(): void;\n pause(): void;\n playbackController?: PlaybackController;\n parentTimegroup?: any;\n rootTimegroup?: any;\n}\n\nexport type PlaybackControllerUpdateEvent = {\n property: \"playing\" | \"loop\" | \"currentTimeMs\";\n value: boolean | number;\n};\n\n/**\n * Manages playback state and audio-driven timing for root temporal elements\n *\n * Created automatically when a temporal element becomes a root (no parent timegroup)\n * Provides playback contexts (playing, loop, currentTimeMs, durationMs) to descendants\n * Handles:\n * - Audio-driven playback with Web Audio API\n * - Seek and frame rendering throttling\n * - Time state management with pending seek handling\n * - Playback loop behavior\n *\n * Works with any temporal element (timegroups or standalone media) via PlaybackHost interface\n */\nexport class PlaybackController implements ReactiveController {\n #host: PlaybackHost;\n #playing = false;\n #loop = false;\n #listeners = new Set<(event: PlaybackControllerUpdateEvent) => void>();\n #playingProvider: ContextProvider<typeof playingContext>;\n #loopProvider: ContextProvider<typeof loopContext>;\n #currentTimeMsProvider: ContextProvider<typeof currentTimeContext>;\n #durationMsProvider: ContextProvider<typeof durationContext>;\n\n #FPS = 30;\n #MS_PER_FRAME = 1000 / this.#FPS;\n #playbackAudioContext: AudioContext | null = null;\n #playbackAnimationFrameRequest: number | null = null;\n #pendingAudioContext: AudioContext | null = null;\n #AUDIO_PLAYBACK_SLICE_MS = ((47 * 1024) / 48000) * 1000;\n\n #currentTime: number | undefined = undefined;\n #seekInProgress = false;\n #pendingSeekTime: number | undefined;\n #processingPendingSeek = false;\n #loopingPlayback = false; // Track if we're in a looping playback session\n #playbackWrapTimeSeconds = 0; // The AudioContext time when we wrapped\n\n #seekAbortController: AbortController | null = null;\n #hasConnected = false;\n\n constructor(host: PlaybackHost) {\n this.#host = host;\n host.addController(this);\n\n this.#playingProvider = new ContextProvider(host, {\n context: playingContext,\n initialValue: this.#playing,\n });\n this.#loopProvider = new ContextProvider(host, {\n context: loopContext,\n initialValue: this.#loop,\n });\n this.#currentTimeMsProvider = new ContextProvider(host, {\n context: currentTimeContext,\n initialValue: host.currentTimeMs,\n });\n this.#durationMsProvider = new ContextProvider(host, {\n context: durationContext,\n initialValue: host.durationMs,\n });\n }\n\n get currentTime(): number {\n const rawTime = this.#currentTime ?? 0;\n // Quantize to frame boundaries based on host's fps\n const fps = (this.#host as any).fps ?? 30;\n if (!fps || fps <= 0) return rawTime;\n const frameDurationS = 1 / fps;\n const quantizedTime = Math.round(rawTime / frameDurationS) * frameDurationS;\n // Clamp to valid range after quantization to prevent exceeding duration\n const durationS = this.#host.durationMs / 1000;\n return Math.max(0, Math.min(quantizedTime, durationS));\n }\n\n set currentTime(time: number) {\n time = Math.max(0, Math.min(this.#host.durationMs / 1000, time));\n if (Number.isNaN(time)) {\n return;\n }\n if (time === this.#currentTime && !this.#processingPendingSeek) {\n return;\n }\n if (this.#pendingSeekTime === time) {\n return;\n }\n\n if (this.#seekInProgress) {\n this.#pendingSeekTime = time;\n this.#currentTime = time;\n return;\n }\n\n this.#currentTime = time;\n this.#seekInProgress = true;\n\n this.#runSeek(time).finally(async () => {\n // CRITICAL: Coordinate animations after seek completes\n // This ensures animations are positioned correctly, not playing naturally\n const { updateAnimations } =\n await import(\"../elements/updateAnimations.js\");\n updateAnimations(this.#host as any);\n\n if (\n this.#pendingSeekTime !== undefined &&\n this.#pendingSeekTime !== time\n ) {\n const pendingTime = this.#pendingSeekTime;\n this.#pendingSeekTime = undefined;\n this.#processingPendingSeek = true;\n try {\n this.currentTime = pendingTime;\n } finally {\n this.#processingPendingSeek = false;\n }\n } else {\n this.#pendingSeekTime = undefined;\n }\n });\n }\n\n async #runSeek(targetTime: number): Promise<number | undefined> {\n // Abort any in-flight seek\n this.#seekAbortController?.abort();\n this.#seekAbortController = new AbortController();\n const signal = this.#seekAbortController.signal;\n\n try {\n signal.throwIfAborted();\n\n await this.#host.waitForMediaDurations?.(signal);\n signal.throwIfAborted();\n\n const newTime = Math.max(\n 0,\n Math.min(targetTime, this.#host.durationMs / 1000),\n );\n this.#currentTime = newTime;\n this.#host.requestUpdate(\"currentTime\");\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n this.#notifyListeners({\n property: \"currentTimeMs\",\n value: this.currentTimeMs,\n });\n\n signal.throwIfAborted();\n\n await this.runThrottledFrameTask();\n signal.throwIfAborted();\n\n // Save to localStorage for persistence (only if not restoring to avoid loops)\n const isRestoring =\n (this.#host as any).isRestoringFromLocalStorage?.() ?? false;\n if (!isRestoring) {\n this.#host.saveTimeToLocalStorage?.(newTime);\n } else {\n (this.#host as any).setRestoringFromLocalStorage?.(false);\n }\n this.#seekInProgress = false;\n return newTime;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n // Expected - don't log\n return undefined;\n }\n throw error;\n }\n }\n\n get playing(): boolean {\n return this.#playing;\n }\n\n setPlaying(value: boolean): void {\n if (this.#playing === value) return;\n this.#playing = value;\n this.#playingProvider.setValue(value);\n this.#host.requestUpdate(\"playing\");\n this.#notifyListeners({ property: \"playing\", value });\n\n if (value) {\n this.startPlayback();\n } else {\n this.stopPlayback();\n }\n }\n\n get loop(): boolean {\n return this.#loop;\n }\n\n setLoop(value: boolean): void {\n if (this.#loop === value) return;\n this.#loop = value;\n this.#loopProvider.setValue(value);\n this.#host.requestUpdate(\"loop\");\n this.#notifyListeners({ property: \"loop\", value });\n }\n\n get currentTimeMs(): number {\n return this.currentTime * 1000;\n }\n\n setCurrentTimeMs(value: number): void {\n this.currentTime = value / 1000;\n }\n\n // Update time during playback without triggering a seek\n // Used by #syncPlayheadToAudioContext to avoid frame drops\n #updatePlaybackTime(timeMs: number): void {\n // Clamp to valid range to prevent time exceeding duration\n const durationMs = this.#host.durationMs;\n const clampedTimeMs = Math.max(0, Math.min(timeMs, durationMs));\n const timeSec = clampedTimeMs / 1000;\n if (this.#currentTime === timeSec) {\n return;\n }\n this.#currentTime = timeSec;\n this.#host.requestUpdate(\"currentTime\");\n this.#currentTimeMsProvider.setValue(clampedTimeMs);\n this.#notifyListeners({\n property: \"currentTimeMs\",\n value: clampedTimeMs,\n });\n // Trigger frame rendering without the async seek mechanism\n this.runThrottledFrameTask();\n }\n\n play(): void {\n this.setPlaying(true);\n }\n\n pause(): void {\n this.setPlaying(false);\n }\n\n #removed = false;\n\n hostConnected(): void {\n const isReconnect = this.#hasConnected;\n this.#hasConnected = true;\n // Defer all operations to avoid blocking during initialization\n // This prevents deadlocks when many timegroups are initializing simultaneously\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n // Check if this controller was removed before the RAF callback executed.\n // This happens when wrapWithWorkbench moves the element, causing disconnect/reconnect.\n if (this.#removed || this.#host.playbackController !== this) {\n return;\n }\n\n if (this.#playing && isReconnect) {\n this.startPlayback();\n } else if (!this.#playing) {\n this.#initializeTime();\n }\n });\n });\n }\n\n async #initializeTime(): Promise<void> {\n try {\n const waitPromise = this.#host.waitForMediaDurations?.();\n if (waitPromise) {\n await waitPromise;\n }\n } catch (err) {\n const isAbortError =\n (err instanceof DOMException && err.name === \"AbortError\") ||\n (err instanceof Error &&\n (err.name === \"AbortError\" ||\n err.message.includes(\"signal is aborted\") ||\n err.message.includes(\"The user aborted a request\")));\n if (!isAbortError) {\n console.error(\"Error in PlaybackController hostConnected:\", err);\n }\n return;\n }\n\n if (this.#removed || this.#host.playbackController !== this) {\n return;\n }\n\n const maybeLoadedTime = this.#host.loadTimeFromLocalStorage?.();\n if (maybeLoadedTime !== undefined) {\n (this.#host as any).setRestoringFromLocalStorage?.(true);\n this.currentTime = maybeLoadedTime;\n } else if (this.#currentTime === undefined) {\n this.currentTime = 0;\n }\n }\n\n hostDisconnected(): void {\n this.pause();\n }\n\n hostUpdated(): void {\n this.#durationMsProvider.setValue(this.#host.durationMs);\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n }\n\n #selfRenderAbortController?: AbortController;\n #selfRenderPromise?: Promise<void>;\n #selfRenderDirty = false;\n #selfRenderSuspended = false;\n\n suspendSelfRender(): void {\n this.#selfRenderSuspended = true;\n this.#selfRenderAbortController?.abort();\n this.#selfRenderAbortController = undefined;\n }\n\n resumeSelfRender(): void {\n this.#selfRenderSuspended = false;\n }\n\n /**\n * Run frame rendering via FrameController, or directly on the host if it\n * implements FrameRenderable (standalone media element without a Timegroup).\n */\n async runThrottledFrameTask(): Promise<void> {\n const timeMs = this.currentTimeMs;\n\n if (this.#host.frameController) {\n try {\n await this.#host.frameController.renderFrame(timeMs, {\n onAnimationsUpdate: (root: Element) => {\n updateAnimations(root as unknown as AnimatableElement);\n },\n });\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\")\n return;\n console.error(\"FrameController error:\", error);\n }\n return;\n }\n\n // Standalone FrameRenderable host (e.g. bare ef-video without a Timegroup)\n const host = this.#host as unknown as Partial<FrameRenderable>;\n if (!host.prepareFrame || !host.renderFrame) return;\n\n if (this.#selfRenderSuspended) return;\n\n // If a render is in-flight, mark dirty so we re-render after it\n // completes (source mapping may have changed due to trim drag).\n if (this.#selfRenderPromise) {\n this.#selfRenderDirty = true;\n return this.#selfRenderPromise;\n }\n\n return this.#startSelfRender(host, timeMs);\n }\n\n #startSelfRender(\n host: Partial<FrameRenderable>,\n timeMs: number,\n ): Promise<void> {\n this.#selfRenderAbortController?.abort();\n this.#selfRenderAbortController = new AbortController();\n const signal = this.#selfRenderAbortController.signal;\n this.#selfRenderDirty = false;\n\n this.#selfRenderPromise = (async () => {\n try {\n await host.prepareFrame!(timeMs, signal);\n signal.throwIfAborted();\n host.renderFrame!(timeMs);\n updateAnimations(this.#host as unknown as AnimatableElement);\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\")\n return;\n if ((error as any)?.name === \"AbortError\") return;\n console.error(\"Standalone frame render error:\", error);\n } finally {\n this.#selfRenderPromise = undefined;\n // Re-render if source mapping changed while we were rendering\n if (this.#selfRenderDirty && !this.#selfRenderSuspended) {\n this.#startSelfRender(host, this.currentTimeMs);\n }\n }\n })();\n\n return this.#selfRenderPromise;\n }\n\n addListener(listener: (event: PlaybackControllerUpdateEvent) => void): void {\n this.#listeners.add(listener);\n }\n\n removeListener(\n listener: (event: PlaybackControllerUpdateEvent) => void,\n ): void {\n this.#listeners.delete(listener);\n }\n\n #notifyListeners(event: PlaybackControllerUpdateEvent): void {\n for (const listener of this.#listeners) {\n listener(event);\n }\n }\n\n remove(): void {\n this.#removed = true; // Mark as removed to abort any pending RAF callbacks\n this.stopPlayback();\n this.#listeners.clear();\n this.#host.removeController(this);\n }\n\n setPendingAudioContext(context: AudioContext): void {\n this.#pendingAudioContext = context;\n }\n\n #syncPlayheadToAudioContext(startMs: number) {\n const audioContextTime = this.#playbackAudioContext?.currentTime ?? 0;\n const endMs = this.#host.endTimeMs;\n\n // Calculate raw time based on audio context\n let rawTimeMs: number;\n if (\n this.#playbackWrapTimeSeconds > 0 &&\n audioContextTime >= this.#playbackWrapTimeSeconds\n ) {\n // After wrap: time since wrap, wrapped to duration\n const timeSinceWrap =\n (audioContextTime - this.#playbackWrapTimeSeconds) * 1000;\n rawTimeMs = timeSinceWrap % endMs;\n } else {\n // Before wrap or no wrap: normal calculation\n rawTimeMs = startMs + audioContextTime * 1000;\n\n // If looping and we've reached the end, wrap around\n if (this.#loopingPlayback && rawTimeMs >= endMs) {\n rawTimeMs = rawTimeMs % endMs;\n }\n }\n\n const nextTimeMs =\n Math.round(rawTimeMs / this.#MS_PER_FRAME) * this.#MS_PER_FRAME;\n\n // During playback, update time directly without triggering seek\n // This avoids frame drops at the loop boundary\n this.#updatePlaybackTime(nextTimeMs);\n\n // Only check for end if we haven't already handled looping\n if (!this.#loopingPlayback && nextTimeMs >= endMs) {\n this.maybeLoopPlayback();\n return;\n }\n\n this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {\n this.#syncPlayheadToAudioContext(startMs);\n });\n }\n\n private async maybeLoopPlayback() {\n if (this.#loop) {\n // Loop enabled: reset to beginning and restart playback\n // We restart the audio system directly without changing #playing state\n // to keep the play button in sync\n this.setCurrentTimeMs(0);\n // Restart in next frame without awaiting to minimize gap\n requestAnimationFrame(() => {\n this.startPlayback();\n });\n } else {\n // No loop: reset to beginning and stop\n // This ensures play button works when clicked again\n this.setCurrentTimeMs(0);\n this.pause();\n }\n }\n\n private async stopPlayback() {\n if (this.#playbackAudioContext) {\n if (this.#playbackAudioContext.state !== \"closed\") {\n await this.#playbackAudioContext.close();\n }\n }\n if (this.#playbackAnimationFrameRequest) {\n cancelAnimationFrame(this.#playbackAnimationFrameRequest);\n }\n this.#playbackAudioContext = null;\n this.#playbackAnimationFrameRequest = null;\n this.#pendingAudioContext = null;\n }\n\n private async startPlayback() {\n // Guard against starting playback on a removed controller\n if (this.#removed) {\n return;\n }\n\n await this.stopPlayback();\n const host = this.#host;\n if (!host) {\n return;\n }\n\n if (host.waitForMediaDurations) {\n await host.waitForMediaDurations();\n }\n\n // Check again after async - controller could have been removed\n if (this.#removed) {\n return;\n }\n\n const currentMs = this.currentTimeMs;\n const fromMs = currentMs;\n const toMs = host.endTimeMs;\n\n if (fromMs >= toMs) {\n this.pause();\n return;\n }\n\n let bufferCount = 0;\n // Check for pre-resumed AudioContext from synchronous user interaction\n if (this.#pendingAudioContext) {\n this.#playbackAudioContext = this.#pendingAudioContext;\n this.#pendingAudioContext = null;\n } else {\n this.#playbackAudioContext = new AudioContext({\n latencyHint: \"playback\",\n });\n }\n this.#loopingPlayback = this.#loop; // Remember if we're in a looping session\n this.#playbackWrapTimeSeconds = 0; // Reset wrap time\n\n if (this.#playbackAnimationFrameRequest) {\n cancelAnimationFrame(this.#playbackAnimationFrameRequest);\n }\n this.#syncPlayheadToAudioContext(currentMs);\n const playbackContext = this.#playbackAudioContext;\n\n // Check if context is suspended (fallback for newly-created contexts)\n if (playbackContext.state === \"suspended\") {\n // Attempt to resume (may not work on mobile if user interaction context is lost)\n try {\n await playbackContext.resume();\n // Check state again after resume attempt\n if (playbackContext.state === \"suspended\") {\n console.warn(\n \"AudioContext is suspended and resume() failed. \" +\n \"On mobile devices, AudioContext.resume() must be called synchronously within a user interaction handler. \" +\n \"Media playback will not work until user has interacted with page.\",\n );\n this.setPlaying(false);\n return;\n }\n } catch (error) {\n console.warn(\n \"Failed to resume AudioContext:\",\n error,\n \"On mobile devices, AudioContext.resume() must be called synchronously within a user interaction handler.\",\n );\n this.setPlaying(false);\n return;\n }\n }\n await playbackContext.suspend();\n\n // Track the logical media time (what position in the media we're rendering)\n // vs the AudioContext schedule time (when to play it)\n let logicalTimeMs = currentMs;\n let audioContextTimeMs = 0; // Tracks the schedule position in the AudioContext timeline\n let hasWrapped = false;\n\n const fillBuffer = async () => {\n if (bufferCount > 2) {\n return;\n }\n const canFillBuffer = await queueBufferSource();\n if (canFillBuffer) {\n fillBuffer().catch(() => {});\n }\n };\n\n const queueBufferSource = async () => {\n // Check if we've already wrapped and aren't looping anymore\n if (hasWrapped && !this.#loopingPlayback) {\n return false;\n }\n\n const startMs = logicalTimeMs;\n const endMs = Math.min(\n logicalTimeMs + this.#AUDIO_PLAYBACK_SLICE_MS,\n toMs,\n );\n\n // Will this slice reach the end?\n const willReachEnd = endMs >= toMs;\n\n if (!host.renderAudio) {\n return false;\n }\n\n const audioBuffer = await host.renderAudio(startMs, endMs);\n bufferCount++;\n const source = playbackContext.createBufferSource();\n source.buffer = audioBuffer;\n source.connect(playbackContext.destination);\n // Schedule this buffer to play at the current audioContextTime position\n source.start(audioContextTimeMs / 1000);\n\n const sliceDurationMs = endMs - startMs;\n\n source.onended = () => {\n bufferCount--;\n\n if (willReachEnd) {\n if (!this.#loopingPlayback) {\n // Not looping, end playback\n this.maybeLoopPlayback();\n } else {\n // Looping: continue filling buffer after wrap\n fillBuffer().catch(() => {});\n }\n } else {\n // Continue filling buffer\n fillBuffer().catch(() => {});\n }\n };\n\n // Advance the AudioContext schedule time\n audioContextTimeMs += sliceDurationMs;\n\n // If this buffer reaches the end and we're looping, immediately queue the wraparound\n if (willReachEnd && this.#loopingPlayback) {\n // Mark that we've wrapped\n hasWrapped = true;\n // Store when we wrapped (relative to when playback started, which is time 0 in AudioContext)\n // This is the duration from start to end\n this.#playbackWrapTimeSeconds = (toMs - fromMs) / 1000;\n // Reset logical time to beginning\n logicalTimeMs = 0;\n // Continue buffering will happen in fillBuffer() call below\n } else {\n // Normal advance\n logicalTimeMs = endMs;\n }\n\n return true;\n };\n\n try {\n await fillBuffer();\n await playbackContext.resume();\n } catch (error) {\n // Ignore errors if AudioContext is closed or during test cleanup\n if (\n error instanceof Error &&\n (error.name === \"InvalidStateError\" || error.message.includes(\"closed\"))\n ) {\n return;\n }\n throw error;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PlaybackController.js","names":["#FPS","#host","#playingProvider","#playing","#loopProvider","#loop","#currentTimeMsProvider","#durationMsProvider","#currentTime","#processingPendingSeek","#pendingSeekTime","#seekInProgress","#runSeek","#seekAbortController","#notifyListeners","#hasConnected","#removed","#initializeTime","#selfRenderSuspended","#selfRenderAbortController","#selfRenderPromise","#selfRenderDirty","#startSelfRender","#listeners","#pendingAudioContext","#playbackAudioContext","rawTimeMs: number","#playbackWrapTimeSeconds","#loopingPlayback","#MS_PER_FRAME","#updatePlaybackTime","#playbackAnimationFrameRequest","#syncPlayheadToAudioContext","#AUDIO_PLAYBACK_SLICE_MS"],"sources":["../../src/gui/PlaybackController.ts"],"sourcesContent":["import { ContextProvider } from \"@lit/context\";\nimport type { ReactiveController, ReactiveControllerHost } from \"lit\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\nimport {\n updateAnimations,\n type AnimatableElement,\n} from \"../elements/updateAnimations.js\";\nimport type {\n RenderFrameOptions,\n FrameRenderable,\n} from \"../preview/FrameController.js\";\n\ninterface PlaybackHost extends HTMLElement, ReactiveControllerHost {\n currentTimeMs: number;\n durationMs: number;\n endTimeMs: number;\n /** Centralized frame controller (present on EFTimegroup) */\n frameController?: {\n renderFrame(timeMs: number, options?: RenderFrameOptions): Promise<void>;\n abort(): void;\n };\n renderAudio?(fromMs: number, toMs: number): Promise<AudioBuffer>;\n waitForMediaDurations?(signal?: AbortSignal): Promise<void>;\n saveTimeToLocalStorage?(time: number): void;\n loadTimeFromLocalStorage?(): number | undefined;\n requestUpdate(property?: string): void;\n updateComplete: Promise<boolean>;\n playing: boolean;\n loop: boolean;\n play(): void;\n pause(): void;\n playbackController?: PlaybackController;\n parentTimegroup?: any;\n rootTimegroup?: any;\n}\n\nexport type PlaybackControllerUpdateEvent = {\n property: \"playing\" | \"loop\" | \"currentTimeMs\";\n value: boolean | number;\n};\n\n/**\n * Manages playback state and audio-driven timing for root temporal elements\n *\n * Created automatically when a temporal element becomes a root (no parent timegroup)\n * Provides playback contexts (playing, loop, currentTimeMs, durationMs) to descendants\n * Handles:\n * - Audio-driven playback with Web Audio API\n * - Seek and frame rendering throttling\n * - Time state management with pending seek handling\n * - Playback loop behavior\n *\n * Works with any temporal element (timegroups or standalone media) via PlaybackHost interface\n */\nexport class PlaybackController implements ReactiveController {\n #host: PlaybackHost;\n #playing = false;\n #loop = false;\n #listeners = new Set<(event: PlaybackControllerUpdateEvent) => void>();\n #playingProvider: ContextProvider<typeof playingContext>;\n #loopProvider: ContextProvider<typeof loopContext>;\n #currentTimeMsProvider: ContextProvider<typeof currentTimeContext>;\n #durationMsProvider: ContextProvider<typeof durationContext>;\n\n #FPS = 30;\n #MS_PER_FRAME = 1000 / this.#FPS;\n #playbackAudioContext: AudioContext | null = null;\n #playbackAnimationFrameRequest: number | null = null;\n #pendingAudioContext: AudioContext | null = null;\n #AUDIO_PLAYBACK_SLICE_MS = ((47 * 1024) / 48000) * 1000;\n\n #currentTime: number | undefined = undefined;\n #seekInProgress = false;\n #pendingSeekTime: number | undefined;\n #processingPendingSeek = false;\n #loopingPlayback = false; // Track if we're in a looping playback session\n #playbackWrapTimeSeconds = 0; // The AudioContext time when we wrapped\n\n #seekAbortController: AbortController | null = null;\n #hasConnected = false;\n\n constructor(host: PlaybackHost) {\n this.#host = host;\n host.addController(this);\n\n this.#playingProvider = new ContextProvider(host, {\n context: playingContext,\n initialValue: this.#playing,\n });\n this.#loopProvider = new ContextProvider(host, {\n context: loopContext,\n initialValue: this.#loop,\n });\n this.#currentTimeMsProvider = new ContextProvider(host, {\n context: currentTimeContext,\n initialValue: host.currentTimeMs,\n });\n this.#durationMsProvider = new ContextProvider(host, {\n context: durationContext,\n initialValue: host.durationMs,\n });\n }\n\n get currentTime(): number {\n const rawTime = this.#currentTime ?? 0;\n // Quantize to frame boundaries based on host's fps\n const fps = (this.#host as any).fps ?? 30;\n if (!fps || fps <= 0) return rawTime;\n const frameDurationS = 1 / fps;\n const quantizedTime = Math.round(rawTime / frameDurationS) * frameDurationS;\n // Clamp to valid range after quantization to prevent exceeding duration\n const durationS = this.#host.durationMs / 1000;\n return Math.max(0, Math.min(quantizedTime, durationS));\n }\n\n set currentTime(time: number) {\n time = Math.max(0, Math.min(this.#host.durationMs / 1000, time));\n if (Number.isNaN(time)) {\n return;\n }\n if (time === this.#currentTime && !this.#processingPendingSeek) {\n return;\n }\n if (this.#pendingSeekTime === time) {\n return;\n }\n\n if (this.#seekInProgress) {\n this.#pendingSeekTime = time;\n this.#currentTime = time;\n return;\n }\n\n this.#currentTime = time;\n this.#seekInProgress = true;\n\n this.#runSeek(time).finally(async () => {\n // CRITICAL: Coordinate animations after seek completes\n // This ensures animations are positioned correctly, not playing naturally\n const { updateAnimations } =\n await import(\"../elements/updateAnimations.js\");\n updateAnimations(this.#host as any);\n\n if (\n this.#pendingSeekTime !== undefined &&\n this.#pendingSeekTime !== time\n ) {\n const pendingTime = this.#pendingSeekTime;\n this.#pendingSeekTime = undefined;\n this.#processingPendingSeek = true;\n try {\n this.currentTime = pendingTime;\n } finally {\n this.#processingPendingSeek = false;\n }\n } else {\n this.#pendingSeekTime = undefined;\n }\n });\n }\n\n async #runSeek(targetTime: number): Promise<number | undefined> {\n // Abort any in-flight seek\n this.#seekAbortController?.abort();\n this.#seekAbortController = new AbortController();\n const signal = this.#seekAbortController.signal;\n\n try {\n signal.throwIfAborted();\n\n await this.#host.waitForMediaDurations?.(signal);\n signal.throwIfAborted();\n\n const newTime = Math.max(\n 0,\n Math.min(targetTime, this.#host.durationMs / 1000),\n );\n this.#currentTime = newTime;\n this.#host.requestUpdate(\"currentTime\");\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n this.#notifyListeners({\n property: \"currentTimeMs\",\n value: this.currentTimeMs,\n });\n\n signal.throwIfAborted();\n\n await this.runThrottledFrameTask();\n signal.throwIfAborted();\n\n // Save to localStorage for persistence (only if not restoring to avoid loops)\n const isRestoring =\n (this.#host as any).isRestoringFromLocalStorage?.() ?? false;\n if (!isRestoring) {\n this.#host.saveTimeToLocalStorage?.(newTime);\n } else {\n (this.#host as any).setRestoringFromLocalStorage?.(false);\n }\n this.#seekInProgress = false;\n return newTime;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n // Expected - don't log\n return undefined;\n }\n throw error;\n }\n }\n\n get playing(): boolean {\n return this.#playing;\n }\n\n setPlaying(value: boolean): void {\n if (this.#playing === value) return;\n this.#playing = value;\n this.#playingProvider.setValue(value);\n this.#host.requestUpdate(\"playing\");\n this.#notifyListeners({ property: \"playing\", value });\n\n if (value) {\n this.startPlayback();\n } else {\n this.stopPlayback();\n }\n }\n\n get loop(): boolean {\n return this.#loop;\n }\n\n setLoop(value: boolean): void {\n if (this.#loop === value) return;\n this.#loop = value;\n this.#loopProvider.setValue(value);\n this.#host.requestUpdate(\"loop\");\n this.#notifyListeners({ property: \"loop\", value });\n }\n\n get currentTimeMs(): number {\n return this.currentTime * 1000;\n }\n\n setCurrentTimeMs(value: number): void {\n this.currentTime = value / 1000;\n }\n\n // Update time during playback without triggering a seek\n // Used by #syncPlayheadToAudioContext to avoid frame drops\n #updatePlaybackTime(timeMs: number): void {\n // Clamp to valid range to prevent time exceeding duration\n const durationMs = this.#host.durationMs;\n const clampedTimeMs = Math.max(0, Math.min(timeMs, durationMs));\n const timeSec = clampedTimeMs / 1000;\n if (this.#currentTime === timeSec) {\n return;\n }\n this.#currentTime = timeSec;\n this.#host.requestUpdate(\"currentTime\");\n this.#currentTimeMsProvider.setValue(clampedTimeMs);\n this.#notifyListeners({\n property: \"currentTimeMs\",\n value: clampedTimeMs,\n });\n // Trigger frame rendering without the async seek mechanism\n this.runThrottledFrameTask();\n }\n\n play(): void {\n this.setPlaying(true);\n }\n\n pause(): void {\n this.setPlaying(false);\n }\n\n #removed = false;\n\n hostConnected(): void {\n const isReconnect = this.#hasConnected;\n this.#hasConnected = true;\n // Defer all operations to avoid blocking during initialization\n // This prevents deadlocks when many timegroups are initializing simultaneously\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n // Check if this controller was removed before the RAF callback executed.\n // This happens when wrapWithWorkbench moves the element, causing disconnect/reconnect.\n if (this.#removed || this.#host.playbackController !== this) {\n return;\n }\n\n if (this.#playing && isReconnect) {\n this.startPlayback();\n } else if (!this.#playing) {\n this.#initializeTime();\n }\n });\n });\n }\n\n async #initializeTime(): Promise<void> {\n try {\n const waitPromise = this.#host.waitForMediaDurations?.();\n if (waitPromise) {\n await waitPromise;\n }\n } catch (err) {\n const isAbortError =\n (err instanceof DOMException && err.name === \"AbortError\") ||\n (err instanceof Error &&\n (err.name === \"AbortError\" ||\n err.message.includes(\"signal is aborted\") ||\n err.message.includes(\"The user aborted a request\")));\n if (!isAbortError) {\n console.error(\"Error in PlaybackController hostConnected:\", err);\n }\n return;\n }\n\n if (this.#removed || this.#host.playbackController !== this) {\n return;\n }\n\n const maybeLoadedTime = this.#host.loadTimeFromLocalStorage?.();\n if (maybeLoadedTime !== undefined) {\n (this.#host as any).setRestoringFromLocalStorage?.(true);\n this.currentTime = maybeLoadedTime;\n } else if (this.#currentTime === undefined) {\n this.currentTime = 0;\n }\n }\n\n hostDisconnected(): void {\n this.pause();\n }\n\n hostUpdated(): void {\n this.#durationMsProvider.setValue(this.#host.durationMs);\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n }\n\n #selfRenderAbortController?: AbortController;\n #selfRenderPromise?: Promise<void>;\n #selfRenderDirty = false;\n #selfRenderSuspended = false;\n\n suspendSelfRender(): void {\n this.#selfRenderSuspended = true;\n this.#selfRenderAbortController?.abort();\n this.#selfRenderAbortController = undefined;\n }\n\n resumeSelfRender(): void {\n this.#selfRenderSuspended = false;\n }\n\n /**\n * Run frame rendering via FrameController, or directly on the host if it\n * implements FrameRenderable (standalone media element without a Timegroup).\n */\n async runThrottledFrameTask(): Promise<void> {\n const timeMs = this.currentTimeMs;\n\n if (this.#host.frameController) {\n try {\n await this.#host.frameController.renderFrame(timeMs, {\n onAnimationsUpdate: (root: Element) => {\n updateAnimations(root as unknown as AnimatableElement);\n },\n });\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\")\n return;\n console.error(\"FrameController error:\", error);\n }\n return;\n }\n\n // Standalone FrameRenderable host (e.g. bare ef-video without a Timegroup)\n const host = this.#host as unknown as Partial<FrameRenderable>;\n if (!host.prepareFrame || !host.renderFrame) return;\n\n if (this.#selfRenderSuspended) return;\n\n // If a render is in-flight, mark dirty so we re-render after it\n // completes (source mapping may have changed due to trim drag).\n if (this.#selfRenderPromise) {\n this.#selfRenderDirty = true;\n return this.#selfRenderPromise;\n }\n\n return this.#startSelfRender(host, timeMs);\n }\n\n #startSelfRender(\n host: Partial<FrameRenderable>,\n timeMs: number,\n ): Promise<void> {\n this.#selfRenderAbortController?.abort();\n this.#selfRenderAbortController = new AbortController();\n const signal = this.#selfRenderAbortController.signal;\n this.#selfRenderDirty = false;\n\n this.#selfRenderPromise = (async () => {\n try {\n await host.prepareFrame!(timeMs, signal);\n signal.throwIfAborted();\n host.renderFrame!(timeMs);\n updateAnimations(this.#host as unknown as AnimatableElement);\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\")\n return;\n if ((error as any)?.name === \"AbortError\") return;\n console.error(\"Standalone frame render error:\", error);\n } finally {\n this.#selfRenderPromise = undefined;\n // Re-render if source mapping changed while we were rendering\n if (this.#selfRenderDirty && !this.#selfRenderSuspended) {\n this.#startSelfRender(host, this.currentTimeMs);\n }\n }\n })();\n\n return this.#selfRenderPromise;\n }\n\n addListener(listener: (event: PlaybackControllerUpdateEvent) => void): void {\n this.#listeners.add(listener);\n }\n\n removeListener(\n listener: (event: PlaybackControllerUpdateEvent) => void,\n ): void {\n this.#listeners.delete(listener);\n }\n\n #notifyListeners(event: PlaybackControllerUpdateEvent): void {\n for (const listener of this.#listeners) {\n listener(event);\n }\n }\n\n remove(): void {\n this.#removed = true; // Mark as removed to abort any pending RAF callbacks\n this.stopPlayback();\n this.#listeners.clear();\n this.#host.removeController(this);\n }\n\n setPendingAudioContext(context: AudioContext): void {\n this.#pendingAudioContext = context;\n }\n\n #syncPlayheadToAudioContext(startMs: number) {\n const audioContextTime = this.#playbackAudioContext?.currentTime ?? 0;\n const endMs = this.#host.endTimeMs;\n\n // Calculate raw time based on audio context\n let rawTimeMs: number;\n if (\n this.#playbackWrapTimeSeconds > 0 &&\n audioContextTime >= this.#playbackWrapTimeSeconds\n ) {\n // After wrap: time since wrap, wrapped to duration\n const timeSinceWrap =\n (audioContextTime - this.#playbackWrapTimeSeconds) * 1000;\n rawTimeMs = timeSinceWrap % endMs;\n } else {\n // Before wrap or no wrap: normal calculation\n rawTimeMs = startMs + audioContextTime * 1000;\n\n // If looping and we've reached the end, wrap around\n if (this.#loopingPlayback && rawTimeMs >= endMs) {\n rawTimeMs = rawTimeMs % endMs;\n }\n }\n\n const nextTimeMs =\n Math.round(rawTimeMs / this.#MS_PER_FRAME) * this.#MS_PER_FRAME;\n\n // During playback, update time directly without triggering seek\n // This avoids frame drops at the loop boundary\n this.#updatePlaybackTime(nextTimeMs);\n\n // Only check for end if we haven't already handled looping\n if (!this.#loopingPlayback && nextTimeMs >= endMs) {\n this.maybeLoopPlayback();\n return;\n }\n\n this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {\n this.#syncPlayheadToAudioContext(startMs);\n });\n }\n\n private async maybeLoopPlayback() {\n if (this.#loop) {\n // Loop enabled: reset to beginning and restart playback\n // We restart the audio system directly without changing #playing state\n // to keep the play button in sync\n this.setCurrentTimeMs(0);\n // Restart in next frame without awaiting to minimize gap\n requestAnimationFrame(() => {\n this.startPlayback();\n });\n } else {\n // No loop: reset to beginning and stop\n // This ensures play button works when clicked again\n this.setCurrentTimeMs(0);\n this.pause();\n }\n }\n\n private async stopPlayback() {\n if (this.#playbackAnimationFrameRequest) {\n cancelAnimationFrame(this.#playbackAnimationFrameRequest);\n this.#playbackAnimationFrameRequest = null;\n }\n if (this.#playbackAudioContext) {\n if (this.#playbackAudioContext.state !== \"closed\") {\n await this.#playbackAudioContext.close();\n }\n }\n this.#playbackAudioContext = null;\n this.#pendingAudioContext = null;\n }\n\n private async startPlayback() {\n // Guard against starting playback on a removed controller\n if (this.#removed) {\n return;\n }\n\n await this.stopPlayback();\n const host = this.#host;\n if (!host) {\n return;\n }\n\n if (host.waitForMediaDurations) {\n await host.waitForMediaDurations();\n }\n\n // Check again after async - controller could have been removed\n if (this.#removed) {\n return;\n }\n\n const currentMs = this.currentTimeMs;\n const fromMs = currentMs;\n const toMs = host.endTimeMs;\n\n if (fromMs >= toMs) {\n this.pause();\n return;\n }\n\n let bufferCount = 0;\n // Check for pre-resumed AudioContext from synchronous user interaction\n if (this.#pendingAudioContext) {\n this.#playbackAudioContext = this.#pendingAudioContext;\n this.#pendingAudioContext = null;\n } else {\n this.#playbackAudioContext = new AudioContext({\n latencyHint: \"playback\",\n });\n }\n this.#loopingPlayback = this.#loop; // Remember if we're in a looping session\n this.#playbackWrapTimeSeconds = 0; // Reset wrap time\n\n if (this.#playbackAnimationFrameRequest) {\n cancelAnimationFrame(this.#playbackAnimationFrameRequest);\n }\n this.#syncPlayheadToAudioContext(currentMs);\n const playbackContext = this.#playbackAudioContext;\n\n // Check if context is suspended (fallback for newly-created contexts)\n if (playbackContext.state === \"suspended\") {\n // Attempt to resume (may not work on mobile if user interaction context is lost)\n try {\n await playbackContext.resume();\n // Check state again after resume attempt\n if (playbackContext.state === \"suspended\") {\n console.warn(\n \"AudioContext is suspended and resume() failed. \" +\n \"On mobile devices, AudioContext.resume() must be called synchronously within a user interaction handler. \" +\n \"Media playback will not work until user has interacted with page.\",\n );\n this.setPlaying(false);\n return;\n }\n } catch (error) {\n console.warn(\n \"Failed to resume AudioContext:\",\n error,\n \"On mobile devices, AudioContext.resume() must be called synchronously within a user interaction handler.\",\n );\n this.setPlaying(false);\n return;\n }\n }\n await playbackContext.suspend();\n\n // Track the logical media time (what position in the media we're rendering)\n // vs the AudioContext schedule time (when to play it)\n let logicalTimeMs = currentMs;\n let audioContextTimeMs = 0; // Tracks the schedule position in the AudioContext timeline\n let hasWrapped = false;\n\n const fillBuffer = async () => {\n if (bufferCount > 2) {\n return;\n }\n const canFillBuffer = await queueBufferSource();\n if (canFillBuffer) {\n fillBuffer().catch(() => {});\n }\n };\n\n const queueBufferSource = async () => {\n // Check if we've already wrapped and aren't looping anymore\n if (hasWrapped && !this.#loopingPlayback) {\n return false;\n }\n\n const startMs = logicalTimeMs;\n const endMs = Math.min(\n logicalTimeMs + this.#AUDIO_PLAYBACK_SLICE_MS,\n toMs,\n );\n\n // Will this slice reach the end?\n const willReachEnd = endMs >= toMs;\n\n if (!host.renderAudio) {\n return false;\n }\n\n const audioBuffer = await host.renderAudio(startMs, endMs);\n bufferCount++;\n const source = playbackContext.createBufferSource();\n source.buffer = audioBuffer;\n source.connect(playbackContext.destination);\n // Schedule this buffer to play at the current audioContextTime position\n source.start(audioContextTimeMs / 1000);\n\n const sliceDurationMs = endMs - startMs;\n\n source.onended = () => {\n bufferCount--;\n\n if (willReachEnd) {\n if (!this.#loopingPlayback) {\n // Not looping, end playback\n this.maybeLoopPlayback();\n } else {\n // Looping: continue filling buffer after wrap\n fillBuffer().catch(() => {});\n }\n } else {\n // Continue filling buffer\n fillBuffer().catch(() => {});\n }\n };\n\n // Advance the AudioContext schedule time\n audioContextTimeMs += sliceDurationMs;\n\n // If this buffer reaches the end and we're looping, immediately queue the wraparound\n if (willReachEnd && this.#loopingPlayback) {\n // Mark that we've wrapped\n hasWrapped = true;\n // Store when we wrapped (relative to when playback started, which is time 0 in AudioContext)\n // This is the duration from start to end\n this.#playbackWrapTimeSeconds = (toMs - fromMs) / 1000;\n // Reset logical time to beginning\n logicalTimeMs = 0;\n // Continue buffering will happen in fillBuffer() call below\n } else {\n // Normal advance\n logicalTimeMs = endMs;\n }\n\n return true;\n };\n\n try {\n await fillBuffer();\n await playbackContext.resume();\n } catch (error) {\n // Ignore errors if AudioContext is closed or during test cleanup\n if (\n error instanceof Error &&\n (error.name === \"InvalidStateError\" || error.message.includes(\"closed\"))\n ) {\n return;\n }\n throw error;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { SelectionContext } from "../../canvas/selection/selectionContext.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit11 from "lit";
|
|
3
3
|
import { LitElement, PropertyValues } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html11 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/hierarchy/EFHierarchy.d.ts
|
|
7
7
|
declare const EFHierarchy_base: typeof LitElement;
|
|
8
8
|
declare class EFHierarchy extends EFHierarchy_base {
|
|
9
|
-
static styles:
|
|
9
|
+
static styles: lit11.CSSResult[];
|
|
10
10
|
target: string;
|
|
11
11
|
header: string;
|
|
12
12
|
showHeader: boolean;
|
|
@@ -53,7 +53,7 @@ declare class EFHierarchy extends EFHierarchy_base {
|
|
|
53
53
|
private autoSelectFirstRootTimegroup;
|
|
54
54
|
private setupSelectionListener;
|
|
55
55
|
private removeSelectionListener;
|
|
56
|
-
render():
|
|
56
|
+
render(): lit_html11.TemplateResult<1>;
|
|
57
57
|
}
|
|
58
58
|
declare global {
|
|
59
59
|
interface HTMLElementTagNameMap {
|
|
@@ -4,13 +4,13 @@ import { EFVideo } from "../../elements/EFVideo.js";
|
|
|
4
4
|
import { EFTimegroup } from "../../elements/EFTimegroup.js";
|
|
5
5
|
import { EFImage } from "../../elements/EFImage.js";
|
|
6
6
|
import { HierarchyContext } from "./hierarchyContext.js";
|
|
7
|
-
import * as
|
|
7
|
+
import * as lit12 from "lit";
|
|
8
8
|
import { LitElement, PropertyValues, TemplateResult, nothing } from "lit";
|
|
9
9
|
|
|
10
10
|
//#region src/gui/hierarchy/EFHierarchyItem.d.ts
|
|
11
11
|
declare const EFHierarchyItem_base: typeof LitElement;
|
|
12
12
|
declare class EFHierarchyItem<ElementType extends HTMLElement = HTMLElement> extends EFHierarchyItem_base {
|
|
13
|
-
static styles:
|
|
13
|
+
static styles: lit12.CSSResult[];
|
|
14
14
|
hierarchyContext?: HierarchyContext;
|
|
15
15
|
canvasSelectionContext?: SelectionContext;
|
|
16
16
|
element: ElementType;
|
|
@@ -4,7 +4,7 @@ import { TimelineState } from "./timelineStateContext.js";
|
|
|
4
4
|
import "./tracks/preloadTracks.js";
|
|
5
5
|
import "./EFTimelineRow.js";
|
|
6
6
|
import "../EFTimelineRuler.js";
|
|
7
|
-
import * as
|
|
7
|
+
import * as lit34 from "lit";
|
|
8
8
|
import { LitElement, PropertyValues, TemplateResult } from "lit";
|
|
9
9
|
|
|
10
10
|
//#region src/gui/timeline/EFTimeline.d.ts
|
|
@@ -17,7 +17,7 @@ declare const EFTimeline_base: typeof LitElement;
|
|
|
17
17
|
*/
|
|
18
18
|
declare class EFTimeline extends EFTimeline_base {
|
|
19
19
|
#private;
|
|
20
|
-
static styles:
|
|
20
|
+
static styles: lit34.CSSResult[];
|
|
21
21
|
/**
|
|
22
22
|
* Target element ID or "selection" to derive from canvas selection.
|
|
23
23
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TimelineEditingContext } from "./timelineEditingContext.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit35 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html32 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/timeline/TrimHandles.d.ts
|
|
7
7
|
interface TrimValue {
|
|
@@ -16,7 +16,7 @@ interface TrimChangeDetail {
|
|
|
16
16
|
declare const EFTrimHandles_base: typeof LitElement;
|
|
17
17
|
declare class EFTrimHandles extends EFTrimHandles_base {
|
|
18
18
|
#private;
|
|
19
|
-
static styles:
|
|
19
|
+
static styles: lit35.CSSResult[];
|
|
20
20
|
mode: "standalone" | "track";
|
|
21
21
|
elementId: string;
|
|
22
22
|
pixelsPerMs: number | null;
|
|
@@ -38,7 +38,7 @@ declare class EFTrimHandles extends EFTrimHandles_base {
|
|
|
38
38
|
private handleRegionPointerDown;
|
|
39
39
|
private handlePointerMove;
|
|
40
40
|
private handlePointerUp;
|
|
41
|
-
render():
|
|
41
|
+
render(): lit_html32.TemplateResult<1>;
|
|
42
42
|
}
|
|
43
43
|
declare global {
|
|
44
44
|
interface HTMLElementTagNameMap {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { TimelineState } from "../timelineStateContext.js";
|
|
2
2
|
import { PreviewSettings } from "../../previewSettingsContext.js";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit36 from "lit";
|
|
4
4
|
import { LitElement } from "lit";
|
|
5
|
-
import * as
|
|
5
|
+
import * as lit_html33 from "lit-html";
|
|
6
6
|
|
|
7
7
|
//#region src/gui/timeline/tracks/EFThumbnailStrip.d.ts
|
|
8
8
|
declare const EFThumbnailStrip_base: typeof LitElement;
|
|
@@ -19,7 +19,7 @@ declare const EFThumbnailStrip_base: typeof LitElement;
|
|
|
19
19
|
*/
|
|
20
20
|
declare class EFThumbnailStrip extends EFThumbnailStrip_base {
|
|
21
21
|
#private;
|
|
22
|
-
static styles:
|
|
22
|
+
static styles: lit36.CSSResult[];
|
|
23
23
|
target: string;
|
|
24
24
|
targetElement: Element | null;
|
|
25
25
|
thumbnailHeight: number;
|
|
@@ -41,7 +41,7 @@ declare class EFThumbnailStrip extends EFThumbnailStrip_base {
|
|
|
41
41
|
disconnectedCallback(): void;
|
|
42
42
|
protected willUpdate(changedProperties: Map<string | number | symbol, unknown>): void;
|
|
43
43
|
updated(changedProperties: Map<string | number | symbol, unknown>): void;
|
|
44
|
-
render():
|
|
44
|
+
render(): lit_html33.TemplateResult<1>;
|
|
45
45
|
}
|
|
46
46
|
declare global {
|
|
47
47
|
interface HTMLElementTagNameMap {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { TreeItem } from "./treeContext.js";
|
|
2
2
|
import "./EFTreeItem.js";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit13 from "lit";
|
|
4
4
|
import { LitElement, PropertyValues } from "lit";
|
|
5
|
-
import * as
|
|
5
|
+
import * as lit_html12 from "lit-html";
|
|
6
6
|
|
|
7
7
|
//#region src/gui/tree/EFTree.d.ts
|
|
8
8
|
|
|
@@ -29,7 +29,7 @@ import * as lit_html13 from "lit-html";
|
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
31
|
declare class EFTree extends LitElement {
|
|
32
|
-
static styles:
|
|
32
|
+
static styles: lit13.CSSResult;
|
|
33
33
|
/** Tree items to display */
|
|
34
34
|
items: TreeItem[];
|
|
35
35
|
/** Optional header text */
|
|
@@ -48,7 +48,7 @@ declare class EFTree extends LitElement {
|
|
|
48
48
|
protected willUpdate(changedProperties: PropertyValues): void;
|
|
49
49
|
protected updated(changedProperties: PropertyValues): void;
|
|
50
50
|
connectedCallback(): void;
|
|
51
|
-
render():
|
|
51
|
+
render(): lit_html12.TemplateResult<1>;
|
|
52
52
|
}
|
|
53
53
|
declare global {
|
|
54
54
|
interface HTMLElementTagNameMap {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TreeContext, TreeItem } from "./treeContext.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit14 from "lit";
|
|
3
3
|
import { LitElement, nothing } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html13 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/tree/EFTreeItem.d.ts
|
|
7
7
|
|
|
@@ -17,7 +17,7 @@ import * as lit_html14 from "lit-html";
|
|
|
17
17
|
* @fires tree-item-click - When item is clicked (for selection)
|
|
18
18
|
*/
|
|
19
19
|
declare class EFTreeItem extends LitElement {
|
|
20
|
-
static styles:
|
|
20
|
+
static styles: lit14.CSSResult;
|
|
21
21
|
treeContext?: TreeContext;
|
|
22
22
|
item: TreeItem;
|
|
23
23
|
private localExpanded;
|
|
@@ -26,7 +26,7 @@ declare class EFTreeItem extends LitElement {
|
|
|
26
26
|
get hasChildren(): boolean;
|
|
27
27
|
private handleClick;
|
|
28
28
|
private handleExpandClick;
|
|
29
|
-
render():
|
|
29
|
+
render(): lit_html13.TemplateResult<1> | typeof nothing;
|
|
30
30
|
}
|
|
31
31
|
declare global {
|
|
32
32
|
interface HTMLElementTagNameMap {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
21
|
"types.json",
|
|
22
|
+
"LICENSE.md",
|
|
22
23
|
"LICENSE-FULL.md"
|
|
23
24
|
],
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"@bramus/style-observer": "^1.3.0",
|
|
26
|
-
"@editframe/assets": "0.
|
|
27
|
+
"@editframe/assets": "0.43.0",
|
|
27
28
|
"@lit/context": "^1.1.6",
|
|
28
29
|
"@opentelemetry/api": "^1.9.0",
|
|
29
30
|
"@opentelemetry/context-zone": "^1.26.0",
|
|
@@ -116,4 +117,4 @@
|
|
|
116
117
|
},
|
|
117
118
|
"main": "./dist/index.js",
|
|
118
119
|
"module": "./dist/index.js"
|
|
119
|
-
}
|
|
120
|
+
}
|