@editframe/elements 0.15.0-beta.1 → 0.15.0-beta.3

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.
@@ -0,0 +1,25 @@
1
+ import { LitElement, ReactiveController } from 'lit';
2
+ type Constructor<T = {}> = new (...args: any[]) => T;
3
+ export declare class TargetableMixinInterface {
4
+ id: string;
5
+ }
6
+ export declare const isEFTargetable: (obj: any) => obj is TargetableMixinInterface;
7
+ export declare const EFTargetable: <T extends Constructor<LitElement>>(superClass: T) => T;
8
+ export declare class TargetController implements ReactiveController {
9
+ private host;
10
+ private targetController;
11
+ private currentTargetString;
12
+ constructor(host: LitElement & {
13
+ targetElement: Element | null;
14
+ target: string;
15
+ });
16
+ private registryCallback;
17
+ private updateTarget;
18
+ private connectToTarget;
19
+ private disconnectFromTarget;
20
+ private get registry();
21
+ hostDisconnected(): void;
22
+ hostConnected(): void;
23
+ hostUpdate(): void;
24
+ }
25
+ export {};
@@ -0,0 +1,164 @@
1
+ import { LitElement } from "lit";
2
+ const EF_TARGETABLE = Symbol("EF_TARGETABLE");
3
+ class TargetRegistry {
4
+ constructor() {
5
+ this.idMap = /* @__PURE__ */ new Map();
6
+ this.callbacks = /* @__PURE__ */ new Map();
7
+ }
8
+ subscribe(id, callback) {
9
+ this.callbacks.set(id, this.callbacks.get(id) ?? /* @__PURE__ */ new Set());
10
+ this.callbacks.get(id)?.add(callback);
11
+ }
12
+ unsubscribe(id, callback) {
13
+ if (id === null) {
14
+ return;
15
+ }
16
+ this.callbacks.get(id)?.delete(callback);
17
+ if (this.callbacks.get(id)?.size === 0) {
18
+ this.callbacks.delete(id);
19
+ }
20
+ }
21
+ get(id) {
22
+ return this.idMap.get(id);
23
+ }
24
+ register(id, target) {
25
+ this.idMap.set(id, target);
26
+ for (const callback of this.callbacks.get(id) ?? []) {
27
+ callback(target);
28
+ }
29
+ }
30
+ unregister(id) {
31
+ for (const callback of this.callbacks.get(id) ?? []) {
32
+ callback(void 0);
33
+ }
34
+ this.idMap.delete(id);
35
+ this.callbacks.delete(id);
36
+ }
37
+ }
38
+ const documentRegistries = /* @__PURE__ */ new WeakMap();
39
+ const getRegistry = (root) => {
40
+ let registry = documentRegistries.get(root);
41
+ if (!registry) {
42
+ registry = new TargetRegistry();
43
+ documentRegistries.set(root, registry);
44
+ }
45
+ return registry;
46
+ };
47
+ const EFTargetable = (superClass) => {
48
+ class TargetableElement extends superClass {
49
+ #registry = null;
50
+ static get observedAttributes() {
51
+ const parentAttributes = superClass.observedAttributes || [];
52
+ return [.../* @__PURE__ */ new Set([...parentAttributes, "id"])];
53
+ }
54
+ updateRegistry(oldValue, newValue) {
55
+ if (!this.#registry) return;
56
+ if (oldValue === newValue) return;
57
+ if (oldValue) {
58
+ this.#registry.unregister(oldValue);
59
+ }
60
+ if (newValue) {
61
+ this.#registry.register(newValue, this);
62
+ }
63
+ }
64
+ connectedCallback() {
65
+ super.connectedCallback();
66
+ this.#registry = getRegistry(this.getRootNode());
67
+ const initialId = this.getAttribute("id");
68
+ if (initialId) {
69
+ this.updateRegistry("", initialId);
70
+ }
71
+ }
72
+ attributeChangedCallback(name, old, value) {
73
+ super.attributeChangedCallback(name, old, value);
74
+ if (name === "id") {
75
+ this.updateRegistry(old ?? "", value ?? "");
76
+ }
77
+ }
78
+ disconnectedCallback() {
79
+ if (this.#registry) {
80
+ this.updateRegistry(this.id, "");
81
+ this.#registry = null;
82
+ }
83
+ super.disconnectedCallback();
84
+ }
85
+ }
86
+ Object.defineProperty(TargetableElement.prototype, EF_TARGETABLE, {
87
+ value: true
88
+ });
89
+ return TargetableElement;
90
+ };
91
+ class TargetUpdateController {
92
+ constructor(host) {
93
+ this.host = host;
94
+ }
95
+ hostConnected() {
96
+ this.host.requestUpdate();
97
+ }
98
+ hostDisconnected() {
99
+ this.host.requestUpdate();
100
+ }
101
+ hostUpdate() {
102
+ this.host.requestUpdate();
103
+ }
104
+ }
105
+ class TargetController {
106
+ constructor(host) {
107
+ this.targetController = null;
108
+ this.currentTargetString = null;
109
+ this.registryCallback = (target) => {
110
+ this.host.targetElement = target ?? null;
111
+ };
112
+ this.host = host;
113
+ this.host.addController(this);
114
+ this.currentTargetString = this.host.target;
115
+ if (this.currentTargetString) {
116
+ this.registry.subscribe(this.currentTargetString, this.registryCallback);
117
+ }
118
+ }
119
+ updateTarget() {
120
+ const newTarget = this.registry.get(this.host.target);
121
+ if (this.host.targetElement !== newTarget) {
122
+ this.disconnectFromTarget();
123
+ this.host.targetElement = newTarget ?? null;
124
+ this.connectToTarget();
125
+ }
126
+ }
127
+ connectToTarget() {
128
+ if (this.host.targetElement instanceof LitElement) {
129
+ this.targetController = new TargetUpdateController(this.host);
130
+ this.host.targetElement.addController(this.targetController);
131
+ }
132
+ }
133
+ disconnectFromTarget() {
134
+ if (this.host.targetElement instanceof LitElement && this.targetController) {
135
+ this.host.targetElement.removeController(this.targetController);
136
+ this.targetController = null;
137
+ }
138
+ }
139
+ get registry() {
140
+ const root = this.host.getRootNode();
141
+ return getRegistry(root);
142
+ }
143
+ hostDisconnected() {
144
+ this.disconnectFromTarget();
145
+ }
146
+ hostConnected() {
147
+ this.updateTarget();
148
+ }
149
+ hostUpdate() {
150
+ if (this.currentTargetString !== this.host.target) {
151
+ this.registry.unsubscribe(
152
+ this.currentTargetString,
153
+ this.registryCallback
154
+ );
155
+ this.registry.subscribe(this.host.target, this.registryCallback);
156
+ this.updateTarget();
157
+ this.currentTargetString = this.host.target;
158
+ }
159
+ }
160
+ }
161
+ export {
162
+ EFTargetable,
163
+ TargetController
164
+ };
@@ -0,0 +1,19 @@
1
+ import { LitElement } from 'lit';
2
+ declare const TargetableTest_base: typeof LitElement;
3
+ declare class TargetableTest extends TargetableTest_base {
4
+ value: string;
5
+ render(): import('lit-html').TemplateResult<1>;
6
+ }
7
+ declare class TargeterTest extends LitElement {
8
+ private targetController;
9
+ targetElement: Element | null;
10
+ target: string;
11
+ render(): import('lit-html').TemplateResult<1>;
12
+ }
13
+ declare global {
14
+ interface HTMLElementTagNameMap {
15
+ "targetable-test": TargetableTest & Element;
16
+ "targeter-test": TargeterTest & Element;
17
+ }
18
+ }
19
+ export {};
@@ -1,9 +1,9 @@
1
1
  import { LitElement } from 'lit';
2
2
  declare const EFPreview_base: (new (...args: any[]) => import('./ContextMixin.js').ContextMixinInterface) & typeof LitElement;
3
3
  export declare class EFPreview extends EFPreview_base {
4
+ static styles: import('lit').CSSResult[];
4
5
  focusedElement?: HTMLElement;
5
6
  constructor();
6
- static styles: import('lit').CSSResult[];
7
7
  render(): import('lit-html').TemplateResult<1>;
8
8
  }
9
9
  declare global {
@@ -39,6 +39,7 @@ let EFPreview = class extends ContextMixin(TWMixin(LitElement)) {
39
39
  EFPreview.styles = [
40
40
  css`
41
41
  :host {
42
+ position: relative;
42
43
  display: block;
43
44
  cursor: crosshair;
44
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.15.0-beta.1",
3
+ "version": "0.15.0-beta.3",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -22,7 +22,7 @@
22
22
  "license": "UNLICENSED",
23
23
  "dependencies": {
24
24
  "@bramus/style-observer": "^1.3.0",
25
- "@editframe/assets": "0.15.0-beta.1",
25
+ "@editframe/assets": "0.15.0-beta.3",
26
26
  "@lit/context": "^1.1.2",
27
27
  "@lit/task": "^1.0.1",
28
28
  "d3": "^7.9.0",
@@ -1,6 +1,6 @@
1
1
  import { Task } from "@lit/task";
2
2
  import { html } from "lit";
3
- import { customElement, property } from "lit/decorators.js";
3
+ import { customElement } from "lit/decorators.js";
4
4
  import { createRef, ref } from "lit/directives/ref.js";
5
5
  import { EFMedia } from "./EFMedia.js";
6
6
 
@@ -8,9 +8,6 @@ import { EFMedia } from "./EFMedia.js";
8
8
  export class EFAudio extends EFMedia {
9
9
  audioElementRef = createRef<HTMLAudioElement>();
10
10
 
11
- @property({ type: String })
12
- src = "";
13
-
14
11
  render() {
15
12
  return html`<audio ${ref(this.audioElementRef)}></audio>`;
16
13
  }
@@ -373,7 +373,7 @@ export class EFCaptions extends EFSourceMixin(
373
373
  return;
374
374
  }
375
375
 
376
- const currentTimeMs = this.targetElement.trimAdjustedOwnCurrentTimeMs;
376
+ const currentTimeMs = this.targetElement.currentSourceTimeMs;
377
377
  const currentTimeSec = currentTimeMs / 1000;
378
378
 
379
379
  // Find the current word from word_segments
@@ -14,6 +14,7 @@ import { EF_RENDERING } from "../EF_RENDERING.js";
14
14
  import { EFSourceMixin } from "./EFSourceMixin.js";
15
15
  import { EFTemporal, isEFTemporal } from "./EFTemporal.js";
16
16
  import { FetchMixin } from "./FetchMixin.js";
17
+ import { EFTargetable } from "./TargetController.ts";
17
18
 
18
19
  const log = debug("ef:elements:EFMedia");
19
20
 
@@ -43,15 +44,14 @@ class LRUCache<K, V> {
43
44
  } else if (this.cache.size >= this.maxSize) {
44
45
  // Remove oldest entry (first item in map)
45
46
  const firstKey = this.cache.keys().next().value;
46
- this.cache.delete(firstKey);
47
+ if (firstKey) {
48
+ this.cache.delete(firstKey);
49
+ }
47
50
  }
48
51
  this.cache.set(key, value);
49
52
  }
50
53
  }
51
54
 
52
- // Cache individual frame analyses
53
- const frequencyDataCache = new LRUCache<string, Uint8Array>(100);
54
-
55
55
  export const deepGetMediaElements = (
56
56
  element: Element,
57
57
  medias: EFMedia[] = [],
@@ -66,9 +66,11 @@ export const deepGetMediaElements = (
66
66
  return medias;
67
67
  };
68
68
 
69
- export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
70
- assetType: "isobmff_files",
71
- }) {
69
+ export class EFMedia extends EFTargetable(
70
+ EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
71
+ assetType: "isobmff_files",
72
+ }),
73
+ ) {
72
74
  static styles = [
73
75
  css`
74
76
  :host {
@@ -319,7 +321,8 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
319
321
  },
320
322
  });
321
323
 
322
- @state() desiredSeekTimeMs = 0;
324
+ @state()
325
+ desiredSeekTimeMs = 0;
323
326
 
324
327
  protected async executeSeek(seekToMs: number) {
325
328
  this.desiredSeekTimeMs = seekToMs;
@@ -329,7 +332,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
329
332
  changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
330
333
  ): void {
331
334
  if (changedProperties.has("ownCurrentTimeMs")) {
332
- this.executeSeek(this.trimAdjustedOwnCurrentTimeMs);
335
+ this.executeSeek(this.currentSourceTimeMs);
333
336
  }
334
337
  // TODO: this is copied straight from EFTimegroup.ts
335
338
  // and should be refactored to be shared/reduce bad duplication of
@@ -344,13 +347,9 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
344
347
  this.endTimeMs < timelineTimeMs
345
348
  ) {
346
349
  this.style.display = "none";
347
- // this.style.zIndex = "";
348
- // this.style.opacity = "0";
349
350
  return;
350
351
  }
351
352
  this.style.display = "";
352
- // this.style.zIndex = "100000";
353
- // this.style.opacity = "";
354
353
  const animations = this.getAnimations({ subtree: true });
355
354
 
356
355
  this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
@@ -617,18 +616,31 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
617
616
  return weights;
618
617
  }
619
618
 
619
+ #frequencyDataCache = new LRUCache<string, Uint8Array>(100);
620
+
620
621
  frequencyDataTask = new Task(this, {
621
622
  autoRun: EF_INTERACTIVE,
622
623
  args: () =>
623
- [this.audioBufferTask.status, this.trimAdjustedOwnCurrentTimeMs] as const,
624
+ [
625
+ this.audioBufferTask.status,
626
+ this.currentSourceTimeMs,
627
+ this.fftSize, // Add fftSize to dependency array
628
+ this.fftDecay, // Add fftDecay to dependency array
629
+ ] as const,
624
630
  task: async () => {
625
631
  await this.audioBufferTask.taskComplete;
626
632
  if (!this.audioBufferTask.value) return null;
627
- if (this.trimAdjustedOwnCurrentTimeMs <= 0) return null;
633
+ if (this.currentSourceTimeMs <= 0) return null;
628
634
 
629
- const currentTimeMs = this.trimAdjustedOwnCurrentTimeMs;
635
+ const currentTimeMs = this.currentSourceTimeMs;
630
636
  const startOffsetMs = this.audioBufferTask.value.startOffsetMs;
631
637
  const audioBuffer = this.audioBufferTask.value.buffer;
638
+ const smoothedKey = `${this.fftSize}:${this.fftDecay}:${startOffsetMs}:${currentTimeMs}`;
639
+
640
+ const cachedSmoothedData = this.#frequencyDataCache.get(smoothedKey);
641
+ if (cachedSmoothedData) {
642
+ return cachedSmoothedData;
643
+ }
632
644
 
633
645
  const framesData = await Promise.all(
634
646
  Array.from({ length: this.fftDecay }, async (_, i) => {
@@ -639,10 +651,10 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
639
651
  );
640
652
 
641
653
  // Cache key for this specific frame
642
- const cacheKey = `${startOffsetMs},${startTime}`;
654
+ const cacheKey = `${this.fftSize}:${startOffsetMs}:${startTime}`;
643
655
 
644
656
  // Check cache for this specific frame
645
- const cachedFrame = frequencyDataCache.get(cacheKey);
657
+ const cachedFrame = this.#frequencyDataCache.get(cacheKey);
646
658
  if (cachedFrame) {
647
659
  return cachedFrame;
648
660
  }
@@ -667,11 +679,11 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
667
679
 
668
680
  try {
669
681
  await audioContext.startRendering();
670
- const frameData = new Uint8Array(analyser.frequencyBinCount);
682
+ const frameData = new Uint8Array(this.fftSize / 2);
671
683
  analyser.getByteFrequencyData(frameData);
672
684
 
673
685
  // Cache this frame's analysis
674
- frequencyDataCache.set(cacheKey, frameData);
686
+ this.#frequencyDataCache.set(cacheKey, frameData);
675
687
  return frameData;
676
688
  } finally {
677
689
  audioBufferSource.disconnect();
@@ -700,12 +712,19 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
700
712
 
701
713
  // Apply frequency weights using instance FREQ_WEIGHTS
702
714
  smoothedData.forEach((value, i) => {
703
- // biome-ignore lint/style/noNonNullAssertion: Trusting FREQ_WEIGHTS to be the correct length
715
+ // biome-ignore lint/style/noNonNullAssertion: Will exist due to forEach
704
716
  const freqWeight = this.FREQ_WEIGHTS[i]!;
705
717
  smoothedData[i] = Math.min(255, Math.round(value * freqWeight));
706
718
  });
707
719
 
708
- return smoothedData;
720
+ // Only return the lower half of the frequency data
721
+ // The top half is zeroed out, which makes for aesthetically unpleasing waveforms
722
+ const slicedData = smoothedData.slice(
723
+ 0,
724
+ Math.floor(smoothedData.length / 2),
725
+ );
726
+ this.#frequencyDataCache.set(smoothedKey, slicedData);
727
+ return slicedData;
709
728
  },
710
729
  });
711
730
  }
@@ -151,18 +151,18 @@ export declare class TemporalMixinInterface {
151
151
  * elements.
152
152
  *
153
153
  * For example, if the media has a `sourcein` value of 10s, when `ownCurrentTimeMs` is 0s,
154
- * `trimAdjustedOwnCurrentTimeMs` will be 10s.
154
+ * `currentSourceTimeMs` will be 10s.
155
155
  *
156
156
  * sourcein=10s sourceout=10s
157
157
  * / / /
158
158
  * |--------|=================|---------|
159
159
  * ^
160
160
  * |_
161
- * trimAdjustedOwnCurrentTimeMs === 10s
161
+ * currentSourceTimeMs === 10s
162
162
  * |_
163
163
  * ownCurrentTimeMs === 0s
164
164
  */
165
- get trimAdjustedOwnCurrentTimeMs(): number;
165
+ get currentSourceTimeMs(): number;
166
166
 
167
167
  set duration(value: string);
168
168
  get duration(): string;
@@ -567,6 +567,10 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
567
567
  return this.startTimeMs + this.durationMs;
568
568
  }
569
569
 
570
+ /**
571
+ * The current time of the element within itself.
572
+ * Compare with `currentTimeMs` to see the current time with respect to the root timegroup
573
+ */
570
574
  get ownCurrentTimeMs() {
571
575
  if (this.rootTimegroup) {
572
576
  return Math.min(
@@ -581,7 +585,7 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
581
585
  * Used to calculate the internal currentTimeMs of the element. This is useful
582
586
  * for mapping to internal media time codes for audio/video elements.
583
587
  */
584
- get trimAdjustedOwnCurrentTimeMs() {
588
+ get currentSourceTimeMs() {
585
589
  if (this.rootTimegroup) {
586
590
  if (this.sourceInMs && this.sourceOutMs) {
587
591
  return Math.min(
@@ -632,17 +636,13 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
632
636
  ) {
633
637
  const timelineTimeMs = (this.rootTimegroup ?? this).ownCurrentTimeMs;
634
638
  if (
635
- this.startTimeMs >= timelineTimeMs ||
636
- this.endTimeMs <= timelineTimeMs
639
+ this.startTimeMs > timelineTimeMs ||
640
+ this.endTimeMs < timelineTimeMs
637
641
  ) {
638
642
  this.style.display = "none";
639
- // this.style.zIndex = "";
640
- // this.style.opacity = "0";
641
643
  return;
642
644
  }
643
645
  this.style.display = "";
644
- // this.style.zIndex = "100000";
645
- // this.style.opacity = "";
646
646
  }
647
647
  }
648
648
  }
@@ -53,7 +53,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
53
53
  type: String,
54
54
  attribute: "mode",
55
55
  })
56
- mode: "fixed" | "sequence" | "contain" = "sequence";
56
+ mode: "fixed" | "sequence" | "contain" = "contain";
57
57
 
58
58
  @property({
59
59
  type: Number,
@@ -67,7 +67,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
67
67
 
68
68
  #resizeObserver?: ResizeObserver;
69
69
 
70
- @property({ type: Number })
70
+ @property({ type: Number, attribute: "currenttime" })
71
71
  set currentTime(time: number) {
72
72
  this.#currentTime = Math.max(0, Math.min(time, this.durationMs / 1000));
73
73
  try {
@@ -227,10 +227,9 @@ export class EFTimegroup extends EFTemporal(LitElement) {
227
227
  * in calculations and it was not clear why.
228
228
  */
229
229
  async waitForMediaDurations() {
230
+ const mediaElements = deepGetMediaElements(this);
230
231
  return await Promise.all(
231
- deepGetMediaElements(this).map(
232
- (media) => media.initSegmentsLoader.taskComplete,
233
- ),
232
+ mediaElements.map((m) => m.trackFragmentIndexLoader.taskComplete),
234
233
  );
235
234
  }
236
235
 
@@ -258,13 +257,9 @@ export class EFTimegroup extends EFTemporal(LitElement) {
258
257
  const timelineTimeMs = (this.rootTimegroup ?? this).currentTimeMs;
259
258
  if (this.startTimeMs > timelineTimeMs || this.endTimeMs < timelineTimeMs) {
260
259
  this.style.display = "none";
261
- // this.style.zIndex = "";
262
- // this.style.opacity = "0";
263
260
  return;
264
261
  }
265
262
  this.style.display = "";
266
- // this.style.zIndex = "100000";
267
- // this.style.opacity = "";
268
263
  const animations = this.getAnimations({ subtree: true });
269
264
  this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
270
265
  this.style.setProperty(