@editframe/elements 0.14.0-beta.2 → 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.
- package/dist/elements/EFImage.d.ts +2 -1
- package/dist/elements/EFImage.js +9 -3
- package/dist/elements/EFMedia.d.ts +7 -0
- package/dist/elements/EFMedia.js +133 -5
- package/dist/elements/EFTemporal.d.ts +4 -0
- package/dist/elements/EFTemporal.js +14 -0
- package/dist/elements/EFTimegroup.js +1 -1
- package/dist/elements/EFWaveform.d.ts +6 -5
- package/dist/elements/EFWaveform.js +152 -144
- package/dist/gui/EFWorkbench.js +1 -1
- package/package.json +3 -2
- package/src/elements/EFImage.browsertest.ts +33 -2
- package/src/elements/EFImage.ts +10 -3
- package/src/elements/EFMedia.ts +163 -1
- package/src/elements/EFTemporal.ts +33 -1
- package/src/elements/EFTimegroup.ts +5 -2
- package/src/elements/EFWaveform.ts +188 -185
- package/src/gui/EFWorkbench.ts +1 -1
package/src/elements/EFMedia.ts
CHANGED
|
@@ -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:
|
|
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(
|