@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
@@ -1,4 +1,3 @@
1
- import { EFTimegroup } from "../elements/EFTimegroup";
2
1
  import {
3
2
  LitElement,
4
3
  html,
@@ -14,19 +13,25 @@ import {
14
13
  eventOptions,
15
14
  state,
16
15
  } from "lit/decorators.js";
16
+ import { consume } from "@lit/context";
17
17
  import { styleMap } from "lit/directives/style-map.js";
18
18
  import { ref, createRef } from "lit/directives/ref.js";
19
- import { EFImage } from "../elements/EFImage";
20
- import { EFAudio } from "../elements/EFAudio";
21
- import { EFVideo } from "../elements/EFVideo";
22
- import { EFCaptions, EFCaptionsActiveWord } from "../elements/EFCaptions";
23
- import { EFWaveform } from "../elements/EFWaveform";
24
- import type { TemporalMixinInterface } from "../elements/EFTemporal";
25
- import { TimegroupController } from "../elements/TimegroupController";
26
- import { consume } from "@lit/context";
27
- import { type FocusContext, focusContext, focusedElement } from "./EFWorkbench";
28
- import { TWMixin } from "./TWMixin";
29
- import { msToTimeCode } from "@/av/msToTimeCode";
19
+
20
+ import { EFImage } from "../elements/EFImage.ts";
21
+ import { EFAudio } from "../elements/EFAudio.ts";
22
+ import { EFVideo } from "../elements/EFVideo.ts";
23
+ import { EFCaptions, EFCaptionsActiveWord } from "../elements/EFCaptions.ts";
24
+ import { EFWaveform } from "../elements/EFWaveform.ts";
25
+ import { EFTimegroup } from "../elements/EFTimegroup.ts";
26
+ import type { TemporalMixinInterface } from "../elements/EFTemporal.ts";
27
+ import { TimegroupController } from "../elements/TimegroupController.ts";
28
+ import { TWMixin } from "./TWMixin.ts";
29
+ 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
+ import type { EFPreview } from "./EFPreview.ts";
30
35
 
31
36
  class ElementFilmstripController implements ReactiveController {
32
37
  constructor(
@@ -68,31 +73,38 @@ class FilmstripItem extends TWMixin(LitElement) {
68
73
  @consume({ context: focusContext, subscribe: true })
69
74
  focusContext?: FocusContext;
70
75
 
71
- @consume({ context: focusedElement, subscribe: true })
76
+ @consume({ context: focusedElementContext, subscribe: true })
72
77
  focusedElement?: HTMLElement | null;
73
78
 
74
79
  get isFocused() {
75
80
  return this.element && this.focusContext?.focusedElement === this.element;
76
81
  }
77
82
 
78
- @property({ type: HTMLElement, attribute: false })
83
+ @property({ type: Object, attribute: false })
79
84
  element: TemporalMixinInterface & LitElement = new EFTimegroup();
80
85
 
81
86
  @property({ type: Number })
82
87
  pixelsPerMs = 0.04;
83
88
 
84
- get styles() {
89
+ get gutterStyles() {
85
90
  return {
86
91
  position: "relative",
87
- left: `${this.pixelsPerMs * this.element.startTimeWithinParentMs}px`,
92
+ left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.trimStartMs)}px`,
93
+ width: `${this.pixelsPerMs * (this.element.durationMs + this.element.trimStartMs + this.element.trimEndMs)}px`,
94
+ };
95
+ }
96
+
97
+ get trimPortionStyles() {
98
+ return {
88
99
  width: `${this.pixelsPerMs * this.element.durationMs}px`,
100
+ left: `${this.pixelsPerMs * this.element.trimStartMs}px`,
89
101
  };
90
102
  }
91
103
 
92
104
  render() {
93
- return html` <div class="" style=${styleMap(this.styles)}>
105
+ return html`<div style=${styleMap(this.gutterStyles)}>
94
106
  <div
95
- 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"
107
+ class="bg-slate-300"
96
108
  ?data-focused=${this.isFocused}
97
109
  @mouseenter=${() => {
98
110
  if (this.focusContext) {
@@ -105,7 +117,13 @@ class FilmstripItem extends TWMixin(LitElement) {
105
117
  }
106
118
  }}
107
119
  >
108
- ${this.animations()}
120
+ <div
121
+ ?data-focused=${this.isFocused}
122
+ 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"
123
+ style=${styleMap(this.trimPortionStyles)}
124
+ >
125
+ ${this.animations()}
126
+ </div>
109
127
  </div>
110
128
  ${this.renderChildren()}
111
129
  </div>`;
@@ -254,14 +272,14 @@ export class EFHTMLFilmstrip extends FilmstripItem {
254
272
  class EFHierarchyItem<
255
273
  ElementType extends HTMLElement = HTMLElement,
256
274
  > extends TWMixin(LitElement) {
257
- @property({ type: HTMLElement, attribute: false })
275
+ @property({ type: Object, attribute: false })
258
276
  // @ts-expect-error This could be initialzed with any HTMLElement
259
277
  element: ElementType = new EFTimegroup();
260
278
 
261
279
  @consume({ context: focusContext })
262
280
  focusContext?: FocusContext;
263
281
 
264
- @consume({ context: focusedElement, subscribe: true })
282
+ @consume({ context: focusedElementContext, subscribe: true })
265
283
  focusedElement?: HTMLElement | null;
266
284
 
267
285
  get icon(): TemplateResult<1> | string {
@@ -485,23 +503,25 @@ export class EFFilmstrip extends TWMixin(LitElement) {
485
503
  @property({ type: Number })
486
504
  pixelsPerMs = 0.04;
487
505
 
488
- @property({ type: Number })
489
- currentTimeMs = 0;
490
-
491
- @property({ type: String, attribute: "target", reflect: true })
492
- targetSelector = "";
493
-
494
506
  @state()
495
507
  scrubbing = false;
496
508
 
497
509
  @state()
498
- playing = false;
510
+ timelineScrolltop = 0;
499
511
 
512
+ @consume({ context: playingContext, subscribe: true })
500
513
  @state()
501
- timelineScrolltop = 0;
514
+ playing?: boolean;
515
+
516
+ @consume({ context: loopContext, subscribe: true })
517
+ @state()
518
+ loop?: boolean;
502
519
 
503
520
  timegroupController?: TimegroupController;
504
521
 
522
+ @state()
523
+ currentTimeMs = 0;
524
+
505
525
  connectedCallback(): void {
506
526
  super.connectedCallback();
507
527
  this.#bindToTargetTimegroup();
@@ -529,19 +549,22 @@ export class EFFilmstrip extends TWMixin(LitElement) {
529
549
  #handleKeyPress = (event: KeyboardEvent) => {
530
550
  // On spacebar, toggle playback
531
551
  if (event.key === " ") {
552
+ const [target] = event.composedPath();
532
553
  // CSS selector to match all interactive elements
533
554
  const interactiveSelector =
534
555
  "input, textarea, button, select, a, [contenteditable]";
535
556
 
536
557
  // Check if the event target or its ancestor matches an interactive element
537
- const closestInteractive = (event.target as HTMLElement | null)?.closest(
558
+ const closestInteractive = (target as HTMLElement | null)?.closest(
538
559
  interactiveSelector,
539
560
  );
540
561
  if (closestInteractive) {
541
562
  return;
542
563
  }
543
564
  event.preventDefault();
544
- this.playing = !this.playing;
565
+ if (this.#contextElement) {
566
+ this.#contextElement.playing = !this.#contextElement.playing;
567
+ }
545
568
  }
546
569
  };
547
570
 
@@ -561,108 +584,6 @@ export class EFFilmstrip extends TWMixin(LitElement) {
561
584
  }
562
585
  }
563
586
 
564
- #lastTick?: DOMHighResTimeStamp;
565
-
566
- #playbackAudioContext: AudioContext | null = null;
567
- #playbackAnimationFrameRequest: number | null = null;
568
- #AUDIO_PLAYBACK_SLICE_MS = 1000;
569
-
570
- #syncPlayheadToAudioContext(target: EFTimegroup, startMs: number) {
571
- target.currentTimeMs =
572
- startMs + (this.#playbackAudioContext?.currentTime ?? 0) * 1000;
573
- this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
574
- this.#syncPlayheadToAudioContext(target, startMs);
575
- });
576
- }
577
-
578
- async #stopPlayback() {
579
- if (this.#playbackAudioContext) {
580
- if (this.#playbackAudioContext.state !== "closed") {
581
- await this.#playbackAudioContext.close();
582
- }
583
- }
584
- if (this.#playbackAnimationFrameRequest) {
585
- cancelAnimationFrame(this.#playbackAnimationFrameRequest);
586
- }
587
- this.#playbackAudioContext = null;
588
- }
589
-
590
- async #startPlayback() {
591
- await this.#stopPlayback();
592
- const timegroup = this.targetTimegroup;
593
- if (!timegroup) {
594
- return;
595
- }
596
-
597
- let currentMs = timegroup.currentTimeMs;
598
- let bufferCount = 0;
599
- this.#playbackAudioContext = new AudioContext({
600
- latencyHint: "playback",
601
- });
602
- if (this.#playbackAnimationFrameRequest) {
603
- cancelAnimationFrame(this.#playbackAnimationFrameRequest);
604
- }
605
- this.#syncPlayheadToAudioContext(timegroup, currentMs);
606
- const playbackContext = this.#playbackAudioContext;
607
- await playbackContext.suspend();
608
-
609
- const fillBuffer = async () => {
610
- if (bufferCount > 1) {
611
- return;
612
- }
613
- const canFillBuffer = await queueBufferSource();
614
- if (canFillBuffer) {
615
- fillBuffer();
616
- }
617
- };
618
-
619
- const fromMs = currentMs;
620
- const toMs = timegroup.endTimeMs;
621
-
622
- const queueBufferSource = async () => {
623
- if (currentMs >= toMs) {
624
- return false;
625
- }
626
- const startMs = currentMs;
627
- const endMs = currentMs + this.#AUDIO_PLAYBACK_SLICE_MS;
628
- currentMs += this.#AUDIO_PLAYBACK_SLICE_MS;
629
- const audioBuffer = await timegroup.renderAudio(startMs, endMs);
630
- bufferCount++;
631
- const source = playbackContext.createBufferSource();
632
- source.buffer = audioBuffer;
633
- source.connect(playbackContext.destination);
634
- source.start((startMs - fromMs) / 1000);
635
- source.onended = () => {
636
- bufferCount--;
637
- if (endMs >= toMs) {
638
- this.playing = false;
639
- } else {
640
- fillBuffer();
641
- }
642
- };
643
- return true;
644
- };
645
-
646
- await fillBuffer();
647
- await playbackContext.resume();
648
- }
649
-
650
- advancePlayhead = (tick?: DOMHighResTimeStamp) => {
651
- if (this.#lastTick && tick && this.targetTimegroup) {
652
- this.targetTimegroup.currentTimeMs += tick - this.#lastTick;
653
- if (
654
- this.targetTimegroup.currentTimeMs >= this.targetTimegroup.durationMs
655
- ) {
656
- this.playing = false;
657
- }
658
- }
659
- this.#lastTick = tick;
660
-
661
- if (this.playing) {
662
- requestAnimationFrame(this.advancePlayhead);
663
- }
664
- };
665
-
666
587
  @eventOptions({ capture: false })
667
588
  scrub(e: MouseEvent) {
668
589
  if (this.playing) {
@@ -773,23 +694,8 @@ export class EFFilmstrip extends TWMixin(LitElement) {
773
694
  />
774
695
  <code>${msToTimeCode(this.currentTimeMs, true)} </code> /
775
696
  <code>${msToTimeCode(target?.durationMs ?? 0, true)}</code>
776
- ${
777
- this.playing
778
- ? html`<button
779
- @click=${() => {
780
- this.playing = false;
781
- }}
782
- >
783
- ⏸️
784
- </button>`
785
- : html`<button
786
- @click=${() => {
787
- this.playing = true;
788
- }}
789
- >
790
- ▶️
791
- </button>`
792
- }
697
+ <ef-toggle-play><button>${this.playing ? "⏸️" : "▶️"}</button></ef-toggle-play>
698
+ <ef-toggle-loop><button>${this.loop ? "🔁" : html`<span class="opacity-50">🔁</span>`}</button></ef-toggle-loop>
793
699
  </div>
794
700
  <div
795
701
  class="z-10 pl-1 pr-1 pt-2 shadow shadow-slate-600 overflow-auto"
@@ -826,17 +732,6 @@ export class EFFilmstrip extends TWMixin(LitElement) {
826
732
  </div>`;
827
733
  }
828
734
 
829
- update(changedProperties: Map<string | number | symbol, unknown>) {
830
- if (changedProperties.has("playing")) {
831
- if (this.playing) {
832
- this.#startPlayback();
833
- } else {
834
- this.#stopPlayback();
835
- }
836
- }
837
- super.update(changedProperties);
838
- }
839
-
840
735
  updated(changes: PropertyValueMap<any> | Map<PropertyKey, unknown>) {
841
736
  if (!this.targetTimegroup) {
842
737
  return;
@@ -846,19 +741,14 @@ export class EFFilmstrip extends TWMixin(LitElement) {
846
741
  this.targetTimegroup.currentTimeMs = this.currentTimeMs;
847
742
  }
848
743
  }
849
- if (changes.has("target")) {
850
- this.#bindToTargetTimegroup();
851
- }
744
+ }
745
+
746
+ get #contextElement(): EFWorkbench | EFPreview | null {
747
+ return this.closest("ef-workbench, ef-preview") as EFWorkbench | EFPreview;
852
748
  }
853
749
 
854
750
  get targetTimegroup() {
855
- if (this.getAttribute("target")) {
856
- const target = document.getElementById(this.getAttribute("target") ?? "");
857
- if (target instanceof EFTimegroup) {
858
- return target;
859
- }
860
- }
861
- return undefined;
751
+ return this.#contextElement?.targetTimegroup;
862
752
  }
863
753
  }
864
754
 
@@ -0,0 +1,39 @@
1
+ import { LitElement, html, css } from "lit";
2
+ import { customElement } from "lit/decorators.js";
3
+ import { ref } from "lit/directives/ref.js";
4
+
5
+ import { TWMixin } from "./TWMixin.ts";
6
+ import { ContextMixin } from "./ContextMixin.ts";
7
+
8
+ @customElement("ef-preview")
9
+ export class EFPreview extends ContextMixin(TWMixin(LitElement)) {
10
+ static styles = [
11
+ css`
12
+ :host {
13
+ display: block;
14
+ width: 100%;
15
+ height: 100%;
16
+ }
17
+ `,
18
+ ];
19
+
20
+ 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
+ ></slot>
30
+ </div>
31
+ `;
32
+ }
33
+ }
34
+
35
+ declare global {
36
+ interface HTMLElementTagNameMap {
37
+ "ef-preview": EFPreview;
38
+ }
39
+ }
@@ -0,0 +1,34 @@
1
+ import { css, html, LitElement } from "lit";
2
+ import { customElement } from "lit/decorators.js";
3
+ import { consume } from "@lit/context";
4
+
5
+ import { efContext } from "./efContext.ts";
6
+ import type { ContextMixinInterface } from "./ContextMixin.ts";
7
+
8
+ @customElement("ef-toggle-loop")
9
+ export class EFToggleLoop extends LitElement {
10
+ static styles = [
11
+ css`
12
+ :host {}
13
+ `,
14
+ ];
15
+
16
+ @consume({ context: efContext })
17
+ context?: ContextMixinInterface | null;
18
+
19
+ render() {
20
+ return html`
21
+ <slot @click=${() => {
22
+ if (this.context) {
23
+ this.context.loop = !this.context.loop;
24
+ }
25
+ }}></slot>
26
+ `;
27
+ }
28
+ }
29
+
30
+ declare global {
31
+ interface HTMLElementTagNameMap {
32
+ "ef-toggle-loop": EFToggleLoop;
33
+ }
34
+ }
@@ -0,0 +1,38 @@
1
+ import { css, html, LitElement } from "lit";
2
+ import { customElement } from "lit/decorators.js";
3
+ import { consume } from "@lit/context";
4
+
5
+ import { efContext } from "./efContext.ts";
6
+ import type { ContextMixinInterface } from "./ContextMixin.ts";
7
+
8
+ @customElement("ef-toggle-play")
9
+ export class EFTogglePlay extends LitElement {
10
+ static styles = [
11
+ css`
12
+ :host {}
13
+ `,
14
+ ];
15
+
16
+ @consume({ context: efContext })
17
+ context?: ContextMixinInterface | null;
18
+
19
+ render() {
20
+ return html`
21
+ <slot @click=${() => {
22
+ if (this.context) {
23
+ if (this.context.playing) {
24
+ this.context.pause();
25
+ } else {
26
+ this.context.play();
27
+ }
28
+ }
29
+ }}></slot>
30
+ `;
31
+ }
32
+ }
33
+
34
+ declare global {
35
+ interface HTMLElementTagNameMap {
36
+ "ef-toggle-play": EFTogglePlay;
37
+ }
38
+ }
@@ -1,34 +1,15 @@
1
- import { createContext, provide } from "@lit/context";
2
1
  import { LitElement, html, css, type PropertyValueMap } from "lit";
3
2
  import { TaskStatus } from "@lit/task";
4
- import {
5
- customElement,
6
- eventOptions,
7
- property,
8
- state,
9
- } from "lit/decorators.js";
3
+ import { customElement, eventOptions } from "lit/decorators.js";
10
4
  import { ref, createRef } from "lit/directives/ref.js";
11
5
 
12
- import { awaitMicrotask } from "@/util/awaitMicrotask";
13
- import { deepGetTemporalElements } from "../elements/EFTemporal";
14
- import { TWMixin } from "./TWMixin";
15
- import { shallowGetTimegroups } from "../elements/EFTimegroup";
16
-
17
- export interface FocusContext {
18
- focusedElement: HTMLElement | null;
19
- }
20
- export const focusContext = createContext<FocusContext>(Symbol("focusContext"));
21
-
22
- export const focusedElement = createContext<HTMLElement | undefined>(
23
- Symbol("focusedElement"),
24
- );
25
-
26
- export const fetchContext = createContext<typeof fetch>(Symbol("fetchContext"));
27
-
28
- export const apiHostContext = createContext<string>(Symbol("apiHostContext"));
6
+ import { deepGetTemporalElements } from "../elements/EFTemporal.ts";
7
+ import { TWMixin } from "./TWMixin.ts";
8
+ import { shallowGetTimegroups } from "../elements/EFTimegroup.ts";
9
+ import { ContextMixin } from "./ContextMixin.ts";
29
10
 
30
11
  @customElement("ef-workbench")
31
- export class EFWorkbench extends TWMixin(LitElement) {
12
+ export class EFWorkbench extends ContextMixin(TWMixin(LitElement)) {
32
13
  static styles = [
33
14
  css`
34
15
  :host {
@@ -38,51 +19,6 @@ export class EFWorkbench extends TWMixin(LitElement) {
38
19
  }
39
20
  `,
40
21
  ];
41
- stageRef = createRef<HTMLDivElement>();
42
- canvasRef = createRef<HTMLSlotElement>();
43
-
44
- @state()
45
- stageScale = 1;
46
-
47
- setStageScale = () => {
48
- if (this.isConnected && !this.rendering) {
49
- const canvasElement = this.canvasRef.value;
50
- const stageElement = this.stageRef.value;
51
- if (stageElement && canvasElement) {
52
- // Determine the appropriate scale factor to make the canvas fit into
53
- // it's parent element.
54
- const stageWidth = stageElement.clientWidth;
55
- const stageHeight = stageElement.clientHeight;
56
- const canvasWidth = canvasElement.clientWidth;
57
- const canvasHeight = canvasElement.clientHeight;
58
- const stageRatio = stageWidth / stageHeight;
59
- const canvasRatio = canvasWidth / canvasHeight;
60
- if (stageRatio > canvasRatio) {
61
- const scale = stageHeight / canvasHeight;
62
- if (this.stageScale !== scale) {
63
- canvasElement.style.transform = `scale(${scale})`;
64
- }
65
- this.stageScale = scale;
66
- } else {
67
- const scale = stageWidth / canvasWidth;
68
- if (this.stageScale !== scale) {
69
- canvasElement.style.transform = `scale(${scale})`;
70
- }
71
- this.stageScale = scale;
72
- }
73
- }
74
- }
75
- if (this.isConnected) {
76
- requestAnimationFrame(this.setStageScale);
77
- }
78
- };
79
-
80
- connectedCallback(): void {
81
- super.connectedCallback();
82
- // Preferrably we would use a resizeObserver, but it is difficult to get the first resize
83
- // timed correctl. So we use requestAnimationFrame as a stop-gap.
84
- requestAnimationFrame(this.setStageScale);
85
- }
86
22
 
87
23
  disconnectedCallback(): void {
88
24
  super.disconnectedCallback();
@@ -93,40 +29,6 @@ export class EFWorkbench extends TWMixin(LitElement) {
93
29
  event.preventDefault();
94
30
  }
95
31
 
96
- @provide({ context: focusContext })
97
- focusContext = this as FocusContext;
98
-
99
- @provide({ context: focusedElement })
100
- @state()
101
- focusedElement?: HTMLElement;
102
-
103
- @provide({ context: fetchContext })
104
- fetch = (path: URL | RequestInfo, init: RequestInit = {}) => {
105
- init.headers ||= {};
106
- Object.assign(init.headers, {
107
- "Content-Type": "application/json",
108
- });
109
-
110
- const bearerToken = this.apiToken;
111
- if (bearerToken) {
112
- Object.assign(init.headers, {
113
- Authorization: `Bearer ${bearerToken}`,
114
- });
115
- }
116
-
117
- return fetch(path, init);
118
- };
119
-
120
- @property({ type: String })
121
- apiToken?: string;
122
-
123
- @provide({ context: apiHostContext })
124
- @property({ type: String })
125
- apiHost = "";
126
-
127
- @property({ type: Boolean })
128
- rendering = false;
129
-
130
32
  focusOverlay = createRef<HTMLDivElement>();
131
33
 
132
34
  update(
@@ -176,21 +78,21 @@ export class EFWorkbench extends TWMixin(LitElement) {
176
78
  >
177
79
  <div
178
80
  ${ref(this.stageRef)}
179
- class="relative grid h-full w-full place-content-center place-items-center overflow-hidden"
81
+ class="relative grid h-full w-full justify-center overflow-hidden"
180
82
  @wheel=${this.handleStageWheel}
181
83
  >
182
84
  <slot
183
85
  ${ref(this.canvasRef)}
184
- class="inline-block"
185
86
  name="canvas"
87
+ class="inline-block"
186
88
  ></slot>
187
89
  <div
188
- class="border border-blue-500 bg-blue-200 bg-opacity-20"
90
+ class="border border-blue-500 bg-blue-200 bg-opacity-20 absolute"
189
91
  ${ref(this.focusOverlay)}
190
92
  ></div>
191
93
  </div>
192
94
 
193
- <slot class="overflow" name="timeline"></slot>
95
+ <slot class="overflow inline-block" name="timeline"></slot>
194
96
  </div>
195
97
  `;
196
98
  }
@@ -215,7 +117,7 @@ export class EFWorkbench extends TWMixin(LitElement) {
215
117
 
216
118
  for (let i = 0; i < frameCount; i++) {
217
119
  firstGroup.currentTimeMs = i * stepDurationMs;
218
- await awaitMicrotask();
120
+ await new Promise<void>(queueMicrotask);
219
121
  const busyTasks = temporals
220
122
  .filter((temporal) => temporal.frameTask.status < TaskStatus.COMPLETE)
221
123
  .map((temporal) => temporal.frameTask);
@@ -2,8 +2,11 @@ import type { LitElement } from "lit";
2
2
  // @ts-expect-error cannot figure out how to declare this module as a string
3
3
  import twStyle from "./TWMixin.css?inline";
4
4
 
5
- const twSheet = new CSSStyleSheet();
6
- twSheet.replaceSync(twStyle);
5
+ let twSheet: CSSStyleSheet | null = null;
6
+ if (typeof window !== "undefined") {
7
+ twSheet = new CSSStyleSheet();
8
+ twSheet.replaceSync(twStyle);
9
+ }
7
10
  export function TWMixin<T extends new (...args: any[]) => LitElement>(Base: T) {
8
11
  class TWElement extends Base {
9
12
  createRenderRoot() {
@@ -13,7 +16,11 @@ export function TWMixin<T extends new (...args: any[]) => LitElement>(Base: T) {
13
16
  "TWMixin can only be applied to elements with shadow roots",
14
17
  );
15
18
  }
16
-
19
+ if (!twSheet) {
20
+ throw new Error(
21
+ "twSheet not found. Probable cause: CSSStyleSheet not supported in this environment",
22
+ );
23
+ }
17
24
  if (renderRoot?.adoptedStyleSheets) {
18
25
  renderRoot.adoptedStyleSheets = [
19
26
  twSheet,
@@ -0,0 +1,3 @@
1
+ import { createContext } from "@lit/context";
2
+
3
+ export const apiHostContext = createContext<string>(Symbol("apiHostContext"));
@@ -0,0 +1,6 @@
1
+ import { createContext } from "@lit/context";
2
+ import type { ContextMixinInterface } from "./ContextMixin.ts";
3
+
4
+ export const efContext = createContext<ContextMixinInterface | null>(
5
+ Symbol("efContext"),
6
+ );
@@ -0,0 +1,5 @@
1
+ import { createContext } from "@lit/context";
2
+
3
+ export const fetchContext = createContext<
4
+ (url: string, init?: RequestInit) => Promise<Response>
5
+ >(Symbol("fetchContext"));
@@ -0,0 +1,7 @@
1
+ import { createContext } from "@lit/context";
2
+
3
+ export interface FocusContext {
4
+ focusedElement: HTMLElement | null;
5
+ }
6
+
7
+ export const focusContext = createContext<FocusContext>(Symbol("focusContext"));