@editframe/elements 0.12.0-beta.6 → 0.13.0-beta.1

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 (95) hide show
  1. package/dist/elements/EFAudio.d.ts +1 -1
  2. package/dist/elements/EFCaptions.d.ts +5 -5
  3. package/dist/elements/EFImage.d.ts +1 -1
  4. package/dist/elements/EFMedia.browsertest.d.ts +1 -1
  5. package/dist/elements/EFMedia.d.ts +6 -1
  6. package/dist/elements/{src/elements/EFMedia.js → EFMedia.js} +3 -3
  7. package/dist/elements/EFTemporal.browsertest.d.ts +1 -1
  8. package/dist/elements/EFTemporal.d.ts +145 -2
  9. package/dist/elements/{src/elements/EFTemporal.js → EFTemporal.js} +3 -0
  10. package/dist/elements/EFTimegroup.browsertest.d.ts +2 -2
  11. package/dist/elements/EFTimegroup.d.ts +8 -3
  12. package/dist/elements/{src/elements/EFTimegroup.js → EFTimegroup.js} +104 -53
  13. package/dist/elements/EFVideo.d.ts +1 -1
  14. package/dist/elements/{src/elements/EFVideo.js → EFVideo.js} +10 -2
  15. package/dist/elements/EFWaveform.d.ts +7 -4
  16. package/dist/elements/{src/elements/EFWaveform.js → EFWaveform.js} +48 -56
  17. package/dist/elements/TimegroupController.d.ts +2 -2
  18. package/dist/elements/util.d.ts +1 -1
  19. package/dist/gui/ContextMixin.browsertest.d.ts +1 -1
  20. package/dist/gui/ContextMixin.d.ts +4 -1
  21. package/dist/{elements/src/gui → gui}/ContextMixin.js +32 -44
  22. package/dist/gui/EFFilmstrip.d.ts +15 -9
  23. package/dist/{elements/src/gui → gui}/EFFilmstrip.js +64 -16
  24. package/dist/gui/EFFocusOverlay.d.ts +17 -0
  25. package/dist/gui/EFFocusOverlay.js +82 -0
  26. package/dist/gui/EFPreview.d.ts +3 -1
  27. package/dist/{elements/src/gui → gui}/EFPreview.js +24 -16
  28. package/dist/gui/EFScrubber.d.ts +1 -1
  29. package/dist/gui/EFTimeDisplay.d.ts +1 -1
  30. package/dist/gui/EFToggleLoop.d.ts +1 -1
  31. package/dist/gui/EFTogglePlay.d.ts +1 -3
  32. package/dist/{elements/src/gui → gui}/EFTogglePlay.js +1 -9
  33. package/dist/gui/EFWorkbench.d.ts +1 -1
  34. package/dist/{elements/src/gui → gui}/TWMixin.css.js +1 -1
  35. package/dist/gui/efContext.d.ts +1 -1
  36. package/dist/index.d.ts +15 -14
  37. package/dist/{elements/src/index.js → index.js} +2 -0
  38. package/dist/style.css +769 -19
  39. package/package.json +5 -4
  40. package/src/elements/EFAudio.ts +3 -3
  41. package/src/elements/EFCaptions.browsertest.ts +3 -3
  42. package/src/elements/EFCaptions.ts +9 -9
  43. package/src/elements/EFImage.browsertest.ts +2 -2
  44. package/src/elements/EFImage.ts +4 -4
  45. package/src/elements/EFMedia.browsertest.ts +6 -6
  46. package/src/elements/EFMedia.ts +14 -7
  47. package/src/elements/EFSourceMixin.ts +3 -3
  48. package/src/elements/EFTemporal.browsertest.ts +1 -1
  49. package/src/elements/EFTemporal.ts +159 -4
  50. package/src/elements/EFTimegroup.browsertest.ts +5 -5
  51. package/src/elements/EFTimegroup.ts +141 -68
  52. package/src/elements/EFVideo.ts +15 -4
  53. package/src/elements/EFWaveform.ts +82 -98
  54. package/src/elements/FetchMixin.ts +2 -2
  55. package/src/elements/TimegroupController.ts +2 -2
  56. package/src/elements/durationConverter.ts +1 -1
  57. package/src/elements/util.ts +1 -1
  58. package/src/gui/ContextMixin.browsertest.ts +3 -3
  59. package/src/gui/ContextMixin.ts +45 -52
  60. package/src/gui/EFFilmstrip.ts +92 -36
  61. package/src/gui/EFFocusOverlay.ts +79 -0
  62. package/src/gui/EFPreview.ts +35 -18
  63. package/src/gui/EFScrubber.ts +3 -3
  64. package/src/gui/EFTimeDisplay.ts +2 -2
  65. package/src/gui/EFToggleLoop.ts +4 -4
  66. package/src/gui/EFTogglePlay.ts +4 -10
  67. package/src/gui/EFWorkbench.ts +2 -2
  68. package/src/gui/efContext.ts +1 -1
  69. package/dist/assets/src/EncodedAsset.js +0 -560
  70. package/dist/assets/src/MP4File.js +0 -223
  71. package/dist/assets/src/memoize.js +0 -14
  72. package/dist/{elements/src/EF_FRAMEGEN.js → EF_FRAMEGEN.js} +0 -0
  73. package/dist/{elements/src/EF_INTERACTIVE.js → EF_INTERACTIVE.js} +0 -0
  74. package/dist/{elements/src/EF_RENDERING.js → EF_RENDERING.js} +0 -0
  75. package/dist/elements/{src/elements/CrossUpdateController.js → CrossUpdateController.js} +0 -0
  76. package/dist/elements/{src/elements/EFAudio.js → EFAudio.js} +2 -2
  77. package/dist/elements/{src/elements/EFCaptions.js → EFCaptions.js} +0 -0
  78. package/dist/elements/{src/elements/EFImage.js → EFImage.js} +0 -0
  79. package/dist/elements/{src/elements/EFSourceMixin.js → EFSourceMixin.js} +1 -1
  80. package/dist/elements/{src/elements/FetchMixin.js → FetchMixin.js} +0 -0
  81. package/dist/elements/{src/elements/TimegroupController.js → TimegroupController.js} +0 -0
  82. package/dist/elements/{src/elements/durationConverter.js → durationConverter.js} +0 -0
  83. package/dist/elements/{src/elements/parseTimeToMs.js → parseTimeToMs.js} +0 -0
  84. package/dist/{elements/src/gui → gui}/EFScrubber.js +0 -0
  85. package/dist/{elements/src/gui → gui}/EFTimeDisplay.js +0 -0
  86. package/dist/{elements/src/gui → gui}/EFToggleLoop.js +1 -1
  87. /package/dist/{elements/src/gui → gui}/EFWorkbench.js +0 -0
  88. /package/dist/{elements/src/gui → gui}/TWMixin.js +0 -0
  89. /package/dist/{elements/src/gui → gui}/apiHostContext.js +0 -0
  90. /package/dist/{elements/src/gui → gui}/efContext.js +0 -0
  91. /package/dist/{elements/src/gui → gui}/fetchContext.js +0 -0
  92. /package/dist/{elements/src/gui → gui}/focusContext.js +0 -0
  93. /package/dist/{elements/src/gui → gui}/focusedElementContext.js +0 -0
  94. /package/dist/{elements/src/gui → gui}/playingContext.js +0 -0
  95. /package/dist/{elements/src/msToTimeCode.js → msToTimeCode.js} +0 -0
@@ -1,15 +1,19 @@
1
- import { provide } from "@lit/context";
1
+ import { createContext, provide } from "@lit/context";
2
2
  import type { LitElement } from "lit";
3
3
  import { property, state } from "lit/decorators.js";
4
4
 
5
5
  import { createRef } from "lit/directives/ref.js";
6
- import type { EFTimegroup } from "../elements/EFTimegroup.ts";
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";
6
+ import type { EFTimegroup } from "../elements/EFTimegroup.js";
7
+ import { apiHostContext } from "./apiHostContext.js";
8
+ import { efContext } from "./efContext.js";
9
+ import { fetchContext } from "./fetchContext.js";
10
+ import { type FocusContext, focusContext } from "./focusContext.js";
11
+ import { focusedElementContext } from "./focusedElementContext.js";
12
+ import { loopContext, playingContext } from "./playingContext.js";
13
+
14
+ export const targetTimegroupContext = createContext<EFTimegroup | null>(
15
+ "target-timegroup",
16
+ );
13
17
 
14
18
  export declare class ContextMixinInterface extends LitElement {
15
19
  signingURL?: string;
@@ -55,6 +59,10 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
55
59
  @provide({ context: efContext })
56
60
  efContext = this;
57
61
 
62
+ @provide({ context: targetTimegroupContext })
63
+ @state()
64
+ targetTimegroup: EFTimegroup | null = null;
65
+
58
66
  @provide({ context: fetchContext })
59
67
  fetch = async (url: string, init: RequestInit = {}) => {
60
68
  init.headers ||= {};
@@ -106,8 +114,8 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
106
114
  @property({ type: Boolean, reflect: true })
107
115
  loop = false;
108
116
 
109
- @state()
110
- private stageScale = 1;
117
+ // @state()
118
+ // private stageScale = 1;
111
119
 
112
120
  @property({ type: Boolean })
113
121
  rendering = false;
@@ -121,49 +129,32 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
121
129
  #FPS = 30;
122
130
  #MS_PER_FRAME = 1000 / this.#FPS;
123
131
 
124
- setStageScale = () => {
125
- if (this.isConnected && !this.rendering) {
126
- const canvasElement = this.canvasRef.value;
127
- const stageElement = this.stageRef.value;
128
- const canvasChild = canvasElement?.assignedElements()[0];
129
- if (stageElement && canvasElement && canvasChild) {
130
- // Determine the appropriate scale factor to make the canvas fit into
131
- canvasElement.style.width = `${canvasChild.clientWidth}px`;
132
- canvasElement.style.height = `${canvasChild.clientHeight}px`;
133
- const stageWidth = stageElement.clientWidth;
134
- const stageHeight = stageElement.clientHeight;
135
- const canvasWidth = canvasElement.clientWidth;
136
- const canvasHeight = canvasElement.clientHeight;
137
- const stageRatio = stageWidth / stageHeight;
138
- const canvasRatio = canvasWidth / canvasHeight;
139
-
140
- if (stageRatio > canvasRatio) {
141
- const scale = stageHeight / canvasHeight;
142
- if (this.stageScale !== scale) {
143
- canvasElement.style.transform = `scale(${scale})`;
144
- canvasElement.style.transformOrigin = "center left";
145
- }
146
- this.stageScale = scale;
147
- } else {
148
- const scale = stageWidth / canvasWidth;
149
- if (this.stageScale !== scale) {
150
- canvasElement.style.transform = `scale(${scale})`;
151
- canvasElement.style.transformOrigin = "center left";
152
- }
153
- this.stageScale = scale;
132
+ #timegroupObserver = new MutationObserver((mutations) => {
133
+ for (const mutation of mutations) {
134
+ if (mutation.type === "childList") {
135
+ const newTimegroup = this.querySelector("ef-timegroup");
136
+ if (newTimegroup !== this.targetTimegroup) {
137
+ this.targetTimegroup = newTimegroup;
154
138
  }
155
139
  }
156
140
  }
157
- if (this.isConnected) {
158
- requestAnimationFrame(this.setStageScale);
159
- }
160
- };
141
+ });
161
142
 
162
143
  connectedCallback(): void {
163
144
  super.connectedCallback();
145
+
146
+ // Initialize targetTimegroup
147
+ this.targetTimegroup = this.querySelector("ef-timegroup");
148
+
149
+ this.#timegroupObserver.observe(this, {
150
+ childList: true,
151
+ subtree: true,
152
+ attributes: true,
153
+ });
154
+
164
155
  // Preferrably we would use a resizeObserver, but it is difficult to get the first resize
165
156
  // timed correctly. So we use requestAnimationFrame as a stop-gap.
166
- requestAnimationFrame(this.setStageScale);
157
+ // requestAnimationFrame(this.setStageScale);
167
158
  if (this.playing) {
168
159
  this.startPlayback();
169
160
  }
@@ -171,6 +162,7 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
171
162
 
172
163
  disconnectedCallback(): void {
173
164
  super.disconnectedCallback();
165
+ this.#timegroupObserver.disconnect();
174
166
  this.stopPlayback();
175
167
  }
176
168
 
@@ -201,10 +193,6 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
201
193
  super.update(changedProperties);
202
194
  }
203
195
 
204
- get targetTimegroup() {
205
- return this.querySelector("ef-timegroup");
206
- }
207
-
208
196
  play() {
209
197
  this.playing = true;
210
198
  }
@@ -252,6 +240,14 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
252
240
  await timegroup.waitForMediaDurations();
253
241
 
254
242
  let currentMs = timegroup.currentTimeMs;
243
+ const fromMs = currentMs;
244
+ const toMs = timegroup.endTimeMs;
245
+
246
+ if (fromMs >= toMs) {
247
+ this.pause();
248
+ return;
249
+ }
250
+
255
251
  let bufferCount = 0;
256
252
  this.#playbackAudioContext = new AudioContext({
257
253
  latencyHint: "playback",
@@ -281,9 +277,6 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
281
277
  }
282
278
  };
283
279
 
284
- const fromMs = currentMs;
285
- const toMs = timegroup.endTimeMs;
286
-
287
280
  const queueBufferSource = async () => {
288
281
  if (currentMs >= toMs) {
289
282
  return false;
@@ -17,21 +17,22 @@ import {
17
17
  import { createRef, ref } from "lit/directives/ref.js";
18
18
  import { styleMap } from "lit/directives/style-map.js";
19
19
 
20
- import { EFAudio } from "../elements/EFAudio.ts";
21
- import { EFCaptions, EFCaptionsActiveWord } from "../elements/EFCaptions.ts";
22
- import { EFImage } from "../elements/EFImage.ts";
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
- import { TimegroupController } from "../elements/TimegroupController.ts";
28
- import { msToTimeCode } from "../msToTimeCode.ts";
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";
20
+ import { EFAudio } from "../elements/EFAudio.js";
21
+ import { EFCaptions, EFCaptionsActiveWord } from "../elements/EFCaptions.js";
22
+ import { EFImage } from "../elements/EFImage.js";
23
+ import type { TemporalMixinInterface } from "../elements/EFTemporal.js";
24
+ import { EFTimegroup } from "../elements/EFTimegroup.js";
25
+ import { EFVideo } from "../elements/EFVideo.js";
26
+ import { EFWaveform } from "../elements/EFWaveform.js";
27
+ import { TimegroupController } from "../elements/TimegroupController.js";
28
+ import { msToTimeCode } from "../msToTimeCode.js";
29
+ import { targetTimegroupContext } from "./ContextMixin.ts";
30
+ import type { EFPreview } from "./EFPreview.js";
31
+ import type { EFWorkbench } from "./EFWorkbench.js";
32
+ import { TWMixin } from "./TWMixin.js";
33
+ import { type FocusContext, focusContext } from "./focusContext.js";
34
+ import { focusedElementContext } from "./focusedElementContext.js";
35
+ import { loopContext, playingContext } from "./playingContext.js";
35
36
 
36
37
  class ElementFilmstripController implements ReactiveController {
37
38
  constructor(
@@ -336,7 +337,7 @@ class EFHierarchyItem<
336
337
  </div>`;
337
338
  }
338
339
 
339
- renderChildren(): Array<TemplateResult<1>> | typeof nothing {
340
+ renderChildren(): Array<TemplateResult<1> | typeof nothing> | typeof nothing {
340
341
  return renderHierarchyChildren(Array.from(this.element.children));
341
342
  }
342
343
  }
@@ -419,8 +420,11 @@ class EFHTMLHierarchyItem extends EFHierarchyItem {
419
420
 
420
421
  const renderHierarchyChildren = (
421
422
  children: Element[],
422
- ): Array<TemplateResult<1>> => {
423
+ ): Array<TemplateResult<1> | typeof nothing> => {
423
424
  return children.map((child) => {
425
+ if (child instanceof HTMLElement && child.dataset?.efHidden) {
426
+ return nothing;
427
+ }
424
428
  if (child instanceof EFTimegroup) {
425
429
  return html`<ef-timegroup-hierarchy-item
426
430
  .element=${child}
@@ -467,6 +471,9 @@ const renderFilmstripChildren = (
467
471
  pixelsPerMs: number,
468
472
  ): Array<TemplateResult<1> | typeof nothing> => {
469
473
  return children.map((child) => {
474
+ if (child instanceof HTMLElement && child.dataset?.efHidden) {
475
+ return nothing;
476
+ }
470
477
  if (child instanceof EFTimegroup) {
471
478
  return html`<ef-timegroup-filmstrip
472
479
  .element=${child}
@@ -513,6 +520,14 @@ const renderFilmstripChildren = (
513
520
 
514
521
  @customElement("ef-filmstrip")
515
522
  export class EFFilmstrip extends TWMixin(LitElement) {
523
+ static styles = [
524
+ css`
525
+ :host {
526
+ display: block;
527
+ overflow: hidden;
528
+ }
529
+ `,
530
+ ];
516
531
  @property({ type: Number })
517
532
  pixelsPerMs = 0.04;
518
533
 
@@ -535,15 +550,35 @@ export class EFFilmstrip extends TWMixin(LitElement) {
535
550
  @state()
536
551
  currentTimeMs = 0;
537
552
 
553
+ @property({ type: Boolean, reflect: true, attribute: "auto-scale" })
554
+ autoScale = false;
555
+
556
+ private resizeObserver = new ResizeObserver(() => {
557
+ if (this.autoScale) {
558
+ this.updatePixelsPerMs();
559
+ }
560
+ });
561
+
538
562
  connectedCallback(): void {
539
563
  super.connectedCallback();
540
564
  this.#bindToTargetTimegroup();
541
565
  window.addEventListener("keypress", this.#handleKeyPress);
566
+
567
+ this.resizeObserver.observe(this);
542
568
  }
543
569
 
544
570
  disconnectedCallback(): void {
545
571
  super.disconnectedCallback();
546
572
  window.removeEventListener("keypress", this.#handleKeyPress);
573
+ this.resizeObserver.disconnect();
574
+ }
575
+
576
+ updatePixelsPerMs() {
577
+ const target = this.targetTimegroup;
578
+ const gutter = this.gutterRef.value;
579
+ if (target && gutter && gutter.clientWidth > 0) {
580
+ this.pixelsPerMs = gutter.clientWidth / (target.durationMs || 1);
581
+ }
547
582
  }
548
583
 
549
584
  #bindToTargetTimegroup() {
@@ -645,7 +680,9 @@ export class EFFilmstrip extends TWMixin(LitElement) {
645
680
  @eventOptions({ passive: false })
646
681
  scrollScrub(e: WheelEvent) {
647
682
  if (this.targetTimegroup && this.gutter && !this.playing) {
648
- e.preventDefault();
683
+ if (e.deltaX !== 0) {
684
+ e.preventDefault(); // Prevent default side scroll behavior only
685
+ }
649
686
  // Avoid over-scrolling to the left
650
687
  if (
651
688
  this.gutterRef.value &&
@@ -694,38 +731,42 @@ export class EFFilmstrip extends TWMixin(LitElement) {
694
731
  <div
695
732
  class="z-20 col-span-2 border-b-slate-600 bg-slate-100 shadow shadow-slate-300"
696
733
  >
697
- <input
698
- type="range"
699
- .value=${this.pixelsPerMs}
700
- min="0.01"
701
- max="0.1"
702
- step="0.001"
703
- @input=${(e: Event) => {
704
- const target = e.target as HTMLInputElement;
705
- this.pixelsPerMs = Number.parseFloat(target.value);
706
- }}
707
- />
734
+ ${
735
+ !this.autoScale
736
+ ? html`<input
737
+ type="range"
738
+ .value=${this.pixelsPerMs}
739
+ min="0.01"
740
+ max="0.1"
741
+ step="0.001"
742
+ @input=${(e: Event) => {
743
+ const target = e.target as HTMLInputElement;
744
+ this.pixelsPerMs = Number.parseFloat(target.value);
745
+ }}
746
+ />`
747
+ : nothing
748
+ }
708
749
  <code>${msToTimeCode(this.currentTimeMs, true)} </code> /
709
750
  <code>${msToTimeCode(target?.durationMs ?? 0, true)}</code>
710
751
  <ef-toggle-play class="inline-block mx-2">
711
752
  <div slot="pause">
712
- <button class="bg-white">&#9654;</button>
753
+ <button>⏸️</button>
713
754
  </div>
714
755
  <div slot="play">
715
- <button>⏸️</button>
756
+ <button>▶️</button>
716
757
  </div>
717
758
  </ef-toggle-play>
718
- <ef-toggle-loop><button>${this.loop ? "🔁" : html`<span class="opacity-50">🔁</span>`}</button></ef-toggle-loop>
759
+ <ef-toggle-loop><button>${this.loop ? "🔁" : html`<span class="opacity-50 line-through">🔁</span>`}</button></ef-toggle-loop>
719
760
  </div>
720
761
  <div
721
- class="z-10 pl-1 pr-1 pt-2 shadow shadow-slate-600 overflow-auto"
762
+ class="z-10 pl-1 pr-1 pt-[8px] shadow shadow-slate-600 overflow-auto"
722
763
  ${ref(this.hierarchyRef)}
723
764
  @scroll=${this.syncHierarchyScroll}
724
765
  >
725
766
  ${renderHierarchyChildren(target ? [target] : [])}
726
767
  </div>
727
768
  <div
728
- class="h-full w-full cursor-crosshair overflow-auto bg-slate-200 pt-2"
769
+ class="flex h-full w-full cursor-crosshair overflow-auto bg-slate-200 pt-[8px]"
729
770
  id="gutter"
730
771
  ${ref(this.gutterRef)}
731
772
  @scroll=${this.syncGutterScroll}
@@ -767,8 +808,23 @@ export class EFFilmstrip extends TWMixin(LitElement) {
767
808
  return this.closest("ef-workbench, ef-preview") as EFWorkbench | EFPreview;
768
809
  }
769
810
 
770
- get targetTimegroup() {
771
- return this.#contextElement?.targetTimegroup;
811
+ @property({ type: String })
812
+ target?: string;
813
+
814
+ @consume({ context: targetTimegroupContext, subscribe: true })
815
+ @state()
816
+ targetTimegroup?: EFTimegroup | null;
817
+
818
+ protected willUpdate(
819
+ changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
820
+ ) {
821
+ if (changedProperties.has("targetTimegroup")) {
822
+ this.#bindToTargetTimegroup();
823
+ }
824
+ if (this.autoScale) {
825
+ this.updatePixelsPerMs();
826
+ }
827
+ super.willUpdate(changedProperties);
772
828
  }
773
829
  }
774
830
 
@@ -0,0 +1,79 @@
1
+ import { consume } from "@lit/context";
2
+ import { LitElement, css, html } from "lit";
3
+ import { customElement } from "lit/decorators.js";
4
+ import { createRef, ref } from "lit/directives/ref.js";
5
+ import { focusedElementContext } from "./focusedElementContext.js";
6
+
7
+ @customElement("ef-focus-overlay")
8
+ export class EFFocusOverlay extends LitElement {
9
+ static styles = css`
10
+ :host {
11
+ display: block;
12
+ position: relative;
13
+ width: 100%;
14
+ height: 100%;
15
+ pointer-events: none;
16
+ }
17
+ .overlay {
18
+ position: fixed;
19
+ outline: 2px solid var(--ef-focus-overlay-color, rgb(59, 130, 246));
20
+ background: var(--ef-focus-overlay-background, rgb(59, 130, 246));
21
+ outline: 2px solid var(--ef-focus-overlay-color, rgb(59, 130, 246));
22
+ mix-blend-mode: multiply;
23
+ opacity: 0.4;
24
+ display: none;
25
+ }
26
+ `;
27
+
28
+ @consume({ context: focusedElementContext, subscribe: true })
29
+ focusedElement?: HTMLElement | null;
30
+
31
+ overlay = createRef<HTMLDivElement>();
32
+
33
+ private animationFrame?: number;
34
+
35
+ drawOverlay = () => {
36
+ const overlay = this.overlay.value;
37
+ if (overlay) {
38
+ if (this.focusedElement) {
39
+ overlay.style.display = "block";
40
+ const rect = this.focusedElement.getBoundingClientRect();
41
+ Object.assign(overlay.style, {
42
+ top: `${rect.top}px`,
43
+ left: `${rect.left}px`,
44
+ width: `${rect.width}px`,
45
+ height: `${rect.height}px`,
46
+ });
47
+ this.animationFrame = requestAnimationFrame(this.drawOverlay);
48
+ } else {
49
+ overlay.style.display = "none";
50
+ }
51
+ }
52
+ };
53
+
54
+ render() {
55
+ return html`<div ${ref(this.overlay)} class="overlay"></div>`;
56
+ }
57
+
58
+ connectedCallback(): void {
59
+ super.connectedCallback();
60
+ this.drawOverlay();
61
+ }
62
+
63
+ disconnectedCallback(): void {
64
+ super.disconnectedCallback();
65
+ if (this.animationFrame) {
66
+ cancelAnimationFrame(this.animationFrame);
67
+ }
68
+ }
69
+
70
+ protected updated(): void {
71
+ this.drawOverlay();
72
+ }
73
+ }
74
+
75
+ declare global {
76
+ interface HTMLElementTagNameMap {
77
+ "ef-focus-overlay": EFFocusOverlay;
78
+ }
79
+ }
@@ -1,36 +1,53 @@
1
1
  import { LitElement, css, html } from "lit";
2
2
  import { customElement } from "lit/decorators.js";
3
- import { ref } from "lit/directives/ref.js";
4
3
 
5
- import { ContextMixin } from "./ContextMixin.ts";
6
- import { TWMixin } from "./TWMixin.ts";
4
+ import { provide } from "@lit/context";
5
+ import { ContextMixin } from "./ContextMixin.js";
6
+ import { TWMixin } from "./TWMixin.js";
7
+ import { focusedElementContext } from "./focusedElementContext.js";
7
8
 
8
9
  @customElement("ef-preview")
9
10
  export class EFPreview extends ContextMixin(TWMixin(LitElement)) {
11
+ @provide({ context: focusedElementContext })
12
+ focusedElement?: HTMLElement;
13
+
14
+ constructor() {
15
+ super();
16
+ this.addEventListener("mouseover", (e) => {
17
+ const target = e.target as HTMLElement;
18
+ const timegroup = target.closest("ef-timegroup");
19
+ if (target !== this && timegroup) {
20
+ this.focusedElement = target;
21
+ }
22
+ });
23
+ this.addEventListener("mouseout", (e) => {
24
+ const relatedTarget = e.relatedTarget as HTMLElement;
25
+ const targetingTimegroup = relatedTarget?.closest("ef-timegroup");
26
+ // Clear focus if:
27
+ // 1. Moving outside the preview entirely, or
28
+ // 2. Moving to the preview itself, or
29
+ // 3. Moving to an element that's not within a timegroup
30
+ if (
31
+ !this.contains(relatedTarget) ||
32
+ relatedTarget === this ||
33
+ !targetingTimegroup
34
+ ) {
35
+ this.focusedElement = undefined;
36
+ }
37
+ });
38
+ }
39
+
10
40
  static styles = [
11
41
  css`
12
42
  :host {
13
43
  display: block;
14
- width: 100%;
15
- height: 100%;
44
+ cursor: crosshair;
16
45
  }
17
46
  `,
18
47
  ];
19
48
 
20
49
  render() {
21
- return html`
22
- <div
23
- ${ref(this.stageRef)}
24
- class="relative grid h-full w-full place-content-center place-items-center overflow-hidden"
25
- >
26
- <slot
27
- ${ref(this.canvasRef)}
28
- class="inline-block"
29
- name="canvas"
30
- ></slot>
31
- <slot name="controls"></slot>
32
- </div>
33
- `;
50
+ return html`<slot></slot>`;
34
51
  }
35
52
  }
36
53
 
@@ -3,9 +3,9 @@ import { LitElement, css, html } from "lit";
3
3
  import { customElement, state } from "lit/decorators.js";
4
4
 
5
5
  import { ref } from "lit/directives/ref.js";
6
- import type { ContextMixinInterface } from "./ContextMixin.ts";
7
- import { efContext } from "./efContext.ts";
8
- import { playingContext } from "./playingContext.ts";
6
+ import type { ContextMixinInterface } from "./ContextMixin.js";
7
+ import { efContext } from "./efContext.js";
8
+ import { playingContext } from "./playingContext.js";
9
9
 
10
10
  @customElement("ef-scrubber")
11
11
  export class EFScrubber extends LitElement {
@@ -1,8 +1,8 @@
1
1
  import { consume } from "@lit/context";
2
2
  import { LitElement, css, html } from "lit";
3
3
  import { customElement } from "lit/decorators.js";
4
- import type { ContextMixinInterface } from "./ContextMixin.ts";
5
- import { efContext } from "./efContext.ts";
4
+ import type { ContextMixinInterface } from "./ContextMixin.js";
5
+ import { efContext } from "./efContext.js";
6
6
 
7
7
  @customElement("ef-time-display")
8
8
  export class EFTimeDisplay extends LitElement {
@@ -1,9 +1,9 @@
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 } from "lit/decorators.js";
4
4
 
5
- import { efContext } from "./efContext.ts";
6
- import type { ContextMixinInterface } from "./ContextMixin.ts";
5
+ import type { ContextMixinInterface } from "./ContextMixin.js";
6
+ import { efContext } from "./efContext.js";
7
7
 
8
8
  @customElement("ef-toggle-loop")
9
9
  export class EFToggleLoop extends LitElement {
@@ -1,10 +1,10 @@
1
1
  import { consume } from "@lit/context";
2
2
  import { LitElement, css, html } from "lit";
3
- import { customElement, property } from "lit/decorators.js";
3
+ import { customElement } from "lit/decorators.js";
4
4
 
5
- import type { ContextMixinInterface } from "./ContextMixin.ts";
6
- import { efContext } from "./efContext.ts";
7
- import { playingContext } from "./playingContext.ts";
5
+ import type { ContextMixinInterface } from "./ContextMixin.js";
6
+ import { efContext } from "./efContext.js";
7
+ import { playingContext } from "./playingContext.js";
8
8
 
9
9
  @customElement("ef-toggle-play")
10
10
  export class EFTogglePlay extends LitElement {
@@ -23,12 +23,6 @@ export class EFTogglePlay extends LitElement {
23
23
  @consume({ context: playingContext, subscribe: true })
24
24
  playing = false;
25
25
 
26
- @property({ type: String })
27
- play = '<button class="text-2xl cursor-pointer">▶️</button>';
28
-
29
- @property({ type: String })
30
- pause = '<button class="text-2xl cursor-pointer">⏸️</button>';
31
-
32
26
  render() {
33
27
  return html`
34
28
  <div
@@ -2,8 +2,8 @@ import { LitElement, type PropertyValueMap, css, html } from "lit";
2
2
  import { customElement, eventOptions } from "lit/decorators.js";
3
3
  import { createRef, ref } from "lit/directives/ref.js";
4
4
 
5
- import { ContextMixin } from "./ContextMixin.ts";
6
- import { TWMixin } from "./TWMixin.ts";
5
+ import { ContextMixin } from "./ContextMixin.js";
6
+ import { TWMixin } from "./TWMixin.js";
7
7
 
8
8
  @customElement("ef-workbench")
9
9
  export class EFWorkbench extends ContextMixin(TWMixin(LitElement)) {
@@ -1,5 +1,5 @@
1
1
  import { createContext } from "@lit/context";
2
- import type { ContextMixinInterface } from "./ContextMixin.ts";
2
+ import type { ContextMixinInterface } from "./ContextMixin.js";
3
3
 
4
4
  export const efContext = createContext<ContextMixinInterface | null>(
5
5
  Symbol("efContext"),