@editframe/elements 0.11.0-beta.9 → 0.12.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EF_FRAMEGEN.d.ts +8 -15
- package/dist/assets/src/MP4File.js +73 -20
- package/dist/elements/EFCaptions.d.ts +50 -6
- package/dist/elements/EFMedia.d.ts +1 -2
- package/dist/elements/EFTimegroup.browsertest.d.ts +4 -0
- package/dist/elements/EFTimegroup.d.ts +23 -2
- package/dist/elements/EFWaveform.d.ts +15 -11
- package/dist/elements/src/EF_FRAMEGEN.js +24 -26
- package/dist/elements/src/elements/EFCaptions.js +295 -42
- package/dist/elements/src/elements/EFImage.js +0 -6
- package/dist/elements/src/elements/EFMedia.js +70 -18
- package/dist/elements/src/elements/EFTemporal.js +13 -10
- package/dist/elements/src/elements/EFTimegroup.js +37 -12
- package/dist/elements/src/elements/EFVideo.js +1 -4
- package/dist/elements/src/elements/EFWaveform.js +250 -143
- package/dist/elements/src/gui/ContextMixin.js +44 -11
- package/dist/elements/src/gui/EFPreview.js +3 -1
- package/dist/elements/src/gui/EFScrubber.js +142 -0
- package/dist/elements/src/gui/EFTimeDisplay.js +81 -0
- package/dist/elements/src/gui/EFTogglePlay.js +11 -19
- package/dist/elements/src/gui/EFWorkbench.js +1 -24
- package/dist/elements/src/gui/TWMixin.css.js +1 -1
- package/dist/elements/src/index.js +8 -1
- package/dist/gui/ContextMixin.d.ts +2 -1
- package/dist/gui/EFScrubber.d.ts +23 -0
- package/dist/gui/EFTimeDisplay.d.ts +17 -0
- package/dist/gui/EFTogglePlay.d.ts +0 -2
- package/dist/gui/EFWorkbench.d.ts +0 -1
- package/dist/index.d.ts +3 -1
- package/dist/style.css +6 -801
- package/package.json +2 -2
- package/src/elements/EFCaptions.browsertest.ts +6 -6
- package/src/elements/EFCaptions.ts +325 -56
- package/src/elements/EFImage.browsertest.ts +4 -17
- package/src/elements/EFImage.ts +0 -6
- package/src/elements/EFMedia.browsertest.ts +10 -19
- package/src/elements/EFMedia.ts +87 -20
- package/src/elements/EFTemporal.browsertest.ts +14 -0
- package/src/elements/EFTemporal.ts +14 -0
- package/src/elements/EFTimegroup.browsertest.ts +37 -0
- package/src/elements/EFTimegroup.ts +42 -17
- package/src/elements/EFVideo.ts +1 -4
- package/src/elements/EFWaveform.ts +339 -314
- package/src/gui/ContextMixin.browsertest.ts +28 -2
- package/src/gui/ContextMixin.ts +52 -14
- package/src/gui/EFPreview.ts +4 -2
- package/src/gui/EFScrubber.ts +145 -0
- package/src/gui/EFTimeDisplay.ts +81 -0
- package/src/gui/EFTogglePlay.ts +19 -25
- package/src/gui/EFWorkbench.ts +3 -36
- package/dist/elements/src/elements/util.js +0 -11
|
@@ -2,10 +2,13 @@ import { LitElement } from "lit";
|
|
|
2
2
|
import { customElement } from "lit/decorators/custom-element.js";
|
|
3
3
|
import { describe, expect, test, vi } from "vitest";
|
|
4
4
|
|
|
5
|
-
import { ContextMixin } from "./ContextMixin.ts";
|
|
6
5
|
import { consume } from "@lit/context";
|
|
6
|
+
import { ContextMixin } from "./ContextMixin.ts";
|
|
7
7
|
import { apiHostContext } from "./apiHostContext.ts";
|
|
8
8
|
|
|
9
|
+
// Required to test timeupdate event, we need a duration, and timegroups are a quick way to do that
|
|
10
|
+
import "../elements/EFTimegroup.ts";
|
|
11
|
+
|
|
9
12
|
@customElement("test-context")
|
|
10
13
|
class TestContext extends ContextMixin(LitElement) {}
|
|
11
14
|
|
|
@@ -49,7 +52,6 @@ describe("ContextMixin", () => {
|
|
|
49
52
|
expect(element.apiHost).toBe("test2");
|
|
50
53
|
});
|
|
51
54
|
});
|
|
52
|
-
|
|
53
55
|
describe("Playback", () => {
|
|
54
56
|
test("should start playback", () => {
|
|
55
57
|
const element = document.createElement("test-context");
|
|
@@ -78,4 +80,28 @@ describe("ContextMixin", () => {
|
|
|
78
80
|
expect(playbackSpy).toHaveBeenCalled();
|
|
79
81
|
});
|
|
80
82
|
});
|
|
83
|
+
|
|
84
|
+
test("Time update event when the currentTimeMs changed", async () => {
|
|
85
|
+
const timegroup = document.createElement("ef-timegroup");
|
|
86
|
+
timegroup.mode = "fixed";
|
|
87
|
+
timegroup.duration = "10s";
|
|
88
|
+
|
|
89
|
+
const preview = document.createElement("test-context");
|
|
90
|
+
preview.append(timegroup);
|
|
91
|
+
document.body.append(preview);
|
|
92
|
+
|
|
93
|
+
type CurrentTimeEvent = CustomEvent<{ currentTimeMs: number }>;
|
|
94
|
+
|
|
95
|
+
// Expect the timeupdate event to be dispatched
|
|
96
|
+
const timeupdatePromise = new Promise<CurrentTimeEvent>((resolve) => {
|
|
97
|
+
preview.addEventListener(
|
|
98
|
+
"timeupdate",
|
|
99
|
+
(event: Event) => resolve(event as CurrentTimeEvent),
|
|
100
|
+
{ once: true },
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
preview.currentTimeMs = 1000;
|
|
104
|
+
const event = await timeupdatePromise;
|
|
105
|
+
expect(event.detail.currentTimeMs).toBe(1000);
|
|
106
|
+
});
|
|
81
107
|
});
|
package/src/gui/ContextMixin.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import type { LitElement } from "lit";
|
|
2
1
|
import { provide } from "@lit/context";
|
|
2
|
+
import type { LitElement } from "lit";
|
|
3
3
|
import { property, state } from "lit/decorators.js";
|
|
4
4
|
|
|
5
|
-
import { focusContext, type FocusContext } from "./focusContext.ts";
|
|
6
|
-
import { focusedElementContext } from "./focusedElementContext.ts";
|
|
7
|
-
import { fetchContext } from "./fetchContext.ts";
|
|
8
5
|
import { createRef } from "lit/directives/ref.js";
|
|
9
|
-
import { loopContext, playingContext } from "./playingContext.ts";
|
|
10
6
|
import type { EFTimegroup } from "../elements/EFTimegroup.ts";
|
|
11
|
-
import { efContext } from "./efContext.ts";
|
|
12
7
|
import { apiHostContext } from "./apiHostContext.ts";
|
|
8
|
+
import { efContext } from "./efContext.ts";
|
|
9
|
+
import { fetchContext } from "./fetchContext.ts";
|
|
10
|
+
import { type FocusContext, focusContext } from "./focusContext.ts";
|
|
11
|
+
import { focusedElementContext } from "./focusedElementContext.ts";
|
|
12
|
+
import { loopContext, playingContext } from "./playingContext.ts";
|
|
13
13
|
|
|
14
|
-
export declare class ContextMixinInterface {
|
|
14
|
+
export declare class ContextMixinInterface extends LitElement {
|
|
15
15
|
signingURL?: string;
|
|
16
16
|
apiHost?: string;
|
|
17
17
|
rendering: boolean;
|
|
@@ -26,9 +26,21 @@ export declare class ContextMixinInterface {
|
|
|
26
26
|
pause(): void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
const contextMixinSymbol = Symbol("contextMixin");
|
|
30
|
+
|
|
31
|
+
export function isContextMixin(value: any): value is ContextMixinInterface {
|
|
32
|
+
return (
|
|
33
|
+
typeof value === "object" &&
|
|
34
|
+
value !== null &&
|
|
35
|
+
contextMixinSymbol in value.constructor
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
type Constructor<T = {}> = new (...args: any[]) => T;
|
|
30
40
|
export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
31
41
|
class ContextElement extends superClass {
|
|
42
|
+
static [contextMixinSymbol] = true;
|
|
43
|
+
|
|
32
44
|
@provide({ context: focusContext })
|
|
33
45
|
focusContext = this as FocusContext;
|
|
34
46
|
|
|
@@ -70,6 +82,8 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
70
82
|
Object.assign(init.headers, {
|
|
71
83
|
authorization: `Bearer ${urlToken}`,
|
|
72
84
|
});
|
|
85
|
+
} else {
|
|
86
|
+
init.credentials = "include";
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
return fetch(url, init);
|
|
@@ -104,6 +118,9 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
104
118
|
stageRef = createRef<HTMLDivElement>();
|
|
105
119
|
canvasRef = createRef<HTMLSlotElement>();
|
|
106
120
|
|
|
121
|
+
#FPS = 30;
|
|
122
|
+
#MS_PER_FRAME = 1000 / this.#FPS;
|
|
123
|
+
|
|
107
124
|
setStageScale = () => {
|
|
108
125
|
if (this.isConnected && !this.rendering) {
|
|
109
126
|
const canvasElement = this.canvasRef.value;
|
|
@@ -119,18 +136,19 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
119
136
|
const canvasHeight = canvasElement.clientHeight;
|
|
120
137
|
const stageRatio = stageWidth / stageHeight;
|
|
121
138
|
const canvasRatio = canvasWidth / canvasHeight;
|
|
139
|
+
|
|
122
140
|
if (stageRatio > canvasRatio) {
|
|
123
141
|
const scale = stageHeight / canvasHeight;
|
|
124
142
|
if (this.stageScale !== scale) {
|
|
125
143
|
canvasElement.style.transform = `scale(${scale})`;
|
|
126
|
-
canvasElement.style.transformOrigin = "
|
|
144
|
+
canvasElement.style.transformOrigin = "center left";
|
|
127
145
|
}
|
|
128
146
|
this.stageScale = scale;
|
|
129
147
|
} else {
|
|
130
148
|
const scale = stageWidth / canvasWidth;
|
|
131
149
|
if (this.stageScale !== scale) {
|
|
132
150
|
canvasElement.style.transform = `scale(${scale})`;
|
|
133
|
-
canvasElement.style.transformOrigin = "
|
|
151
|
+
canvasElement.style.transformOrigin = "center left";
|
|
134
152
|
}
|
|
135
153
|
this.stageScale = scale;
|
|
136
154
|
}
|
|
@@ -164,10 +182,20 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
164
182
|
this.stopPlayback();
|
|
165
183
|
}
|
|
166
184
|
}
|
|
167
|
-
|
|
168
185
|
if (changedProperties.has("currentTimeMs") && this.targetTimegroup) {
|
|
169
186
|
if (this.targetTimegroup.currentTimeMs !== this.currentTimeMs) {
|
|
170
187
|
this.targetTimegroup.currentTimeMs = this.currentTimeMs;
|
|
188
|
+
if (this.isConnected) {
|
|
189
|
+
this.dispatchEvent(
|
|
190
|
+
new CustomEvent("timeupdate", {
|
|
191
|
+
detail: {
|
|
192
|
+
currentTimeMs: this.currentTimeMs,
|
|
193
|
+
progress:
|
|
194
|
+
this.currentTimeMs / this.targetTimegroup.durationMs,
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
171
199
|
}
|
|
172
200
|
}
|
|
173
201
|
super.update(changedProperties);
|
|
@@ -190,8 +218,13 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
190
218
|
#AUDIO_PLAYBACK_SLICE_MS = 1000;
|
|
191
219
|
|
|
192
220
|
#syncPlayheadToAudioContext(target: EFTimegroup, startMs: number) {
|
|
193
|
-
|
|
221
|
+
const rawTimeMs =
|
|
194
222
|
startMs + (this.#playbackAudioContext?.currentTime ?? 0) * 1000;
|
|
223
|
+
const nextTimeMs =
|
|
224
|
+
Math.round(rawTimeMs / this.#MS_PER_FRAME) * this.#MS_PER_FRAME;
|
|
225
|
+
if (nextTimeMs !== this.currentTimeMs) {
|
|
226
|
+
this.currentTimeMs = nextTimeMs;
|
|
227
|
+
}
|
|
195
228
|
this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
|
|
196
229
|
this.#syncPlayheadToAudioContext(target, startMs);
|
|
197
230
|
});
|
|
@@ -219,6 +252,14 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
219
252
|
await timegroup.waitForMediaDurations();
|
|
220
253
|
|
|
221
254
|
let currentMs = timegroup.currentTimeMs;
|
|
255
|
+
const fromMs = currentMs;
|
|
256
|
+
const toMs = timegroup.endTimeMs;
|
|
257
|
+
|
|
258
|
+
if (fromMs >= toMs) {
|
|
259
|
+
this.pause();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
222
263
|
let bufferCount = 0;
|
|
223
264
|
this.#playbackAudioContext = new AudioContext({
|
|
224
265
|
latencyHint: "playback",
|
|
@@ -248,9 +289,6 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
248
289
|
}
|
|
249
290
|
};
|
|
250
291
|
|
|
251
|
-
const fromMs = currentMs;
|
|
252
|
-
const toMs = timegroup.endTimeMs;
|
|
253
|
-
|
|
254
292
|
const queueBufferSource = async () => {
|
|
255
293
|
if (currentMs >= toMs) {
|
|
256
294
|
return false;
|
package/src/gui/EFPreview.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { LitElement,
|
|
1
|
+
import { LitElement, css, html } from "lit";
|
|
2
2
|
import { customElement } from "lit/decorators.js";
|
|
3
3
|
import { ref } from "lit/directives/ref.js";
|
|
4
4
|
|
|
5
|
-
import { TWMixin } from "./TWMixin.ts";
|
|
6
5
|
import { ContextMixin } from "./ContextMixin.ts";
|
|
6
|
+
import { TWMixin } from "./TWMixin.ts";
|
|
7
7
|
|
|
8
8
|
@customElement("ef-preview")
|
|
9
9
|
export class EFPreview extends ContextMixin(TWMixin(LitElement)) {
|
|
@@ -26,7 +26,9 @@ export class EFPreview extends ContextMixin(TWMixin(LitElement)) {
|
|
|
26
26
|
<slot
|
|
27
27
|
${ref(this.canvasRef)}
|
|
28
28
|
class="inline-block"
|
|
29
|
+
name="canvas"
|
|
29
30
|
></slot>
|
|
31
|
+
<slot name="controls"></slot>
|
|
30
32
|
</div>
|
|
31
33
|
`;
|
|
32
34
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { consume } from "@lit/context";
|
|
2
|
+
import { LitElement, css, html } from "lit";
|
|
3
|
+
import { customElement, state } from "lit/decorators.js";
|
|
4
|
+
|
|
5
|
+
import { ref } from "lit/directives/ref.js";
|
|
6
|
+
import type { ContextMixinInterface } from "./ContextMixin.ts";
|
|
7
|
+
import { efContext } from "./efContext.ts";
|
|
8
|
+
import { playingContext } from "./playingContext.ts";
|
|
9
|
+
|
|
10
|
+
@customElement("ef-scrubber")
|
|
11
|
+
export class EFScrubber extends LitElement {
|
|
12
|
+
static styles = [
|
|
13
|
+
css`
|
|
14
|
+
:host {
|
|
15
|
+
--ef-scrubber-height: 4px;
|
|
16
|
+
--ef-scrubber-background: rgb(209 213 219);
|
|
17
|
+
--ef-scrubber-progress-color: rgb(37 99 235);
|
|
18
|
+
--ef-scrubber-handle-size: 12px;
|
|
19
|
+
display: block;
|
|
20
|
+
width: 100%;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.scrubber {
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: var(--ef-scrubber-height);
|
|
26
|
+
background: var(--ef-scrubber-background);
|
|
27
|
+
position: relative;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
border-radius: 2px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.progress {
|
|
33
|
+
position: absolute;
|
|
34
|
+
height: 100%;
|
|
35
|
+
background: var(--ef-scrubber-progress-color);
|
|
36
|
+
border-radius: 2px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.handle {
|
|
40
|
+
position: absolute;
|
|
41
|
+
width: var(--ef-scrubber-handle-size);
|
|
42
|
+
height: var(--ef-scrubber-handle-size);
|
|
43
|
+
background: var(--ef-scrubber-progress-color);
|
|
44
|
+
border-radius: 50%;
|
|
45
|
+
top: 50%;
|
|
46
|
+
transform: translate(-50%, -50%);
|
|
47
|
+
cursor: grab;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Add CSS Shadow Parts */
|
|
51
|
+
::part(scrubber) { }
|
|
52
|
+
::part(progress) { }
|
|
53
|
+
::part(handle) { }
|
|
54
|
+
`,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
@consume({ context: efContext, subscribe: true })
|
|
58
|
+
context?: ContextMixinInterface | null;
|
|
59
|
+
|
|
60
|
+
@consume({ context: playingContext, subscribe: true })
|
|
61
|
+
playing = false;
|
|
62
|
+
|
|
63
|
+
@state()
|
|
64
|
+
private lastTimeUpdateProgress = 0;
|
|
65
|
+
|
|
66
|
+
@state()
|
|
67
|
+
private scrubProgress = 0;
|
|
68
|
+
|
|
69
|
+
@state()
|
|
70
|
+
private isDragging = false;
|
|
71
|
+
|
|
72
|
+
private scrubberRef?: HTMLElement;
|
|
73
|
+
|
|
74
|
+
private updateProgress(e: MouseEvent) {
|
|
75
|
+
if (!this.context || !this.scrubberRef) return;
|
|
76
|
+
|
|
77
|
+
const rect = this.scrubberRef.getBoundingClientRect();
|
|
78
|
+
const x = e.clientX - rect.left;
|
|
79
|
+
const progress = Math.max(0, Math.min(1, x / rect.width));
|
|
80
|
+
|
|
81
|
+
this.scrubProgress = progress;
|
|
82
|
+
this.context.currentTimeMs =
|
|
83
|
+
progress * (this.context.targetTimegroup?.durationMs ?? 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private boundHandleMouseDown = (e: MouseEvent) => {
|
|
87
|
+
this.isDragging = true;
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
this.updateProgress(e);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
private boundHandleMouseMove = (e: MouseEvent) => {
|
|
93
|
+
if (this.isDragging) {
|
|
94
|
+
this.updateProgress(e);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
private boundHandleMouseUp = () => {
|
|
99
|
+
this.isDragging = false;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
render() {
|
|
103
|
+
const displayProgress = this.isDragging
|
|
104
|
+
? this.scrubProgress
|
|
105
|
+
: this.lastTimeUpdateProgress;
|
|
106
|
+
|
|
107
|
+
return html`
|
|
108
|
+
<div
|
|
109
|
+
${ref((el) => {
|
|
110
|
+
this.scrubberRef = el as HTMLElement;
|
|
111
|
+
})}
|
|
112
|
+
part="scrubber"
|
|
113
|
+
class="scrubber"
|
|
114
|
+
@mousedown=${this.boundHandleMouseDown}
|
|
115
|
+
>
|
|
116
|
+
<div class="progress" style="width: ${displayProgress * 100}%"></div>
|
|
117
|
+
<div class="handle" style="left: ${displayProgress * 100}%"></div>
|
|
118
|
+
</div>
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
connectedCallback() {
|
|
123
|
+
super.connectedCallback();
|
|
124
|
+
window.addEventListener("mouseup", this.boundHandleMouseUp);
|
|
125
|
+
window.addEventListener("mousemove", this.boundHandleMouseMove);
|
|
126
|
+
|
|
127
|
+
if (this.context) {
|
|
128
|
+
this.context.addEventListener("timeupdate", (e: Event) => {
|
|
129
|
+
this.lastTimeUpdateProgress = (e as CustomEvent).detail.progress;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
disconnectedCallback() {
|
|
135
|
+
super.disconnectedCallback();
|
|
136
|
+
window.removeEventListener("mouseup", this.boundHandleMouseUp);
|
|
137
|
+
window.removeEventListener("mousemove", this.boundHandleMouseMove);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
declare global {
|
|
142
|
+
interface HTMLElementTagNameMap {
|
|
143
|
+
"ef-scrubber": EFScrubber;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { consume } from "@lit/context";
|
|
2
|
+
import { LitElement, css, html } from "lit";
|
|
3
|
+
import { customElement } from "lit/decorators.js";
|
|
4
|
+
import type { ContextMixinInterface } from "./ContextMixin.ts";
|
|
5
|
+
import { efContext } from "./efContext.ts";
|
|
6
|
+
|
|
7
|
+
@customElement("ef-time-display")
|
|
8
|
+
export class EFTimeDisplay extends LitElement {
|
|
9
|
+
static styles = css`
|
|
10
|
+
:host {
|
|
11
|
+
display: inline-block;
|
|
12
|
+
font-family: var(--ef-font-family, system-ui);
|
|
13
|
+
font-size: var(--ef-font-size-xs, 0.75rem);
|
|
14
|
+
color: var(--ef-text-color, rgb(75 85 99));
|
|
15
|
+
white-space: nowrap;
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
@consume({ context: efContext, subscribe: true })
|
|
20
|
+
context?: ContextMixinInterface | null;
|
|
21
|
+
|
|
22
|
+
private _onTimeUpdate = () => {
|
|
23
|
+
this.requestUpdate();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
connectedCallback(): void {
|
|
27
|
+
super.connectedCallback();
|
|
28
|
+
this.context?.addEventListener(
|
|
29
|
+
"timeupdate",
|
|
30
|
+
this._onTimeUpdate as EventListener,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected updated(changedProperties: Map<PropertyKey, unknown>): void {
|
|
35
|
+
if (changedProperties.has("context")) {
|
|
36
|
+
// Clean up old listener
|
|
37
|
+
const oldContext = changedProperties.get(
|
|
38
|
+
"context",
|
|
39
|
+
) as ContextMixinInterface | null;
|
|
40
|
+
oldContext?.removeEventListener(
|
|
41
|
+
"timeupdate",
|
|
42
|
+
this._onTimeUpdate as EventListener,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Add new listener
|
|
46
|
+
this.context?.addEventListener(
|
|
47
|
+
"timeupdate",
|
|
48
|
+
this._onTimeUpdate as EventListener,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
disconnectedCallback(): void {
|
|
54
|
+
this.context?.removeEventListener("timeupdate", this._onTimeUpdate);
|
|
55
|
+
super.disconnectedCallback();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private formatTime(ms: number): string {
|
|
59
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
60
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
61
|
+
const seconds = totalSeconds % 60;
|
|
62
|
+
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
render() {
|
|
66
|
+
const currentTime = this.context?.currentTimeMs ?? 0;
|
|
67
|
+
const totalTime = this.context?.targetTimegroup?.durationMs ?? 0;
|
|
68
|
+
|
|
69
|
+
return html`
|
|
70
|
+
<span part="time">
|
|
71
|
+
${this.formatTime(currentTime)} / ${this.formatTime(totalTime)}
|
|
72
|
+
</span>
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
declare global {
|
|
78
|
+
interface HTMLElementTagNameMap {
|
|
79
|
+
"ef-time-display": EFTimeDisplay;
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/gui/EFTogglePlay.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { consume } from "@lit/context";
|
|
2
2
|
import { LitElement, css, html } from "lit";
|
|
3
|
-
import { customElement
|
|
3
|
+
import { customElement } from "lit/decorators.js";
|
|
4
4
|
|
|
5
5
|
import type { ContextMixinInterface } from "./ContextMixin.ts";
|
|
6
6
|
import { efContext } from "./efContext.ts";
|
|
7
|
+
import { playingContext } from "./playingContext.ts";
|
|
7
8
|
|
|
8
9
|
@customElement("ef-toggle-play")
|
|
9
10
|
export class EFTogglePlay extends LitElement {
|
|
@@ -19,41 +20,34 @@ export class EFTogglePlay extends LitElement {
|
|
|
19
20
|
@consume({ context: efContext, subscribe: true })
|
|
20
21
|
context?: ContextMixinInterface | null;
|
|
21
22
|
|
|
22
|
-
@
|
|
23
|
-
play = '<button class="text-2xl cursor-pointer">▶️</button>';
|
|
24
|
-
|
|
25
|
-
@property({ type: String })
|
|
26
|
-
pause = '<button class="text-2xl cursor-pointer">⏸️</button>';
|
|
27
|
-
|
|
28
|
-
@state()
|
|
23
|
+
@consume({ context: playingContext, subscribe: true })
|
|
29
24
|
playing = false;
|
|
30
25
|
|
|
31
26
|
render() {
|
|
32
27
|
return html`
|
|
33
28
|
<div
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.playing = true;
|
|
29
|
+
@click=${() => {
|
|
30
|
+
if (this.context) {
|
|
31
|
+
if (this.playing) {
|
|
32
|
+
this.context.pause();
|
|
33
|
+
} else {
|
|
34
|
+
this.context.play();
|
|
35
|
+
}
|
|
42
36
|
}
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
${
|
|
40
|
+
this.playing
|
|
41
|
+
? html`<slot name="pause"></slot>`
|
|
42
|
+
: html`<slot name="play"></slot>`
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.playing
|
|
47
|
-
? html`<slot name="play"></slot>`
|
|
48
|
-
: html`<slot name="pause"></slot>`
|
|
49
|
-
}
|
|
50
|
-
</div>`;
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
51
46
|
}
|
|
52
47
|
|
|
53
48
|
togglePlay() {
|
|
54
|
-
this.requestUpdate();
|
|
55
49
|
if (this.context) {
|
|
56
|
-
if (this.
|
|
50
|
+
if (this.playing) {
|
|
57
51
|
this.context.pause();
|
|
58
52
|
} else {
|
|
59
53
|
this.context.play();
|
package/src/gui/EFWorkbench.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { LitElement,
|
|
2
|
-
import { TaskStatus } from "@lit/task";
|
|
1
|
+
import { LitElement, type PropertyValueMap, css, html } from "lit";
|
|
3
2
|
import { customElement, eventOptions } from "lit/decorators.js";
|
|
4
|
-
import {
|
|
3
|
+
import { createRef, ref } from "lit/directives/ref.js";
|
|
5
4
|
|
|
6
|
-
import { deepGetTemporalElements } from "../elements/EFTemporal.ts";
|
|
7
|
-
import { TWMixin } from "./TWMixin.ts";
|
|
8
|
-
import { shallowGetTimegroups } from "../elements/EFTimegroup.ts";
|
|
9
5
|
import { ContextMixin } from "./ContextMixin.ts";
|
|
6
|
+
import { TWMixin } from "./TWMixin.ts";
|
|
10
7
|
|
|
11
8
|
@customElement("ef-workbench")
|
|
12
9
|
export class EFWorkbench extends ContextMixin(TWMixin(LitElement)) {
|
|
@@ -96,36 +93,6 @@ export class EFWorkbench extends ContextMixin(TWMixin(LitElement)) {
|
|
|
96
93
|
</div>
|
|
97
94
|
`;
|
|
98
95
|
}
|
|
99
|
-
|
|
100
|
-
async stepThrough() {
|
|
101
|
-
const stepDurationMs = 1000 / 30;
|
|
102
|
-
const timegroups = shallowGetTimegroups(this);
|
|
103
|
-
const firstGroup = timegroups[0];
|
|
104
|
-
if (!firstGroup) {
|
|
105
|
-
throw new Error("No temporal elements found");
|
|
106
|
-
}
|
|
107
|
-
firstGroup.currentTimeMs = 0;
|
|
108
|
-
|
|
109
|
-
const temporals = deepGetTemporalElements(this);
|
|
110
|
-
const frameCount = Math.ceil(firstGroup.durationMs / stepDurationMs);
|
|
111
|
-
|
|
112
|
-
const busyTasks = temporals
|
|
113
|
-
.filter((temporal) => temporal.frameTask.status < TaskStatus.COMPLETE)
|
|
114
|
-
.map((temporal) => temporal.frameTask);
|
|
115
|
-
|
|
116
|
-
await Promise.all(busyTasks.map((task) => task.taskComplete));
|
|
117
|
-
|
|
118
|
-
for (let i = 0; i < frameCount; i++) {
|
|
119
|
-
firstGroup.currentTimeMs = i * stepDurationMs;
|
|
120
|
-
await new Promise<void>(queueMicrotask);
|
|
121
|
-
const busyTasks = temporals
|
|
122
|
-
.filter((temporal) => temporal.frameTask.status < TaskStatus.COMPLETE)
|
|
123
|
-
.map((temporal) => temporal.frameTask);
|
|
124
|
-
|
|
125
|
-
await Promise.all(busyTasks.map((task) => task.taskComplete));
|
|
126
|
-
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
96
|
}
|
|
130
97
|
|
|
131
98
|
declare global {
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { EFTimegroup } from "./EFTimegroup.js";
|
|
2
|
-
const getStartTimeMs = (element) => {
|
|
3
|
-
const nearestTimeGroup = element.closest("ef-timegroup");
|
|
4
|
-
if (!(nearestTimeGroup instanceof EFTimegroup)) {
|
|
5
|
-
return 0;
|
|
6
|
-
}
|
|
7
|
-
return nearestTimeGroup.startTimeMs;
|
|
8
|
-
};
|
|
9
|
-
export {
|
|
10
|
-
getStartTimeMs
|
|
11
|
-
};
|