@editframe/elements 0.14.0-beta.3 → 0.15.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.
@@ -17,6 +17,41 @@ import { FetchMixin } from "./FetchMixin.js";
17
17
 
18
18
  const log = debug("ef:elements:EFMedia");
19
19
 
20
+ const freqWeightsCache = new Map<number, Float32Array>();
21
+
22
+ class LRUCache<K, V> {
23
+ private cache = new Map<K, V>();
24
+ private readonly maxSize: number;
25
+
26
+ constructor(maxSize: number) {
27
+ this.maxSize = maxSize;
28
+ }
29
+
30
+ get(key: K): V | undefined {
31
+ const value = this.cache.get(key);
32
+ if (value) {
33
+ // Refresh position by removing and re-adding
34
+ this.cache.delete(key);
35
+ this.cache.set(key, value);
36
+ }
37
+ return value;
38
+ }
39
+
40
+ set(key: K, value: V): void {
41
+ if (this.cache.has(key)) {
42
+ this.cache.delete(key);
43
+ } else if (this.cache.size >= this.maxSize) {
44
+ // Remove oldest entry (first item in map)
45
+ const firstKey = this.cache.keys().next().value;
46
+ this.cache.delete(firstKey);
47
+ }
48
+ this.cache.set(key, value);
49
+ }
50
+ }
51
+
52
+ // Cache individual frame analyses
53
+ const frequencyDataCache = new LRUCache<string, Uint8Array>(100);
54
+
20
55
  export const deepGetMediaElements = (
21
56
  element: Element,
22
57
  medias: EFMedia[] = [],
@@ -309,10 +344,13 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
309
344
  this.endTimeMs < timelineTimeMs
310
345
  ) {
311
346
  this.style.display = "none";
347
+ // this.style.zIndex = "";
348
+ // this.style.opacity = "0";
312
349
  return;
313
350
  }
314
351
  this.style.display = "";
315
-
352
+ // this.style.zIndex = "100000";
353
+ // this.style.opacity = "";
316
354
  const animations = this.getAnimations({ subtree: true });
317
355
 
318
356
  this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
@@ -546,4 +584,128 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
546
584
  this.trimEndMs,
547
585
  };
548
586
  }
587
+
588
+ @property({ type: Number })
589
+ fftSize = 512; // Default FFT size
590
+
591
+ @property({ type: Number })
592
+ fftDecay = 8; // Default number of frames to analyze
593
+
594
+ private static readonly MIN_DB = -90;
595
+ private static readonly MAX_DB = -20;
596
+ private static readonly DECAY_WEIGHT = 0.7;
597
+
598
+ // Update FREQ_WEIGHTS to use the instance fftSize instead of a static value
599
+ get FREQ_WEIGHTS() {
600
+ if (freqWeightsCache.has(this.fftSize)) {
601
+ // biome-ignore lint/style/noNonNullAssertion: Will exist due to prior has check
602
+ return freqWeightsCache.get(this.fftSize)!;
603
+ }
604
+
605
+ const weights = new Float32Array(this.fftSize / 2).map((_, i) => {
606
+ const frequency = (i * 48000) / this.fftSize;
607
+ if (frequency < 60) return 0.3;
608
+ if (frequency < 250) return 0.4;
609
+ if (frequency < 500) return 0.6;
610
+ if (frequency < 2000) return 0.8;
611
+ if (frequency < 4000) return 1.2;
612
+ if (frequency < 8000) return 1.6;
613
+ return 2.0;
614
+ });
615
+
616
+ freqWeightsCache.set(this.fftSize, weights);
617
+ return weights;
618
+ }
619
+
620
+ frequencyDataTask = new Task(this, {
621
+ autoRun: EF_INTERACTIVE,
622
+ args: () =>
623
+ [this.audioBufferTask.status, this.trimAdjustedOwnCurrentTimeMs] as const,
624
+ task: async () => {
625
+ await this.audioBufferTask.taskComplete;
626
+ if (!this.audioBufferTask.value) return null;
627
+ if (this.trimAdjustedOwnCurrentTimeMs <= 0) return null;
628
+
629
+ const currentTimeMs = this.trimAdjustedOwnCurrentTimeMs;
630
+ const startOffsetMs = this.audioBufferTask.value.startOffsetMs;
631
+ const audioBuffer = this.audioBufferTask.value.buffer;
632
+
633
+ const framesData = await Promise.all(
634
+ Array.from({ length: this.fftDecay }, async (_, i) => {
635
+ const frameOffset = i * (1000 / 30);
636
+ const startTime = Math.max(
637
+ 0,
638
+ (currentTimeMs - frameOffset - startOffsetMs) / 1000,
639
+ );
640
+
641
+ // Cache key for this specific frame
642
+ const cacheKey = `${startOffsetMs},${startTime}`;
643
+
644
+ // Check cache for this specific frame
645
+ const cachedFrame = frequencyDataCache.get(cacheKey);
646
+ if (cachedFrame) {
647
+ return cachedFrame;
648
+ }
649
+
650
+ const audioContext = new OfflineAudioContext(
651
+ 2,
652
+ 48000 * (1 / 30),
653
+ 48000,
654
+ );
655
+ const analyser = audioContext.createAnalyser();
656
+ analyser.fftSize = this.fftSize;
657
+ analyser.minDecibels = EFMedia.MIN_DB;
658
+ analyser.maxDecibels = EFMedia.MAX_DB;
659
+
660
+ const audioBufferSource = audioContext.createBufferSource();
661
+ audioBufferSource.buffer = audioBuffer;
662
+
663
+ audioBufferSource.connect(analyser);
664
+ analyser.connect(audioContext.destination);
665
+
666
+ audioBufferSource.start(0, startTime, 1 / 30);
667
+
668
+ try {
669
+ await audioContext.startRendering();
670
+ const frameData = new Uint8Array(analyser.frequencyBinCount);
671
+ analyser.getByteFrequencyData(frameData);
672
+
673
+ // Cache this frame's analysis
674
+ frequencyDataCache.set(cacheKey, frameData);
675
+ return frameData;
676
+ } finally {
677
+ audioBufferSource.disconnect();
678
+ analyser.disconnect();
679
+ }
680
+ }),
681
+ );
682
+
683
+ const frameLength = framesData[0]?.length ?? 0;
684
+
685
+ // Combine frames with decay
686
+ const smoothedData = new Uint8Array(frameLength);
687
+ for (let i = 0; i < frameLength; i++) {
688
+ let weightedSum = 0;
689
+ let weightSum = 0;
690
+
691
+ framesData.forEach((frame, frameIndex) => {
692
+ const decayWeight = EFMedia.DECAY_WEIGHT ** frameIndex;
693
+ // biome-ignore lint/style/noNonNullAssertion: Will exist due to forEach
694
+ weightedSum += frame[i]! * decayWeight;
695
+ weightSum += decayWeight;
696
+ });
697
+
698
+ smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));
699
+ }
700
+
701
+ // Apply frequency weights using instance FREQ_WEIGHTS
702
+ smoothedData.forEach((value, i) => {
703
+ // biome-ignore lint/style/noNonNullAssertion: Trusting FREQ_WEIGHTS to be the correct length
704
+ const freqWeight = this.FREQ_WEIGHTS[i]!;
705
+ smoothedData[i] = Math.min(255, Math.round(value * freqWeight));
706
+ });
707
+
708
+ return smoothedData;
709
+ },
710
+ });
549
711
  }
@@ -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.
@@ -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
  }
@@ -613,6 +621,30 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
613
621
  }
614
622
  },
615
623
  });
624
+
625
+ protected updated(
626
+ changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
627
+ ): void {
628
+ super.updated(changedProperties);
629
+ if (
630
+ changedProperties.has("currentTime") ||
631
+ changedProperties.has("ownCurrentTimeMs")
632
+ ) {
633
+ const timelineTimeMs = (this.rootTimegroup ?? this).ownCurrentTimeMs;
634
+ if (
635
+ this.startTimeMs >= timelineTimeMs ||
636
+ this.endTimeMs <= timelineTimeMs
637
+ ) {
638
+ this.style.display = "none";
639
+ // this.style.zIndex = "";
640
+ // this.style.opacity = "0";
641
+ return;
642
+ }
643
+ this.style.display = "";
644
+ // this.style.zIndex = "100000";
645
+ // this.style.opacity = "";
646
+ }
647
+ }
616
648
  }
617
649
 
618
650
  Object.defineProperty(TemporalMixinClass.prototype, EF_TEMPORAL, {
@@ -39,7 +39,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
39
39
  display: block;
40
40
  width: 100%;
41
41
  height: 100%;
42
- position: relative;
42
+ position: absolute;
43
43
  transform-origin: center center;
44
44
  }
45
45
  `;
@@ -258,10 +258,13 @@ export class EFTimegroup extends EFTemporal(LitElement) {
258
258
  const timelineTimeMs = (this.rootTimegroup ?? this).currentTimeMs;
259
259
  if (this.startTimeMs > timelineTimeMs || this.endTimeMs < timelineTimeMs) {
260
260
  this.style.display = "none";
261
+ // this.style.zIndex = "";
262
+ // this.style.opacity = "0";
261
263
  return;
262
264
  }
263
265
  this.style.display = "";
264
-
266
+ // this.style.zIndex = "100000";
267
+ // this.style.opacity = "";
265
268
  const animations = this.getAnimations({ subtree: true });
266
269
  this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
267
270
  this.style.setProperty(