@editframe/elements 0.7.0-beta.9 → 0.8.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.
Files changed (107) hide show
  1. package/dist/EF_FRAMEGEN.d.ts +43 -0
  2. package/dist/EF_INTERACTIVE.d.ts +1 -0
  3. package/dist/assets/dist/EncodedAsset.js +560 -0
  4. package/dist/assets/dist/MP4File.js +170 -0
  5. package/dist/assets/dist/memoize.js +14 -0
  6. package/dist/elements/CrossUpdateController.d.ts +8 -0
  7. package/dist/elements/EFAudio.d.ts +9 -0
  8. package/dist/elements/EFCaptions.d.ts +38 -0
  9. package/dist/elements/EFImage.d.ts +13 -0
  10. package/dist/elements/EFMedia.d.ts +63 -0
  11. package/dist/elements/EFSourceMixin.d.ts +11 -0
  12. package/dist/elements/EFTemporal.d.ts +40 -0
  13. package/dist/elements/EFTimegroup.browsertest.d.ts +11 -0
  14. package/dist/elements/EFTimegroup.d.ts +36 -0
  15. package/dist/elements/EFVideo.d.ts +13 -0
  16. package/dist/elements/EFWaveform.d.ts +29 -0
  17. package/dist/elements/FetchMixin.d.ts +7 -0
  18. package/dist/elements/TimegroupController.d.ts +13 -0
  19. package/dist/elements/durationConverter.d.ts +12 -0
  20. package/dist/elements/parseTimeToMs.d.ts +1 -0
  21. package/{src/EF_FRAMEGEN.ts → dist/elements/src/EF_FRAMEGEN.js} +35 -115
  22. package/dist/elements/src/EF_INTERACTIVE.js +7 -0
  23. package/dist/elements/src/elements/CrossUpdateController.js +16 -0
  24. package/dist/elements/src/elements/EFAudio.js +54 -0
  25. package/dist/elements/src/elements/EFCaptions.js +169 -0
  26. package/dist/elements/src/elements/EFImage.js +80 -0
  27. package/dist/elements/src/elements/EFMedia.js +356 -0
  28. package/dist/elements/src/elements/EFSourceMixin.js +55 -0
  29. package/dist/elements/src/elements/EFTemporal.js +283 -0
  30. package/dist/elements/src/elements/EFTimegroup.js +338 -0
  31. package/dist/elements/src/elements/EFVideo.js +110 -0
  32. package/dist/elements/src/elements/EFWaveform.js +226 -0
  33. package/dist/elements/src/elements/FetchMixin.js +28 -0
  34. package/dist/elements/src/elements/TimegroupController.js +20 -0
  35. package/dist/elements/src/elements/durationConverter.js +8 -0
  36. package/dist/elements/src/elements/parseTimeToMs.js +13 -0
  37. package/dist/elements/src/elements/util.js +11 -0
  38. package/dist/elements/src/gui/ContextMixin.js +246 -0
  39. package/dist/elements/src/gui/EFFilmstrip.js +731 -0
  40. package/dist/elements/src/gui/EFPreview.js +45 -0
  41. package/dist/elements/src/gui/EFToggleLoop.js +39 -0
  42. package/dist/elements/src/gui/EFTogglePlay.js +43 -0
  43. package/dist/elements/src/gui/EFWorkbench.js +128 -0
  44. package/dist/elements/src/gui/TWMixin.css.js +4 -0
  45. package/dist/elements/src/gui/TWMixin.js +36 -0
  46. package/dist/elements/src/gui/apiHostContext.js +5 -0
  47. package/dist/elements/src/gui/efContext.js +7 -0
  48. package/dist/elements/src/gui/fetchContext.js +5 -0
  49. package/dist/elements/src/gui/focusContext.js +5 -0
  50. package/dist/elements/src/gui/focusedElementContext.js +7 -0
  51. package/dist/elements/src/gui/playingContext.js +7 -0
  52. package/dist/elements/src/index.js +31 -0
  53. package/dist/elements/src/msToTimeCode.js +15 -0
  54. package/dist/elements/util.d.ts +3 -0
  55. package/dist/gui/ContextMixin.d.ts +19 -0
  56. package/dist/gui/EFFilmstrip.d.ts +148 -0
  57. package/dist/gui/EFPreview.d.ts +12 -0
  58. package/dist/gui/EFToggleLoop.d.ts +12 -0
  59. package/dist/gui/EFTogglePlay.d.ts +12 -0
  60. package/dist/gui/EFWorkbench.d.ts +18 -0
  61. package/dist/gui/TWMixin.d.ts +2 -0
  62. package/dist/gui/apiHostContext.d.ts +3 -0
  63. package/dist/gui/efContext.d.ts +4 -0
  64. package/dist/gui/fetchContext.d.ts +3 -0
  65. package/dist/gui/focusContext.d.ts +6 -0
  66. package/dist/gui/focusedElementContext.d.ts +3 -0
  67. package/dist/gui/playingContext.d.ts +6 -0
  68. package/dist/index.d.ts +12 -0
  69. package/dist/msToTimeCode.d.ts +1 -0
  70. package/dist/style.css +802 -0
  71. package/package.json +7 -10
  72. package/src/elements/EFAudio.ts +1 -1
  73. package/src/elements/EFCaptions.ts +23 -17
  74. package/src/elements/EFImage.ts +3 -3
  75. package/src/elements/EFMedia.ts +48 -17
  76. package/src/elements/EFSourceMixin.ts +1 -1
  77. package/src/elements/EFTemporal.ts +101 -6
  78. package/src/elements/EFTimegroup.browsertest.ts +3 -3
  79. package/src/elements/EFTimegroup.ts +30 -47
  80. package/src/elements/EFVideo.ts +2 -2
  81. package/src/elements/EFWaveform.ts +9 -9
  82. package/src/elements/FetchMixin.ts +5 -3
  83. package/src/elements/TimegroupController.ts +1 -1
  84. package/src/elements/durationConverter.ts +21 -1
  85. package/src/elements/parseTimeToMs.ts +1 -0
  86. package/src/elements/util.ts +1 -1
  87. package/src/gui/ContextMixin.ts +268 -0
  88. package/src/gui/EFFilmstrip.ts +61 -171
  89. package/src/gui/EFPreview.ts +39 -0
  90. package/src/gui/EFToggleLoop.ts +34 -0
  91. package/src/gui/EFTogglePlay.ts +38 -0
  92. package/src/gui/EFWorkbench.ts +11 -109
  93. package/src/gui/TWMixin.ts +10 -3
  94. package/src/gui/apiHostContext.ts +3 -0
  95. package/src/gui/efContext.ts +6 -0
  96. package/src/gui/fetchContext.ts +5 -0
  97. package/src/gui/focusContext.ts +7 -0
  98. package/src/gui/focusedElementContext.ts +5 -0
  99. package/src/gui/playingContext.ts +5 -0
  100. package/CHANGELOG.md +0 -7
  101. package/postcss.config.cjs +0 -12
  102. package/src/EF_INTERACTIVE.ts +0 -2
  103. package/src/elements.css +0 -22
  104. package/src/index.ts +0 -33
  105. package/tailwind.config.ts +0 -10
  106. package/tsconfig.json +0 -4
  107. package/vite.config.ts +0 -8
package/package.json CHANGED
@@ -1,16 +1,12 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.7.0-beta.9",
3
+ "version": "0.8.0-beta.10",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
7
7
  "import": {
8
- "default": "./dist/packages/elements/src/index.js",
9
- "types": "./dist/packages/elements/src/index.d.ts"
10
- },
11
- "require": {
12
- "default": "./dist/packages/elements/src/index.cjs",
13
- "types": "./dist/packages/elements/src/index.d.ts"
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/elements/src/index.js"
14
10
  }
15
11
  },
16
12
  "./styles.css": "./dist/style.css"
@@ -24,7 +20,7 @@
24
20
  "author": "",
25
21
  "license": "UNLICENSED",
26
22
  "dependencies": {
27
- "@editframe/assets": "0.7.0-beta.8",
23
+ "@editframe/assets": "0.8.0-beta.10",
28
24
  "@lit/context": "^1.1.2",
29
25
  "@lit/task": "^1.0.1",
30
26
  "d3": "^7.9.0",
@@ -35,10 +31,11 @@
35
31
  "devDependencies": {
36
32
  "@types/d3": "^7.4.3",
37
33
  "@types/dom-webcodecs": "^0.1.11",
38
- "@types/node": "^20.14.9",
34
+ "@types/node": "^20.14.13",
39
35
  "autoprefixer": "^10.4.19",
40
36
  "rollup-plugin-tsconfig-paths": "^1.5.2",
41
- "vite-plugin-dts": "^3.9.1",
37
+ "typescript": "^5.5.4",
38
+ "vite-plugin-dts": "^4.0.3",
42
39
  "vite-tsconfig-paths": "^4.3.2"
43
40
  }
44
41
  }
@@ -1,7 +1,7 @@
1
1
  import { html } from "lit";
2
2
  import { createRef, ref } from "lit/directives/ref.js";
3
3
  import { customElement, property } from "lit/decorators.js";
4
- import { EFMedia } from "./EFMedia";
4
+ import { EFMedia } from "./EFMedia.ts";
5
5
  import { Task } from "@lit/task";
6
6
 
7
7
  @customElement("ef-audio")
@@ -1,13 +1,13 @@
1
- import { EFAudio } from "./EFAudio";
2
1
  import { LitElement, type PropertyValueMap, html, css } from "lit";
3
2
  import { Task } from "@lit/task";
4
3
  import { customElement, property } from "lit/decorators.js";
5
- import { EFVideo } from "./EFVideo";
6
- import { EFTemporal } from "./EFTemporal";
7
- import { CrossUpdateController } from "./CrossUpdateController";
8
- import { FetchMixin } from "./FetchMixin";
9
- import { EFSourceMixin } from "./EFSourceMixin";
10
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
4
+ import { EFVideo } from "./EFVideo.ts";
5
+ import { EFAudio } from "./EFAudio.ts";
6
+ import { EFTemporal } from "./EFTemporal.ts";
7
+ import { CrossUpdateController } from "./CrossUpdateController.ts";
8
+ import { FetchMixin } from "./FetchMixin.ts";
9
+ import { EFSourceMixin } from "./EFSourceMixin.ts";
10
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
11
11
 
12
12
  interface Word {
13
13
  text: string;
@@ -75,8 +75,12 @@ export class EFCaptions extends EFSourceMixin(
75
75
  `,
76
76
  ];
77
77
 
78
- @property({ type: String, attribute: "target" })
79
- target = null;
78
+ @property({ type: String, attribute: "target", reflect: true })
79
+ targetSelector = "";
80
+
81
+ set target(value: string) {
82
+ this.targetSelector = value;
83
+ }
80
84
 
81
85
  @property({ attribute: "word-style" })
82
86
  wordStyle = "";
@@ -93,8 +97,8 @@ export class EFCaptions extends EFSourceMixin(
93
97
 
94
98
  protected md5SumLoader = new Task(this, {
95
99
  autoRun: false,
96
- args: () => [this.target] as const,
97
- task: async ([], { signal }) => {
100
+ args: () => [this.target, this.fetch] as const,
101
+ task: async ([_target, fetch], { signal }) => {
98
102
  const md5Path = `/@ef-asset/${this.targetElement.src ?? ""}`;
99
103
  const response = await fetch(md5Path, { method: "HEAD", signal });
100
104
  return response.headers.get("etag") ?? undefined;
@@ -149,13 +153,15 @@ export class EFCaptions extends EFSourceMixin(
149
153
  let endMs = 0;
150
154
  for (const segment of caption.segments) {
151
155
  if (
152
- this.targetElement.ownCurrentTimeMs >= segment.start * 1000 &&
153
- this.targetElement.ownCurrentTimeMs <= segment.end * 1000
156
+ this.targetElement.trimAdjustedOwnCurrentTimeMs >=
157
+ segment.start * 1000 &&
158
+ this.targetElement.trimAdjustedOwnCurrentTimeMs <= segment.end * 1000
154
159
  ) {
155
160
  for (const word of segment.words) {
156
161
  if (
157
- this.targetElement.ownCurrentTimeMs >= word.start * 1000 &&
158
- this.targetElement.ownCurrentTimeMs <= word.end * 1000
162
+ this.targetElement.trimAdjustedOwnCurrentTimeMs >=
163
+ word.start * 1000 &&
164
+ this.targetElement.trimAdjustedOwnCurrentTimeMs <= word.end * 1000
159
165
  ) {
160
166
  words.push(word.text);
161
167
  startMs = word.start * 1000;
@@ -172,11 +178,11 @@ export class EFCaptions extends EFSourceMixin(
172
178
  }
173
179
 
174
180
  get targetElement() {
175
- const target = document.getElementById(this.getAttribute("target") ?? "");
181
+ const target = document.getElementById(this.targetSelector ?? "");
176
182
  if (target instanceof EFAudio || target instanceof EFVideo) {
177
183
  return target;
178
184
  }
179
- throw new Error("Invalid target, must be an EFAudio or EFVideo element");
185
+ throw new Error("Invalid target, must be an EFAudio or EFVideo element");
180
186
  }
181
187
  }
182
188
 
@@ -2,9 +2,9 @@ import { Task } from "@lit/task";
2
2
  import { LitElement, html, css } from "lit";
3
3
  import { customElement } from "lit/decorators.js";
4
4
  import { createRef, ref } from "lit/directives/ref.js";
5
- import { FetchMixin } from "./FetchMixin";
6
- import { EFSourceMixin } from "./EFSourceMixin";
7
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
5
+ import { FetchMixin } from "./FetchMixin.ts";
6
+ import { EFSourceMixin } from "./EFSourceMixin.ts";
7
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
8
8
 
9
9
  @customElement("ef-image")
10
10
  export class EFImage extends EFSourceMixin(FetchMixin(LitElement), {
@@ -8,14 +8,14 @@ import debug from "debug";
8
8
 
9
9
  import type { TrackFragmentIndex, TrackSegment } from "@editframe/assets";
10
10
 
11
- import { MP4File } from "@/av/MP4File";
12
- import { EFTemporal } from "./EFTemporal";
13
- import { VideoAsset } from "@/av/EncodedAsset";
14
- import { FetchMixin } from "./FetchMixin";
15
- import { apiHostContext } from "../gui/EFWorkbench";
16
- import { EFSourceMixin } from "./EFSourceMixin";
17
- import { getStartTimeMs } from "./util";
18
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
11
+ import { MP4File } from "@editframe/assets/MP4File.js";
12
+ import { VideoAsset } from "@editframe/assets/EncodedAsset.js";
13
+ import { EFTemporal } from "./EFTemporal.ts";
14
+ import { FetchMixin } from "./FetchMixin.ts";
15
+ import { EFSourceMixin } from "./EFSourceMixin.ts";
16
+ import { getStartTimeMs } from "./util.ts";
17
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
18
+ import { apiHostContext } from "../gui/apiHostContext.ts";
19
19
 
20
20
  const log = debug("ef:elements:EFMedia");
21
21
 
@@ -70,6 +70,9 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
70
70
  public trackFragmentIndexLoader = new Task(this, {
71
71
  args: () => [this.fragmentIndexPath(), this.fetch] as const,
72
72
  task: async ([fragmentIndexPath, fetch], { signal }) => {
73
+ if (this.src === "") {
74
+ return;
75
+ }
73
76
  const response = await fetch(fragmentIndexPath, { signal });
74
77
  return (await response.json()) as Record<number, TrackFragmentIndex>;
75
78
  },
@@ -142,7 +145,11 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
142
145
 
143
146
  const result: Record<
144
147
  string,
145
- { segment: TrackSegment; track: MP4Box.TrackInfo }
148
+ {
149
+ segment: TrackSegment;
150
+ track: MP4Box.TrackInfo;
151
+ nextSegment?: TrackSegment;
152
+ }
146
153
  > = {};
147
154
 
148
155
  for (const index of Object.values(fragmentIndex)) {
@@ -158,11 +165,15 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
158
165
  return (segment.dts / track.timescale) * 1000 <= seekToMs;
159
166
  });
160
167
 
168
+ const nextSegment = index.segments.find((segment) => {
169
+ return (segment.dts / track.timescale) * 1000 > seekToMs;
170
+ });
171
+
161
172
  if (!segment) {
162
173
  return;
163
174
  }
164
175
 
165
- result[index.track] = { segment, track };
176
+ result[index.track] = { segment, track, nextSegment };
166
177
  }
167
178
 
168
179
  return result;
@@ -184,7 +195,9 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
184
195
 
185
196
  const files: Record<string, File> = {};
186
197
 
187
- for (const [trackId, { segment, track }] of Object.entries(seekResult)) {
198
+ for (const [trackId, { segment, track, nextSegment }] of Object.entries(
199
+ seekResult,
200
+ )) {
188
201
  const start = segment.offset;
189
202
  const end = segment.offset + segment.size;
190
203
 
@@ -193,6 +206,21 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
193
206
  headers: { Range: `bytes=${start}-${end}` },
194
207
  });
195
208
 
209
+ if (nextSegment) {
210
+ const nextStart = nextSegment.offset;
211
+ const nextEnd = nextSegment.offset + nextSegment.size;
212
+ fetch(this.fragmentTrackPath(trackId), {
213
+ signal,
214
+ headers: { Range: `bytes=${nextStart}-${nextEnd}` },
215
+ })
216
+ .then(() => {
217
+ log("Prefetched next segment");
218
+ })
219
+ .catch((error) => {
220
+ log("Failed to prefetch next segment", error);
221
+ });
222
+ }
223
+
196
224
  const initSegment = Object.values(initSegments).find(
197
225
  (initSegment) => initSegment.trackId === String(track.id),
198
226
  );
@@ -250,7 +278,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
250
278
  changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
251
279
  ): void {
252
280
  if (changedProperties.has("ownCurrentTimeMs")) {
253
- this.executeSeek(this.ownCurrentTimeMs);
281
+ this.executeSeek(this.trimAdjustedOwnCurrentTimeMs);
254
282
  }
255
283
  }
256
284
 
@@ -271,7 +299,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
271
299
  if (durations.length === 0) {
272
300
  return 0;
273
301
  }
274
- return Math.max(...durations);
302
+ return Math.max(...durations) - this.trimStartMs - this.trimEndMs;
275
303
  }
276
304
 
277
305
  get startTimeMs() {
@@ -312,8 +340,8 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
312
340
 
313
341
  async fetchAudioSpanningTime(fromMs: number, toMs: number) {
314
342
  // Adjust range for track's own time
315
- fromMs -= this.startTimeMs;
316
- toMs -= this.startTimeMs;
343
+ fromMs -= this.startTimeMs - this.trimStartMs;
344
+ toMs -= this.startTimeMs - this.trimStartMs;
317
345
 
318
346
  await this.trackFragmentIndexLoader.taskComplete;
319
347
  const audioTrackId = this.defaultAudioTrackId;
@@ -380,10 +408,13 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
380
408
 
381
409
  return {
382
410
  blob: audioBlob,
383
- startMs: (firstFragment.dts / audioTrackIndex.timescale) * 1000,
411
+ startMs:
412
+ (firstFragment.dts / audioTrackIndex.timescale) * 1000 -
413
+ this.trimStartMs,
384
414
  endMs:
385
415
  (lastFragment.dts / audioTrackIndex.timescale) * 1000 +
386
- (lastFragment.duration / audioTrackIndex.timescale) * 1000,
416
+ (lastFragment.duration / audioTrackIndex.timescale) * 1000 -
417
+ this.trimEndMs,
387
418
  };
388
419
  }
389
420
  }
@@ -1,9 +1,9 @@
1
1
  import { consume } from "@lit/context";
2
2
  import type { LitElement } from "lit";
3
- import { apiHostContext } from "../gui/EFWorkbench";
4
3
  import { state } from "lit/decorators/state.js";
5
4
  import { Task } from "@lit/task";
6
5
  import { property } from "lit/decorators/property.js";
6
+ import { apiHostContext } from "../gui/apiHostContext.ts";
7
7
 
8
8
  export declare class EFSourceMixinInterface {
9
9
  productionSrc(): string;
@@ -1,11 +1,11 @@
1
1
  import type { LitElement, ReactiveController } from "lit";
2
2
  import { consume, createContext } from "@lit/context";
3
3
  import { property, state } from "lit/decorators.js";
4
- import type { EFTimegroup } from "./EFTimegroup";
4
+ import type { EFTimegroup } from "./EFTimegroup.ts";
5
5
 
6
- import { durationConverter } from "./durationConverter";
6
+ import { durationConverter } from "./durationConverter.ts";
7
7
  import { Task } from "@lit/task";
8
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
8
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
9
9
 
10
10
  export const timegroupContext = createContext<EFTimegroup>(
11
11
  Symbol("timeGroupContext"),
@@ -13,11 +13,14 @@ export const timegroupContext = createContext<EFTimegroup>(
13
13
 
14
14
  export declare class TemporalMixinInterface {
15
15
  get hasOwnDuration(): boolean;
16
+ get trimStartMs(): number;
17
+ get trimEndMs(): number;
16
18
  get durationMs(): number;
17
19
  get startTimeMs(): number;
18
20
  get startTimeWithinParentMs(): number;
19
21
  get endTimeMs(): number;
20
22
  get ownCurrentTimeMs(): number;
23
+ get trimAdjustedOwnCurrentTimeMs(): number;
21
24
 
22
25
  set duration(value: string);
23
26
  get duration(): string;
@@ -59,10 +62,23 @@ export const deepGetElementsWithFrameTasks = (
59
62
  return elements;
60
63
  };
61
64
 
65
+ let temporalCache: Map<Element, TemporalMixinInterface[]>;
66
+ const resetTemporalCache = () => {
67
+ temporalCache = new Map();
68
+ if (typeof requestAnimationFrame !== "undefined") {
69
+ requestAnimationFrame(resetTemporalCache);
70
+ }
71
+ };
72
+ resetTemporalCache();
73
+
62
74
  export const shallowGetTemporalElements = (
63
75
  element: Element,
64
76
  temporals: TemporalMixinInterface[] = [],
65
77
  ) => {
78
+ const cachedResult = temporalCache.get(element);
79
+ if (cachedResult) {
80
+ return cachedResult;
81
+ }
66
82
  for (const child of element.children) {
67
83
  if (isEFTemporal(child)) {
68
84
  temporals.push(child);
@@ -70,6 +86,7 @@ export const shallowGetTemporalElements = (
70
86
  shallowGetTemporalElements(child, temporals);
71
87
  }
72
88
  }
89
+ temporalCache.set(element, temporals);
73
90
  return temporals;
74
91
  };
75
92
 
@@ -92,6 +109,15 @@ export class OwnCurrentTimeController implements ReactiveController {
92
109
 
93
110
  type Constructor<T = {}> = new (...args: any[]) => T;
94
111
 
112
+ let startTimeMsCache = new WeakMap<Element, number>();
113
+ const resetStartTimeMsCache = () => {
114
+ startTimeMsCache = new WeakMap();
115
+ if (typeof requestAnimationFrame !== "undefined") {
116
+ requestAnimationFrame(resetStartTimeMsCache);
117
+ }
118
+ };
119
+ resetStartTimeMsCache();
120
+
95
121
  export const EFTemporal = <T extends Constructor<LitElement>>(
96
122
  superClass: T,
97
123
  ) => {
@@ -130,6 +156,36 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
130
156
  })
131
157
  private _durationMs?: number;
132
158
 
159
+ @property({
160
+ type: Number,
161
+ attribute: "trimstart",
162
+ converter: durationConverter,
163
+ })
164
+ private _trimStartMs = 0;
165
+ public get trimStartMs(): number {
166
+ return this._trimStartMs;
167
+ }
168
+
169
+ @property({
170
+ type: Number,
171
+ attribute: "trimend",
172
+ converter: durationConverter,
173
+ })
174
+ private _trimEndMs = 0;
175
+ public get trimEndMs(): number {
176
+ return this._trimEndMs;
177
+ }
178
+
179
+ @property({
180
+ type: Number,
181
+ attribute: "startoffset",
182
+ converter: durationConverter,
183
+ })
184
+ private _startOffsetMs = 0;
185
+ public get startOffsetMs(): number {
186
+ return this._startOffsetMs;
187
+ }
188
+
133
189
  @state()
134
190
  rootTimegroup?: EFTimegroup = this.getRootTimegroup();
135
191
 
@@ -172,27 +228,47 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
172
228
  }
173
229
 
174
230
  get startTimeMs(): number {
231
+ const cachedStartTime = startTimeMsCache.get(this);
232
+ if (cachedStartTime !== undefined) {
233
+ return cachedStartTime;
234
+ }
175
235
  const parentTimegroup = this.parentTimegroup;
176
236
  if (!parentTimegroup) {
237
+ startTimeMsCache.set(this, 0);
177
238
  return 0;
178
239
  }
179
240
  switch (parentTimegroup.mode) {
180
241
  case "sequence": {
181
242
  const siblingTemorals = shallowGetTemporalElements(parentTimegroup);
182
- const ownIndex = siblingTemorals.indexOf(
243
+ const ownIndex = siblingTemorals?.indexOf(
183
244
  this as InstanceType<Constructor<TemporalMixinInterface> & T>,
184
245
  );
185
246
  if (ownIndex === 0) {
247
+ startTimeMsCache.set(this, parentTimegroup.startTimeMs);
186
248
  return parentTimegroup.startTimeMs;
187
249
  }
188
- const previous = siblingTemorals[ownIndex - 1];
250
+ const previous = siblingTemorals?.[(ownIndex ?? 0) - 1];
189
251
  if (!previous) {
190
252
  throw new Error("Previous temporal element not found");
191
253
  }
192
- return previous.startTimeMs + previous.durationMs;
254
+ startTimeMsCache.set(
255
+ this,
256
+ previous.startTimeMs +
257
+ previous.durationMs -
258
+ parentTimegroup.overlapMs,
259
+ );
260
+ return (
261
+ previous.startTimeMs +
262
+ previous.durationMs -
263
+ parentTimegroup.overlapMs
264
+ );
193
265
  }
194
266
  case "contain":
195
267
  case "fixed":
268
+ startTimeMsCache.set(
269
+ this,
270
+ parentTimegroup.startTimeMs + this.offsetMs,
271
+ );
196
272
  return parentTimegroup.startTimeMs + this.offsetMs;
197
273
  default:
198
274
  throw new Error(`Invalid time mode: ${parentTimegroup.mode}`);
@@ -213,6 +289,25 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
213
289
  return 0;
214
290
  }
215
291
 
292
+ /**
293
+ * Used to calculate the internal currentTimeMs of the element. This is useful
294
+ * for mapping to internal media time codes for audio/video elements.
295
+ */
296
+ get trimAdjustedOwnCurrentTimeMs() {
297
+ if (this.rootTimegroup) {
298
+ return Math.min(
299
+ Math.max(
300
+ 0,
301
+ this.rootTimegroup.currentTimeMs -
302
+ this.startTimeMs +
303
+ this.trimStartMs,
304
+ ),
305
+ this.durationMs + Math.abs(this.startOffsetMs) + this.trimStartMs,
306
+ );
307
+ }
308
+ return 0;
309
+ }
310
+
216
311
  frameTask = new Task(this, {
217
312
  autoRun: EF_INTERACTIVE,
218
313
  args: () => [this.ownCurrentTimeMs] as const,
@@ -5,10 +5,10 @@ import {
5
5
  html,
6
6
  render as litRender,
7
7
  } from "lit";
8
- import { EFTimegroup } from "./EFTimegroup";
9
- import "./EFTimegroup";
8
+ import { EFTimegroup } from "./EFTimegroup.ts";
9
+ import "./EFTimegroup.ts";
10
10
  import { customElement } from "lit/decorators/custom-element.js";
11
- import { EFTemporal } from "./EFTemporal";
11
+ import { EFTemporal } from "./EFTemporal.ts";
12
12
 
13
13
  beforeEach(() => {
14
14
  for (let i = 0; i < localStorage.length; i++) {
@@ -9,10 +9,11 @@ import {
9
9
  isEFTemporal,
10
10
  shallowGetTemporalElements,
11
11
  timegroupContext,
12
- } from "./EFTemporal";
13
- import { TimegroupController } from "./TimegroupController";
14
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
15
- import { deepGetMediaElements } from "./EFMedia";
12
+ } from "./EFTemporal.ts";
13
+ import { TimegroupController } from "./TimegroupController.ts";
14
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
15
+ import { deepGetMediaElements } from "./EFMedia.ts";
16
+ import { durationConverter } from "./durationConverter.ts";
16
17
 
17
18
  const log = debug("ef:elements:EFTimegroup");
18
19
 
@@ -37,7 +38,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
37
38
  display: block;
38
39
  width: 100%;
39
40
  height: 100%;
40
- position: relative;
41
+ position: absolute;
41
42
  top: 0;
42
43
  }
43
44
  `;
@@ -53,6 +54,13 @@ export class EFTimegroup extends EFTemporal(LitElement) {
53
54
  })
54
55
  mode: "fixed" | "sequence" | "contain" = "sequence";
55
56
 
57
+ @property({
58
+ type: Number,
59
+ converter: durationConverter,
60
+ attribute: "overlap",
61
+ })
62
+ overlapMs = 0;
63
+
56
64
  @property({ type: Number })
57
65
  set currentTime(time: number) {
58
66
  this.#currentTime = Math.max(0, Math.min(time, this.durationMs / 1000));
@@ -76,25 +84,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
76
84
  this.currentTime = ms / 1000;
77
85
  }
78
86
 
79
- @property({
80
- attribute: "crossover",
81
- converter: {
82
- fromAttribute: (value: string): number => {
83
- if (value.endsWith("ms")) {
84
- return Number.parseFloat(value);
85
- }
86
- if (value.endsWith("s")) {
87
- return Number.parseFloat(value) * 1000;
88
- }
89
- throw new Error(
90
- "`crossover` MUST be in milliseconds or seconds (10s, 10000ms)",
91
- );
92
- },
93
- toAttribute: (value: number) => `${value}ms`,
94
- },
95
- })
96
- crossoverMs = 0;
97
-
98
87
  render() {
99
88
  return html`<slot></slot> `;
100
89
  }
@@ -113,7 +102,9 @@ export class EFTimegroup extends EFTemporal(LitElement) {
113
102
  connectedCallback() {
114
103
  super.connectedCallback();
115
104
  if (this.id) {
116
- this.#currentTime = this.maybeLoadTimeFromLocalStorage();
105
+ this.waitForMediaDurations().then(() => {
106
+ this.#currentTime = this.maybeLoadTimeFromLocalStorage();
107
+ });
117
108
  }
118
109
 
119
110
  if (this.parentTimegroup) {
@@ -132,32 +123,18 @@ export class EFTimegroup extends EFTemporal(LitElement) {
132
123
  return `ef-timegroup-${this.id}`;
133
124
  }
134
125
 
135
- get crossoverStartMs() {
136
- const parentTimeGroup = this.parentTimegroup;
137
- if (!parentTimeGroup || !this.previousElementSibling) {
138
- return 0;
139
- }
140
-
141
- return parentTimeGroup.crossoverMs;
142
- }
143
- get crossoverEndMs() {
144
- const parentTimeGroup = this.parentTimegroup;
145
- if (!parentTimeGroup || !this.nextElementSibling) {
146
- return 0;
147
- }
148
-
149
- return parentTimeGroup.crossoverMs;
150
- }
151
-
152
126
  get durationMs() {
153
127
  switch (this.mode) {
154
128
  case "fixed":
155
129
  return super.durationMs;
156
130
  case "sequence": {
157
131
  let duration = 0;
158
- for (const node of this.childTemporals) {
132
+ this.childTemporals.forEach((node, index) => {
133
+ if (index > 0) {
134
+ duration -= this.overlapMs;
135
+ }
159
136
  duration += node.durationMs;
160
- }
137
+ });
161
138
  return duration;
162
139
  }
163
140
  case "contain": {
@@ -205,9 +182,14 @@ export class EFTimegroup extends EFTemporal(LitElement) {
205
182
  this.style.display = "";
206
183
 
207
184
  const animations = this.getAnimations({ subtree: true });
185
+ this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
186
+ this.style.setProperty(
187
+ "--ef-transition--duration",
188
+ `${this.parentTimegroup?.overlapMs ?? 0}ms`,
189
+ );
208
190
  this.style.setProperty(
209
- "--ef-duration",
210
- `${this.durationMs + this.crossoverEndMs + this.crossoverStartMs}ms`,
191
+ "--ef-transition-out-start",
192
+ `${this.durationMs - (this.parentTimegroup?.overlapMs ?? 0)}ms`,
211
193
  );
212
194
 
213
195
  for (const animation of animations) {
@@ -265,7 +247,8 @@ export class EFTimegroup extends EFTemporal(LitElement) {
265
247
  return (
266
248
  EF_INTERACTIVE &&
267
249
  this.closest("ef-timegroup") === this &&
268
- this.closest("ef-workbench") === null
250
+ this.closest("ef-workbench") === null &&
251
+ this.closest("ef-preview") === null
269
252
  );
270
253
  }
271
254
 
@@ -3,8 +3,8 @@ import { Task } from "@lit/task";
3
3
  import { createRef, ref } from "lit/directives/ref.js";
4
4
  import { customElement } from "lit/decorators.js";
5
5
 
6
- import { EFMedia } from "./EFMedia";
7
- import { TWMixin } from "../gui/TWMixin";
6
+ import { EFMedia } from "./EFMedia.ts";
7
+ import { TWMixin } from "../gui/TWMixin.ts";
8
8
 
9
9
  @customElement("ef-video")
10
10
  export class EFVideo extends TWMixin(EFMedia) {