@editframe/elements 0.11.0-beta.1 → 0.11.0-beta.14

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 (41) hide show
  1. package/dist/EF_FRAMEGEN.d.ts +8 -15
  2. package/dist/elements/EFMedia.d.ts +2 -2
  3. package/dist/elements/EFTemporal.browsertest.d.ts +10 -0
  4. package/dist/elements/EFTemporal.d.ts +10 -2
  5. package/dist/elements/EFTimegroup.browsertest.d.ts +4 -0
  6. package/dist/elements/EFTimegroup.d.ts +12 -1
  7. package/dist/elements/EFWaveform.d.ts +3 -3
  8. package/dist/elements/durationConverter.d.ts +4 -0
  9. package/dist/elements/src/EF_FRAMEGEN.js +24 -26
  10. package/dist/elements/src/elements/EFImage.js +3 -7
  11. package/dist/elements/src/elements/EFMedia.js +27 -9
  12. package/dist/elements/src/elements/EFTemporal.js +106 -13
  13. package/dist/elements/src/elements/EFTimegroup.js +26 -5
  14. package/dist/elements/src/elements/EFVideo.js +7 -7
  15. package/dist/elements/src/elements/EFWaveform.js +14 -8
  16. package/dist/elements/src/gui/ContextMixin.js +22 -6
  17. package/dist/elements/src/gui/EFFilmstrip.js +28 -8
  18. package/dist/elements/src/gui/EFTogglePlay.js +38 -6
  19. package/dist/elements/src/gui/EFWorkbench.js +1 -24
  20. package/dist/elements/src/gui/TWMixin.css.js +1 -1
  21. package/dist/gui/ContextMixin.d.ts +1 -0
  22. package/dist/gui/EFFilmstrip.d.ts +4 -4
  23. package/dist/gui/EFTogglePlay.d.ts +4 -0
  24. package/dist/gui/EFWorkbench.d.ts +0 -1
  25. package/dist/style.css +15 -4
  26. package/package.json +2 -2
  27. package/src/elements/EFImage.ts +4 -8
  28. package/src/elements/EFMedia.browsertest.ts +231 -2
  29. package/src/elements/EFMedia.ts +48 -9
  30. package/src/elements/EFTemporal.browsertest.ts +79 -0
  31. package/src/elements/EFTemporal.ts +139 -6
  32. package/src/elements/EFTimegroup.browsertest.ts +38 -1
  33. package/src/elements/EFTimegroup.ts +28 -5
  34. package/src/elements/EFVideo.ts +7 -8
  35. package/src/elements/EFWaveform.ts +14 -9
  36. package/src/elements/durationConverter.ts +4 -0
  37. package/src/gui/ContextMixin.browsertest.ts +28 -2
  38. package/src/gui/ContextMixin.ts +25 -7
  39. package/src/gui/EFFilmstrip.ts +37 -17
  40. package/src/gui/EFTogglePlay.ts +38 -7
  41. package/src/gui/EFWorkbench.ts +3 -36
@@ -1,9 +1,12 @@
1
- import { LitElement, html, css, type PropertyValueMap } from "lit";
2
1
  import { provide } from "@lit/context";
3
2
  import { Task } from "@lit/task";
4
- import { customElement, property } from "lit/decorators.js";
5
3
  import debug from "debug";
4
+ import { LitElement, type PropertyValueMap, css, html } from "lit";
5
+ import { customElement, property } from "lit/decorators.js";
6
6
 
7
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
8
+ import { isContextMixin } from "../gui/ContextMixin.ts";
9
+ import { deepGetMediaElements } from "./EFMedia.ts";
7
10
  import {
8
11
  EFTemporal,
9
12
  isEFTemporal,
@@ -11,8 +14,6 @@ import {
11
14
  timegroupContext,
12
15
  } from "./EFTemporal.ts";
13
16
  import { TimegroupController } from "./TimegroupController.ts";
14
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
15
- import { deepGetMediaElements } from "./EFMedia.ts";
16
17
  import { durationConverter } from "./durationConverter.ts";
17
18
 
18
19
  const log = debug("ef:elements:EFTimegroup");
@@ -243,12 +244,34 @@ export class EFTimegroup extends EFTemporal(LitElement) {
243
244
  }
244
245
  }
245
246
 
247
+ get contextProvider() {
248
+ let parent = this.parentNode;
249
+ while (parent) {
250
+ if (isContextMixin(parent)) {
251
+ return parent;
252
+ }
253
+ parent = parent.parentNode;
254
+ }
255
+ return null;
256
+ }
257
+
258
+ /**
259
+ * Returns true if the timegroup should be wrapped with a workbench.
260
+ *
261
+ * A timegroup should be wrapped with a workbench if it is the root-most timegroup
262
+ * and EF_INTERACTIVE is true.
263
+ *
264
+ * If the timegroup is already wrappedin a context provider like ef-preview,
265
+ * it should NOT be wrapped in a workbench.
266
+ *
267
+ */
246
268
  shouldWrapWithWorkbench() {
247
269
  return (
248
270
  EF_INTERACTIVE &&
249
271
  this.closest("ef-timegroup") === this &&
272
+ this.closest("ef-preview") === null &&
250
273
  this.closest("ef-workbench") === null &&
251
- this.closest("ef-preview") === null
274
+ this.closest("test-context") === null
252
275
  );
253
276
  }
254
277
 
@@ -1,10 +1,10 @@
1
- import { html, css } from "lit";
2
1
  import { Task } from "@lit/task";
3
- import { createRef, ref } from "lit/directives/ref.js";
2
+ import { css, html } from "lit";
4
3
  import { customElement } from "lit/decorators.js";
4
+ import { createRef, ref } from "lit/directives/ref.js";
5
5
 
6
- import { EFMedia } from "./EFMedia.ts";
7
6
  import { TWMixin } from "../gui/TWMixin.ts";
7
+ import { EFMedia } from "./EFMedia.ts";
8
8
 
9
9
  @customElement("ef-video")
10
10
  export class EFVideo extends TWMixin(EFMedia) {
@@ -13,15 +13,14 @@ export class EFVideo extends TWMixin(EFMedia) {
13
13
  :host {
14
14
  display: block;
15
15
  }
16
+ canvas {
17
+ all: inherit;
18
+ }
16
19
  `,
17
20
  ];
18
21
  canvasRef = createRef<HTMLCanvasElement>();
19
-
20
22
  render() {
21
- return html` <canvas
22
- class="h-full w-full object-fill"
23
- ${ref(this.canvasRef)}
24
- ></canvas>`;
23
+ return html` <canvas ${ref(this.canvasRef)}></canvas>`;
25
24
  }
26
25
 
27
26
  get canvasElement() {
@@ -1,27 +1,32 @@
1
1
  import { EFAudio } from "./EFAudio.ts";
2
2
 
3
- import { LitElement, html } from "lit";
4
- import { customElement, property } from "lit/decorators.js";
5
- import { EFVideo } from "./EFVideo.ts";
6
- import { EFTemporal } from "./EFTemporal.ts";
7
- import { CrossUpdateController } from "./CrossUpdateController.ts";
8
- import { TWMixin } from "../gui/TWMixin.ts";
9
3
  import { Task } from "@lit/task";
10
4
  import * as d3 from "d3";
5
+ import { LitElement, css, html } from "lit";
6
+ import { customElement, property } from "lit/decorators.js";
11
7
  import { type Ref, createRef, ref } from "lit/directives/ref.js";
12
8
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
9
+ import { TWMixin } from "../gui/TWMixin.ts";
10
+ import { CrossUpdateController } from "./CrossUpdateController.ts";
11
+ import { EFTemporal } from "./EFTemporal.ts";
12
+ import { EFVideo } from "./EFVideo.ts";
13
13
 
14
14
  @customElement("ef-waveform")
15
15
  export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
16
- static styles = [];
16
+ static styles = [
17
+ css`
18
+ svg {
19
+ all: inherit;
20
+ }
21
+ `,
22
+ ];
17
23
  svgRef: Ref<SVGElement> = createRef();
18
24
  createRenderRoot() {
19
25
  return this;
20
26
  }
21
27
  render() {
22
- return html` <svg ${ref(this.svgRef)} class="h-full w-full" store></svg> `;
28
+ return html` <svg ${ref(this.svgRef)} store></svg> `;
23
29
  }
24
-
25
30
  @property({
26
31
  type: String,
27
32
  attribute: "mode",
@@ -24,3 +24,7 @@ export const trimDurationConverter = positiveDurationConverter(
24
24
  export const imageDurationConverter = positiveDurationConverter(
25
25
  "Image duration must be a positive value in milliseconds or seconds (1s, 1000ms)",
26
26
  );
27
+
28
+ export const sourceDurationConverter = positiveDurationConverter(
29
+ "Sourcein & sourceout must be a positive value in milliseconds or seconds (1s, 1000ms)",
30
+ );
@@ -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
  });
@@ -1,15 +1,15 @@
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
14
  export declare class ContextMixinInterface {
15
15
  signingURL?: string;
@@ -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
 
@@ -164,10 +176,16 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
164
176
  this.stopPlayback();
165
177
  }
166
178
  }
167
-
168
179
  if (changedProperties.has("currentTimeMs") && this.targetTimegroup) {
169
180
  if (this.targetTimegroup.currentTimeMs !== this.currentTimeMs) {
170
181
  this.targetTimegroup.currentTimeMs = this.currentTimeMs;
182
+ if (this.isConnected) {
183
+ this.dispatchEvent(
184
+ new CustomEvent("timeupdate", {
185
+ detail: { currentTimeMs: this.currentTimeMs },
186
+ }),
187
+ );
188
+ }
171
189
  }
172
190
  }
173
191
  super.update(changedProperties);
@@ -1,37 +1,37 @@
1
+ import { consume } from "@lit/context";
1
2
  import {
2
3
  LitElement,
3
- html,
4
+ type PropertyValueMap,
5
+ type ReactiveController,
6
+ type TemplateResult,
4
7
  css,
8
+ html,
5
9
  nothing,
6
- type TemplateResult,
7
- type ReactiveController,
8
- type PropertyValueMap,
9
10
  } from "lit";
10
11
  import {
11
12
  customElement,
12
- property,
13
13
  eventOptions,
14
+ property,
14
15
  state,
15
16
  } from "lit/decorators.js";
16
- import { consume } from "@lit/context";
17
+ import { createRef, ref } from "lit/directives/ref.js";
17
18
  import { styleMap } from "lit/directives/style-map.js";
18
- import { ref, createRef } from "lit/directives/ref.js";
19
19
 
20
- import { EFImage } from "../elements/EFImage.ts";
21
20
  import { EFAudio } from "../elements/EFAudio.ts";
22
- import { EFVideo } from "../elements/EFVideo.ts";
23
21
  import { EFCaptions, EFCaptionsActiveWord } from "../elements/EFCaptions.ts";
24
- import { EFWaveform } from "../elements/EFWaveform.ts";
25
- import { EFTimegroup } from "../elements/EFTimegroup.ts";
22
+ import { EFImage } from "../elements/EFImage.ts";
26
23
  import type { TemporalMixinInterface } from "../elements/EFTemporal.ts";
24
+ import { EFTimegroup } from "../elements/EFTimegroup.ts";
25
+ import { EFVideo } from "../elements/EFVideo.ts";
26
+ import { EFWaveform } from "../elements/EFWaveform.ts";
27
27
  import { TimegroupController } from "../elements/TimegroupController.ts";
28
- import { TWMixin } from "./TWMixin.ts";
29
28
  import { msToTimeCode } from "../msToTimeCode.ts";
30
- import { focusedElementContext } from "./focusedElementContext.ts";
31
- import { type FocusContext, focusContext } from "./focusContext.ts";
32
- import { playingContext, loopContext } from "./playingContext.ts";
33
- import type { EFWorkbench } from "./EFWorkbench.ts";
34
29
  import type { EFPreview } from "./EFPreview.ts";
30
+ import type { EFWorkbench } from "./EFWorkbench.ts";
31
+ import { TWMixin } from "./TWMixin.ts";
32
+ import { type FocusContext, focusContext } from "./focusContext.ts";
33
+ import { focusedElementContext } from "./focusedElementContext.ts";
34
+ import { loopContext, playingContext } from "./playingContext.ts";
35
35
 
36
36
  class ElementFilmstripController implements ReactiveController {
37
37
  constructor(
@@ -87,6 +87,13 @@ class FilmstripItem extends TWMixin(LitElement) {
87
87
  pixelsPerMs = 0.04;
88
88
 
89
89
  get gutterStyles() {
90
+ if (this.element.sourceInMs || this.element.sourceOutMs) {
91
+ return {
92
+ position: "relative",
93
+ left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.trimStartMs - this.element.sourceInMs)}px`,
94
+ width: `${this.pixelsPerMs * (this.element.durationMs + this.element.trimStartMs + this.element.trimEndMs + this.element.sourceOutMs + this.element.sourceInMs)}px`,
95
+ };
96
+ }
90
97
  return {
91
98
  position: "relative",
92
99
  left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.trimStartMs)}px`,
@@ -95,6 +102,12 @@ class FilmstripItem extends TWMixin(LitElement) {
95
102
  }
96
103
 
97
104
  get trimPortionStyles() {
105
+ if (this.element.sourceInMs || this.element.sourceOutMs) {
106
+ return {
107
+ width: `${this.pixelsPerMs * this.element.durationMs}px`,
108
+ left: `${this.pixelsPerMs * (this.element.trimStartMs + this.element.sourceInMs)}px`,
109
+ };
110
+ }
98
111
  return {
99
112
  width: `${this.pixelsPerMs * this.element.durationMs}px`,
100
113
  left: `${this.pixelsPerMs * this.element.trimStartMs}px`,
@@ -694,7 +707,14 @@ export class EFFilmstrip extends TWMixin(LitElement) {
694
707
  />
695
708
  <code>${msToTimeCode(this.currentTimeMs, true)} </code> /
696
709
  <code>${msToTimeCode(target?.durationMs ?? 0, true)}</code>
697
- <ef-toggle-play><button>${this.playing ? "⏸️" : "▶️"}</button></ef-toggle-play>
710
+ <ef-toggle-play class="inline-block mx-2">
711
+ <div slot="pause">
712
+ <button class="bg-white">&#9654;</button>
713
+ </div>
714
+ <div slot="play">
715
+ <button>⏸️</button>
716
+ </div>
717
+ </ef-toggle-play>
698
718
  <ef-toggle-loop><button>${this.loop ? "🔁" : html`<span class="opacity-50">🔁</span>`}</button></ef-toggle-loop>
699
719
  </div>
700
720
  <div
@@ -1,33 +1,64 @@
1
- import { css, html, LitElement } from "lit";
2
- import { customElement } from "lit/decorators.js";
3
1
  import { consume } from "@lit/context";
2
+ import { LitElement, css, html } from "lit";
3
+ import { customElement, property, state } from "lit/decorators.js";
4
4
 
5
- import { efContext } from "./efContext.ts";
6
5
  import type { ContextMixinInterface } from "./ContextMixin.ts";
6
+ import { efContext } from "./efContext.ts";
7
7
 
8
8
  @customElement("ef-toggle-play")
9
9
  export class EFTogglePlay extends LitElement {
10
10
  static styles = [
11
11
  css`
12
12
  :host {}
13
+ div {
14
+ all: inherit;
15
+ }
13
16
  `,
14
17
  ];
15
18
 
16
- @consume({ context: efContext })
19
+ @consume({ context: efContext, subscribe: true })
17
20
  context?: ContextMixinInterface | null;
18
21
 
22
+ @property({ type: String })
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()
29
+ playing = false;
30
+
19
31
  render() {
20
32
  return html`
21
- <slot @click=${() => {
33
+ <div
34
+ @click=${() => {
22
35
  if (this.context) {
23
36
  if (this.context.playing) {
24
37
  this.context.pause();
38
+ this.playing = false;
25
39
  } else {
26
40
  this.context.play();
41
+ this.playing = true;
27
42
  }
28
43
  }
29
- }}></slot>
30
- `;
44
+ }}>
45
+ ${
46
+ this.playing
47
+ ? html`<slot name="play"></slot>`
48
+ : html`<slot name="pause"></slot>`
49
+ }
50
+ </div>`;
51
+ }
52
+
53
+ togglePlay() {
54
+ this.requestUpdate();
55
+ if (this.context) {
56
+ if (this.context.playing) {
57
+ this.context.pause();
58
+ } else {
59
+ this.context.play();
60
+ }
61
+ }
31
62
  }
32
63
  }
33
64
 
@@ -1,12 +1,9 @@
1
- import { LitElement, html, css, type PropertyValueMap } from "lit";
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 { ref, createRef } from "lit/directives/ref.js";
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 {