@editframe/elements 0.14.0-beta.3 → 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,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
  }
@@ -62,7 +62,7 @@ let EFWorkbench = class extends ContextMixin(TWMixin(LitElement)) {
62
62
  focusOverlay.style.display = "block";
63
63
  const rect = this.focusedElement.getBoundingClientRect();
64
64
  Object.assign(focusOverlay.style, {
65
- position: "absolute",
65
+ position: "fixed",
66
66
  top: `${rect.top}px`,
67
67
  left: `${rect.left}px`,
68
68
  width: `${rect.width}px`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.14.0-beta.3",
3
+ "version": "0.15.0-beta.3",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -21,7 +21,8 @@
21
21
  "author": "",
22
22
  "license": "UNLICENSED",
23
23
  "dependencies": {
24
- "@editframe/assets": "0.14.0-beta.3",
24
+ "@bramus/style-observer": "^1.3.0",
25
+ "@editframe/assets": "0.15.0-beta.3",
25
26
  "@lit/context": "^1.1.2",
26
27
  "@lit/task": "^1.0.1",
27
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
@@ -18,9 +18,9 @@ describe("EFImage", () => {
18
18
  const workbench = document.createElement("ef-workbench");
19
19
  const element = document.createElement("ef-image");
20
20
  workbench.appendChild(element);
21
- element.assetId = "550e8400-e29b-41d4-a716-446655440000:example.jpg";
21
+ element.assetId = "550e8400-e29b-41d4-a716-446655440000";
22
22
  expect(element.assetPath()).toBe(
23
- "editframe://api/v1/image_files/550e8400-e29b-41d4-a716-446655440000:example.jpg",
23
+ "editframe://api/v1/image_files/550e8400-e29b-41d4-a716-446655440000",
24
24
  );
25
25
  });
26
26
  });
@@ -46,4 +46,35 @@ describe("EFImage", () => {
46
46
  expect(image.assetPath()).toBe(`test:///api/v1/image_files/${id}`);
47
47
  });
48
48
  });
49
+
50
+ describe("hasOwnDuration", () => {
51
+ test("is false by default", () => {
52
+ const image = document.createElement("ef-image");
53
+ expect(image.hasOwnDuration).toBe(false);
54
+ });
55
+
56
+ test("is true when duration is set", () => {
57
+ const image = document.createElement("ef-image");
58
+ image.setAttribute("duration", "1s");
59
+ expect(image.hasOwnDuration).toBe(true);
60
+ });
61
+ });
62
+
63
+ describe("durationMs", () => {
64
+ test("Can be set on element directly", () => {
65
+ const image = document.createElement("ef-image");
66
+ image.src =
67
+ "https://editframe.dev/api/v1/image_files/550e8400-e29b-41d4-a716-446655440000";
68
+ image.duration = "1s";
69
+ expect(image.durationMs).toBe(1000);
70
+ });
71
+
72
+ test("Can be set through setAttribute", () => {
73
+ const image = document.createElement("ef-image");
74
+ image.src =
75
+ "https://editframe.dev/api/v1/image_files/550e8400-e29b-41d4-a716-446655440000";
76
+ image.setAttribute("duration", "1s");
77
+ expect(image.durationMs).toBe(1000);
78
+ });
79
+ });
49
80
  });
@@ -5,12 +5,15 @@ import { createRef, ref } from "lit/directives/ref.js";
5
5
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
6
6
  import { EF_RENDERING } from "../EF_RENDERING.js";
7
7
  import { EFSourceMixin } from "./EFSourceMixin.js";
8
+ import { EFTemporal } from "./EFTemporal.js";
8
9
  import { FetchMixin } from "./FetchMixin.js";
9
10
 
10
11
  @customElement("ef-image")
11
- export class EFImage extends EFSourceMixin(FetchMixin(LitElement), {
12
- assetType: "image_files",
13
- }) {
12
+ export class EFImage extends EFTemporal(
13
+ EFSourceMixin(FetchMixin(LitElement), {
14
+ assetType: "image_files",
15
+ }),
16
+ ) {
14
17
  static styles = [
15
18
  css`
16
19
  :host {
@@ -52,6 +55,10 @@ export class EFImage extends EFSourceMixin(FetchMixin(LitElement), {
52
55
  return `/@ef-image/${this.src}`;
53
56
  }
54
57
 
58
+ get hasOwnDuration() {
59
+ return this.hasExplicitDuration;
60
+ }
61
+
55
62
  fetchImage = new Task(this, {
56
63
  autoRun: EF_INTERACTIVE,
57
64
  args: () => [this.assetPath(), this.fetch] as const,
@@ -14,9 +14,44 @@ 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
 
21
+ const freqWeightsCache = new Map<number, Float32Array>();
22
+
23
+ class LRUCache<K, V> {
24
+ private cache = new Map<K, V>();
25
+ private readonly maxSize: number;
26
+
27
+ constructor(maxSize: number) {
28
+ this.maxSize = maxSize;
29
+ }
30
+
31
+ get(key: K): V | undefined {
32
+ const value = this.cache.get(key);
33
+ if (value) {
34
+ // Refresh position by removing and re-adding
35
+ this.cache.delete(key);
36
+ this.cache.set(key, value);
37
+ }
38
+ return value;
39
+ }
40
+
41
+ set(key: K, value: V): void {
42
+ if (this.cache.has(key)) {
43
+ this.cache.delete(key);
44
+ } else if (this.cache.size >= this.maxSize) {
45
+ // Remove oldest entry (first item in map)
46
+ const firstKey = this.cache.keys().next().value;
47
+ if (firstKey) {
48
+ this.cache.delete(firstKey);
49
+ }
50
+ }
51
+ this.cache.set(key, value);
52
+ }
53
+ }
54
+
20
55
  export const deepGetMediaElements = (
21
56
  element: Element,
22
57
  medias: EFMedia[] = [],
@@ -31,9 +66,11 @@ export const deepGetMediaElements = (
31
66
  return medias;
32
67
  };
33
68
 
34
- export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
35
- assetType: "isobmff_files",
36
- }) {
69
+ export class EFMedia extends EFTargetable(
70
+ EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
71
+ assetType: "isobmff_files",
72
+ }),
73
+ ) {
37
74
  static styles = [
38
75
  css`
39
76
  :host {
@@ -284,7 +321,8 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
284
321
  },
285
322
  });
286
323
 
287
- @state() desiredSeekTimeMs = 0;
324
+ @state()
325
+ desiredSeekTimeMs = 0;
288
326
 
289
327
  protected async executeSeek(seekToMs: number) {
290
328
  this.desiredSeekTimeMs = seekToMs;
@@ -294,7 +332,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
294
332
  changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
295
333
  ): void {
296
334
  if (changedProperties.has("ownCurrentTimeMs")) {
297
- this.executeSeek(this.trimAdjustedOwnCurrentTimeMs);
335
+ this.executeSeek(this.currentSourceTimeMs);
298
336
  }
299
337
  // TODO: this is copied straight from EFTimegroup.ts
300
338
  // and should be refactored to be shared/reduce bad duplication of
@@ -312,7 +350,6 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
312
350
  return;
313
351
  }
314
352
  this.style.display = "";
315
-
316
353
  const animations = this.getAnimations({ subtree: true });
317
354
 
318
355
  this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
@@ -546,4 +583,148 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
546
583
  this.trimEndMs,
547
584
  };
548
585
  }
586
+
587
+ @property({ type: Number })
588
+ fftSize = 512; // Default FFT size
589
+
590
+ @property({ type: Number })
591
+ fftDecay = 8; // Default number of frames to analyze
592
+
593
+ private static readonly MIN_DB = -90;
594
+ private static readonly MAX_DB = -20;
595
+ private static readonly DECAY_WEIGHT = 0.7;
596
+
597
+ // Update FREQ_WEIGHTS to use the instance fftSize instead of a static value
598
+ get FREQ_WEIGHTS() {
599
+ if (freqWeightsCache.has(this.fftSize)) {
600
+ // biome-ignore lint/style/noNonNullAssertion: Will exist due to prior has check
601
+ return freqWeightsCache.get(this.fftSize)!;
602
+ }
603
+
604
+ const weights = new Float32Array(this.fftSize / 2).map((_, i) => {
605
+ const frequency = (i * 48000) / this.fftSize;
606
+ if (frequency < 60) return 0.3;
607
+ if (frequency < 250) return 0.4;
608
+ if (frequency < 500) return 0.6;
609
+ if (frequency < 2000) return 0.8;
610
+ if (frequency < 4000) return 1.2;
611
+ if (frequency < 8000) return 1.6;
612
+ return 2.0;
613
+ });
614
+
615
+ freqWeightsCache.set(this.fftSize, weights);
616
+ return weights;
617
+ }
618
+
619
+ #frequencyDataCache = new LRUCache<string, Uint8Array>(100);
620
+
621
+ frequencyDataTask = new Task(this, {
622
+ autoRun: EF_INTERACTIVE,
623
+ args: () =>
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,
630
+ task: async () => {
631
+ await this.audioBufferTask.taskComplete;
632
+ if (!this.audioBufferTask.value) return null;
633
+ if (this.currentSourceTimeMs <= 0) return null;
634
+
635
+ const currentTimeMs = this.currentSourceTimeMs;
636
+ const startOffsetMs = this.audioBufferTask.value.startOffsetMs;
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
+ }
644
+
645
+ const framesData = await Promise.all(
646
+ Array.from({ length: this.fftDecay }, async (_, i) => {
647
+ const frameOffset = i * (1000 / 30);
648
+ const startTime = Math.max(
649
+ 0,
650
+ (currentTimeMs - frameOffset - startOffsetMs) / 1000,
651
+ );
652
+
653
+ // Cache key for this specific frame
654
+ const cacheKey = `${this.fftSize}:${startOffsetMs}:${startTime}`;
655
+
656
+ // Check cache for this specific frame
657
+ const cachedFrame = this.#frequencyDataCache.get(cacheKey);
658
+ if (cachedFrame) {
659
+ return cachedFrame;
660
+ }
661
+
662
+ const audioContext = new OfflineAudioContext(
663
+ 2,
664
+ 48000 * (1 / 30),
665
+ 48000,
666
+ );
667
+ const analyser = audioContext.createAnalyser();
668
+ analyser.fftSize = this.fftSize;
669
+ analyser.minDecibels = EFMedia.MIN_DB;
670
+ analyser.maxDecibels = EFMedia.MAX_DB;
671
+
672
+ const audioBufferSource = audioContext.createBufferSource();
673
+ audioBufferSource.buffer = audioBuffer;
674
+
675
+ audioBufferSource.connect(analyser);
676
+ analyser.connect(audioContext.destination);
677
+
678
+ audioBufferSource.start(0, startTime, 1 / 30);
679
+
680
+ try {
681
+ await audioContext.startRendering();
682
+ const frameData = new Uint8Array(this.fftSize / 2);
683
+ analyser.getByteFrequencyData(frameData);
684
+
685
+ // Cache this frame's analysis
686
+ this.#frequencyDataCache.set(cacheKey, frameData);
687
+ return frameData;
688
+ } finally {
689
+ audioBufferSource.disconnect();
690
+ analyser.disconnect();
691
+ }
692
+ }),
693
+ );
694
+
695
+ const frameLength = framesData[0]?.length ?? 0;
696
+
697
+ // Combine frames with decay
698
+ const smoothedData = new Uint8Array(frameLength);
699
+ for (let i = 0; i < frameLength; i++) {
700
+ let weightedSum = 0;
701
+ let weightSum = 0;
702
+
703
+ framesData.forEach((frame, frameIndex) => {
704
+ const decayWeight = EFMedia.DECAY_WEIGHT ** frameIndex;
705
+ // biome-ignore lint/style/noNonNullAssertion: Will exist due to forEach
706
+ weightedSum += frame[i]! * decayWeight;
707
+ weightSum += decayWeight;
708
+ });
709
+
710
+ smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));
711
+ }
712
+
713
+ // Apply frequency weights using instance FREQ_WEIGHTS
714
+ smoothedData.forEach((value, i) => {
715
+ // biome-ignore lint/style/noNonNullAssertion: Will exist due to forEach
716
+ const freqWeight = this.FREQ_WEIGHTS[i]!;
717
+ smoothedData[i] = Math.min(255, Math.round(value * freqWeight));
718
+ });
719
+
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;
728
+ },
729
+ });
549
730
  }
@@ -1,5 +1,5 @@
1
1
  import { consume, createContext } from "@lit/context";
2
- import type { LitElement, ReactiveController } from "lit";
2
+ import type { LitElement, PropertyValueMap, ReactiveController } from "lit";
3
3
  import { property, state } from "lit/decorators.js";
4
4
  import type { EFTimegroup } from "./EFTimegroup.js";
5
5
 
@@ -13,6 +13,10 @@ export const timegroupContext = createContext<EFTimegroup>(
13
13
 
14
14
  export declare class TemporalMixinInterface {
15
15
  get hasOwnDuration(): boolean;
16
+ /**
17
+ * Whether the element has a duration set as an attribute.
18
+ */
19
+ get hasExplicitDuration(): boolean;
16
20
 
17
21
  /**
18
22
  * Used to trim the start of the media.
@@ -147,18 +151,18 @@ export declare class TemporalMixinInterface {
147
151
  * elements.
148
152
  *
149
153
  * For example, if the media has a `sourcein` value of 10s, when `ownCurrentTimeMs` is 0s,
150
- * `trimAdjustedOwnCurrentTimeMs` will be 10s.
154
+ * `currentSourceTimeMs` will be 10s.
151
155
  *
152
156
  * sourcein=10s sourceout=10s
153
157
  * / / /
154
158
  * |--------|=================|---------|
155
159
  * ^
156
160
  * |_
157
- * trimAdjustedOwnCurrentTimeMs === 10s
161
+ * currentSourceTimeMs === 10s
158
162
  * |_
159
163
  * ownCurrentTimeMs === 0s
160
164
  */
161
- get trimAdjustedOwnCurrentTimeMs(): number;
165
+ get currentSourceTimeMs(): number;
162
166
 
163
167
  set duration(value: string);
164
168
  get duration(): string;
@@ -461,6 +465,10 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
461
465
  return parent as EFTimegroup | undefined;
462
466
  }
463
467
 
468
+ get hasExplicitDuration() {
469
+ return this._durationMs !== undefined;
470
+ }
471
+
464
472
  get hasOwnDuration() {
465
473
  return false;
466
474
  }
@@ -559,6 +567,10 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
559
567
  return this.startTimeMs + this.durationMs;
560
568
  }
561
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
+ */
562
574
  get ownCurrentTimeMs() {
563
575
  if (this.rootTimegroup) {
564
576
  return Math.min(
@@ -573,7 +585,7 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
573
585
  * Used to calculate the internal currentTimeMs of the element. This is useful
574
586
  * for mapping to internal media time codes for audio/video elements.
575
587
  */
576
- get trimAdjustedOwnCurrentTimeMs() {
588
+ get currentSourceTimeMs() {
577
589
  if (this.rootTimegroup) {
578
590
  if (this.sourceInMs && this.sourceOutMs) {
579
591
  return Math.min(
@@ -613,6 +625,26 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
613
625
  }
614
626
  },
615
627
  });
628
+
629
+ protected updated(
630
+ changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
631
+ ): void {
632
+ super.updated(changedProperties);
633
+ if (
634
+ changedProperties.has("currentTime") ||
635
+ changedProperties.has("ownCurrentTimeMs")
636
+ ) {
637
+ const timelineTimeMs = (this.rootTimegroup ?? this).ownCurrentTimeMs;
638
+ if (
639
+ this.startTimeMs > timelineTimeMs ||
640
+ this.endTimeMs < timelineTimeMs
641
+ ) {
642
+ this.style.display = "none";
643
+ return;
644
+ }
645
+ this.style.display = "";
646
+ }
647
+ }
616
648
  }
617
649
 
618
650
  Object.defineProperty(TemporalMixinClass.prototype, EF_TEMPORAL, {