@editframe/elements 0.8.0-beta.2 → 0.8.0-beta.4

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 (44) hide show
  1. package/dist/elements/EFCaptions.d.ts +3 -2
  2. package/dist/elements/EFTemporal.d.ts +3 -0
  3. package/dist/elements/EFTimegroup.d.ts +1 -3
  4. package/dist/elements/durationConverter.d.ts +8 -0
  5. package/dist/elements/src/elements/EFCaptions.js +10 -7
  6. package/dist/elements/src/elements/EFMedia.js +6 -6
  7. package/dist/elements/src/elements/EFTemporal.js +51 -2
  8. package/dist/elements/src/elements/EFTimegroup.js +22 -39
  9. package/dist/elements/src/elements/EFWaveform.js +3 -3
  10. package/dist/elements/src/elements/parseTimeToMs.js +1 -0
  11. package/dist/elements/src/gui/ContextMixin.js +28 -18
  12. package/dist/elements/src/gui/EFFilmstrip.js +27 -25
  13. package/dist/elements/src/gui/EFToggleLoop.js +39 -0
  14. package/dist/elements/src/gui/EFTogglePlay.js +43 -0
  15. package/dist/elements/src/gui/EFWorkbench.js +4 -4
  16. package/dist/elements/src/gui/TWMixin.css.js +1 -1
  17. package/dist/elements/src/gui/efContext.js +7 -0
  18. package/dist/elements/src/gui/playingContext.js +2 -0
  19. package/dist/elements/src/index.js +4 -0
  20. package/dist/gui/ContextMixin.d.ts +8 -11
  21. package/dist/gui/EFFilmstrip.d.ts +7 -2
  22. package/dist/gui/EFPreview.d.ts +1 -15
  23. package/dist/gui/EFToggleLoop.d.ts +13 -0
  24. package/dist/gui/EFTogglePlay.d.ts +13 -0
  25. package/dist/gui/EFWorkbench.d.ts +1 -16
  26. package/dist/gui/efContext.d.ts +5 -0
  27. package/dist/gui/playingContext.d.ts +3 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/style.css +7 -5
  30. package/package.json +2 -2
  31. package/src/elements/EFCaptions.ts +14 -8
  32. package/src/elements/EFMedia.ts +9 -6
  33. package/src/elements/EFTemporal.ts +60 -2
  34. package/src/elements/EFTimegroup.ts +21 -41
  35. package/src/elements/EFWaveform.ts +3 -3
  36. package/src/elements/durationConverter.ts +20 -0
  37. package/src/elements/parseTimeToMs.ts +1 -0
  38. package/src/gui/ContextMixin.ts +42 -30
  39. package/src/gui/EFFilmstrip.ts +27 -28
  40. package/src/gui/EFToggleLoop.ts +34 -0
  41. package/src/gui/EFTogglePlay.ts +38 -0
  42. package/src/gui/EFWorkbench.ts +4 -4
  43. package/src/gui/efContext.ts +6 -0
  44. package/src/gui/playingContext.ts +2 -0
@@ -16,11 +16,12 @@ export declare class EFCaptionsActiveWord extends EFCaptionsActiveWord_base {
16
16
  declare const EFCaptions_base: (new (...args: any[]) => import('./EFSourceMixin.ts').EFSourceMixinInterface) & (new (...args: any[]) => import('./EFTemporal.ts').TemporalMixinInterface) & (new (...args: any[]) => import('./FetchMixin.ts').FetchMixinInterface) & typeof LitElement;
17
17
  export declare class EFCaptions extends EFCaptions_base {
18
18
  static styles: import('lit').CSSResult[];
19
- target: null;
19
+ targetSelector: string;
20
+ set target(value: string);
20
21
  wordStyle: string;
21
22
  activeWordContainers: HTMLCollectionOf<EFCaptionsActiveWord>;
22
23
  captionsPath(): string;
23
- protected md5SumLoader: Task<readonly [null, typeof fetch], string | undefined>;
24
+ protected md5SumLoader: Task<readonly [string, typeof fetch], string | undefined>;
24
25
  private captionsDataTask;
25
26
  frameTask: Task<import('@lit/task').TaskStatus[], void>;
26
27
  connectedCallback(): void;
@@ -7,11 +7,14 @@ export declare const timegroupContext: {
7
7
  };
8
8
  export declare class TemporalMixinInterface {
9
9
  get hasOwnDuration(): boolean;
10
+ get trimStartMs(): number;
11
+ get trimEndMs(): number;
10
12
  get durationMs(): number;
11
13
  get startTimeMs(): number;
12
14
  get startTimeWithinParentMs(): number;
13
15
  get endTimeMs(): number;
14
16
  get ownCurrentTimeMs(): number;
17
+ get trimAdjustedOwnCurrentTimeMs(): number;
15
18
  set duration(value: string);
16
19
  get duration(): string;
17
20
  parentTimegroup?: EFTimegroup;
@@ -8,17 +8,15 @@ export declare class EFTimegroup extends EFTimegroup_base {
8
8
  static styles: import('lit').CSSResult;
9
9
  _timeGroupContext: this;
10
10
  mode: "fixed" | "sequence" | "contain";
11
+ overlapMs: number;
11
12
  set currentTime(time: number);
12
13
  get currentTime(): number;
13
14
  get currentTimeMs(): number;
14
15
  set currentTimeMs(ms: number);
15
- crossoverMs: number;
16
16
  render(): import('lit-html').TemplateResult<1>;
17
17
  maybeLoadTimeFromLocalStorage(): number;
18
18
  connectedCallback(): void;
19
19
  get storageKey(): string;
20
- get crossoverStartMs(): number;
21
- get crossoverEndMs(): number;
22
20
  get durationMs(): number;
23
21
  waitForMediaDurations(): Promise<(Record<number, import('packages/assets/dist/Probe.js').TrackFragmentIndex> | undefined)[]>;
24
22
  get childTemporals(): import('./EFTemporal.ts').TemporalMixinInterface[];
@@ -2,3 +2,11 @@ export declare const durationConverter: {
2
2
  fromAttribute: (value: string) => number;
3
3
  toAttribute: (value: number) => string;
4
4
  };
5
+ export declare const trimDurationConverter: {
6
+ fromAttribute: (value: string) => number;
7
+ toAttribute: (value: number) => string;
8
+ };
9
+ export declare const imageDurationConverter: {
10
+ fromAttribute: (value: string) => number;
11
+ toAttribute: (value: number) => string;
12
+ };
@@ -60,7 +60,7 @@ let EFCaptions = class extends EFSourceMixin(
60
60
  ) {
61
61
  constructor() {
62
62
  super(...arguments);
63
- this.target = null;
63
+ this.targetSelector = "";
64
64
  this.wordStyle = "";
65
65
  this.activeWordContainers = this.getElementsByTagName("ef-captions-active-word");
66
66
  this.md5SumLoader = new Task(this, {
@@ -88,6 +88,9 @@ let EFCaptions = class extends EFSourceMixin(
88
88
  }
89
89
  });
90
90
  }
91
+ set target(value) {
92
+ this.targetSelector = value;
93
+ }
91
94
  captionsPath() {
92
95
  const targetSrc = this.targetElement.src;
93
96
  if (targetSrc.startsWith("editframe://") || targetSrc.startsWith("http")) {
@@ -120,9 +123,9 @@ let EFCaptions = class extends EFSourceMixin(
120
123
  let startMs = 0;
121
124
  let endMs = 0;
122
125
  for (const segment of caption.segments) {
123
- if (this.targetElement.ownCurrentTimeMs >= segment.start * 1e3 && this.targetElement.ownCurrentTimeMs <= segment.end * 1e3) {
126
+ if (this.targetElement.trimAdjustedOwnCurrentTimeMs >= segment.start * 1e3 && this.targetElement.trimAdjustedOwnCurrentTimeMs <= segment.end * 1e3) {
124
127
  for (const word of segment.words) {
125
- if (this.targetElement.ownCurrentTimeMs >= word.start * 1e3 && this.targetElement.ownCurrentTimeMs <= word.end * 1e3) {
128
+ if (this.targetElement.trimAdjustedOwnCurrentTimeMs >= word.start * 1e3 && this.targetElement.trimAdjustedOwnCurrentTimeMs <= word.end * 1e3) {
126
129
  words.push(word.text);
127
130
  startMs = word.start * 1e3;
128
131
  endMs = word.end * 1e3;
@@ -137,11 +140,11 @@ let EFCaptions = class extends EFSourceMixin(
137
140
  }
138
141
  }
139
142
  get targetElement() {
140
- const target = document.getElementById(this.getAttribute("target") ?? "");
143
+ const target = document.getElementById(this.targetSelector ?? "");
141
144
  if (target instanceof EFAudio || target instanceof EFVideo) {
142
145
  return target;
143
146
  }
144
- throw new Error("Invalid target, must be an EFAudio or EFVideo element");
147
+ throw new Error("Invalid target, must be an EFAudio or EFVideo element");
145
148
  }
146
149
  };
147
150
  EFCaptions.styles = [
@@ -152,8 +155,8 @@ EFCaptions.styles = [
152
155
  `
153
156
  ];
154
157
  __decorateClass([
155
- property({ type: String, attribute: "target" })
156
- ], EFCaptions.prototype, "target", 2);
158
+ property({ type: String, attribute: "target", reflect: true })
159
+ ], EFCaptions.prototype, "targetSelector", 2);
157
160
  __decorateClass([
158
161
  property({ attribute: "word-style" })
159
162
  ], EFCaptions.prototype, "wordStyle", 2);
@@ -255,7 +255,7 @@ class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
255
255
  }
256
256
  updated(changedProperties) {
257
257
  if (changedProperties.has("ownCurrentTimeMs")) {
258
- this.executeSeek(this.ownCurrentTimeMs);
258
+ this.executeSeek(this.trimAdjustedOwnCurrentTimeMs);
259
259
  }
260
260
  }
261
261
  get hasOwnDuration() {
@@ -273,15 +273,15 @@ class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
273
273
  if (durations.length === 0) {
274
274
  return 0;
275
275
  }
276
- return Math.max(...durations);
276
+ return Math.max(...durations) - this.trimStartMs - this.trimEndMs;
277
277
  }
278
278
  get startTimeMs() {
279
279
  return getStartTimeMs(this);
280
280
  }
281
281
  #audioContext;
282
282
  async fetchAudioSpanningTime(fromMs, toMs) {
283
- fromMs -= this.startTimeMs;
284
- toMs -= this.startTimeMs;
283
+ fromMs -= this.startTimeMs - this.trimStartMs;
284
+ toMs -= this.startTimeMs - this.trimStartMs;
285
285
  await this.trackFragmentIndexLoader.taskComplete;
286
286
  const audioTrackId = this.defaultAudioTrackId;
287
287
  if (!audioTrackId) {
@@ -335,8 +335,8 @@ class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
335
335
  });
336
336
  return {
337
337
  blob: audioBlob,
338
- startMs: firstFragment.dts / audioTrackIndex.timescale * 1e3,
339
- endMs: lastFragment.dts / audioTrackIndex.timescale * 1e3 + lastFragment.duration / audioTrackIndex.timescale * 1e3
338
+ startMs: firstFragment.dts / audioTrackIndex.timescale * 1e3 - this.trimStartMs,
339
+ endMs: lastFragment.dts / audioTrackIndex.timescale * 1e3 + lastFragment.duration / audioTrackIndex.timescale * 1e3 - this.trimEndMs
340
340
  };
341
341
  }
342
342
  }
@@ -85,6 +85,9 @@ const EFTemporal = (superClass) => {
85
85
  constructor() {
86
86
  super(...arguments);
87
87
  this._offsetMs = 0;
88
+ this._trimStartMs = 0;
89
+ this._trimEndMs = 0;
90
+ this._startOffsetMs = 0;
88
91
  this.rootTimegroup = this.getRootTimegroup();
89
92
  this.frameTask = new Task(this, {
90
93
  autoRun: EF_INTERACTIVE,
@@ -112,6 +115,15 @@ const EFTemporal = (superClass) => {
112
115
  get parentTimegroup() {
113
116
  return this.#parentTimegroup;
114
117
  }
118
+ get trimStartMs() {
119
+ return this._trimStartMs;
120
+ }
121
+ get trimEndMs() {
122
+ return this._trimEndMs;
123
+ }
124
+ get startOffsetMs() {
125
+ return this._startOffsetMs;
126
+ }
115
127
  getRootTimegroup() {
116
128
  let parent = this.tagName === "EF-TIMEGROUP" ? this : this.parentTimegroup;
117
129
  while (parent?.parentTimegroup) {
@@ -169,9 +181,9 @@ const EFTemporal = (superClass) => {
169
181
  }
170
182
  startTimeMsCache.set(
171
183
  this,
172
- previous.startTimeMs + previous.durationMs
184
+ previous.startTimeMs + previous.durationMs - parentTimegroup.overlapMs
173
185
  );
174
- return previous.startTimeMs + previous.durationMs;
186
+ return previous.startTimeMs + previous.durationMs - parentTimegroup.overlapMs;
175
187
  }
176
188
  case "contain":
177
189
  case "fixed":
@@ -196,6 +208,22 @@ const EFTemporal = (superClass) => {
196
208
  }
197
209
  return 0;
198
210
  }
211
+ /**
212
+ * Used to calculate the internal currentTimeMs of the element. This is useful
213
+ * for mapping to internal media time codes for audio/video elements.
214
+ */
215
+ get trimAdjustedOwnCurrentTimeMs() {
216
+ if (this.rootTimegroup) {
217
+ return Math.min(
218
+ Math.max(
219
+ 0,
220
+ this.rootTimegroup.currentTimeMs - this.startTimeMs + this.trimStartMs
221
+ ),
222
+ this.durationMs + Math.abs(this.startOffsetMs) + this.trimStartMs
223
+ );
224
+ }
225
+ return 0;
226
+ }
199
227
  }
200
228
  __decorateClass([
201
229
  consume({ context: timegroupContext, subscribe: true }),
@@ -215,6 +243,27 @@ const EFTemporal = (superClass) => {
215
243
  converter: durationConverter
216
244
  })
217
245
  ], TemporalMixinClass.prototype, "_durationMs", 2);
246
+ __decorateClass([
247
+ property({
248
+ type: Number,
249
+ attribute: "trimstart",
250
+ converter: durationConverter
251
+ })
252
+ ], TemporalMixinClass.prototype, "_trimStartMs", 2);
253
+ __decorateClass([
254
+ property({
255
+ type: Number,
256
+ attribute: "trimend",
257
+ converter: durationConverter
258
+ })
259
+ ], TemporalMixinClass.prototype, "_trimEndMs", 2);
260
+ __decorateClass([
261
+ property({
262
+ type: Number,
263
+ attribute: "startoffset",
264
+ converter: durationConverter
265
+ })
266
+ ], TemporalMixinClass.prototype, "_startOffsetMs", 2);
218
267
  __decorateClass([
219
268
  state()
220
269
  ], TemporalMixinClass.prototype, "rootTimegroup", 2);
@@ -7,6 +7,7 @@ import { EFTemporal, shallowGetTemporalElements, isEFTemporal, timegroupContext
7
7
  import { TimegroupController } from "./TimegroupController.js";
8
8
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
9
9
  import { deepGetMediaElements } from "./EFMedia.js";
10
+ import { durationConverter } from "./durationConverter.js";
10
11
  var __defProp = Object.defineProperty;
11
12
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
13
  var __typeError = (msg) => {
@@ -44,7 +45,7 @@ let EFTimegroup = class extends EFTemporal(LitElement) {
44
45
  this._timeGroupContext = this;
45
46
  __privateAdd(this, _currentTime, 0);
46
47
  this.mode = "sequence";
47
- this.crossoverMs = 0;
48
+ this.overlapMs = 0;
48
49
  this.frameTask = new Task(this, {
49
50
  autoRun: EF_INTERACTIVE,
50
51
  args: () => [this.ownCurrentTimeMs, this.currentTimeMs],
@@ -110,29 +111,18 @@ let EFTimegroup = class extends EFTemporal(LitElement) {
110
111
  }
111
112
  return `ef-timegroup-${this.id}`;
112
113
  }
113
- get crossoverStartMs() {
114
- const parentTimeGroup = this.parentTimegroup;
115
- if (!parentTimeGroup || !this.previousElementSibling) {
116
- return 0;
117
- }
118
- return parentTimeGroup.crossoverMs;
119
- }
120
- get crossoverEndMs() {
121
- const parentTimeGroup = this.parentTimegroup;
122
- if (!parentTimeGroup || !this.nextElementSibling) {
123
- return 0;
124
- }
125
- return parentTimeGroup.crossoverMs;
126
- }
127
114
  get durationMs() {
128
115
  switch (this.mode) {
129
116
  case "fixed":
130
117
  return super.durationMs;
131
118
  case "sequence": {
132
119
  let duration = 0;
133
- for (const node of this.childTemporals) {
120
+ this.childTemporals.forEach((node, index) => {
121
+ if (index > 0) {
122
+ duration -= this.overlapMs;
123
+ }
134
124
  duration += node.durationMs;
135
- }
125
+ });
136
126
  return duration;
137
127
  }
138
128
  case "contain": {
@@ -168,9 +158,14 @@ let EFTimegroup = class extends EFTemporal(LitElement) {
168
158
  }
169
159
  this.style.display = "";
170
160
  const animations = this.getAnimations({ subtree: true });
161
+ this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
162
+ this.style.setProperty(
163
+ "--ef-transition--duration",
164
+ `${this.parentTimegroup?.overlapMs ?? 0}ms`
165
+ );
171
166
  this.style.setProperty(
172
- "--ef-duration",
173
- `${this.durationMs + this.crossoverEndMs + this.crossoverStartMs}ms`
167
+ "--ef-transition-out-start",
168
+ `${this.durationMs - (this.parentTimegroup?.overlapMs ?? 0)}ms`
174
169
  );
175
170
  for (const animation of animations) {
176
171
  if (animation.playState === "running") {
@@ -311,7 +306,7 @@ EFTimegroup.styles = css`
311
306
  display: block;
312
307
  width: 100%;
313
308
  height: 100%;
314
- position: relative;
309
+ position: absolute;
315
310
  top: 0;
316
311
  }
317
312
  `;
@@ -324,28 +319,16 @@ __decorateClass([
324
319
  attribute: "mode"
325
320
  })
326
321
  ], EFTimegroup.prototype, "mode", 2);
327
- __decorateClass([
328
- property({ type: Number })
329
- ], EFTimegroup.prototype, "currentTime", 1);
330
322
  __decorateClass([
331
323
  property({
332
- attribute: "crossover",
333
- converter: {
334
- fromAttribute: (value) => {
335
- if (value.endsWith("ms")) {
336
- return Number.parseFloat(value);
337
- }
338
- if (value.endsWith("s")) {
339
- return Number.parseFloat(value) * 1e3;
340
- }
341
- throw new Error(
342
- "`crossover` MUST be in milliseconds or seconds (10s, 10000ms)"
343
- );
344
- },
345
- toAttribute: (value) => `${value}ms`
346
- }
324
+ type: Number,
325
+ converter: durationConverter,
326
+ attribute: "overlap"
347
327
  })
348
- ], EFTimegroup.prototype, "crossoverMs", 2);
328
+ ], EFTimegroup.prototype, "overlapMs", 2);
329
+ __decorateClass([
330
+ property({ type: Number })
331
+ ], EFTimegroup.prototype, "currentTime", 1);
349
332
  EFTimegroup = __decorateClass([
350
333
  customElement("ef-timegroup")
351
334
  ], EFTimegroup);
@@ -148,7 +148,7 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
148
148
  if (!this.targetElement.audioBufferTask.value) {
149
149
  return;
150
150
  }
151
- if (this.targetElement.ownCurrentTimeMs > 0) {
151
+ if (this.targetElement.trimAdjustedOwnCurrentTimeMs > 0) {
152
152
  const audioContext = new OfflineAudioContext(2, 48e3 / 25, 48e3);
153
153
  const audioBufferSource = audioContext.createBufferSource();
154
154
  audioBufferSource.buffer = this.targetElement.audioBufferTask.value.buffer;
@@ -159,7 +159,7 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
159
159
  0,
160
160
  Math.max(
161
161
  0,
162
- (this.targetElement.ownCurrentTimeMs - this.targetElement.audioBufferTask.value.startOffsetMs) / 1e3
162
+ (this.targetElement.trimAdjustedOwnCurrentTimeMs - this.targetElement.audioBufferTask.value.startOffsetMs) / 1e3
163
163
  ),
164
164
  48e3 / 1e3
165
165
  );
@@ -202,7 +202,7 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
202
202
  if (target instanceof EFAudio || target instanceof EFVideo) {
203
203
  return target;
204
204
  }
205
- throw new Error("Invalid target, must be an EFAudio element");
205
+ throw new Error("Invalid target, must be an EFAudio or EFVideo element");
206
206
  }
207
207
  };
208
208
  EFWaveform.styles = [];
@@ -1,4 +1,5 @@
1
1
  const parseTimeToMs = (time) => {
2
+ console.log("parseTimeToMs", time);
2
3
  if (time.endsWith("ms")) {
3
4
  return Number.parseFloat(time);
4
5
  }
@@ -3,9 +3,9 @@ import { state, property } from "lit/decorators.js";
3
3
  import { focusContext } from "./focusContext.js";
4
4
  import { focusedElementContext } from "./focusedElementContext.js";
5
5
  import { fetchContext } from "./fetchContext.js";
6
- import { apiHostContext } from "./apiHostContext.js";
7
6
  import { createRef } from "lit/directives/ref.js";
8
- import { playingContext } from "./playingContext.js";
7
+ import { playingContext, loopContext } from "./playingContext.js";
8
+ import { efContext } from "./efContext.js";
9
9
  var __defProp = Object.defineProperty;
10
10
  var __decorateClass = (decorators, target, key, kind) => {
11
11
  var result = void 0;
@@ -20,17 +20,12 @@ function ContextMixin(superClass) {
20
20
  constructor() {
21
21
  super(...arguments);
22
22
  this.focusContext = this;
23
+ this.efContext = this;
23
24
  this.fetch = async (url, init = {}) => {
24
25
  init.headers ||= {};
25
26
  Object.assign(init.headers, {
26
27
  "Content-Type": "application/json"
27
28
  });
28
- const bearerToken = this.apiToken;
29
- if (bearerToken) {
30
- Object.assign(init.headers, {
31
- Authorization: `Bearer ${bearerToken}`
32
- });
33
- }
34
29
  if (this.signingURL) {
35
30
  if (!this.#URLTokens[url]) {
36
31
  this.#URLTokens[url] = fetch(this.signingURL, {
@@ -53,7 +48,6 @@ function ContextMixin(superClass) {
53
48
  return fetch(url, init);
54
49
  };
55
50
  this.#URLTokens = {};
56
- this.apiHost = "";
57
51
  this.playing = false;
58
52
  this.loop = false;
59
53
  this.stageScale = 1;
@@ -65,7 +59,10 @@ function ContextMixin(superClass) {
65
59
  if (this.isConnected && !this.rendering) {
66
60
  const canvasElement = this.canvasRef.value;
67
61
  const stageElement = this.stageRef.value;
68
- if (stageElement && canvasElement) {
62
+ const canvasChild = canvasElement?.assignedElements()[0];
63
+ if (stageElement && canvasElement && canvasChild) {
64
+ canvasElement.style.width = `${canvasChild.clientWidth}px`;
65
+ canvasElement.style.height = `${canvasChild.clientHeight}px`;
69
66
  const stageWidth = stageElement.clientWidth;
70
67
  const stageHeight = stageElement.clientHeight;
71
68
  const canvasWidth = canvasElement.clientWidth;
@@ -76,12 +73,14 @@ function ContextMixin(superClass) {
76
73
  const scale = stageHeight / canvasHeight;
77
74
  if (this.stageScale !== scale) {
78
75
  canvasElement.style.transform = `scale(${scale})`;
76
+ canvasElement.style.transformOrigin = "top";
79
77
  }
80
78
  this.stageScale = scale;
81
79
  } else {
82
80
  const scale = stageWidth / canvasWidth;
83
81
  if (this.stageScale !== scale) {
84
82
  canvasElement.style.transform = `scale(${scale})`;
83
+ canvasElement.style.transformOrigin = "top";
85
84
  }
86
85
  this.stageScale = scale;
87
86
  }
@@ -118,6 +117,12 @@ function ContextMixin(superClass) {
118
117
  get targetTimegroup() {
119
118
  return this.querySelector("ef-timegroup");
120
119
  }
120
+ play() {
121
+ this.playing = true;
122
+ }
123
+ pause() {
124
+ this.playing = false;
125
+ }
121
126
  #playbackAudioContext;
122
127
  #playbackAnimationFrameRequest;
123
128
  #AUDIO_PLAYBACK_SLICE_MS;
@@ -182,7 +187,15 @@ function ContextMixin(superClass) {
182
187
  source.onended = () => {
183
188
  bufferCount--;
184
189
  if (endMs >= toMs) {
185
- this.playing = false;
190
+ this.pause();
191
+ if (this.loop) {
192
+ this.updateComplete.then(() => {
193
+ this.currentTimeMs = 0;
194
+ this.updateComplete.then(() => {
195
+ this.play();
196
+ });
197
+ });
198
+ }
186
199
  } else {
187
200
  fillBuffer();
188
201
  }
@@ -200,24 +213,21 @@ function ContextMixin(superClass) {
200
213
  provide({ context: focusedElementContext }),
201
214
  state()
202
215
  ], ContextElement.prototype, "focusedElement");
216
+ __decorateClass([
217
+ provide({ context: efContext })
218
+ ], ContextElement.prototype, "efContext");
203
219
  __decorateClass([
204
220
  provide({ context: fetchContext })
205
221
  ], ContextElement.prototype, "fetch");
206
222
  __decorateClass([
207
223
  property({ type: String })
208
224
  ], ContextElement.prototype, "signingURL");
209
- __decorateClass([
210
- property({ type: String })
211
- ], ContextElement.prototype, "apiToken");
212
- __decorateClass([
213
- provide({ context: apiHostContext }),
214
- property({ type: String })
215
- ], ContextElement.prototype, "apiHost");
216
225
  __decorateClass([
217
226
  provide({ context: playingContext }),
218
227
  property({ type: Boolean, reflect: true })
219
228
  ], ContextElement.prototype, "playing");
220
229
  __decorateClass([
230
+ provide({ context: loopContext }),
221
231
  property({ type: Boolean, reflect: true })
222
232
  ], ContextElement.prototype, "loop");
223
233
  __decorateClass([
@@ -14,7 +14,7 @@ import { TWMixin } from "./TWMixin.js";
14
14
  import { msToTimeCode } from "../msToTimeCode.js";
15
15
  import { focusedElementContext } from "./focusedElementContext.js";
16
16
  import { focusContext } from "./focusContext.js";
17
- import { playingContext } from "./playingContext.js";
17
+ import { playingContext, loopContext } from "./playingContext.js";
18
18
  var __defProp = Object.defineProperty;
19
19
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
20
20
  var __typeError = (msg) => {
@@ -73,17 +73,23 @@ class FilmstripItem extends TWMixin(LitElement) {
73
73
  get isFocused() {
74
74
  return this.element && this.focusContext?.focusedElement === this.element;
75
75
  }
76
- get styles() {
76
+ get gutterStyles() {
77
77
  return {
78
78
  position: "relative",
79
- left: `${this.pixelsPerMs * this.element.startTimeWithinParentMs}px`,
80
- width: `${this.pixelsPerMs * this.element.durationMs}px`
79
+ left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.trimStartMs)}px`,
80
+ width: `${this.pixelsPerMs * (this.element.durationMs + this.element.trimStartMs + this.element.trimEndMs)}px`
81
+ };
82
+ }
83
+ get trimPortionStyles() {
84
+ return {
85
+ width: `${this.pixelsPerMs * this.element.durationMs}px`,
86
+ left: `${this.pixelsPerMs * this.element.trimStartMs}px`
81
87
  };
82
88
  }
83
89
  render() {
84
- return html` <div class="" style=${styleMap(this.styles)}>
90
+ return html`<div style=${styleMap(this.gutterStyles)}>
85
91
  <div
86
- class="border-outset relative mb-[1px] block h-[1.1rem] text-nowrap border border-slate-500 bg-blue-200 hover:bg-blue-400 text-sm data-[focused]:bg-slate-400"
92
+ class="bg-slate-300"
87
93
  ?data-focused=${this.isFocused}
88
94
  @mouseenter=${() => {
89
95
  if (this.focusContext) {
@@ -96,7 +102,13 @@ class FilmstripItem extends TWMixin(LitElement) {
96
102
  }
97
103
  }}
98
104
  >
99
- ${this.animations()}
105
+ <div
106
+ ?data-focused=${this.isFocused}
107
+ class="border-outset relative mb-[1px] block h-[1.1rem] text-nowrap border border-slate-500 bg-blue-200 text-sm data-[focused]:bg-slate-400"
108
+ style=${styleMap(this.trimPortionStyles)}
109
+ >
110
+ ${this.animations()}
111
+ </div>
100
112
  </div>
101
113
  ${this.renderChildren()}
102
114
  </div>`;
@@ -480,8 +492,9 @@ let EFFilmstrip = class extends TWMixin(LitElement) {
480
492
  this.currentTimeMs = 0;
481
493
  __privateAdd(this, _handleKeyPress, (event) => {
482
494
  if (event.key === " ") {
495
+ const [target] = event.composedPath();
483
496
  const interactiveSelector = "input, textarea, button, select, a, [contenteditable]";
484
- const closestInteractive = event.target?.closest(
497
+ const closestInteractive = target?.closest(
485
498
  interactiveSelector
486
499
  );
487
500
  if (closestInteractive) {
@@ -602,23 +615,8 @@ let EFFilmstrip = class extends TWMixin(LitElement) {
602
615
  />
603
616
  <code>${msToTimeCode(this.currentTimeMs, true)} </code> /
604
617
  <code>${msToTimeCode(target?.durationMs ?? 0, true)}</code>
605
- ${this.playing ? html`<button
606
- @click=${() => {
607
- if (__privateGet(this, _EFFilmstrip_instances, contextElement_get)) {
608
- __privateGet(this, _EFFilmstrip_instances, contextElement_get).playing = false;
609
- }
610
- }}
611
- >
612
- ⏸️
613
- </button>` : html`<button
614
- @click=${() => {
615
- if (__privateGet(this, _EFFilmstrip_instances, contextElement_get)) {
616
- __privateGet(this, _EFFilmstrip_instances, contextElement_get).playing = true;
617
- }
618
- }}
619
- >
620
- ▶️
621
- </button>`}
618
+ <ef-toggle-play><button>${this.playing ? "⏸️" : "▶️"}</button></ef-toggle-play>
619
+ <ef-toggle-loop><button>${this.loop ? "🔁" : html`<span class="opacity-50">🔁</span>`}</button></ef-toggle-loop>
622
620
  </div>
623
621
  <div
624
622
  class="z-10 pl-1 pr-1 pt-2 shadow shadow-slate-600 overflow-auto"
@@ -696,6 +694,10 @@ __decorateClass([
696
694
  consume({ context: playingContext, subscribe: true }),
697
695
  state()
698
696
  ], EFFilmstrip.prototype, "playing", 2);
697
+ __decorateClass([
698
+ consume({ context: loopContext, subscribe: true }),
699
+ state()
700
+ ], EFFilmstrip.prototype, "loop", 2);
699
701
  __decorateClass([
700
702
  state()
701
703
  ], EFFilmstrip.prototype, "currentTimeMs", 2);
@@ -0,0 +1,39 @@
1
+ import { css, LitElement, html } from "lit";
2
+ import { customElement } from "lit/decorators.js";
3
+ import { consume } from "@lit/context";
4
+ import { efContext } from "./efContext.js";
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __decorateClass = (decorators, target, key, kind) => {
8
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
9
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
10
+ if (decorator = decorators[i])
11
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
12
+ if (kind && result) __defProp(target, key, result);
13
+ return result;
14
+ };
15
+ let EFToggleLoop = class extends LitElement {
16
+ render() {
17
+ return html`
18
+ <slot @click=${() => {
19
+ if (this.context) {
20
+ this.context.loop = !this.context.loop;
21
+ }
22
+ }}></slot>
23
+ `;
24
+ }
25
+ };
26
+ EFToggleLoop.styles = [
27
+ css`
28
+ :host {}
29
+ `
30
+ ];
31
+ __decorateClass([
32
+ consume({ context: efContext })
33
+ ], EFToggleLoop.prototype, "context", 2);
34
+ EFToggleLoop = __decorateClass([
35
+ customElement("ef-toggle-loop")
36
+ ], EFToggleLoop);
37
+ export {
38
+ EFToggleLoop
39
+ };