@editframe/elements 0.6.0-beta.16 → 0.6.0-beta.17
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/package.json +2 -2
- package/src/elements/CrossUpdateController.ts +22 -0
- package/src/elements/EFAudio.ts +40 -0
- package/src/elements/EFCaptions.ts +188 -0
- package/src/elements/EFImage.ts +68 -0
- package/src/elements/EFMedia.ts +384 -0
- package/src/elements/EFSourceMixin.ts +57 -0
- package/src/elements/EFTemporal.ts +231 -0
- package/src/elements/EFTimegroup.browsertest.ts +333 -0
- package/src/elements/EFTimegroup.ts +381 -0
- package/src/elements/EFTimeline.ts +13 -0
- package/src/elements/EFVideo.ts +103 -0
- package/src/elements/EFWaveform.ts +409 -0
- package/src/elements/FetchMixin.ts +19 -0
- package/src/elements/TimegroupController.ts +25 -0
- package/src/elements/durationConverter.ts +6 -0
- package/src/elements/parseTimeToMs.ts +9 -0
- package/src/elements/util.ts +24 -0
- package/src/gui/EFFilmstrip.ts +766 -0
- package/src/gui/EFWorkbench.ts +231 -0
- package/src/gui/TWMixin.css +3 -0
- package/src/gui/TWMixin.ts +30 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { describe, test, assert, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
LitElement,
|
|
4
|
+
type TemplateResult,
|
|
5
|
+
html,
|
|
6
|
+
render as litRender,
|
|
7
|
+
} from "lit";
|
|
8
|
+
import { EFTimegroup } from "./EFTimegroup";
|
|
9
|
+
import "./EFTimegroup";
|
|
10
|
+
import { customElement } from "lit/decorators/custom-element.js";
|
|
11
|
+
import { EFTemporal } from "./EFTemporal";
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
15
|
+
const key = localStorage.key(i);
|
|
16
|
+
if (typeof key !== "string") continue;
|
|
17
|
+
localStorage.removeItem(key);
|
|
18
|
+
}
|
|
19
|
+
while (document.body.children.length) {
|
|
20
|
+
document.body.children[0]?.remove();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
@customElement("test-temporal")
|
|
25
|
+
class TestTemporal extends EFTemporal(LitElement) {
|
|
26
|
+
get hasOwnDuration(): boolean {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare global {
|
|
32
|
+
interface HTMLElementTagNameMap {
|
|
33
|
+
"test-temporal": TestTemporal;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const renderTimegroup = (result: TemplateResult) => {
|
|
38
|
+
const container = document.createElement("div");
|
|
39
|
+
litRender(result, container);
|
|
40
|
+
const firstChild = container.firstElementChild;
|
|
41
|
+
if (!firstChild) {
|
|
42
|
+
throw new Error("No first child found");
|
|
43
|
+
}
|
|
44
|
+
if (!(firstChild instanceof EFTimegroup)) {
|
|
45
|
+
throw new Error("First child is not an EFTimegroup");
|
|
46
|
+
}
|
|
47
|
+
document.body.appendChild(firstChild);
|
|
48
|
+
return firstChild;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
describe(`<ef-timegroup mode="fixed">`, () => {
|
|
52
|
+
test("can explicitly set a duration in seconds", async () => {
|
|
53
|
+
const timegroup = renderTimegroup(
|
|
54
|
+
html`<ef-timegroup mode="fixed" duration="10s"></ef-timegroup>`,
|
|
55
|
+
);
|
|
56
|
+
assert.equal(timegroup.durationMs, 10_000);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("can explicitly set a duration in milliseconds", async () => {
|
|
60
|
+
const timegroup = renderTimegroup(
|
|
61
|
+
html`<ef-timegroup mode="fixed" duration="10ms"></ef-timegroup>`,
|
|
62
|
+
);
|
|
63
|
+
assert.equal(timegroup.durationMs, 10);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe(`<ef-timegroup mode="sequence">`, () => {
|
|
68
|
+
test("fixed duration is ignored", () => {
|
|
69
|
+
const timegroup = renderTimegroup(
|
|
70
|
+
html`<ef-timegroup mode="sequence" duration="10s"></ef-timegroup>`,
|
|
71
|
+
);
|
|
72
|
+
assert.equal(timegroup.durationMs, 0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("duration is the sum of child time groups", async () => {
|
|
76
|
+
const timegroup = renderTimegroup(
|
|
77
|
+
html`
|
|
78
|
+
<ef-timegroup mode="sequence">
|
|
79
|
+
<ef-timegroup mode="fixed" duration="5s"></ef-timegroup>
|
|
80
|
+
<ef-timegroup mode="fixed" duration="5s"></ef-timegroup>
|
|
81
|
+
</ef-timegroup>
|
|
82
|
+
`,
|
|
83
|
+
);
|
|
84
|
+
assert.equal(timegroup.durationMs, 10_000);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("duration can include any element with a durationMs value", async () => {
|
|
88
|
+
const timegroup = renderTimegroup(
|
|
89
|
+
html`
|
|
90
|
+
<ef-timegroup mode="sequence">
|
|
91
|
+
<ef-timegroup mode="fixed" duration="5s"></ef-timegroup>
|
|
92
|
+
<test-temporal duration="5s"></test-temporal>
|
|
93
|
+
</ef-timegroup>
|
|
94
|
+
`,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
assert.equal(timegroup.durationMs, 10_000);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("arbitrary html does not factor into the calculation of a sequence duration", () => {
|
|
101
|
+
const timegroup = renderTimegroup(
|
|
102
|
+
html`
|
|
103
|
+
<ef-timegroup mode="sequence">
|
|
104
|
+
<div>
|
|
105
|
+
<ef-timegroup mode="fixed" duration="5s"></ef-timegroup>
|
|
106
|
+
</div>
|
|
107
|
+
</ef-timegroup>
|
|
108
|
+
`,
|
|
109
|
+
);
|
|
110
|
+
assert.equal(timegroup.durationMs, 5_000);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("nested time groups do not factor into the calculation of a sequence duration", async () => {
|
|
114
|
+
const timegroup = renderTimegroup(
|
|
115
|
+
html`
|
|
116
|
+
<ef-timegroup mode="sequence">
|
|
117
|
+
<ef-timegroup mode="fixed" duration="5s">
|
|
118
|
+
<ef-timegroup mode="fixed" duration="5s"></ef-timegroup>
|
|
119
|
+
</ef-timegroup>
|
|
120
|
+
</ef-timegroup>
|
|
121
|
+
`,
|
|
122
|
+
);
|
|
123
|
+
assert.equal(timegroup.durationMs, 5_000);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe(`<ef-timegroup mode="contain">`, () => {
|
|
128
|
+
test("fixed duration is ignored", () => {
|
|
129
|
+
const timegroup = renderTimegroup(
|
|
130
|
+
html`<ef-timegroup mode="contain" duration="10s"></ef-timegroup>`,
|
|
131
|
+
);
|
|
132
|
+
assert.equal(timegroup.durationMs, 0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("duration is the maximum of it's child time groups", async () => {
|
|
136
|
+
const timegroup = renderTimegroup(
|
|
137
|
+
html`
|
|
138
|
+
<ef-timegroup mode="contain">
|
|
139
|
+
<ef-timegroup mode="fixed" duration="5s"></ef-timegroup>
|
|
140
|
+
<ef-timegroup mode="fixed" duration="10s"></ef-timegroup>
|
|
141
|
+
</ef-timegroup>
|
|
142
|
+
`,
|
|
143
|
+
);
|
|
144
|
+
assert.equal(timegroup.durationMs, 10_000);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("startTimeMs", () => {
|
|
149
|
+
test("is computed relative to the root time group", async () => {
|
|
150
|
+
const timegroup = renderTimegroup(
|
|
151
|
+
html`<ef-timegroup id="root" mode="sequence">
|
|
152
|
+
<ef-timegroup id="a" mode="fixed" duration="5s"></ef-timegroup>
|
|
153
|
+
<ef-timegroup id="b" mode="sequence">
|
|
154
|
+
<ef-timegroup id="c" mode="fixed" duration="5s"></ef-timegroup>
|
|
155
|
+
<ef-timegroup id="d" mode="contain">
|
|
156
|
+
<ef-timegroup id="e" mode="fixed" duration="5s"></ef-timegroup>
|
|
157
|
+
<ef-timegroup id="f" mode="fixed" duration="5s"></ef-timegroup>
|
|
158
|
+
</ef-timegroup>
|
|
159
|
+
</ef-timegroup>
|
|
160
|
+
</ef-timegroup>`,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const a = timegroup.querySelector("#a") as EFTimegroup;
|
|
164
|
+
const b = timegroup.querySelector("#b") as EFTimegroup;
|
|
165
|
+
const c = timegroup.querySelector("#c") as EFTimegroup;
|
|
166
|
+
const d = timegroup.querySelector("#d") as EFTimegroup;
|
|
167
|
+
const e = timegroup.querySelector("#e") as EFTimegroup;
|
|
168
|
+
const f = timegroup.querySelector("#f") as EFTimegroup;
|
|
169
|
+
|
|
170
|
+
assert.equal(a.startTimeMs, 0);
|
|
171
|
+
assert.equal(b.startTimeMs, 5_000);
|
|
172
|
+
assert.equal(c.startTimeMs, 5_000);
|
|
173
|
+
assert.equal(d.startTimeMs, 10_000);
|
|
174
|
+
assert.equal(e.startTimeMs, 10_000);
|
|
175
|
+
assert.equal(f.startTimeMs, 10_000);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// // TODO: Rethink offset math, it shouldn't effect the duration of a temporal item
|
|
179
|
+
// // but actually change the start and end time.
|
|
180
|
+
// litTest.skip("can be offset with offset attribute", async ({ container }) => {
|
|
181
|
+
// render(
|
|
182
|
+
// html`<ef-timegroup id="root" mode="contain">
|
|
183
|
+
// <test-temporal id="a" duration="5s" offset="5s"></test-temporal>
|
|
184
|
+
// </ef-timegroup> `,
|
|
185
|
+
// container,
|
|
186
|
+
// );
|
|
187
|
+
|
|
188
|
+
// const root = container.querySelector("#root") as EFTimegroup;
|
|
189
|
+
// const a = container.querySelector("#a") as TestTemporal;
|
|
190
|
+
|
|
191
|
+
// assert.equal(a.durationMs, 5_000);
|
|
192
|
+
// assert.equal(root.durationMs, 10_000);
|
|
193
|
+
// assert.equal(a.startTimeMs, 5_000);
|
|
194
|
+
// });
|
|
195
|
+
|
|
196
|
+
// litTest.skip(
|
|
197
|
+
// "offsets do not affect start time when in a sequence group",
|
|
198
|
+
// async ({ container }) => {
|
|
199
|
+
// render(
|
|
200
|
+
// html`<ef-timegroup id="root" mode="sequence">
|
|
201
|
+
// <test-temporal id="a" duration="5s"></test-temporal>
|
|
202
|
+
// <test-temporal id="b" duration="5s" offset="5s"></test-temporal>
|
|
203
|
+
// </ef-timegroup> `,
|
|
204
|
+
// container,
|
|
205
|
+
// );
|
|
206
|
+
|
|
207
|
+
// const root = container.querySelector("#root") as EFTimegroup;
|
|
208
|
+
// const a = container.querySelector("#a") as TestTemporal;
|
|
209
|
+
// const b = container.querySelector("#b") as TestTemporal;
|
|
210
|
+
|
|
211
|
+
// assert.equal(root.durationMs, 10_000);
|
|
212
|
+
// assert.equal(a.startTimeMs, 0);
|
|
213
|
+
// assert.equal(b.startTimeMs, 5_000);
|
|
214
|
+
// },
|
|
215
|
+
// );
|
|
216
|
+
|
|
217
|
+
test("temporal elements default to expand to fill a timegroup", async () => {
|
|
218
|
+
const timegroup = renderTimegroup(
|
|
219
|
+
html`<ef-timegroup id="root" mode="fixed" duration="10s">
|
|
220
|
+
<test-temporal id="a"></test-temporal>
|
|
221
|
+
</ef-timegroup> `,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const a = timegroup.querySelector("#a") as TestTemporal;
|
|
225
|
+
|
|
226
|
+
assert.equal(timegroup.durationMs, 10_000);
|
|
227
|
+
assert.equal(a.durationMs, 10_000);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("element's parentTimegroup updates as they move", async () => {
|
|
231
|
+
const container = document.createElement("div");
|
|
232
|
+
document.body.appendChild(container);
|
|
233
|
+
const timegroup1 = document.createElement("ef-timegroup");
|
|
234
|
+
timegroup1.setAttribute("mode", "fixed");
|
|
235
|
+
timegroup1.setAttribute("duration", "5s");
|
|
236
|
+
|
|
237
|
+
const timegroup2 = document.createElement("ef-timegroup");
|
|
238
|
+
timegroup2.setAttribute("mode", "fixed");
|
|
239
|
+
timegroup2.setAttribute("duration", "5s");
|
|
240
|
+
|
|
241
|
+
container.appendChild(timegroup1);
|
|
242
|
+
container.appendChild(timegroup2);
|
|
243
|
+
|
|
244
|
+
const temporal = document.createElement("test-temporal");
|
|
245
|
+
|
|
246
|
+
timegroup1.appendChild(temporal);
|
|
247
|
+
|
|
248
|
+
assert.equal(temporal.parentTimegroup, timegroup1);
|
|
249
|
+
|
|
250
|
+
timegroup2.appendChild(temporal);
|
|
251
|
+
assert.equal(temporal.parentTimegroup, timegroup2);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("elements can access their root temporal element", async () => {
|
|
255
|
+
const root = renderTimegroup(
|
|
256
|
+
html`<ef-timegroup id="root" mode="contain" duration="10s">
|
|
257
|
+
<ef-timegroup id="a" mode="contain">
|
|
258
|
+
<div>
|
|
259
|
+
<ef-timegroup id="b" mode="contain">
|
|
260
|
+
<div>
|
|
261
|
+
<test-temporal id="c" duration="5s"></test-temporal>
|
|
262
|
+
</div>
|
|
263
|
+
</ef-timegroup>
|
|
264
|
+
</div>
|
|
265
|
+
</ef-timegroup>
|
|
266
|
+
</ef-timegroup> `,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const a = root.querySelector("#a") as EFTimegroup;
|
|
270
|
+
const b = root.querySelector("#b") as EFTimegroup;
|
|
271
|
+
const c = root.querySelector("#c") as TestTemporal;
|
|
272
|
+
|
|
273
|
+
assert.equal(root.rootTimegroup, root);
|
|
274
|
+
|
|
275
|
+
assert.equal(a.rootTimegroup, root);
|
|
276
|
+
assert.equal(b.rootTimegroup, root);
|
|
277
|
+
assert.equal(c.rootTimegroup, root);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("setting currentTime", () => {
|
|
282
|
+
test("persists in localStorage if the timegroup has an id and is in the dom", async () => {
|
|
283
|
+
const timegroup = renderTimegroup(
|
|
284
|
+
html`<ef-timegroup id="root" mode="fixed" duration="10s"></ef-timegroup>`,
|
|
285
|
+
);
|
|
286
|
+
document.body.appendChild(timegroup);
|
|
287
|
+
assert.isNull(localStorage.getItem(timegroup.storageKey));
|
|
288
|
+
timegroup.currentTime = 5_000;
|
|
289
|
+
assert.equal(localStorage.getItem(timegroup.storageKey), "5000");
|
|
290
|
+
timegroup.remove();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("does not persist in localStorage if the timegroup has no id", async () => {
|
|
294
|
+
const timegroup = renderTimegroup(
|
|
295
|
+
html`<ef-timegroup mode="fixed" duration="10s"></ef-timegroup>`,
|
|
296
|
+
);
|
|
297
|
+
document.body.appendChild(timegroup);
|
|
298
|
+
timegroup.currentTime = 5_000;
|
|
299
|
+
timegroup.removeAttribute("id");
|
|
300
|
+
assert.throws(() => {
|
|
301
|
+
assert.isNull(localStorage.getItem(timegroup.storageKey));
|
|
302
|
+
}, "Timegroup must have an id to use localStorage");
|
|
303
|
+
timegroup.remove();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("nested items derive their ownCurrentTimeMs", async () => {
|
|
307
|
+
const timegroup = renderTimegroup(
|
|
308
|
+
html`
|
|
309
|
+
<ef-timegroup id="root" mode="sequence">
|
|
310
|
+
<ef-timegroup id="a" mode="fixed" duration="5s"> </ef-timegroup>
|
|
311
|
+
<ef-timegroup id="b" mode="fixed" duration="5s"> </ef-timegroup>
|
|
312
|
+
</ef-timegroup>
|
|
313
|
+
`,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const root = timegroup;
|
|
317
|
+
const a = timegroup.querySelector("#a") as EFTimegroup;
|
|
318
|
+
const b = timegroup.querySelector("#b") as EFTimegroup;
|
|
319
|
+
|
|
320
|
+
assert.equal(a.ownCurrentTimeMs, 0);
|
|
321
|
+
assert.equal(b.ownCurrentTimeMs, 0);
|
|
322
|
+
|
|
323
|
+
root.currentTimeMs = 2_500;
|
|
324
|
+
|
|
325
|
+
assert.equal(a.ownCurrentTimeMs, 2_500);
|
|
326
|
+
assert.equal(b.ownCurrentTimeMs, 0);
|
|
327
|
+
|
|
328
|
+
root.currentTimeMs = 7_500;
|
|
329
|
+
|
|
330
|
+
assert.equal(a.ownCurrentTimeMs, 5_000);
|
|
331
|
+
assert.equal(b.ownCurrentTimeMs, 2_500);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { LitElement, html, css, type PropertyValueMap } from "lit";
|
|
2
|
+
import { provide } from "@lit/context";
|
|
3
|
+
import { customElement, property } from "lit/decorators.js";
|
|
4
|
+
import {
|
|
5
|
+
EFTemporal,
|
|
6
|
+
isEFTemporal,
|
|
7
|
+
shallowGetTemporalElements,
|
|
8
|
+
timegroupContext,
|
|
9
|
+
} from "./EFTemporal";
|
|
10
|
+
import { TimegroupController } from "./TimegroupController";
|
|
11
|
+
import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
|
|
12
|
+
import { deepGetMediaElements } from "./EFMedia";
|
|
13
|
+
import { Task } from "@lit/task";
|
|
14
|
+
|
|
15
|
+
export const shallowGetTimegroups = (
|
|
16
|
+
element: Element,
|
|
17
|
+
groups: EFTimegroup[] = [],
|
|
18
|
+
) => {
|
|
19
|
+
for (const child of Array.from(element.children)) {
|
|
20
|
+
if (child instanceof EFTimegroup) {
|
|
21
|
+
groups.push(child);
|
|
22
|
+
} else {
|
|
23
|
+
shallowGetTimegroups(child, groups);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return groups;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
@customElement("ef-timegroup")
|
|
30
|
+
export class EFTimegroup extends EFTemporal(LitElement) {
|
|
31
|
+
static styles = css`
|
|
32
|
+
:host {
|
|
33
|
+
display: block;
|
|
34
|
+
width: 100%;
|
|
35
|
+
height: 100%;
|
|
36
|
+
position: relative;
|
|
37
|
+
top: 0;
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
@provide({ context: timegroupContext })
|
|
42
|
+
_timeGroupContext = this;
|
|
43
|
+
|
|
44
|
+
#currentTime = 0;
|
|
45
|
+
|
|
46
|
+
@property({
|
|
47
|
+
type: String,
|
|
48
|
+
attribute: "mode",
|
|
49
|
+
})
|
|
50
|
+
mode: "fixed" | "sequence" | "contain" = "sequence";
|
|
51
|
+
|
|
52
|
+
@property({ type: Number })
|
|
53
|
+
set currentTime(time: number) {
|
|
54
|
+
this.#currentTime = Math.max(0, Math.min(time, this.durationMs / 1000));
|
|
55
|
+
try {
|
|
56
|
+
if (this.id) {
|
|
57
|
+
if (this.isConnected) {
|
|
58
|
+
localStorage.setItem(this.storageKey, time.toString());
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn("Failed to save time to localStorage", error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
get currentTime() {
|
|
66
|
+
return this.#currentTime;
|
|
67
|
+
}
|
|
68
|
+
get currentTimeMs() {
|
|
69
|
+
return this.currentTime * 1000;
|
|
70
|
+
}
|
|
71
|
+
set currentTimeMs(ms: number) {
|
|
72
|
+
this.currentTime = ms / 1000;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@property({
|
|
76
|
+
attribute: "crossover",
|
|
77
|
+
converter: {
|
|
78
|
+
fromAttribute: (value: string): number => {
|
|
79
|
+
if (value.endsWith("ms")) {
|
|
80
|
+
return Number.parseFloat(value);
|
|
81
|
+
}
|
|
82
|
+
if (value.endsWith("s")) {
|
|
83
|
+
return Number.parseFloat(value) * 1000;
|
|
84
|
+
}
|
|
85
|
+
throw new Error(
|
|
86
|
+
"`crossover` MUST be in milliseconds or seconds (10s, 10000ms)",
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
toAttribute: (value: number) => `${value}ms`,
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
crossoverMs = 0;
|
|
93
|
+
|
|
94
|
+
render() {
|
|
95
|
+
return html`<slot></slot> `;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
maybeLoadTimeFromLocalStorage() {
|
|
99
|
+
if (this.id) {
|
|
100
|
+
try {
|
|
101
|
+
return Number.parseFloat(localStorage.getItem(this.storageKey) || "0");
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.warn("Failed to load time from localStorage", error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
connectedCallback() {
|
|
110
|
+
super.connectedCallback();
|
|
111
|
+
if (this.id) {
|
|
112
|
+
this.#currentTime = this.maybeLoadTimeFromLocalStorage();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.parentTimegroup) {
|
|
116
|
+
new TimegroupController(this.parentTimegroup, this);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (this.shouldWrapWithWorkbench()) {
|
|
120
|
+
this.wrapWithWorkbench();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get storageKey() {
|
|
125
|
+
if (!this.id) {
|
|
126
|
+
throw new Error("Timegroup must have an id to use localStorage.");
|
|
127
|
+
}
|
|
128
|
+
return `ef-timegroup-${this.id}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get crossoverStartMs() {
|
|
132
|
+
const parentTimeGroup = this.parentTimegroup;
|
|
133
|
+
if (!parentTimeGroup || !this.previousElementSibling) {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return parentTimeGroup.crossoverMs;
|
|
138
|
+
}
|
|
139
|
+
get crossoverEndMs() {
|
|
140
|
+
const parentTimeGroup = this.parentTimegroup;
|
|
141
|
+
if (!parentTimeGroup || !this.nextElementSibling) {
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return parentTimeGroup.crossoverMs;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get durationMs() {
|
|
149
|
+
switch (this.mode) {
|
|
150
|
+
case "fixed":
|
|
151
|
+
return super.durationMs || 0;
|
|
152
|
+
case "sequence": {
|
|
153
|
+
let duration = 0;
|
|
154
|
+
for (const node of this.childTemporals) {
|
|
155
|
+
duration += node.durationMs;
|
|
156
|
+
}
|
|
157
|
+
return duration;
|
|
158
|
+
}
|
|
159
|
+
case "contain": {
|
|
160
|
+
let maxDuration = 0;
|
|
161
|
+
for (const node of this.childTemporals) {
|
|
162
|
+
if (node.hasOwnDuration) {
|
|
163
|
+
maxDuration = Math.max(maxDuration, node.durationMs);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return maxDuration;
|
|
167
|
+
}
|
|
168
|
+
default:
|
|
169
|
+
throw new Error(`Invalid time mode: ${this.mode}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async waitForMediaDurations() {
|
|
174
|
+
return await Promise.all(
|
|
175
|
+
deepGetMediaElements(this).map(
|
|
176
|
+
(media) => media.trackFragmentIndexLoader.taskComplete,
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
get childTemporals() {
|
|
182
|
+
return shallowGetTemporalElements(this);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
protected updated(
|
|
186
|
+
changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
|
|
187
|
+
): void {
|
|
188
|
+
super.updated(changedProperties);
|
|
189
|
+
if (
|
|
190
|
+
changedProperties.has("currentTime") ||
|
|
191
|
+
changedProperties.has("ownCurrentTimeMs")
|
|
192
|
+
) {
|
|
193
|
+
const timelineTimeMs = (this.rootTimegroup ?? this).currentTimeMs;
|
|
194
|
+
if (
|
|
195
|
+
this.startTimeMs > timelineTimeMs ||
|
|
196
|
+
this.endTimeMs < timelineTimeMs
|
|
197
|
+
) {
|
|
198
|
+
this.style.display = "none";
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.style.display = "";
|
|
202
|
+
|
|
203
|
+
const animations = this.getAnimations({ subtree: true });
|
|
204
|
+
this.style.setProperty(
|
|
205
|
+
"--ef-duration",
|
|
206
|
+
`${this.durationMs + this.crossoverEndMs + this.crossoverStartMs}ms`,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
for (const animation of animations) {
|
|
210
|
+
if (animation.playState === "running") {
|
|
211
|
+
animation.pause();
|
|
212
|
+
}
|
|
213
|
+
const effect = animation.effect;
|
|
214
|
+
if (!(effect && effect instanceof KeyframeEffect)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const target = effect.target;
|
|
218
|
+
// TODO: better generalize work avoidance for temporal elements
|
|
219
|
+
if (!target) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (target.closest("ef-timegroup") !== this) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Important to avoid going to the end of the animation
|
|
227
|
+
// or it will reset awkwardly.
|
|
228
|
+
if (isEFTemporal(target)) {
|
|
229
|
+
const timing = effect.getTiming();
|
|
230
|
+
const duration = Number(timing.duration) ?? 0;
|
|
231
|
+
const delay = Number(timing.delay);
|
|
232
|
+
const newTime = Math.floor(
|
|
233
|
+
Math.min(target.ownCurrentTimeMs, duration - 1 + delay),
|
|
234
|
+
);
|
|
235
|
+
if (Number.isNaN(newTime)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
animation.currentTime = newTime;
|
|
239
|
+
} else if (target) {
|
|
240
|
+
const nearestTimegroup = target.closest("ef-timegroup");
|
|
241
|
+
if (!nearestTimegroup) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const timing = effect.getTiming();
|
|
245
|
+
const duration = Number(timing.duration) ?? 0;
|
|
246
|
+
const delay = Number(timing.delay);
|
|
247
|
+
const newTime = Math.floor(
|
|
248
|
+
Math.min(nearestTimegroup.ownCurrentTimeMs, duration - 1 + delay),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
if (Number.isNaN(newTime)) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
animation.currentTime = newTime;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
shouldWrapWithWorkbench() {
|
|
261
|
+
return (
|
|
262
|
+
EF_INTERACTIVE &&
|
|
263
|
+
this.closest("ef-timegroup") === this &&
|
|
264
|
+
this.closest("ef-workbench") === null
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
wrapWithWorkbench() {
|
|
269
|
+
const workbench = document.createElement("ef-workbench");
|
|
270
|
+
document.body.append(workbench);
|
|
271
|
+
if (!this.hasAttribute("id")) {
|
|
272
|
+
this.setAttribute("id", "root-this");
|
|
273
|
+
}
|
|
274
|
+
this.setAttribute("slot", "canvas");
|
|
275
|
+
workbench.append(this as unknown as Element);
|
|
276
|
+
|
|
277
|
+
const filmstrip = document.createElement("ef-filmstrip");
|
|
278
|
+
filmstrip.setAttribute("slot", "timeline");
|
|
279
|
+
filmstrip.setAttribute("target", `#${this.id}`);
|
|
280
|
+
workbench.append(filmstrip);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get hasOwnDuration() {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
get efElements() {
|
|
288
|
+
return Array.from(
|
|
289
|
+
this.querySelectorAll(
|
|
290
|
+
"ef-audio, ef-video, ef-image, ef-captions, ef-waveform",
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async renderAudio(fromMs: number, toMs: number) {
|
|
296
|
+
await this.waitForMediaDurations();
|
|
297
|
+
|
|
298
|
+
const durationMs = toMs - fromMs;
|
|
299
|
+
const audioContext = new OfflineAudioContext(
|
|
300
|
+
2,
|
|
301
|
+
Math.round((48000 * durationMs) / 1000),
|
|
302
|
+
48000,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
await Promise.all(
|
|
306
|
+
deepGetMediaElements(this).map(async (mediaElement) => {
|
|
307
|
+
await mediaElement.trackFragmentIndexLoader.taskComplete;
|
|
308
|
+
|
|
309
|
+
const mediaStartsBeforeEnd = mediaElement.startTimeMs <= toMs;
|
|
310
|
+
const mediaEndsAfterStart = mediaElement.endTimeMs >= fromMs;
|
|
311
|
+
const mediaOverlaps = mediaStartsBeforeEnd && mediaEndsAfterStart;
|
|
312
|
+
if (!mediaOverlaps || mediaElement.defaultAudioTrackId === undefined) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const audio = await mediaElement.fetchAudioSpanningTime(fromMs, toMs);
|
|
317
|
+
if (!audio) {
|
|
318
|
+
throw new Error("Failed to fetch audio");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const ctxStartMs = Math.max(0, mediaElement.startTimeMs - fromMs);
|
|
322
|
+
const ctxEndMs = Math.min(durationMs, mediaElement.endTimeMs - fromMs);
|
|
323
|
+
const ctxDurationMs = ctxEndMs - ctxStartMs;
|
|
324
|
+
|
|
325
|
+
const offset =
|
|
326
|
+
Math.max(0, fromMs - mediaElement.startTimeMs) - audio.startMs;
|
|
327
|
+
|
|
328
|
+
const bufferSource = audioContext.createBufferSource();
|
|
329
|
+
bufferSource.buffer = await audioContext.decodeAudioData(
|
|
330
|
+
await audio.blob.arrayBuffer(),
|
|
331
|
+
);
|
|
332
|
+
bufferSource.connect(audioContext.destination);
|
|
333
|
+
|
|
334
|
+
bufferSource.start(
|
|
335
|
+
ctxStartMs / 1000,
|
|
336
|
+
offset / 1000,
|
|
337
|
+
ctxDurationMs / 1000,
|
|
338
|
+
);
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
return await audioContext.startRendering();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async loadMd5Sums() {
|
|
346
|
+
const efElements = this.efElements;
|
|
347
|
+
const loaderTasks: Promise<any>[] = [];
|
|
348
|
+
for (const el of efElements) {
|
|
349
|
+
const md5SumLoader = (el as any).md5SumLoader;
|
|
350
|
+
if (md5SumLoader instanceof Task) {
|
|
351
|
+
md5SumLoader.run();
|
|
352
|
+
loaderTasks.push(md5SumLoader.taskComplete);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
await Promise.all(loaderTasks);
|
|
357
|
+
|
|
358
|
+
efElements.map((el) => {
|
|
359
|
+
if ("productionSrc" in el && el.productionSrc instanceof Function) {
|
|
360
|
+
el.setAttribute("src", el.productionSrc());
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
frameTask = new Task(this, {
|
|
366
|
+
autoRun: EF_INTERACTIVE,
|
|
367
|
+
args: () => [this.ownCurrentTimeMs, this.currentTimeMs] as const,
|
|
368
|
+
task: async ([], { signal: _signal }) => {
|
|
369
|
+
let fullyUpdated = await this.updateComplete;
|
|
370
|
+
while (!fullyUpdated) {
|
|
371
|
+
fullyUpdated = await this.updateComplete;
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
declare global {
|
|
378
|
+
interface HTMLElementTagNameMap {
|
|
379
|
+
"ef-timegroup": EFTimegroup & Element;
|
|
380
|
+
}
|
|
381
|
+
}
|