@editframe/elements 0.26.3-beta.0 → 0.30.0-beta.13
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/EFSourceMixin.js +1 -1
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +4 -4
- package/dist/elements/EFText.d.ts +52 -0
- package/dist/elements/EFText.js +319 -0
- package/dist/elements/EFText.js.map +1 -0
- package/dist/elements/EFTextSegment.d.ts +30 -0
- package/dist/elements/EFTextSegment.js +94 -0
- package/dist/elements/EFTextSegment.js.map +1 -0
- package/dist/elements/EFThumbnailStrip.d.ts +4 -4
- package/dist/elements/EFWaveform.d.ts +4 -4
- package/dist/elements/FetchMixin.js +22 -7
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/easingUtils.js +62 -0
- package/dist/elements/easingUtils.js.map +1 -0
- package/dist/elements/updateAnimations.js +57 -10
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/gui/ContextMixin.js +11 -2
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/EFConfiguration.d.ts +4 -4
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFDial.js +4 -2
- package/dist/gui/EFDial.js.map +1 -1
- package/dist/gui/EFFilmstrip.d.ts +32 -6
- package/dist/gui/EFFilmstrip.js +314 -50
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.js +39 -15
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +4 -4
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFPreview.d.ts +4 -4
- package/dist/gui/EFPreview.js +2 -2
- package/dist/gui/EFPreview.js.map +1 -1
- package/dist/gui/EFResizableBox.d.ts +4 -4
- package/dist/gui/EFResizableBox.js +6 -3
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +8 -5
- package/dist/gui/EFScrubber.js +64 -12
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFWorkbench.d.ts +4 -4
- package/dist/gui/EFWorkbench.js +16 -3
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/style.css +7 -120
- package/package.json +3 -3
- package/scripts/build-css.js +5 -9
- package/test/constants.ts +8 -0
- package/test/recordReplayProxyPlugin.js +76 -10
- package/test/setup.ts +32 -0
- package/test/useMSW.ts +3 -0
- package/test/useTranscodeMSW.ts +191 -0
- package/tsdown.config.ts +7 -5
- package/types.json +1 -1
- package/src/elements/ContextProxiesController.ts +0 -124
- package/src/elements/CrossUpdateController.ts +0 -22
- package/src/elements/EFAudio.browsertest.ts +0 -706
- package/src/elements/EFAudio.ts +0 -56
- package/src/elements/EFCaptions.browsertest.ts +0 -1960
- package/src/elements/EFCaptions.ts +0 -823
- package/src/elements/EFImage.browsertest.ts +0 -120
- package/src/elements/EFImage.ts +0 -113
- package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
- package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
- package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
- package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
- package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
- package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
- package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
- package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
- package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
- package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
- package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
- package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
- package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
- package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
- package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
- package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
- package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
- package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
- package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
- package/src/elements/EFMedia.browsertest.ts +0 -872
- package/src/elements/EFMedia.ts +0 -341
- package/src/elements/EFSourceMixin.ts +0 -60
- package/src/elements/EFSurface.browsertest.ts +0 -151
- package/src/elements/EFSurface.ts +0 -142
- package/src/elements/EFTemporal.browsertest.ts +0 -215
- package/src/elements/EFTemporal.ts +0 -800
- package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
- package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
- package/src/elements/EFThumbnailStrip.ts +0 -906
- package/src/elements/EFTimegroup.browsertest.ts +0 -934
- package/src/elements/EFTimegroup.ts +0 -882
- package/src/elements/EFVideo.browsertest.ts +0 -1482
- package/src/elements/EFVideo.ts +0 -564
- package/src/elements/EFWaveform.ts +0 -547
- package/src/elements/FetchContext.browsertest.ts +0 -401
- package/src/elements/FetchMixin.ts +0 -38
- package/src/elements/SampleBuffer.ts +0 -94
- package/src/elements/TargetController.browsertest.ts +0 -230
- package/src/elements/TargetController.ts +0 -224
- package/src/elements/TimegroupController.ts +0 -26
- package/src/elements/durationConverter.ts +0 -35
- package/src/elements/parseTimeToMs.ts +0 -9
- package/src/elements/printTaskStatus.ts +0 -16
- package/src/elements/renderTemporalAudio.ts +0 -108
- package/src/elements/updateAnimations.browsertest.ts +0 -1884
- package/src/elements/updateAnimations.ts +0 -217
- package/src/elements/util.ts +0 -24
- package/src/gui/ContextMixin.browsertest.ts +0 -860
- package/src/gui/ContextMixin.ts +0 -562
- package/src/gui/Controllable.browsertest.ts +0 -258
- package/src/gui/Controllable.ts +0 -41
- package/src/gui/EFConfiguration.ts +0 -40
- package/src/gui/EFControls.browsertest.ts +0 -389
- package/src/gui/EFControls.ts +0 -195
- package/src/gui/EFDial.browsertest.ts +0 -84
- package/src/gui/EFDial.ts +0 -172
- package/src/gui/EFFilmstrip.browsertest.ts +0 -712
- package/src/gui/EFFilmstrip.ts +0 -1349
- package/src/gui/EFFitScale.ts +0 -152
- package/src/gui/EFFocusOverlay.ts +0 -79
- package/src/gui/EFPause.browsertest.ts +0 -202
- package/src/gui/EFPause.ts +0 -73
- package/src/gui/EFPlay.browsertest.ts +0 -202
- package/src/gui/EFPlay.ts +0 -73
- package/src/gui/EFPreview.ts +0 -74
- package/src/gui/EFResizableBox.browsertest.ts +0 -79
- package/src/gui/EFResizableBox.ts +0 -898
- package/src/gui/EFScrubber.ts +0 -151
- package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
- package/src/gui/EFTimeDisplay.ts +0 -55
- package/src/gui/EFToggleLoop.ts +0 -35
- package/src/gui/EFTogglePlay.ts +0 -70
- package/src/gui/EFWorkbench.ts +0 -115
- package/src/gui/PlaybackController.ts +0 -527
- package/src/gui/TWMixin.css +0 -6
- package/src/gui/TWMixin.ts +0 -61
- package/src/gui/TargetOrContextMixin.ts +0 -185
- package/src/gui/currentTimeContext.ts +0 -5
- package/src/gui/durationContext.ts +0 -3
- package/src/gui/efContext.ts +0 -6
- package/src/gui/fetchContext.ts +0 -5
- package/src/gui/focusContext.ts +0 -7
- package/src/gui/focusedElementContext.ts +0 -5
- package/src/gui/playingContext.ts +0 -5
- package/src/otel/BridgeSpanExporter.ts +0 -150
- package/src/otel/setupBrowserTracing.ts +0 -73
- package/src/otel/tracingHelpers.ts +0 -251
- package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
- package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
- package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
- package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
- package/src/transcoding/types/index.ts +0 -312
- package/src/transcoding/utils/MediaUtils.ts +0 -63
- package/src/transcoding/utils/UrlGenerator.ts +0 -68
- package/src/transcoding/utils/constants.ts +0 -36
- package/src/utils/LRUCache.test.ts +0 -274
- package/src/utils/LRUCache.ts +0 -696
|
@@ -3,21 +3,21 @@ import { EFAudio } from "./EFAudio.js";
|
|
|
3
3
|
import { EFVideo } from "./EFVideo.js";
|
|
4
4
|
import { TargetController } from "./TargetController.js";
|
|
5
5
|
import { Task } from "@lit/task";
|
|
6
|
-
import * as
|
|
6
|
+
import * as lit10 from "lit";
|
|
7
7
|
import { LitElement, PropertyValueMap } from "lit";
|
|
8
8
|
import { Ref } from "lit/directives/ref.js";
|
|
9
|
-
import * as
|
|
9
|
+
import * as lit_html10 from "lit-html";
|
|
10
10
|
|
|
11
11
|
//#region src/elements/EFWaveform.d.ts
|
|
12
12
|
declare const EFWaveform_base: (new (...args: any[]) => TemporalMixinInterface) & typeof LitElement;
|
|
13
13
|
declare class EFWaveform extends EFWaveform_base {
|
|
14
|
-
static styles:
|
|
14
|
+
static styles: lit10.CSSResult;
|
|
15
15
|
canvasRef: Ref<HTMLCanvasElement>;
|
|
16
16
|
private ctx;
|
|
17
17
|
private styleObserver;
|
|
18
18
|
private resizeObserver?;
|
|
19
19
|
private mutationObserver?;
|
|
20
|
-
render():
|
|
20
|
+
render(): lit_html10.TemplateResult<1>;
|
|
21
21
|
mode: "roundBars" | "bars" | "bricks" | "line" | "curve" | "pixel" | "wave" | "spikes";
|
|
22
22
|
color: string;
|
|
23
23
|
target: string;
|
|
@@ -5,15 +5,30 @@ function FetchMixin(superClass) {
|
|
|
5
5
|
super(..._args);
|
|
6
6
|
this.fetch = (url, init) => {
|
|
7
7
|
try {
|
|
8
|
+
let fetchPromise;
|
|
8
9
|
const workbench = this.closest("ef-workbench");
|
|
9
|
-
if (workbench?.fetch)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
if (workbench?.fetch) fetchPromise = workbench.fetch(url, init);
|
|
11
|
+
else {
|
|
12
|
+
const preview = this.closest("ef-preview");
|
|
13
|
+
if (preview?.fetch) fetchPromise = preview.fetch(url, init);
|
|
14
|
+
else {
|
|
15
|
+
const configuration = this.closest("ef-configuration");
|
|
16
|
+
if (configuration?.fetch) fetchPromise = configuration.fetch(url, init);
|
|
17
|
+
else fetchPromise = window.fetch(url, init);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return fetchPromise.catch((error) => {
|
|
21
|
+
console.error("FetchMixin fetch error", url, error, window.location.href);
|
|
22
|
+
const enhancedError = new (error instanceof Error ? error.constructor : Error)(`Failed to fetch: ${url}. Original error: ${error instanceof Error ? error.message : String(error)}`);
|
|
23
|
+
if (error instanceof Error) {
|
|
24
|
+
enhancedError.name = error.name;
|
|
25
|
+
enhancedError.stack = error.stack;
|
|
26
|
+
Object.assign(enhancedError, error);
|
|
27
|
+
}
|
|
28
|
+
throw enhancedError;
|
|
29
|
+
});
|
|
15
30
|
} catch (error) {
|
|
16
|
-
console.error("FetchMixin error", url, error);
|
|
31
|
+
console.error("FetchMixin error (synchronous)", url, error);
|
|
17
32
|
throw error;
|
|
18
33
|
}
|
|
19
34
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FetchMixin.js","names":[],"sources":["../../src/elements/FetchMixin.ts"],"sourcesContent":["import type { LitElement } from \"lit\";\n\nexport declare class FetchMixinInterface {\n fetch: typeof fetch;\n}\n\ntype Constructor<T = {}> = new (...args: any[]) => T;\nexport function FetchMixin<T extends Constructor<LitElement>>(superClass: T) {\n class FetchElement extends superClass {\n fetch = (url: string, init?: RequestInit): Promise<Response> => {\n try {\n // Look for context providers up the DOM tree\n const workbench = this.closest(\"ef-workbench\") as any;\n if (workbench?.fetch) {\n
|
|
1
|
+
{"version":3,"file":"FetchMixin.js","names":["fetchPromise: Promise<Response>"],"sources":["../../src/elements/FetchMixin.ts"],"sourcesContent":["import type { LitElement } from \"lit\";\n\nexport declare class FetchMixinInterface {\n fetch: typeof fetch;\n}\n\ntype Constructor<T = {}> = new (...args: any[]) => T;\nexport function FetchMixin<T extends Constructor<LitElement>>(superClass: T) {\n class FetchElement extends superClass {\n fetch = (url: string, init?: RequestInit): Promise<Response> => {\n try {\n let fetchPromise: Promise<Response>;\n\n // Look for context providers up the DOM tree\n const workbench = this.closest(\"ef-workbench\") as any;\n if (workbench?.fetch) {\n fetchPromise = workbench.fetch(url, init);\n } else {\n const preview = this.closest(\"ef-preview\") as any;\n if (preview?.fetch) {\n fetchPromise = preview.fetch(url, init);\n } else {\n const configuration = this.closest(\"ef-configuration\") as any;\n if (configuration?.fetch) {\n fetchPromise = configuration.fetch(url, init);\n } else {\n // Fallback to window.fetch\n fetchPromise = window.fetch(url, init);\n }\n }\n }\n\n // Wrap the promise to catch rejections and log the URL\n // Return the promise chain so errors are logged but still propagate\n return fetchPromise.catch((error) => {\n console.error(\n \"FetchMixin fetch error\",\n url,\n error,\n window.location.href,\n );\n // Create a new error with the URL in the message, preserving the original error type\n const ErrorConstructor =\n error instanceof Error ? error.constructor : Error;\n const enhancedError = new (ErrorConstructor as typeof Error)(\n `Failed to fetch: ${url}. Original error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Preserve the original error's properties\n if (error instanceof Error) {\n enhancedError.name = error.name;\n enhancedError.stack = error.stack;\n // Copy any additional properties from the original error\n Object.assign(enhancedError, error);\n }\n throw enhancedError;\n });\n } catch (error) {\n console.error(\"FetchMixin error (synchronous)\", url, error);\n throw error;\n }\n };\n }\n\n return FetchElement as Constructor<FetchMixinInterface> & T;\n}\n"],"mappings":";AAOA,SAAgB,WAA8C,YAAe;CAC3E,MAAM,qBAAqB,WAAW;;;iBAC3B,KAAa,SAA0C;AAC9D,QAAI;KACF,IAAIA;KAGJ,MAAM,YAAY,KAAK,QAAQ,eAAe;AAC9C,SAAI,WAAW,MACb,gBAAe,UAAU,MAAM,KAAK,KAAK;UACpC;MACL,MAAM,UAAU,KAAK,QAAQ,aAAa;AAC1C,UAAI,SAAS,MACX,gBAAe,QAAQ,MAAM,KAAK,KAAK;WAClC;OACL,MAAM,gBAAgB,KAAK,QAAQ,mBAAmB;AACtD,WAAI,eAAe,MACjB,gBAAe,cAAc,MAAM,KAAK,KAAK;WAG7C,gBAAe,OAAO,MAAM,KAAK,KAAK;;;AAO5C,YAAO,aAAa,OAAO,UAAU;AACnC,cAAQ,MACN,0BACA,KACA,OACA,OAAO,SAAS,KACjB;MAID,MAAM,gBAAgB,KADpB,iBAAiB,QAAQ,MAAM,cAAc,OAE7C,oBAAoB,IAAI,oBAAoB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,GACnG;AAED,UAAI,iBAAiB,OAAO;AAC1B,qBAAc,OAAO,MAAM;AAC3B,qBAAc,QAAQ,MAAM;AAE5B,cAAO,OAAO,eAAe,MAAM;;AAErC,YAAM;OACN;aACK,OAAO;AACd,aAAQ,MAAM,kCAAkC,KAAK,MAAM;AAC3D,WAAM;;;;;AAKZ,QAAO"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/elements/easingUtils.ts
|
|
2
|
+
/**
|
|
3
|
+
* Evaluates a CSS easing function at a given progress (0-1)
|
|
4
|
+
* Supports standard CSS easing functions and cubic-bezier
|
|
5
|
+
*/
|
|
6
|
+
function evaluateEasing(easing, progress) {
|
|
7
|
+
progress = Math.max(0, Math.min(1, progress));
|
|
8
|
+
switch (easing.trim().toLowerCase()) {
|
|
9
|
+
case "linear": return progress;
|
|
10
|
+
case "ease": return cubicBezier(.25, .1, .25, 1, progress);
|
|
11
|
+
case "ease-in": return cubicBezier(.42, 0, 1, 1, progress);
|
|
12
|
+
case "ease-out": return cubicBezier(0, 0, .58, 1, progress);
|
|
13
|
+
case "ease-in-out": return cubicBezier(.42, 0, .58, 1, progress);
|
|
14
|
+
default: {
|
|
15
|
+
const cubicBezierMatch = easing.match(/cubic-bezier\s*\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)/i);
|
|
16
|
+
if (cubicBezierMatch?.[1] && cubicBezierMatch[2] && cubicBezierMatch[3] && cubicBezierMatch[4]) return cubicBezier(Number.parseFloat(cubicBezierMatch[1]), Number.parseFloat(cubicBezierMatch[2]), Number.parseFloat(cubicBezierMatch[3]), Number.parseFloat(cubicBezierMatch[4]), progress);
|
|
17
|
+
return progress;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Evaluates a cubic-bezier curve at a given progress (0-1)
|
|
23
|
+
* Uses binary search to find the t value that corresponds to the given x
|
|
24
|
+
*/
|
|
25
|
+
function cubicBezier(x1, y1, x2, y2, x) {
|
|
26
|
+
x = Math.max(0, Math.min(1, x));
|
|
27
|
+
if (x === 0) return 0;
|
|
28
|
+
if (x === 1) return 1;
|
|
29
|
+
let t = .5;
|
|
30
|
+
let minT = 0;
|
|
31
|
+
let maxT = 1;
|
|
32
|
+
for (let i = 0; i < 20; i++) {
|
|
33
|
+
const diff = bezierX(t, x1, x2) - x;
|
|
34
|
+
if (Math.abs(diff) < 1e-4) break;
|
|
35
|
+
if (diff > 0) maxT = t;
|
|
36
|
+
else minT = t;
|
|
37
|
+
t = (minT + maxT) / 2;
|
|
38
|
+
}
|
|
39
|
+
return bezierY(t, y1, y2);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Calculates the x coordinate of a cubic bezier curve at parameter t
|
|
43
|
+
*/
|
|
44
|
+
function bezierX(t, x1, x2) {
|
|
45
|
+
const t2 = t * t;
|
|
46
|
+
const t3 = t2 * t;
|
|
47
|
+
const mt = 1 - t;
|
|
48
|
+
return 3 * (mt * mt) * t * x1 + 3 * mt * t2 * x2 + t3;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Calculates the y coordinate of a cubic bezier curve at parameter t
|
|
52
|
+
*/
|
|
53
|
+
function bezierY(t, y1, y2) {
|
|
54
|
+
const t2 = t * t;
|
|
55
|
+
const t3 = t2 * t;
|
|
56
|
+
const mt = 1 - t;
|
|
57
|
+
return 3 * (mt * mt) * t * y1 + 3 * mt * t2 * y2 + t3;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
export { evaluateEasing };
|
|
62
|
+
//# sourceMappingURL=easingUtils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"easingUtils.js","names":[],"sources":["../../src/elements/easingUtils.ts"],"sourcesContent":["/**\n * Evaluates a CSS easing function at a given progress (0-1)\n * Supports standard CSS easing functions and cubic-bezier\n */\nexport function evaluateEasing(easing: string, progress: number): number {\n // Clamp progress to 0-1\n progress = Math.max(0, Math.min(1, progress));\n\n // Handle standard CSS easing functions\n switch (easing.trim().toLowerCase()) {\n case \"linear\":\n return progress;\n case \"ease\":\n return cubicBezier(0.25, 0.1, 0.25, 1, progress);\n case \"ease-in\":\n return cubicBezier(0.42, 0, 1, 1, progress);\n case \"ease-out\":\n return cubicBezier(0, 0, 0.58, 1, progress);\n case \"ease-in-out\":\n return cubicBezier(0.42, 0, 0.58, 1, progress);\n default: {\n // Try to parse as cubic-bezier\n const cubicBezierMatch = easing.match(\n /cubic-bezier\\s*\\(\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*\\)/i,\n );\n if (\n cubicBezierMatch?.[1] &&\n cubicBezierMatch[2] &&\n cubicBezierMatch[3] &&\n cubicBezierMatch[4]\n ) {\n const x1 = Number.parseFloat(cubicBezierMatch[1]);\n const y1 = Number.parseFloat(cubicBezierMatch[2]);\n const x2 = Number.parseFloat(cubicBezierMatch[3]);\n const y2 = Number.parseFloat(cubicBezierMatch[4]);\n return cubicBezier(x1, y1, x2, y2, progress);\n }\n // Default to linear if unknown\n return progress;\n }\n }\n}\n\n/**\n * Evaluates a cubic-bezier curve at a given progress (0-1)\n * Uses binary search to find the t value that corresponds to the given x\n */\nfunction cubicBezier(\n x1: number,\n y1: number,\n x2: number,\n y2: number,\n x: number,\n): number {\n // Clamp x to 0-1\n x = Math.max(0, Math.min(1, x));\n\n // Handle edge cases\n if (x === 0) return 0;\n if (x === 1) return 1;\n\n // Binary search for t that gives us the desired x\n let t = 0.5;\n let minT = 0;\n let maxT = 1;\n\n // Iterate until we're close enough\n for (let i = 0; i < 20; i++) {\n const currentX = bezierX(t, x1, x2);\n const diff = currentX - x;\n\n if (Math.abs(diff) < 0.0001) {\n break;\n }\n\n if (diff > 0) {\n maxT = t;\n } else {\n minT = t;\n }\n\n t = (minT + maxT) / 2;\n }\n\n // Return the y value at this t\n return bezierY(t, y1, y2);\n}\n\n/**\n * Calculates the x coordinate of a cubic bezier curve at parameter t\n */\nfunction bezierX(t: number, x1: number, x2: number): number {\n const t2 = t * t;\n const t3 = t2 * t;\n const mt = 1 - t;\n const mt2 = mt * mt;\n\n return 3 * mt2 * t * x1 + 3 * mt * t2 * x2 + t3;\n}\n\n/**\n * Calculates the y coordinate of a cubic bezier curve at parameter t\n */\nfunction bezierY(t: number, y1: number, y2: number): number {\n const t2 = t * t;\n const t3 = t2 * t;\n const mt = 1 - t;\n const mt2 = mt * mt;\n\n return 3 * mt2 * t * y1 + 3 * mt * t2 * y2 + t3;\n}\n"],"mappings":";;;;;AAIA,SAAgB,eAAe,QAAgB,UAA0B;AAEvE,YAAW,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC;AAG7C,SAAQ,OAAO,MAAM,CAAC,aAAa,EAAnC;EACE,KAAK,SACH,QAAO;EACT,KAAK,OACH,QAAO,YAAY,KAAM,IAAK,KAAM,GAAG,SAAS;EAClD,KAAK,UACH,QAAO,YAAY,KAAM,GAAG,GAAG,GAAG,SAAS;EAC7C,KAAK,WACH,QAAO,YAAY,GAAG,GAAG,KAAM,GAAG,SAAS;EAC7C,KAAK,cACH,QAAO,YAAY,KAAM,GAAG,KAAM,GAAG,SAAS;EAChD,SAAS;GAEP,MAAM,mBAAmB,OAAO,MAC9B,kFACD;AACD,OACE,mBAAmB,MACnB,iBAAiB,MACjB,iBAAiB,MACjB,iBAAiB,GAMjB,QAAO,YAJI,OAAO,WAAW,iBAAiB,GAAG,EACtC,OAAO,WAAW,iBAAiB,GAAG,EACtC,OAAO,WAAW,iBAAiB,GAAG,EACtC,OAAO,WAAW,iBAAiB,GAAG,EACd,SAAS;AAG9C,UAAO;;;;;;;;AASb,SAAS,YACP,IACA,IACA,IACA,IACA,GACQ;AAER,KAAI,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,EAAE,CAAC;AAG/B,KAAI,MAAM,EAAG,QAAO;AACpB,KAAI,MAAM,EAAG,QAAO;CAGpB,IAAI,IAAI;CACR,IAAI,OAAO;CACX,IAAI,OAAO;AAGX,MAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK;EAE3B,MAAM,OADW,QAAQ,GAAG,IAAI,GAAG,GACX;AAExB,MAAI,KAAK,IAAI,KAAK,GAAG,KACnB;AAGF,MAAI,OAAO,EACT,QAAO;MAEP,QAAO;AAGT,OAAK,OAAO,QAAQ;;AAItB,QAAO,QAAQ,GAAG,IAAI,GAAG;;;;;AAM3B,SAAS,QAAQ,GAAW,IAAY,IAAoB;CAC1D,MAAM,KAAK,IAAI;CACf,MAAM,KAAK,KAAK;CAChB,MAAM,KAAK,IAAI;AAGf,QAAO,KAFK,KAAK,MAEA,IAAI,KAAK,IAAI,KAAK,KAAK,KAAK;;;;;AAM/C,SAAS,QAAQ,GAAW,IAAY,IAAoB;CAC1D,MAAM,KAAK,IAAI;CACf,MAAM,KAAK,KAAK;CAChB,MAAM,KAAK,IAAI;AAGf,QAAO,KAFK,KAAK,MAEA,IAAI,KAAK,IAAI,KAAK,KAAK,KAAK"}
|
|
@@ -15,7 +15,8 @@ const evaluateTemporalState = (element) => {
|
|
|
15
15
|
const progress = element.durationMs <= 0 ? 1 : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));
|
|
16
16
|
const isRootElement = !element.parentTimegroup;
|
|
17
17
|
const isLastElementInComposition = element.endTimeMs === element.rootTimegroup?.endTimeMs;
|
|
18
|
-
const
|
|
18
|
+
const isTextSegment = element.tagName === "EF-TEXT-SEGMENT";
|
|
19
|
+
const useInclusiveEnd = isRootElement || isLastElementInComposition || isTextSegment;
|
|
19
20
|
return {
|
|
20
21
|
progress,
|
|
21
22
|
isVisible: element.startTimeMs <= timelineTimeMs && (useInclusiveEnd ? element.endTimeMs >= timelineTimeMs : element.endTimeMs > timelineTimeMs),
|
|
@@ -54,33 +55,79 @@ const updateVisualState = (element, state) => {
|
|
|
54
55
|
const coordinateAnimationsForSingleElement = (element) => {
|
|
55
56
|
const animations = element.getAnimations({ subtree: true });
|
|
56
57
|
for (const animation of animations) {
|
|
57
|
-
if (animation.playState === "
|
|
58
|
+
if (animation.playState === "finished") {
|
|
59
|
+
animation.cancel();
|
|
60
|
+
animation.play();
|
|
61
|
+
animation.pause();
|
|
62
|
+
} else if (animation.playState === "running") animation.pause();
|
|
58
63
|
const effect = animation.effect;
|
|
59
64
|
if (!(effect && effect instanceof KeyframeEffect)) continue;
|
|
60
|
-
|
|
65
|
+
const target = effect.target;
|
|
66
|
+
if (!target) continue;
|
|
61
67
|
const timing = effect.getTiming();
|
|
62
68
|
const duration = Number(timing.duration) || 0;
|
|
63
|
-
|
|
69
|
+
let delay = Number(timing.delay) || 0;
|
|
70
|
+
if (target instanceof HTMLElement) {
|
|
71
|
+
const animationDelays = window.getComputedStyle(target).animationDelay.split(", ").map((s) => s.trim());
|
|
72
|
+
const parseDelay = (delayStr) => {
|
|
73
|
+
if (delayStr === "0s" || delayStr === "0ms") return 0;
|
|
74
|
+
const delayMatch = delayStr.match(/^([\d.]+)(s|ms)$/);
|
|
75
|
+
if (delayMatch?.[1] && delayMatch[2]) {
|
|
76
|
+
const value = Number.parseFloat(delayMatch[1]);
|
|
77
|
+
return delayMatch[2] === "s" ? value * 1e3 : value;
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
};
|
|
81
|
+
if (animationDelays.length === 1 && animationDelays[0]) {
|
|
82
|
+
const parsedDelay = parseDelay(animationDelays[0]);
|
|
83
|
+
if (parsedDelay > 0) delay = parsedDelay;
|
|
84
|
+
else if ((animationDelays[0] === "0s" || animationDelays[0] === "0ms") && delay === 0) delay = 0;
|
|
85
|
+
} else if (animationDelays.length > 1) {
|
|
86
|
+
const animationIndex = Array.from(target.getAnimations()).indexOf(animation);
|
|
87
|
+
if (animationIndex >= 0 && animationIndex < animationDelays.length && animationDelays[animationIndex]) {
|
|
88
|
+
const parsedDelay = parseDelay(animationDelays[animationIndex]);
|
|
89
|
+
if (parsedDelay > 0) delay = parsedDelay;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
64
93
|
const iterations = Number(timing.iterations) || DEFAULT_ANIMATION_ITERATIONS;
|
|
65
94
|
if (duration <= 0) {
|
|
66
95
|
animation.currentTime = 0;
|
|
67
96
|
continue;
|
|
68
97
|
}
|
|
69
98
|
const currentTime = element.ownCurrentTimeMs ?? 0;
|
|
70
|
-
|
|
99
|
+
let effectiveDelay = delay;
|
|
100
|
+
if (element.tagName === "EF-TEXT-SEGMENT" && element.staggerOffsetMs !== void 0) {
|
|
101
|
+
const staggerOffsetMs = element.staggerOffsetMs;
|
|
102
|
+
effectiveDelay = delay + staggerOffsetMs;
|
|
103
|
+
}
|
|
104
|
+
if (currentTime < effectiveDelay) {
|
|
71
105
|
animation.currentTime = 0;
|
|
72
106
|
continue;
|
|
73
107
|
}
|
|
74
|
-
const adjustedTime = currentTime -
|
|
108
|
+
const adjustedTime = currentTime - effectiveDelay;
|
|
75
109
|
const currentIteration = Math.floor(adjustedTime / duration);
|
|
76
110
|
let currentIterationTime = adjustedTime % duration;
|
|
77
111
|
const direction = timing.direction || "normal";
|
|
112
|
+
const isAlternate = direction === "alternate" || direction === "alternate-reverse";
|
|
78
113
|
if (direction === "reverse" || direction === "alternate" && currentIteration % 2 === 1 || direction === "alternate-reverse" && currentIteration % 2 === 0) currentIterationTime = duration - currentIterationTime;
|
|
79
|
-
|
|
80
|
-
|
|
114
|
+
if (currentIteration >= iterations) {
|
|
115
|
+
const maxSafeAnimationTime = duration * iterations - ANIMATION_PRECISION_OFFSET;
|
|
116
|
+
if (isAlternate) {
|
|
117
|
+
const finalIteration = iterations - 1;
|
|
118
|
+
if (direction === "alternate" && finalIteration % 2 === 1 || direction === "alternate-reverse" && finalIteration % 2 === 0) animation.currentTime = Math.min(duration - ANIMATION_PRECISION_OFFSET, maxSafeAnimationTime);
|
|
119
|
+
else animation.currentTime = maxSafeAnimationTime;
|
|
120
|
+
} else animation.currentTime = maxSafeAnimationTime;
|
|
121
|
+
} else if (isAlternate) if (effectiveDelay > 0) if (currentIteration === 0) animation.currentTime = currentTime;
|
|
122
|
+
else {
|
|
123
|
+
const maxSafeAnimationTime = duration * iterations - ANIMATION_PRECISION_OFFSET;
|
|
124
|
+
animation.currentTime = Math.min(adjustedTime, maxSafeAnimationTime);
|
|
125
|
+
}
|
|
126
|
+
else animation.currentTime = currentIterationTime;
|
|
81
127
|
else {
|
|
82
|
-
const
|
|
83
|
-
|
|
128
|
+
const timeWithinAnimation = currentIteration * duration + currentIterationTime;
|
|
129
|
+
const maxSafeAnimationTime = duration * iterations - ANIMATION_PRECISION_OFFSET;
|
|
130
|
+
animation.currentTime = Math.min(timeWithinAnimation, maxSafeAnimationTime);
|
|
84
131
|
}
|
|
85
132
|
}
|
|
86
133
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"updateAnimations.js","names":[],"sources":["../../src/elements/updateAnimations.ts"],"sourcesContent":["import {\n deepGetTemporalElements,\n type TemporalMixinInterface,\n} from \"./EFTemporal.ts\";\n\n// All animatable elements are temporal elements with HTMLElement interface\nexport type AnimatableElement = TemporalMixinInterface & HTMLElement;\n\n// Constants\nconst ANIMATION_PRECISION_OFFSET = 0.1; // Use 0.1ms to safely avoid completion threshold\nconst DEFAULT_ANIMATION_ITERATIONS = 1;\nconst PROGRESS_PROPERTY = \"--ef-progress\";\nconst DURATION_PROPERTY = \"--ef-duration\";\nconst TRANSITION_DURATION_PROPERTY = \"--ef-transition-duration\";\nconst TRANSITION_OUT_START_PROPERTY = \"--ef-transition-out-start\";\n\n/**\n * Represents the temporal state of an element relative to the timeline\n */\ninterface TemporalState {\n progress: number;\n isVisible: boolean;\n timelineTimeMs: number;\n}\n\n/**\n * Evaluates what the element's state should be based on the timeline\n */\nexport const evaluateTemporalState = (\n element: AnimatableElement,\n): TemporalState => {\n // Get timeline time from root timegroup, or use element's own time if it IS a timegroup\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n\n const progress =\n element.durationMs <= 0\n ? 1\n : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));\n\n // Root elements and elements aligned with composition end should remain visible at exact end time\n // Other elements use exclusive end for clean transitions\n const isRootElement = !(element as any).parentTimegroup;\n const isLastElementInComposition =\n element.endTimeMs === element.rootTimegroup?.endTimeMs;\n const useInclusiveEnd = isRootElement || isLastElementInComposition;\n\n const isVisible =\n element.startTimeMs <= timelineTimeMs &&\n (useInclusiveEnd\n ? element.endTimeMs >= timelineTimeMs\n : element.endTimeMs > timelineTimeMs);\n\n return { progress, isVisible, timelineTimeMs };\n};\n\n/**\n * Evaluates element visibility specifically for animation coordination\n * Uses inclusive end boundaries to prevent animation jumps at exact boundaries\n */\nexport const evaluateTemporalStateForAnimation = (\n element: AnimatableElement,\n): TemporalState => {\n // Get timeline time from root timegroup, or use element's own time if it IS a timegroup\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n\n const progress =\n element.durationMs <= 0\n ? 1\n : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));\n\n // For animation coordination, use inclusive end for ALL elements to prevent visual jumps\n const isVisible =\n element.startTimeMs <= timelineTimeMs &&\n element.endTimeMs >= timelineTimeMs;\n\n return { progress, isVisible, timelineTimeMs };\n};\n\n/**\n * Updates the visual state (CSS + display) to match temporal state\n */\nconst updateVisualState = (\n element: AnimatableElement,\n state: TemporalState,\n): void => {\n // Always set progress (needed for many use cases)\n element.style.setProperty(PROGRESS_PROPERTY, `${state.progress * 100}%`);\n\n // Handle visibility\n if (!state.isVisible) {\n if (element.style.display !== \"none\") {\n element.style.display = \"none\";\n }\n return;\n }\n\n if (element.style.display === \"none\") {\n element.style.display = \"\";\n }\n\n // Set other CSS properties for visible elements only\n element.style.setProperty(DURATION_PROPERTY, `${element.durationMs}ms`);\n element.style.setProperty(\n TRANSITION_DURATION_PROPERTY,\n `${element.parentTimegroup?.overlapMs ?? 0}ms`,\n );\n element.style.setProperty(\n TRANSITION_OUT_START_PROPERTY,\n `${element.durationMs - (element.parentTimegroup?.overlapMs ?? 0)}ms`,\n );\n};\n\n/**\n * Coordinates animations for a single element and its subtree, using the element as the time source\n */\nconst coordinateAnimationsForSingleElement = (\n element: AnimatableElement,\n): void => {\n const animations = element.getAnimations({ subtree: true });\n\n for (const animation of animations) {\n if (animation.playState === \"running\") {\n animation.pause();\n }\n\n const effect = animation.effect;\n if (!(effect && effect instanceof KeyframeEffect)) {\n continue;\n }\n\n const target = effect.target;\n if (!target) {\n continue;\n }\n\n // For animations in this element's subtree, always use this element as the time source\n // This handles both animations directly on the temporal element and on its non-temporal children\n const timing = effect.getTiming();\n const duration = Number(timing.duration) || 0;\n const delay = Number(timing.delay) || 0;\n const iterations =\n Number(timing.iterations) || DEFAULT_ANIMATION_ITERATIONS;\n\n if (duration <= 0) {\n animation.currentTime = 0;\n continue;\n }\n\n // Use the element itself as the time source (it's guaranteed to be temporal)\n const currentTime = element.ownCurrentTimeMs ?? 0;\n\n if (currentTime < delay) {\n animation.currentTime = 0;\n continue;\n }\n\n const adjustedTime = currentTime - delay;\n const currentIteration = Math.floor(adjustedTime / duration);\n let currentIterationTime = adjustedTime % duration;\n\n // Handle animation-direction\n const direction = timing.direction || \"normal\";\n const shouldReverse =\n direction === \"reverse\" ||\n (direction === \"alternate\" && currentIteration % 2 === 1) ||\n (direction === \"alternate-reverse\" && currentIteration % 2 === 0);\n\n if (shouldReverse) {\n currentIterationTime = duration - currentIterationTime;\n }\n\n // Calculate the total animation timeline length (delay + duration * iterations)\n const totalAnimationLength = delay + duration * iterations;\n\n // CRITICAL: Always keep currentTime below totalAnimationLength to prevent completion\n const maxSafeCurrentTime =\n totalAnimationLength - ANIMATION_PRECISION_OFFSET;\n\n if (currentIteration >= iterations) {\n // Animation would be complete - clamp to just before completion\n animation.currentTime = maxSafeCurrentTime;\n } else {\n // Animation in progress - clamp to safe value within current iteration\n const proposedCurrentTime =\n Math.min(currentIterationTime, duration - ANIMATION_PRECISION_OFFSET) +\n delay;\n animation.currentTime = Math.min(proposedCurrentTime, maxSafeCurrentTime);\n }\n }\n};\n\n/**\n * Main function: synchronizes DOM element with timeline\n */\nexport const updateAnimations = (element: AnimatableElement): void => {\n const temporalState = evaluateTemporalState(element);\n deepGetTemporalElements(element).forEach((temporalElement) => {\n const temporalState = evaluateTemporalState(temporalElement);\n updateVisualState(temporalElement, temporalState);\n });\n updateVisualState(element, temporalState);\n\n // Coordinate animations - use animation-specific visibility to prevent jumps at exact boundaries\n const animationState = evaluateTemporalStateForAnimation(element);\n if (animationState.isVisible) {\n coordinateAnimationsForSingleElement(element);\n }\n\n // Coordinate animations for child elements using animation-specific visibility\n deepGetTemporalElements(element).forEach((temporalElement) => {\n const childAnimationState =\n evaluateTemporalStateForAnimation(temporalElement);\n if (childAnimationState.isVisible) {\n coordinateAnimationsForSingleElement(temporalElement);\n }\n });\n};\n"],"mappings":";;;AASA,MAAM,6BAA6B;AACnC,MAAM,+BAA+B;AACrC,MAAM,oBAAoB;AAC1B,MAAM,oBAAoB;AAC1B,MAAM,+BAA+B;AACrC,MAAM,gCAAgC;;;;AActC,MAAa,yBACX,YACkB;CAElB,MAAM,kBAAkB,QAAQ,iBAAiB,SAAS;CAE1D,MAAM,WACJ,QAAQ,cAAc,IAClB,IACA,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,gBAAgB,QAAQ,WAAW,CAAC;CAI1E,MAAM,gBAAgB,CAAE,QAAgB;CACxC,MAAM,6BACJ,QAAQ,cAAc,QAAQ,eAAe;CAC/C,MAAM,kBAAkB,iBAAiB;AAQzC,QAAO;EAAE;EAAU,WALjB,QAAQ,eAAe,mBACtB,kBACG,QAAQ,aAAa,iBACrB,QAAQ,YAAY;EAEI;EAAgB;;;;;;AAOhD,MAAa,qCACX,YACkB;CAElB,MAAM,kBAAkB,QAAQ,iBAAiB,SAAS;AAY1D,QAAO;EAAE,UATP,QAAQ,cAAc,IAClB,IACA,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,gBAAgB,QAAQ,WAAW,CAAC;EAOvD,WAHjB,QAAQ,eAAe,kBACvB,QAAQ,aAAa;EAEO;EAAgB;;;;;AAMhD,MAAM,qBACJ,SACA,UACS;AAET,SAAQ,MAAM,YAAY,mBAAmB,GAAG,MAAM,WAAW,IAAI,GAAG;AAGxE,KAAI,CAAC,MAAM,WAAW;AACpB,MAAI,QAAQ,MAAM,YAAY,OAC5B,SAAQ,MAAM,UAAU;AAE1B;;AAGF,KAAI,QAAQ,MAAM,YAAY,OAC5B,SAAQ,MAAM,UAAU;AAI1B,SAAQ,MAAM,YAAY,mBAAmB,GAAG,QAAQ,WAAW,IAAI;AACvE,SAAQ,MAAM,YACZ,8BACA,GAAG,QAAQ,iBAAiB,aAAa,EAAE,IAC5C;AACD,SAAQ,MAAM,YACZ,+BACA,GAAG,QAAQ,cAAc,QAAQ,iBAAiB,aAAa,GAAG,IACnE;;;;;AAMH,MAAM,wCACJ,YACS;CACT,MAAM,aAAa,QAAQ,cAAc,EAAE,SAAS,MAAM,CAAC;AAE3D,MAAK,MAAM,aAAa,YAAY;AAClC,MAAI,UAAU,cAAc,UAC1B,WAAU,OAAO;EAGnB,MAAM,SAAS,UAAU;AACzB,MAAI,EAAE,UAAU,kBAAkB,gBAChC;AAIF,MAAI,CADW,OAAO,OAEpB;EAKF,MAAM,SAAS,OAAO,WAAW;EACjC,MAAM,WAAW,OAAO,OAAO,SAAS,IAAI;EAC5C,MAAM,QAAQ,OAAO,OAAO,MAAM,IAAI;EACtC,MAAM,aACJ,OAAO,OAAO,WAAW,IAAI;AAE/B,MAAI,YAAY,GAAG;AACjB,aAAU,cAAc;AACxB;;EAIF,MAAM,cAAc,QAAQ,oBAAoB;AAEhD,MAAI,cAAc,OAAO;AACvB,aAAU,cAAc;AACxB;;EAGF,MAAM,eAAe,cAAc;EACnC,MAAM,mBAAmB,KAAK,MAAM,eAAe,SAAS;EAC5D,IAAI,uBAAuB,eAAe;EAG1C,MAAM,YAAY,OAAO,aAAa;AAMtC,MAJE,cAAc,aACb,cAAc,eAAe,mBAAmB,MAAM,KACtD,cAAc,uBAAuB,mBAAmB,MAAM,EAG/D,wBAAuB,WAAW;EAOpC,MAAM,qBAHuB,QAAQ,WAAW,aAIvB;AAEzB,MAAI,oBAAoB,WAEtB,WAAU,cAAc;OACnB;GAEL,MAAM,sBACJ,KAAK,IAAI,sBAAsB,WAAW,2BAA2B,GACrE;AACF,aAAU,cAAc,KAAK,IAAI,qBAAqB,mBAAmB;;;;;;;AAQ/E,MAAa,oBAAoB,YAAqC;CACpE,MAAM,gBAAgB,sBAAsB,QAAQ;AACpD,yBAAwB,QAAQ,CAAC,SAAS,oBAAoB;AAE5D,oBAAkB,iBADI,sBAAsB,gBAAgB,CACX;GACjD;AACF,mBAAkB,SAAS,cAAc;AAIzC,KADuB,kCAAkC,QAAQ,CAC9C,UACjB,sCAAqC,QAAQ;AAI/C,yBAAwB,QAAQ,CAAC,SAAS,oBAAoB;AAG5D,MADE,kCAAkC,gBAAgB,CAC5B,UACtB,sCAAqC,gBAAgB;GAEvD"}
|
|
1
|
+
{"version":3,"file":"updateAnimations.js","names":[],"sources":["../../src/elements/updateAnimations.ts"],"sourcesContent":["import {\n deepGetTemporalElements,\n type TemporalMixinInterface,\n} from \"./EFTemporal.ts\";\n\n// All animatable elements are temporal elements with HTMLElement interface\nexport type AnimatableElement = TemporalMixinInterface & HTMLElement;\n\n// Constants\nconst ANIMATION_PRECISION_OFFSET = 0.1; // Use 0.1ms to safely avoid completion threshold\nconst DEFAULT_ANIMATION_ITERATIONS = 1;\nconst PROGRESS_PROPERTY = \"--ef-progress\";\nconst DURATION_PROPERTY = \"--ef-duration\";\nconst TRANSITION_DURATION_PROPERTY = \"--ef-transition-duration\";\nconst TRANSITION_OUT_START_PROPERTY = \"--ef-transition-out-start\";\n\n/**\n * Represents the temporal state of an element relative to the timeline\n */\ninterface TemporalState {\n progress: number;\n isVisible: boolean;\n timelineTimeMs: number;\n}\n\n/**\n * Evaluates what the element's state should be based on the timeline\n */\nexport const evaluateTemporalState = (\n element: AnimatableElement,\n): TemporalState => {\n // Get timeline time from root timegroup, or use element's own time if it IS a timegroup\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n\n const progress =\n element.durationMs <= 0\n ? 1\n : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));\n\n // Root elements and elements aligned with composition end should remain visible at exact end time\n // Text segments should also use inclusive end since they're meant to be visible for full duration\n // Other elements use exclusive end for clean transitions\n const isRootElement = !(element as any).parentTimegroup;\n const isLastElementInComposition =\n element.endTimeMs === element.rootTimegroup?.endTimeMs;\n const isTextSegment = element.tagName === \"EF-TEXT-SEGMENT\";\n const useInclusiveEnd =\n isRootElement || isLastElementInComposition || isTextSegment;\n\n const isVisible =\n element.startTimeMs <= timelineTimeMs &&\n (useInclusiveEnd\n ? element.endTimeMs >= timelineTimeMs\n : element.endTimeMs > timelineTimeMs);\n\n return { progress, isVisible, timelineTimeMs };\n};\n\n/**\n * Evaluates element visibility specifically for animation coordination\n * Uses inclusive end boundaries to prevent animation jumps at exact boundaries\n */\nexport const evaluateTemporalStateForAnimation = (\n element: AnimatableElement,\n): TemporalState => {\n // Get timeline time from root timegroup, or use element's own time if it IS a timegroup\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n\n const progress =\n element.durationMs <= 0\n ? 1\n : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));\n\n // For animation coordination, use inclusive end for ALL elements to prevent visual jumps\n const isVisible =\n element.startTimeMs <= timelineTimeMs &&\n element.endTimeMs >= timelineTimeMs;\n\n return { progress, isVisible, timelineTimeMs };\n};\n\n/**\n * Updates the visual state (CSS + display) to match temporal state\n */\nconst updateVisualState = (\n element: AnimatableElement,\n state: TemporalState,\n): void => {\n // Always set progress (needed for many use cases)\n element.style.setProperty(PROGRESS_PROPERTY, `${state.progress * 100}%`);\n\n // Handle visibility\n if (!state.isVisible) {\n if (element.style.display !== \"none\") {\n element.style.display = \"none\";\n }\n return;\n }\n\n if (element.style.display === \"none\") {\n element.style.display = \"\";\n }\n\n // Set other CSS properties for visible elements only\n element.style.setProperty(DURATION_PROPERTY, `${element.durationMs}ms`);\n element.style.setProperty(\n TRANSITION_DURATION_PROPERTY,\n `${element.parentTimegroup?.overlapMs ?? 0}ms`,\n );\n element.style.setProperty(\n TRANSITION_OUT_START_PROPERTY,\n `${element.durationMs - (element.parentTimegroup?.overlapMs ?? 0)}ms`,\n );\n};\n\n/**\n * Coordinates animations for a single element and its subtree, using the element as the time source\n */\nconst coordinateAnimationsForSingleElement = (\n element: AnimatableElement,\n): void => {\n // Get animations on the element itself and its subtree\n // CSS animations created via the 'animation' property are included\n const animations = element.getAnimations({ subtree: true });\n\n for (const animation of animations) {\n // Ensure animation is in a playable state (not finished)\n // Finished animations can't be controlled, so reset them\n if (animation.playState === \"finished\") {\n animation.cancel();\n // Re-initialize the animation so it can be controlled\n animation.play();\n animation.pause();\n } else if (animation.playState === \"running\") {\n // Pause running animations so we can control them manually\n animation.pause();\n }\n\n const effect = animation.effect;\n if (!(effect && effect instanceof KeyframeEffect)) {\n continue;\n }\n\n const target = effect.target;\n if (!target) {\n continue;\n }\n\n // For animations in this element's subtree, always use this element as the time source\n // This handles both animations directly on the temporal element and on its non-temporal children\n const timing = effect.getTiming();\n // Duration and delay from getTiming() are already in milliseconds\n // They include CSS animation-duration and animation-delay values\n const duration = Number(timing.duration) || 0;\n let delay = Number(timing.delay) || 0;\n\n // For Web Animations API animations, getTiming().delay is always correct.\n // For CSS animations, we may need to read from computed styles.\n // Try to read delay from computed styles as a fallback/override for CSS animations\n if (target instanceof HTMLElement) {\n const computedStyle = window.getComputedStyle(target);\n const animationDelays = computedStyle.animationDelay\n .split(\", \")\n .map((s) => s.trim());\n\n // Parse CSS delay value\n const parseDelay = (delayStr: string): number => {\n if (delayStr === \"0s\" || delayStr === \"0ms\") {\n return 0;\n }\n const delayMatch = delayStr.match(/^([\\d.]+)(s|ms)$/);\n if (delayMatch?.[1] && delayMatch[2]) {\n const value = Number.parseFloat(delayMatch[1]);\n const unit = delayMatch[2];\n return unit === \"s\" ? value * 1000 : value;\n }\n return 0;\n };\n\n // Only override delay from computed styles if:\n // 1. We have a valid parsed delay value, OR\n // 2. The computed style explicitly says \"0s\" or \"0ms\" (meaning no CSS delay)\n // This ensures we don't override WAAPI delay with 0 from computed styles when there's no CSS animation\n if (animationDelays.length === 1 && animationDelays[0]) {\n const parsedDelay = parseDelay(animationDelays[0]);\n // Only override if we got a valid parse AND it's not just a default \"0s\" from computed styles\n // OR if it's explicitly \"0s\"/\"0ms\" and getTiming().delay is also 0 (CSS animation with no delay)\n if (parsedDelay > 0) {\n delay = parsedDelay;\n } else if (\n (animationDelays[0] === \"0s\" || animationDelays[0] === \"0ms\") &&\n delay === 0\n ) {\n // Both are 0, so keep 0\n delay = 0;\n }\n // Otherwise, keep getTiming().delay (for WAAPI animations)\n } else if (animationDelays.length > 1) {\n // Multiple animations: try to match by index\n const allAnimations = Array.from(target.getAnimations());\n const animationIndex = allAnimations.indexOf(animation);\n if (\n animationIndex >= 0 &&\n animationIndex < animationDelays.length &&\n animationDelays[animationIndex]\n ) {\n const parsedDelay = parseDelay(animationDelays[animationIndex]);\n if (parsedDelay > 0) {\n delay = parsedDelay;\n }\n // Otherwise, keep getTiming().delay\n }\n }\n // If no computed styles match, keep getTiming().delay (for WAAPI animations)\n }\n\n const iterations =\n Number(timing.iterations) || DEFAULT_ANIMATION_ITERATIONS;\n\n if (duration <= 0) {\n animation.currentTime = 0;\n continue;\n }\n\n // Use the element itself as the time source (it's guaranteed to be temporal)\n const currentTime = element.ownCurrentTimeMs ?? 0;\n\n // Special case for ef-text-segment: apply stagger offset for animation timing\n // This allows staggered animations while keeping visibility timing unchanged\n // We ADD the stagger offset to the delay, so animations start later for later segments\n let effectiveDelay = delay;\n if (\n element.tagName === \"EF-TEXT-SEGMENT\" &&\n (element as any).staggerOffsetMs !== undefined\n ) {\n const staggerOffsetMs = (element as any).staggerOffsetMs as number;\n effectiveDelay = delay + staggerOffsetMs;\n }\n\n // If before delay, show initial keyframe state (0% of animation)\n // Use strict < instead of <= so animations can start immediately when delay is reached\n if (currentTime < effectiveDelay) {\n // Set to 0 to show initial keyframe (animation time, not including delay)\n // When manually controlling animation.currentTime, 0 represents the start of the animation\n animation.currentTime = 0;\n continue;\n }\n\n const adjustedTime = currentTime - effectiveDelay;\n const currentIteration = Math.floor(adjustedTime / duration);\n let currentIterationTime = adjustedTime % duration;\n\n // Handle animation-direction\n const direction = timing.direction || \"normal\";\n const isAlternate =\n direction === \"alternate\" || direction === \"alternate-reverse\";\n const shouldReverse =\n direction === \"reverse\" ||\n (direction === \"alternate\" && currentIteration % 2 === 1) ||\n (direction === \"alternate-reverse\" && currentIteration % 2 === 0);\n\n if (shouldReverse) {\n currentIterationTime = duration - currentIterationTime;\n }\n\n if (currentIteration >= iterations) {\n // Animation would be complete - clamp to just before completion\n // This prevents the animation from being removed from the element\n // animation.currentTime is the time within the animation (not including delay)\n const maxSafeAnimationTime =\n duration * iterations - ANIMATION_PRECISION_OFFSET;\n\n // For alternate directions at completion, we need to set currentTime based on the final iteration\n // The final iteration for alternate is iteration (iterations - 1), which is forward if iterations is odd\n if (isAlternate) {\n const finalIteration = iterations - 1;\n const isFinalIterationReversed =\n (direction === \"alternate\" && finalIteration % 2 === 1) ||\n (direction === \"alternate-reverse\" && finalIteration % 2 === 0);\n if (isFinalIterationReversed) {\n // At end of reversed iteration, currentTime should be near 0 (but clamped)\n animation.currentTime = Math.min(\n duration - ANIMATION_PRECISION_OFFSET,\n maxSafeAnimationTime,\n );\n } else {\n // At end of forward iteration, currentTime should be near duration (but clamped)\n animation.currentTime = maxSafeAnimationTime;\n }\n } else {\n animation.currentTime = maxSafeAnimationTime;\n }\n } else {\n // Animation in progress\n // For alternate/alternate-reverse directions, currentTime should be set to the time within\n // the current iteration (after applying direction), not cumulative time.\n // However, when there's a delay, we need to use cumulative time (adjustedTime) instead.\n // For normal/reverse directions, currentTime is always cumulative time.\n if (isAlternate) {\n // For alternate directions without delay, use iteration time (after direction applied)\n // For alternate directions with delay:\n // - Iteration 0: use ownCurrentTimeMs (which equals adjustedTime + delay for iteration 0)\n // - Iteration 1+: use cumulative time (adjustedTime)\n if (effectiveDelay > 0) {\n if (currentIteration === 0) {\n // For iteration 0 with delay, use ownCurrentTimeMs (matches test expectations)\n animation.currentTime = currentTime;\n } else {\n // With delay and iteration > 0, use cumulative time\n const maxSafeAnimationTime =\n duration * iterations - ANIMATION_PRECISION_OFFSET;\n animation.currentTime = Math.min(\n adjustedTime,\n maxSafeAnimationTime,\n );\n }\n } else {\n // Without delay: use iteration time (after direction applied)\n animation.currentTime = currentIterationTime;\n }\n } else {\n // For normal/reverse directions, use cumulative time\n const timeWithinAnimation =\n currentIteration * duration + currentIterationTime;\n const maxSafeAnimationTime =\n duration * iterations - ANIMATION_PRECISION_OFFSET;\n animation.currentTime = Math.min(\n timeWithinAnimation,\n maxSafeAnimationTime,\n );\n }\n }\n }\n};\n\n/**\n * Main function: synchronizes DOM element with timeline\n */\nexport const updateAnimations = (element: AnimatableElement): void => {\n const temporalState = evaluateTemporalState(element);\n deepGetTemporalElements(element).forEach((temporalElement) => {\n const temporalState = evaluateTemporalState(temporalElement);\n updateVisualState(temporalElement, temporalState);\n });\n updateVisualState(element, temporalState);\n\n // Coordinate animations - use animation-specific visibility to prevent jumps at exact boundaries\n const animationState = evaluateTemporalStateForAnimation(element);\n if (animationState.isVisible) {\n coordinateAnimationsForSingleElement(element);\n }\n\n // Coordinate animations for child elements using animation-specific visibility\n deepGetTemporalElements(element).forEach((temporalElement) => {\n const childAnimationState =\n evaluateTemporalStateForAnimation(temporalElement);\n if (childAnimationState.isVisible) {\n coordinateAnimationsForSingleElement(temporalElement);\n }\n });\n};\n"],"mappings":";;;AASA,MAAM,6BAA6B;AACnC,MAAM,+BAA+B;AACrC,MAAM,oBAAoB;AAC1B,MAAM,oBAAoB;AAC1B,MAAM,+BAA+B;AACrC,MAAM,gCAAgC;;;;AActC,MAAa,yBACX,YACkB;CAElB,MAAM,kBAAkB,QAAQ,iBAAiB,SAAS;CAE1D,MAAM,WACJ,QAAQ,cAAc,IAClB,IACA,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,gBAAgB,QAAQ,WAAW,CAAC;CAK1E,MAAM,gBAAgB,CAAE,QAAgB;CACxC,MAAM,6BACJ,QAAQ,cAAc,QAAQ,eAAe;CAC/C,MAAM,gBAAgB,QAAQ,YAAY;CAC1C,MAAM,kBACJ,iBAAiB,8BAA8B;AAQjD,QAAO;EAAE;EAAU,WALjB,QAAQ,eAAe,mBACtB,kBACG,QAAQ,aAAa,iBACrB,QAAQ,YAAY;EAEI;EAAgB;;;;;;AAOhD,MAAa,qCACX,YACkB;CAElB,MAAM,kBAAkB,QAAQ,iBAAiB,SAAS;AAY1D,QAAO;EAAE,UATP,QAAQ,cAAc,IAClB,IACA,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,gBAAgB,QAAQ,WAAW,CAAC;EAOvD,WAHjB,QAAQ,eAAe,kBACvB,QAAQ,aAAa;EAEO;EAAgB;;;;;AAMhD,MAAM,qBACJ,SACA,UACS;AAET,SAAQ,MAAM,YAAY,mBAAmB,GAAG,MAAM,WAAW,IAAI,GAAG;AAGxE,KAAI,CAAC,MAAM,WAAW;AACpB,MAAI,QAAQ,MAAM,YAAY,OAC5B,SAAQ,MAAM,UAAU;AAE1B;;AAGF,KAAI,QAAQ,MAAM,YAAY,OAC5B,SAAQ,MAAM,UAAU;AAI1B,SAAQ,MAAM,YAAY,mBAAmB,GAAG,QAAQ,WAAW,IAAI;AACvE,SAAQ,MAAM,YACZ,8BACA,GAAG,QAAQ,iBAAiB,aAAa,EAAE,IAC5C;AACD,SAAQ,MAAM,YACZ,+BACA,GAAG,QAAQ,cAAc,QAAQ,iBAAiB,aAAa,GAAG,IACnE;;;;;AAMH,MAAM,wCACJ,YACS;CAGT,MAAM,aAAa,QAAQ,cAAc,EAAE,SAAS,MAAM,CAAC;AAE3D,MAAK,MAAM,aAAa,YAAY;AAGlC,MAAI,UAAU,cAAc,YAAY;AACtC,aAAU,QAAQ;AAElB,aAAU,MAAM;AAChB,aAAU,OAAO;aACR,UAAU,cAAc,UAEjC,WAAU,OAAO;EAGnB,MAAM,SAAS,UAAU;AACzB,MAAI,EAAE,UAAU,kBAAkB,gBAChC;EAGF,MAAM,SAAS,OAAO;AACtB,MAAI,CAAC,OACH;EAKF,MAAM,SAAS,OAAO,WAAW;EAGjC,MAAM,WAAW,OAAO,OAAO,SAAS,IAAI;EAC5C,IAAI,QAAQ,OAAO,OAAO,MAAM,IAAI;AAKpC,MAAI,kBAAkB,aAAa;GAEjC,MAAM,kBADgB,OAAO,iBAAiB,OAAO,CACf,eACnC,MAAM,KAAK,CACX,KAAK,MAAM,EAAE,MAAM,CAAC;GAGvB,MAAM,cAAc,aAA6B;AAC/C,QAAI,aAAa,QAAQ,aAAa,MACpC,QAAO;IAET,MAAM,aAAa,SAAS,MAAM,mBAAmB;AACrD,QAAI,aAAa,MAAM,WAAW,IAAI;KACpC,MAAM,QAAQ,OAAO,WAAW,WAAW,GAAG;AAE9C,YADa,WAAW,OACR,MAAM,QAAQ,MAAO;;AAEvC,WAAO;;AAOT,OAAI,gBAAgB,WAAW,KAAK,gBAAgB,IAAI;IACtD,MAAM,cAAc,WAAW,gBAAgB,GAAG;AAGlD,QAAI,cAAc,EAChB,SAAQ;cAEP,gBAAgB,OAAO,QAAQ,gBAAgB,OAAO,UACvD,UAAU,EAGV,SAAQ;cAGD,gBAAgB,SAAS,GAAG;IAGrC,MAAM,iBADgB,MAAM,KAAK,OAAO,eAAe,CAAC,CACnB,QAAQ,UAAU;AACvD,QACE,kBAAkB,KAClB,iBAAiB,gBAAgB,UACjC,gBAAgB,iBAChB;KACA,MAAM,cAAc,WAAW,gBAAgB,gBAAgB;AAC/D,SAAI,cAAc,EAChB,SAAQ;;;;EAQhB,MAAM,aACJ,OAAO,OAAO,WAAW,IAAI;AAE/B,MAAI,YAAY,GAAG;AACjB,aAAU,cAAc;AACxB;;EAIF,MAAM,cAAc,QAAQ,oBAAoB;EAKhD,IAAI,iBAAiB;AACrB,MACE,QAAQ,YAAY,qBACnB,QAAgB,oBAAoB,QACrC;GACA,MAAM,kBAAmB,QAAgB;AACzC,oBAAiB,QAAQ;;AAK3B,MAAI,cAAc,gBAAgB;AAGhC,aAAU,cAAc;AACxB;;EAGF,MAAM,eAAe,cAAc;EACnC,MAAM,mBAAmB,KAAK,MAAM,eAAe,SAAS;EAC5D,IAAI,uBAAuB,eAAe;EAG1C,MAAM,YAAY,OAAO,aAAa;EACtC,MAAM,cACJ,cAAc,eAAe,cAAc;AAM7C,MAJE,cAAc,aACb,cAAc,eAAe,mBAAmB,MAAM,KACtD,cAAc,uBAAuB,mBAAmB,MAAM,EAG/D,wBAAuB,WAAW;AAGpC,MAAI,oBAAoB,YAAY;GAIlC,MAAM,uBACJ,WAAW,aAAa;AAI1B,OAAI,aAAa;IACf,MAAM,iBAAiB,aAAa;AAIpC,QAFG,cAAc,eAAe,iBAAiB,MAAM,KACpD,cAAc,uBAAuB,iBAAiB,MAAM,EAG7D,WAAU,cAAc,KAAK,IAC3B,WAAW,4BACX,qBACD;QAGD,WAAU,cAAc;SAG1B,WAAU,cAAc;aAQtB,YAKF,KAAI,iBAAiB,EACnB,KAAI,qBAAqB,EAEvB,WAAU,cAAc;OACnB;GAEL,MAAM,uBACJ,WAAW,aAAa;AAC1B,aAAU,cAAc,KAAK,IAC3B,cACA,qBACD;;MAIH,WAAU,cAAc;OAErB;GAEL,MAAM,sBACJ,mBAAmB,WAAW;GAChC,MAAM,uBACJ,WAAW,aAAa;AAC1B,aAAU,cAAc,KAAK,IAC3B,qBACA,qBACD;;;;;;;AAST,MAAa,oBAAoB,YAAqC;CACpE,MAAM,gBAAgB,sBAAsB,QAAQ;AACpD,yBAAwB,QAAQ,CAAC,SAAS,oBAAoB;AAE5D,oBAAkB,iBADI,sBAAsB,gBAAgB,CACX;GACjD;AACF,mBAAkB,SAAS,cAAc;AAIzC,KADuB,kCAAkC,QAAQ,CAC9C,UACjB,sCAAqC,QAAQ;AAI/C,yBAAwB,QAAQ,CAAC,SAAS,oBAAoB;AAG5D,MADE,kCAAkC,gBAAgB,CAC5B,UACtB,sCAAqC,gBAAgB;GAEvD"}
|
package/dist/gui/ContextMixin.js
CHANGED
|
@@ -49,9 +49,18 @@ function ContextMixin(superClass) {
|
|
|
49
49
|
Object.assign(init.headers, { authorization: `Bearer ${urlToken}` });
|
|
50
50
|
} else init.credentials = "include";
|
|
51
51
|
try {
|
|
52
|
-
return fetch(url, init)
|
|
52
|
+
return fetch(url, init).catch((error) => {
|
|
53
|
+
console.error("ContextMixin fetch error", url, error, window.location.href);
|
|
54
|
+
const enhancedError = new (error instanceof Error ? error.constructor : Error)(`Failed to fetch: ${url}. Original error: ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
+
if (error instanceof Error) {
|
|
56
|
+
enhancedError.name = error.name;
|
|
57
|
+
enhancedError.stack = error.stack;
|
|
58
|
+
Object.assign(enhancedError, error);
|
|
59
|
+
}
|
|
60
|
+
throw enhancedError;
|
|
61
|
+
});
|
|
53
62
|
} catch (error) {
|
|
54
|
-
console.error("ContextMixin fetch error", url, error, window.location.href);
|
|
63
|
+
console.error("ContextMixin fetch error (synchronous)", url, error, window.location.href);
|
|
55
64
|
throw error;
|
|
56
65
|
}
|
|
57
66
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextMixin.js","names":["#getTokenCacheKey","#parseTokenExpiration","#apiHost","#targetTemporal","#onControllerUpdate","#controllerSubscribed","#targetTemporalProvider","#loop","#playingProvider","#loopProvider","#currentTimeMsProvider","#signingURL","#timegroupObserver"],"sources":["../../src/gui/ContextMixin.ts"],"sourcesContent":["import { ContextProvider, consume, createContext, provide } from \"@lit/context\";\nimport type { LitElement } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\nimport { EF_RENDERING } from \"../EF_RENDERING.ts\";\nimport {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../elements/EFTemporal.js\";\nimport { globalURLTokenDeduplicator } from \"../transcoding/cache/URLTokenDeduplicator.js\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport {\n type EFConfiguration,\n efConfigurationContext,\n} from \"./EFConfiguration.ts\";\nimport { efContext } from \"./efContext.js\";\nimport { fetchContext } from \"./fetchContext.js\";\nimport { type FocusContext, focusContext } from \"./focusContext.js\";\nimport { focusedElementContext } from \"./focusedElementContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\n\nexport const targetTemporalContext =\n createContext<TemporalMixinInterface | null>(Symbol(\"target-temporal\"));\n\nexport declare class ContextMixinInterface extends LitElement {\n signingURL?: string;\n apiHost?: string;\n rendering: boolean;\n playing: boolean;\n loop: boolean;\n currentTimeMs: number;\n focusedElement?: HTMLElement;\n targetTemporal: TemporalMixinInterface | null;\n play(): Promise<void>;\n pause(): void;\n}\n\nconst contextMixinSymbol = Symbol(\"contextMixin\");\n\nexport function isContextMixin(value: any): value is ContextMixinInterface {\n return (\n typeof value === \"object\" &&\n value !== null &&\n contextMixinSymbol in value.constructor\n );\n}\n\ntype Constructor<T = {}> = new (...args: any[]) => T;\nexport function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {\n class ContextElement extends superClass {\n static [contextMixinSymbol] = true;\n\n @consume({ context: efConfigurationContext, subscribe: true })\n efConfiguration: EFConfiguration | null = null;\n\n @provide({ context: focusContext })\n focusContext = this as FocusContext;\n\n @provide({ context: focusedElementContext })\n @state()\n focusedElement?: HTMLElement;\n\n #playingProvider!: ContextProvider<typeof playingContext>;\n #loopProvider!: ContextProvider<typeof loopContext>;\n #currentTimeMsProvider!: ContextProvider<typeof currentTimeContext>;\n #targetTemporalProvider!: ContextProvider<typeof targetTemporalContext>;\n\n #loop = false;\n\n #apiHost?: string;\n @property({ type: String, attribute: \"api-host\" })\n get apiHost() {\n return this.#apiHost ?? this.efConfiguration?.apiHost ?? \"\";\n }\n\n set apiHost(value: string) {\n this.#apiHost = value;\n }\n\n @provide({ context: efContext })\n efContext = this;\n\n #targetTemporal: TemporalMixinInterface | null = null;\n\n @state()\n get targetTemporal(): TemporalMixinInterface | null {\n return this.#targetTemporal;\n }\n #controllerSubscribed = false;\n\n /**\n * Find the first root temporal element (recursively searches through children)\n * Supports ef-timegroup, ef-video, ef-audio, and any other temporal elements\n * even when they're wrapped in non-temporal elements like divs\n */\n private findRootTemporal(): TemporalMixinInterface | null {\n const findRecursive = (\n element: Element,\n ): TemporalMixinInterface | null => {\n if (isEFTemporal(element)) {\n return element as TemporalMixinInterface & HTMLElement;\n }\n\n for (const child of element.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n };\n\n for (const child of this.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n }\n\n set targetTemporal(value: TemporalMixinInterface | null) {\n if (this.#targetTemporal === value) return;\n\n // Unsubscribe from old controller updates\n if (this.#targetTemporal?.playbackController) {\n this.#targetTemporal.playbackController.removeListener(\n this.#onControllerUpdate,\n );\n this.#controllerSubscribed = false;\n }\n\n this.#targetTemporal = value;\n this.#targetTemporalProvider?.setValue(value);\n\n // Sync all provided contexts\n this.requestUpdate(\"targetTemporal\");\n this.requestUpdate(\"playing\");\n this.requestUpdate(\"loop\");\n this.requestUpdate(\"currentTimeMs\");\n\n // If the new targetTemporal has a playbackController, apply stored loop value immediately\n if (value?.playbackController && this.#loop) {\n value.playbackController.setLoop(this.#loop);\n }\n\n // If the new targetTemporal doesn't have a playbackController yet,\n // wait for it to complete its updates (it might be initializing)\n if (value && !value.playbackController) {\n // Wait for the temporal element to initialize\n (value as any).updateComplete?.then(() => {\n if (value === this.#targetTemporal && !this.#controllerSubscribed) {\n this.requestUpdate();\n }\n });\n }\n }\n\n #onControllerUpdate = (\n event: import(\"./PlaybackController.js\").PlaybackControllerUpdateEvent,\n ) => {\n switch (event.property) {\n case \"playing\":\n this.#playingProvider.setValue(event.value as boolean);\n break;\n case \"loop\":\n this.#loopProvider.setValue(event.value as boolean);\n break;\n case \"currentTimeMs\":\n this.#currentTimeMsProvider.setValue(event.value as number);\n break;\n }\n };\n\n // Add reactive properties that depend on the targetTemporal\n @provide({ context: durationContext })\n @property({ type: Number })\n durationMs = 0;\n\n @property({ type: Number })\n endTimeMs = 0;\n\n @provide({ context: fetchContext })\n fetch = async (url: string, init: RequestInit = {}) => {\n init.headers ||= {};\n Object.assign(init.headers, {\n \"Content-Type\": \"application/json\",\n });\n\n if (!EF_RENDERING() && this.signingURL) {\n const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);\n\n // Use global token deduplicator to share tokens across all context providers\n const urlToken = await globalURLTokenDeduplicator.getToken(\n cacheKey,\n async () => {\n try {\n const response = await fetch(this.signingURL, {\n method: \"POST\",\n body: JSON.stringify(signingPayload),\n });\n\n if (response.ok) {\n const tokenData = await response.json();\n return tokenData.token;\n }\n throw new Error(\n `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,\n );\n } catch (error) {\n console.error(\"ContextMixin urlToken fetch error\", url, error);\n throw error;\n }\n },\n (token: string) => this.#parseTokenExpiration(token),\n );\n\n Object.assign(init.headers, {\n authorization: `Bearer ${urlToken}`,\n });\n } else {\n init.credentials = \"include\";\n }\n\n try {\n return fetch(url, init);\n } catch (error) {\n console.error(\n \"ContextMixin fetch error\",\n url,\n error,\n window.location.href,\n );\n throw error;\n }\n };\n\n // Note: URL token caching is now handled globally via URLTokenDeduplicator\n // Keeping these for any potential backwards compatibility, but they're no longer used\n\n /**\n * Generate a cache key for URL token based on signing strategy\n *\n * Uses unified prefix + parameter matching approach:\n * - For transcode URLs: signs base \"/api/v1/transcode\" + params like {url: \"source.mp4\"}\n * - For regular URLs: signs full URL with empty params {}\n * - All validation uses prefix matching + exhaustive parameter matching\n * - Multiple transcode segments with same source share one token (reduces round-trips)\n */\n #getTokenCacheKey(url: string): {\n cacheKey: string;\n signingPayload: { url: string; params?: Record<string, string> };\n } {\n try {\n const urlObj = new URL(url);\n\n // Check if this is a transcode URL pattern\n if (urlObj.pathname.includes(\"/api/v1/transcode/\")) {\n const urlParam = urlObj.searchParams.get(\"url\");\n if (urlParam) {\n // For transcode URLs, sign the base path + url parameter\n const basePath = `${urlObj.origin}/api/v1/transcode`;\n const cacheKey = `${basePath}?url=${urlParam}`;\n return {\n cacheKey,\n signingPayload: { url: basePath, params: { url: urlParam } },\n };\n }\n }\n\n // For non-transcode URLs, use full URL (existing behavior)\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n } catch {\n // If URL parsing fails, fall back to full URL\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n }\n }\n\n /**\n * Parse JWT token to extract safe expiration time (with buffer)\n * @param token JWT token string\n * @returns Safe expiration timestamp in milliseconds (actual expiry minus buffer), or 0 if parsing fails\n */\n #parseTokenExpiration(token: string): number {\n try {\n // JWT has 3 parts separated by dots: header.payload.signature\n const parts = token.split(\".\");\n if (parts.length !== 3) return 0;\n\n // Decode the payload (second part)\n const payload = parts[1];\n if (!payload) return 0;\n\n const decoded = atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n const parsed = JSON.parse(decoded);\n\n // Extract timestamps (in seconds)\n const exp = parsed.exp;\n const iat = parsed.iat;\n if (!exp) return 0;\n\n // Calculate token lifetime and buffer\n const lifetimeSeconds = iat ? exp - iat : 3600; // Default to 1 hour if no iat\n const tenPercentBufferMs = lifetimeSeconds * 0.1 * 1000; // 10% of lifetime in ms\n const fiveMinutesMs = 5 * 60 * 1000; // 5 minutes in ms\n\n // Use whichever buffer is smaller (more conservative)\n const bufferMs = Math.min(fiveMinutesMs, tenPercentBufferMs);\n\n // Return expiration time minus buffer\n return exp * 1000 - bufferMs;\n } catch {\n return 0;\n }\n }\n\n #signingURL?: string;\n /**\n * A URL that will be used to generated signed tokens for accessing media files from the\n * editframe API. This is used to authenticate media requests per-user.\n */\n @property({ type: String, attribute: \"signing-url\" })\n get signingURL() {\n return this.#signingURL ?? this.efConfiguration?.signingURL ?? \"\";\n }\n set signingURL(value: string) {\n this.#signingURL = value;\n }\n\n @property({ type: Boolean, reflect: true })\n get playing(): boolean {\n return this.targetTemporal?.playbackController?.playing ?? false;\n }\n set playing(value: boolean) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setPlaying(value);\n }\n }\n\n @property({ type: Boolean, reflect: true, attribute: \"loop\" })\n get loop(): boolean {\n return this.targetTemporal?.playbackController?.loop ?? this.#loop;\n }\n set loop(value: boolean) {\n const oldValue = this.#loop;\n this.#loop = value;\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setLoop(value);\n }\n this.requestUpdate(\"loop\", oldValue);\n }\n\n @property({ type: Boolean })\n rendering = false;\n\n @property({ type: Number })\n get currentTimeMs(): number {\n return (\n this.targetTemporal?.playbackController?.currentTimeMs ?? Number.NaN\n );\n }\n set currentTimeMs(value: number) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setCurrentTimeMs(value);\n }\n }\n\n #timegroupObserver = new MutationObserver((mutations) => {\n let shouldUpdate = false;\n\n for (const mutation of mutations) {\n if (mutation.type === \"childList\") {\n const newTemporal = this.findRootTemporal();\n if (newTemporal !== this.targetTemporal) {\n this.targetTemporal = newTemporal;\n shouldUpdate = true;\n } else if (\n mutation.target instanceof Element &&\n isEFTemporal(mutation.target)\n ) {\n // Handle childList changes within existing temporal elements\n shouldUpdate = true;\n }\n } else if (mutation.type === \"attributes\") {\n // Watch for attribute changes that might affect duration\n const durationAffectingAttributes = [\n \"duration\",\n \"mode\",\n \"trimstart\",\n \"trimend\",\n \"sourcein\",\n \"sourceout\",\n ];\n\n if (\n durationAffectingAttributes.includes(\n mutation.attributeName || \"\",\n ) ||\n (mutation.target instanceof Element &&\n isEFTemporal(mutation.target))\n ) {\n shouldUpdate = true;\n }\n }\n }\n\n if (shouldUpdate) {\n // Trigger an update to ensure reactive properties recalculate\n // Use a microtask to ensure DOM updates are complete\n queueMicrotask(() => {\n // Recalculate duration and endTime when temporal element changes\n this.updateDurationProperties();\n this.requestUpdate();\n // Also ensure the targetTemporal updates its computed properties\n if (this.targetTemporal) {\n (this.targetTemporal as any).requestUpdate();\n }\n });\n }\n });\n\n /**\n * Update duration properties when temporal element changes\n */\n updateDurationProperties(): void {\n const newDuration = this.targetTemporal?.durationMs ?? 0;\n const newEndTime = this.targetTemporal?.endTimeMs ?? 0;\n\n if (this.durationMs !== newDuration) {\n this.durationMs = newDuration;\n }\n\n if (this.endTimeMs !== newEndTime) {\n this.endTimeMs = newEndTime;\n }\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Create manual context providers for playback state\n this.#playingProvider = new ContextProvider(this, {\n context: playingContext,\n initialValue: this.playing,\n });\n this.#loopProvider = new ContextProvider(this, {\n context: loopContext,\n initialValue: this.loop,\n });\n this.#currentTimeMsProvider = new ContextProvider(this, {\n context: currentTimeContext,\n initialValue: this.currentTimeMs,\n });\n this.#targetTemporalProvider = new ContextProvider(this, {\n context: targetTemporalContext,\n initialValue: this.targetTemporal,\n });\n\n // Initialize targetTemporal to first root temporal element\n this.targetTemporal = this.findRootTemporal();\n // Initialize duration properties\n this.updateDurationProperties();\n\n this.#timegroupObserver.observe(this, {\n childList: true,\n subtree: true,\n attributes: true,\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#timegroupObserver.disconnect();\n\n // Unsubscribe from controller\n if (this.#targetTemporal?.playbackController) {\n this.#targetTemporal.playbackController.removeListener(\n this.#onControllerUpdate,\n );\n this.#controllerSubscribed = false;\n }\n\n this.pause();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>) {\n super.updated?.(changedProperties);\n\n // Subscribe to controller when it becomes available\n if (\n !this.#controllerSubscribed &&\n this.#targetTemporal?.playbackController\n ) {\n this.#targetTemporal.playbackController.addListener(\n this.#onControllerUpdate,\n );\n this.#controllerSubscribed = true;\n\n // Apply stored loop value when playbackController becomes available\n if (this.#loop) {\n this.#targetTemporal.playbackController.setLoop(this.#loop);\n }\n\n // Trigger initial sync of context providers\n this.#playingProvider.setValue(this.playing);\n this.#loopProvider.setValue(this.loop);\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n }\n }\n\n async play() {\n // If targetTemporal is not set, try to find it now\n // This handles cases where the DOM may not have been fully ready during connectedCallback\n if (!this.targetTemporal) {\n // Wait for any temporal custom elements to be defined\n const potentialTemporalTags = Array.from(this.children)\n .map((el) => el.tagName.toLowerCase())\n .filter((tag) => tag.startsWith(\"ef-\"));\n\n await Promise.all(\n potentialTemporalTags.map((tag) =>\n customElements.whenDefined(tag).catch(() => {}),\n ),\n );\n\n const foundTemporal = this.findRootTemporal();\n if (foundTemporal) {\n this.targetTemporal = foundTemporal;\n // Wait for it to initialize\n await (foundTemporal as any).updateComplete;\n } else {\n console.warn(\"No temporal element found to play\");\n return;\n }\n }\n\n // If playbackController doesn't exist yet, wait for it\n if (!this.targetTemporal.playbackController) {\n await (this.targetTemporal as any).updateComplete;\n // After waiting, check again\n if (!this.targetTemporal.playbackController) {\n console.warn(\"PlaybackController not available for temporal element\");\n return;\n }\n }\n\n this.targetTemporal.playbackController.play();\n }\n\n pause() {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.pause();\n }\n }\n }\n\n return ContextElement as Constructor<ContextMixinInterface> & T;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ContextMixin.js","names":["#getTokenCacheKey","#parseTokenExpiration","#apiHost","#targetTemporal","#onControllerUpdate","#controllerSubscribed","#targetTemporalProvider","#loop","#playingProvider","#loopProvider","#currentTimeMsProvider","#signingURL","#timegroupObserver"],"sources":["../../src/gui/ContextMixin.ts"],"sourcesContent":["import { ContextProvider, consume, createContext, provide } from \"@lit/context\";\nimport type { LitElement } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\nimport { EF_RENDERING } from \"../EF_RENDERING.ts\";\nimport {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../elements/EFTemporal.js\";\nimport { globalURLTokenDeduplicator } from \"../transcoding/cache/URLTokenDeduplicator.js\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport {\n type EFConfiguration,\n efConfigurationContext,\n} from \"./EFConfiguration.ts\";\nimport { efContext } from \"./efContext.js\";\nimport { fetchContext } from \"./fetchContext.js\";\nimport { type FocusContext, focusContext } from \"./focusContext.js\";\nimport { focusedElementContext } from \"./focusedElementContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\n\nexport const targetTemporalContext =\n createContext<TemporalMixinInterface | null>(Symbol(\"target-temporal\"));\n\nexport declare class ContextMixinInterface extends LitElement {\n signingURL?: string;\n apiHost?: string;\n rendering: boolean;\n playing: boolean;\n loop: boolean;\n currentTimeMs: number;\n focusedElement?: HTMLElement;\n targetTemporal: TemporalMixinInterface | null;\n play(): Promise<void>;\n pause(): void;\n}\n\nconst contextMixinSymbol = Symbol(\"contextMixin\");\n\nexport function isContextMixin(value: any): value is ContextMixinInterface {\n return (\n typeof value === \"object\" &&\n value !== null &&\n contextMixinSymbol in value.constructor\n );\n}\n\ntype Constructor<T = {}> = new (...args: any[]) => T;\nexport function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {\n class ContextElement extends superClass {\n static [contextMixinSymbol] = true;\n\n @consume({ context: efConfigurationContext, subscribe: true })\n efConfiguration: EFConfiguration | null = null;\n\n @provide({ context: focusContext })\n focusContext = this as FocusContext;\n\n @provide({ context: focusedElementContext })\n @state()\n focusedElement?: HTMLElement;\n\n #playingProvider!: ContextProvider<typeof playingContext>;\n #loopProvider!: ContextProvider<typeof loopContext>;\n #currentTimeMsProvider!: ContextProvider<typeof currentTimeContext>;\n #targetTemporalProvider!: ContextProvider<typeof targetTemporalContext>;\n\n #loop = false;\n\n #apiHost?: string;\n @property({ type: String, attribute: \"api-host\" })\n get apiHost() {\n return this.#apiHost ?? this.efConfiguration?.apiHost ?? \"\";\n }\n\n set apiHost(value: string) {\n this.#apiHost = value;\n }\n\n @provide({ context: efContext })\n efContext = this;\n\n #targetTemporal: TemporalMixinInterface | null = null;\n\n @state()\n get targetTemporal(): TemporalMixinInterface | null {\n return this.#targetTemporal;\n }\n #controllerSubscribed = false;\n\n /**\n * Find the first root temporal element (recursively searches through children)\n * Supports ef-timegroup, ef-video, ef-audio, and any other temporal elements\n * even when they're wrapped in non-temporal elements like divs\n */\n private findRootTemporal(): TemporalMixinInterface | null {\n const findRecursive = (\n element: Element,\n ): TemporalMixinInterface | null => {\n if (isEFTemporal(element)) {\n return element as TemporalMixinInterface & HTMLElement;\n }\n\n for (const child of element.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n };\n\n for (const child of this.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n }\n\n set targetTemporal(value: TemporalMixinInterface | null) {\n if (this.#targetTemporal === value) return;\n\n // Unsubscribe from old controller updates\n if (this.#targetTemporal?.playbackController) {\n this.#targetTemporal.playbackController.removeListener(\n this.#onControllerUpdate,\n );\n this.#controllerSubscribed = false;\n }\n\n this.#targetTemporal = value;\n this.#targetTemporalProvider?.setValue(value);\n\n // Sync all provided contexts\n this.requestUpdate(\"targetTemporal\");\n this.requestUpdate(\"playing\");\n this.requestUpdate(\"loop\");\n this.requestUpdate(\"currentTimeMs\");\n\n // If the new targetTemporal has a playbackController, apply stored loop value immediately\n if (value?.playbackController && this.#loop) {\n value.playbackController.setLoop(this.#loop);\n }\n\n // If the new targetTemporal doesn't have a playbackController yet,\n // wait for it to complete its updates (it might be initializing)\n if (value && !value.playbackController) {\n // Wait for the temporal element to initialize\n (value as any).updateComplete?.then(() => {\n if (value === this.#targetTemporal && !this.#controllerSubscribed) {\n this.requestUpdate();\n }\n });\n }\n }\n\n #onControllerUpdate = (\n event: import(\"./PlaybackController.js\").PlaybackControllerUpdateEvent,\n ) => {\n switch (event.property) {\n case \"playing\":\n this.#playingProvider.setValue(event.value as boolean);\n break;\n case \"loop\":\n this.#loopProvider.setValue(event.value as boolean);\n break;\n case \"currentTimeMs\":\n this.#currentTimeMsProvider.setValue(event.value as number);\n break;\n }\n };\n\n // Add reactive properties that depend on the targetTemporal\n @provide({ context: durationContext })\n @property({ type: Number })\n durationMs = 0;\n\n @property({ type: Number })\n endTimeMs = 0;\n\n @provide({ context: fetchContext })\n fetch = async (url: string, init: RequestInit = {}) => {\n init.headers ||= {};\n Object.assign(init.headers, {\n \"Content-Type\": \"application/json\",\n });\n\n if (!EF_RENDERING() && this.signingURL) {\n const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);\n\n // Use global token deduplicator to share tokens across all context providers\n const urlToken = await globalURLTokenDeduplicator.getToken(\n cacheKey,\n async () => {\n try {\n const response = await fetch(this.signingURL, {\n method: \"POST\",\n body: JSON.stringify(signingPayload),\n });\n\n if (response.ok) {\n const tokenData = await response.json();\n return tokenData.token;\n }\n throw new Error(\n `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,\n );\n } catch (error) {\n console.error(\"ContextMixin urlToken fetch error\", url, error);\n throw error;\n }\n },\n (token: string) => this.#parseTokenExpiration(token),\n );\n\n Object.assign(init.headers, {\n authorization: `Bearer ${urlToken}`,\n });\n } else {\n init.credentials = \"include\";\n }\n\n try {\n const fetchPromise = fetch(url, init);\n // Wrap the promise to catch rejections and log the URL\n // Return the promise chain so errors are logged but still propagate\n return fetchPromise.catch((error) => {\n console.error(\n \"ContextMixin fetch error\",\n url,\n error,\n window.location.href,\n );\n // Create a new error with the URL in the message, preserving the original error type\n const ErrorConstructor =\n error instanceof Error ? error.constructor : Error;\n const enhancedError = new (ErrorConstructor as typeof Error)(\n `Failed to fetch: ${url}. Original error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Preserve the original error's properties\n if (error instanceof Error) {\n enhancedError.name = error.name;\n enhancedError.stack = error.stack;\n // Copy any additional properties from the original error\n Object.assign(enhancedError, error);\n }\n throw enhancedError;\n });\n } catch (error) {\n console.error(\n \"ContextMixin fetch error (synchronous)\",\n url,\n error,\n window.location.href,\n );\n throw error;\n }\n };\n\n // Note: URL token caching is now handled globally via URLTokenDeduplicator\n // Keeping these for any potential backwards compatibility, but they're no longer used\n\n /**\n * Generate a cache key for URL token based on signing strategy\n *\n * Uses unified prefix + parameter matching approach:\n * - For transcode URLs: signs base \"/api/v1/transcode\" + params like {url: \"source.mp4\"}\n * - For regular URLs: signs full URL with empty params {}\n * - All validation uses prefix matching + exhaustive parameter matching\n * - Multiple transcode segments with same source share one token (reduces round-trips)\n */\n #getTokenCacheKey(url: string): {\n cacheKey: string;\n signingPayload: { url: string; params?: Record<string, string> };\n } {\n try {\n const urlObj = new URL(url);\n\n // Check if this is a transcode URL pattern\n if (urlObj.pathname.includes(\"/api/v1/transcode/\")) {\n const urlParam = urlObj.searchParams.get(\"url\");\n if (urlParam) {\n // For transcode URLs, sign the base path + url parameter\n const basePath = `${urlObj.origin}/api/v1/transcode`;\n const cacheKey = `${basePath}?url=${urlParam}`;\n return {\n cacheKey,\n signingPayload: { url: basePath, params: { url: urlParam } },\n };\n }\n }\n\n // For non-transcode URLs, use full URL (existing behavior)\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n } catch {\n // If URL parsing fails, fall back to full URL\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n }\n }\n\n /**\n * Parse JWT token to extract safe expiration time (with buffer)\n * @param token JWT token string\n * @returns Safe expiration timestamp in milliseconds (actual expiry minus buffer), or 0 if parsing fails\n */\n #parseTokenExpiration(token: string): number {\n try {\n // JWT has 3 parts separated by dots: header.payload.signature\n const parts = token.split(\".\");\n if (parts.length !== 3) return 0;\n\n // Decode the payload (second part)\n const payload = parts[1];\n if (!payload) return 0;\n\n const decoded = atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n const parsed = JSON.parse(decoded);\n\n // Extract timestamps (in seconds)\n const exp = parsed.exp;\n const iat = parsed.iat;\n if (!exp) return 0;\n\n // Calculate token lifetime and buffer\n const lifetimeSeconds = iat ? exp - iat : 3600; // Default to 1 hour if no iat\n const tenPercentBufferMs = lifetimeSeconds * 0.1 * 1000; // 10% of lifetime in ms\n const fiveMinutesMs = 5 * 60 * 1000; // 5 minutes in ms\n\n // Use whichever buffer is smaller (more conservative)\n const bufferMs = Math.min(fiveMinutesMs, tenPercentBufferMs);\n\n // Return expiration time minus buffer\n return exp * 1000 - bufferMs;\n } catch {\n return 0;\n }\n }\n\n #signingURL?: string;\n /**\n * A URL that will be used to generated signed tokens for accessing media files from the\n * editframe API. This is used to authenticate media requests per-user.\n */\n @property({ type: String, attribute: \"signing-url\" })\n get signingURL() {\n return this.#signingURL ?? this.efConfiguration?.signingURL ?? \"\";\n }\n set signingURL(value: string) {\n this.#signingURL = value;\n }\n\n @property({ type: Boolean, reflect: true })\n get playing(): boolean {\n return this.targetTemporal?.playbackController?.playing ?? false;\n }\n set playing(value: boolean) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setPlaying(value);\n }\n }\n\n @property({ type: Boolean, reflect: true, attribute: \"loop\" })\n get loop(): boolean {\n return this.targetTemporal?.playbackController?.loop ?? this.#loop;\n }\n set loop(value: boolean) {\n const oldValue = this.#loop;\n this.#loop = value;\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setLoop(value);\n }\n this.requestUpdate(\"loop\", oldValue);\n }\n\n @property({ type: Boolean })\n rendering = false;\n\n @property({ type: Number })\n get currentTimeMs(): number {\n return (\n this.targetTemporal?.playbackController?.currentTimeMs ?? Number.NaN\n );\n }\n set currentTimeMs(value: number) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setCurrentTimeMs(value);\n }\n }\n\n #timegroupObserver = new MutationObserver((mutations) => {\n let shouldUpdate = false;\n\n for (const mutation of mutations) {\n if (mutation.type === \"childList\") {\n const newTemporal = this.findRootTemporal();\n if (newTemporal !== this.targetTemporal) {\n this.targetTemporal = newTemporal;\n shouldUpdate = true;\n } else if (\n mutation.target instanceof Element &&\n isEFTemporal(mutation.target)\n ) {\n // Handle childList changes within existing temporal elements\n shouldUpdate = true;\n }\n } else if (mutation.type === \"attributes\") {\n // Watch for attribute changes that might affect duration\n const durationAffectingAttributes = [\n \"duration\",\n \"mode\",\n \"trimstart\",\n \"trimend\",\n \"sourcein\",\n \"sourceout\",\n ];\n\n if (\n durationAffectingAttributes.includes(\n mutation.attributeName || \"\",\n ) ||\n (mutation.target instanceof Element &&\n isEFTemporal(mutation.target))\n ) {\n shouldUpdate = true;\n }\n }\n }\n\n if (shouldUpdate) {\n // Trigger an update to ensure reactive properties recalculate\n // Use a microtask to ensure DOM updates are complete\n queueMicrotask(() => {\n // Recalculate duration and endTime when temporal element changes\n this.updateDurationProperties();\n this.requestUpdate();\n // Also ensure the targetTemporal updates its computed properties\n if (this.targetTemporal) {\n (this.targetTemporal as any).requestUpdate();\n }\n });\n }\n });\n\n /**\n * Update duration properties when temporal element changes\n */\n updateDurationProperties(): void {\n const newDuration = this.targetTemporal?.durationMs ?? 0;\n const newEndTime = this.targetTemporal?.endTimeMs ?? 0;\n\n if (this.durationMs !== newDuration) {\n this.durationMs = newDuration;\n }\n\n if (this.endTimeMs !== newEndTime) {\n this.endTimeMs = newEndTime;\n }\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Create manual context providers for playback state\n this.#playingProvider = new ContextProvider(this, {\n context: playingContext,\n initialValue: this.playing,\n });\n this.#loopProvider = new ContextProvider(this, {\n context: loopContext,\n initialValue: this.loop,\n });\n this.#currentTimeMsProvider = new ContextProvider(this, {\n context: currentTimeContext,\n initialValue: this.currentTimeMs,\n });\n this.#targetTemporalProvider = new ContextProvider(this, {\n context: targetTemporalContext,\n initialValue: this.targetTemporal,\n });\n\n // Initialize targetTemporal to first root temporal element\n this.targetTemporal = this.findRootTemporal();\n // Initialize duration properties\n this.updateDurationProperties();\n\n this.#timegroupObserver.observe(this, {\n childList: true,\n subtree: true,\n attributes: true,\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#timegroupObserver.disconnect();\n\n // Unsubscribe from controller\n if (this.#targetTemporal?.playbackController) {\n this.#targetTemporal.playbackController.removeListener(\n this.#onControllerUpdate,\n );\n this.#controllerSubscribed = false;\n }\n\n this.pause();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>) {\n super.updated?.(changedProperties);\n\n // Subscribe to controller when it becomes available\n if (\n !this.#controllerSubscribed &&\n this.#targetTemporal?.playbackController\n ) {\n this.#targetTemporal.playbackController.addListener(\n this.#onControllerUpdate,\n );\n this.#controllerSubscribed = true;\n\n // Apply stored loop value when playbackController becomes available\n if (this.#loop) {\n this.#targetTemporal.playbackController.setLoop(this.#loop);\n }\n\n // Trigger initial sync of context providers\n this.#playingProvider.setValue(this.playing);\n this.#loopProvider.setValue(this.loop);\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n }\n }\n\n async play() {\n // If targetTemporal is not set, try to find it now\n // This handles cases where the DOM may not have been fully ready during connectedCallback\n if (!this.targetTemporal) {\n // Wait for any temporal custom elements to be defined\n const potentialTemporalTags = Array.from(this.children)\n .map((el) => el.tagName.toLowerCase())\n .filter((tag) => tag.startsWith(\"ef-\"));\n\n await Promise.all(\n potentialTemporalTags.map((tag) =>\n customElements.whenDefined(tag).catch(() => {}),\n ),\n );\n\n const foundTemporal = this.findRootTemporal();\n if (foundTemporal) {\n this.targetTemporal = foundTemporal;\n // Wait for it to initialize\n await (foundTemporal as any).updateComplete;\n } else {\n console.warn(\"No temporal element found to play\");\n return;\n }\n }\n\n // If playbackController doesn't exist yet, wait for it\n if (!this.targetTemporal.playbackController) {\n await (this.targetTemporal as any).updateComplete;\n // After waiting, check again\n if (!this.targetTemporal.playbackController) {\n console.warn(\"PlaybackController not available for temporal element\");\n return;\n }\n }\n\n this.targetTemporal.playbackController.play();\n }\n\n pause() {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.pause();\n }\n }\n }\n\n return ContextElement as Constructor<ContextMixinInterface> & T;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAqBA,MAAa,wBACX,cAA6C,OAAO,kBAAkB,CAAC;AAezE,MAAM,qBAAqB,OAAO,eAAe;AAEjD,SAAgB,eAAe,OAA4C;AACzE,QACE,OAAO,UAAU,YACjB,UAAU,QACV,sBAAsB,MAAM;;AAKhC,SAAgB,aAAgD,YAAe;CAC7E,MAAM,uBAAuB,WAAW;;;0BAII;uBAG3B;oBAwBH;qBA+FC;oBAGD;gBAGJ,OAAO,KAAa,OAAoB,EAAE,KAAK;AACrD,SAAK,YAAY,EAAE;AACnB,WAAO,OAAO,KAAK,SAAS,EAC1B,gBAAgB,oBACjB,CAAC;AAEF,QAAI,CAAC,cAAc,IAAI,KAAK,YAAY;KACtC,MAAM,EAAE,UAAU,mBAAmB,MAAKA,iBAAkB,IAAI;KAGhE,MAAM,WAAW,MAAM,2BAA2B,SAChD,UACA,YAAY;AACV,UAAI;OACF,MAAM,WAAW,MAAM,MAAM,KAAK,YAAY;QAC5C,QAAQ;QACR,MAAM,KAAK,UAAU,eAAe;QACrC,CAAC;AAEF,WAAI,SAAS,GAEX,SADkB,MAAM,SAAS,MAAM,EACtB;AAEnB,aAAM,IAAI,MACR,uBAAuB,IAAI,gBAAgB,KAAK,WAAW,GAAG,SAAS,OAAO,GAAG,SAAS,aAC3F;eACM,OAAO;AACd,eAAQ,MAAM,qCAAqC,KAAK,MAAM;AAC9D,aAAM;;SAGT,UAAkB,MAAKC,qBAAsB,MAAM,CACrD;AAED,YAAO,OAAO,KAAK,SAAS,EAC1B,eAAe,UAAU,YAC1B,CAAC;UAEF,MAAK,cAAc;AAGrB,QAAI;AAIF,YAHqB,MAAM,KAAK,KAAK,CAGjB,OAAO,UAAU;AACnC,cAAQ,MACN,4BACA,KACA,OACA,OAAO,SAAS,KACjB;MAID,MAAM,gBAAgB,KADpB,iBAAiB,QAAQ,MAAM,cAAc,OAE7C,oBAAoB,IAAI,oBAAoB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,GACnG;AAED,UAAI,iBAAiB,OAAO;AAC1B,qBAAc,OAAO,MAAM;AAC3B,qBAAc,QAAQ,MAAM;AAE5B,cAAO,OAAO,eAAe,MAAM;;AAErC,YAAM;OACN;aACK,OAAO;AACd,aAAQ,MACN,0CACA,KACA,OACA,OAAO,SAAS,KACjB;AACD,WAAM;;;oBA8HE;;;QA3UJ,sBAAsB;;EAY9B;EACA;EACA;EACA;EAEA,QAAQ;EAER;EACA,IACI,UAAU;AACZ,UAAO,MAAKC,WAAY,KAAK,iBAAiB,WAAW;;EAG3D,IAAI,QAAQ,OAAe;AACzB,SAAKA,UAAW;;EAMlB,kBAAiD;EAEjD,IACI,iBAAgD;AAClD,UAAO,MAAKC;;EAEd,wBAAwB;;;;;;EAOxB,AAAQ,mBAAkD;GACxD,MAAM,iBACJ,YACkC;AAClC,QAAI,aAAa,QAAQ,CACvB,QAAO;AAGT,SAAK,MAAM,SAAS,QAAQ,UAAU;KACpC,MAAM,QAAQ,cAAc,MAAM;AAClC,SAAI,MAAO,QAAO;;AAGpB,WAAO;;AAGT,QAAK,MAAM,SAAS,KAAK,UAAU;IACjC,MAAM,QAAQ,cAAc,MAAM;AAClC,QAAI,MAAO,QAAO;;AAGpB,UAAO;;EAGT,IAAI,eAAe,OAAsC;AACvD,OAAI,MAAKA,mBAAoB,MAAO;AAGpC,OAAI,MAAKA,gBAAiB,oBAAoB;AAC5C,UAAKA,eAAgB,mBAAmB,eACtC,MAAKC,mBACN;AACD,UAAKC,uBAAwB;;AAG/B,SAAKF,iBAAkB;AACvB,SAAKG,wBAAyB,SAAS,MAAM;AAG7C,QAAK,cAAc,iBAAiB;AACpC,QAAK,cAAc,UAAU;AAC7B,QAAK,cAAc,OAAO;AAC1B,QAAK,cAAc,gBAAgB;AAGnC,OAAI,OAAO,sBAAsB,MAAKC,KACpC,OAAM,mBAAmB,QAAQ,MAAKA,KAAM;AAK9C,OAAI,SAAS,CAAC,MAAM,mBAElB,CAAC,MAAc,gBAAgB,WAAW;AACxC,QAAI,UAAU,MAAKJ,kBAAmB,CAAC,MAAKE,qBAC1C,MAAK,eAAe;KAEtB;;EAIN,uBACE,UACG;AACH,WAAQ,MAAM,UAAd;IACE,KAAK;AACH,WAAKG,gBAAiB,SAAS,MAAM,MAAiB;AACtD;IACF,KAAK;AACH,WAAKC,aAAc,SAAS,MAAM,MAAiB;AACnD;IACF,KAAK;AACH,WAAKC,sBAAuB,SAAS,MAAM,MAAgB;AAC3D;;;;;;;;;;;;EAuGN,kBAAkB,KAGhB;AACA,OAAI;IACF,MAAM,SAAS,IAAI,IAAI,IAAI;AAG3B,QAAI,OAAO,SAAS,SAAS,qBAAqB,EAAE;KAClD,MAAM,WAAW,OAAO,aAAa,IAAI,MAAM;AAC/C,SAAI,UAAU;MAEZ,MAAM,WAAW,GAAG,OAAO,OAAO;AAElC,aAAO;OACL,UAFe,GAAG,SAAS,OAAO;OAGlC,gBAAgB;QAAE,KAAK;QAAU,QAAQ,EAAE,KAAK,UAAU;QAAE;OAC7D;;;AAKL,WAAO;KACL,UAAU;KACV,gBAAgB,EAAE,KAAK;KACxB;WACK;AAEN,WAAO;KACL,UAAU;KACV,gBAAgB,EAAE,KAAK;KACxB;;;;;;;;EASL,sBAAsB,OAAuB;AAC3C,OAAI;IAEF,MAAM,QAAQ,MAAM,MAAM,IAAI;AAC9B,QAAI,MAAM,WAAW,EAAG,QAAO;IAG/B,MAAM,UAAU,MAAM;AACtB,QAAI,CAAC,QAAS,QAAO;IAErB,MAAM,UAAU,KAAK,QAAQ,QAAQ,MAAM,IAAI,CAAC,QAAQ,MAAM,IAAI,CAAC;IACnE,MAAM,SAAS,KAAK,MAAM,QAAQ;IAGlC,MAAM,MAAM,OAAO;IACnB,MAAM,MAAM,OAAO;AACnB,QAAI,CAAC,IAAK,QAAO;IAIjB,MAAM,sBADkB,MAAM,MAAM,MAAM,QACG,KAAM;IAInD,MAAM,WAAW,KAAK,IAHA,MAAS,KAGU,mBAAmB;AAG5D,WAAO,MAAM,MAAO;WACd;AACN,WAAO;;;EAIX;;;;;EAKA,IACI,aAAa;AACf,UAAO,MAAKC,cAAe,KAAK,iBAAiB,cAAc;;EAEjE,IAAI,WAAW,OAAe;AAC5B,SAAKA,aAAc;;EAGrB,IACI,UAAmB;AACrB,UAAO,KAAK,gBAAgB,oBAAoB,WAAW;;EAE7D,IAAI,QAAQ,OAAgB;AAC1B,OAAI,KAAK,gBAAgB,mBACvB,MAAK,eAAe,mBAAmB,WAAW,MAAM;;EAI5D,IACI,OAAgB;AAClB,UAAO,KAAK,gBAAgB,oBAAoB,QAAQ,MAAKJ;;EAE/D,IAAI,KAAK,OAAgB;GACvB,MAAM,WAAW,MAAKA;AACtB,SAAKA,OAAQ;AACb,OAAI,KAAK,gBAAgB,mBACvB,MAAK,eAAe,mBAAmB,QAAQ,MAAM;AAEvD,QAAK,cAAc,QAAQ,SAAS;;EAMtC,IACI,gBAAwB;AAC1B,UACE,KAAK,gBAAgB,oBAAoB,iBAAiB;;EAG9D,IAAI,cAAc,OAAe;AAC/B,OAAI,KAAK,gBAAgB,mBACvB,MAAK,eAAe,mBAAmB,iBAAiB,MAAM;;EAIlE,qBAAqB,IAAI,kBAAkB,cAAc;GACvD,IAAI,eAAe;AAEnB,QAAK,MAAM,YAAY,UACrB,KAAI,SAAS,SAAS,aAAa;IACjC,MAAM,cAAc,KAAK,kBAAkB;AAC3C,QAAI,gBAAgB,KAAK,gBAAgB;AACvC,UAAK,iBAAiB;AACtB,oBAAe;eAEf,SAAS,kBAAkB,WAC3B,aAAa,SAAS,OAAO,CAG7B,gBAAe;cAER,SAAS,SAAS,cAW3B;QAToC;KAClC;KACA;KACA;KACA;KACA;KACA;KACD,CAG6B,SAC1B,SAAS,iBAAiB,GAC3B,IACA,SAAS,kBAAkB,WAC1B,aAAa,SAAS,OAAO,CAE/B,gBAAe;;AAKrB,OAAI,aAGF,sBAAqB;AAEnB,SAAK,0BAA0B;AAC/B,SAAK,eAAe;AAEpB,QAAI,KAAK,eACP,CAAC,KAAK,eAAuB,eAAe;KAE9C;IAEJ;;;;EAKF,2BAAiC;GAC/B,MAAM,cAAc,KAAK,gBAAgB,cAAc;GACvD,MAAM,aAAa,KAAK,gBAAgB,aAAa;AAErD,OAAI,KAAK,eAAe,YACtB,MAAK,aAAa;AAGpB,OAAI,KAAK,cAAc,WACrB,MAAK,YAAY;;EAIrB,oBAA0B;AACxB,SAAM,mBAAmB;AAGzB,SAAKC,kBAAmB,IAAI,gBAAgB,MAAM;IAChD,SAAS;IACT,cAAc,KAAK;IACpB,CAAC;AACF,SAAKC,eAAgB,IAAI,gBAAgB,MAAM;IAC7C,SAAS;IACT,cAAc,KAAK;IACpB,CAAC;AACF,SAAKC,wBAAyB,IAAI,gBAAgB,MAAM;IACtD,SAAS;IACT,cAAc,KAAK;IACpB,CAAC;AACF,SAAKJ,yBAA0B,IAAI,gBAAgB,MAAM;IACvD,SAAS;IACT,cAAc,KAAK;IACpB,CAAC;AAGF,QAAK,iBAAiB,KAAK,kBAAkB;AAE7C,QAAK,0BAA0B;AAE/B,SAAKM,kBAAmB,QAAQ,MAAM;IACpC,WAAW;IACX,SAAS;IACT,YAAY;IACb,CAAC;;EAGJ,uBAA6B;AAC3B,SAAM,sBAAsB;AAC5B,SAAKA,kBAAmB,YAAY;AAGpC,OAAI,MAAKT,gBAAiB,oBAAoB;AAC5C,UAAKA,eAAgB,mBAAmB,eACtC,MAAKC,mBACN;AACD,UAAKC,uBAAwB;;AAG/B,QAAK,OAAO;;EAGd,QAAQ,mBAA2D;AACjE,SAAM,UAAU,kBAAkB;AAGlC,OACE,CAAC,MAAKA,wBACN,MAAKF,gBAAiB,oBACtB;AACA,UAAKA,eAAgB,mBAAmB,YACtC,MAAKC,mBACN;AACD,UAAKC,uBAAwB;AAG7B,QAAI,MAAKE,KACP,OAAKJ,eAAgB,mBAAmB,QAAQ,MAAKI,KAAM;AAI7D,UAAKC,gBAAiB,SAAS,KAAK,QAAQ;AAC5C,UAAKC,aAAc,SAAS,KAAK,KAAK;AACtC,UAAKC,sBAAuB,SAAS,KAAK,cAAc;;;EAI5D,MAAM,OAAO;AAGX,OAAI,CAAC,KAAK,gBAAgB;IAExB,MAAM,wBAAwB,MAAM,KAAK,KAAK,SAAS,CACpD,KAAK,OAAO,GAAG,QAAQ,aAAa,CAAC,CACrC,QAAQ,QAAQ,IAAI,WAAW,MAAM,CAAC;AAEzC,UAAM,QAAQ,IACZ,sBAAsB,KAAK,QACzB,eAAe,YAAY,IAAI,CAAC,YAAY,GAAG,CAChD,CACF;IAED,MAAM,gBAAgB,KAAK,kBAAkB;AAC7C,QAAI,eAAe;AACjB,UAAK,iBAAiB;AAEtB,WAAO,cAAsB;WACxB;AACL,aAAQ,KAAK,oCAAoC;AACjD;;;AAKJ,OAAI,CAAC,KAAK,eAAe,oBAAoB;AAC3C,UAAO,KAAK,eAAuB;AAEnC,QAAI,CAAC,KAAK,eAAe,oBAAoB;AAC3C,aAAQ,KAAK,wDAAwD;AACrE;;;AAIJ,QAAK,eAAe,mBAAmB,MAAM;;EAG/C,QAAQ;AACN,OAAI,KAAK,gBAAgB,mBACvB,MAAK,eAAe,mBAAmB,OAAO;;;aA/gBjD,QAAQ;EAAE,SAAS;EAAwB,WAAW;EAAM,CAAC;aAG7D,QAAQ,EAAE,SAAS,cAAc,CAAC;aAGlC,QAAQ,EAAE,SAAS,uBAAuB,CAAC,EAC3C,OAAO;aAWP,SAAS;EAAE,MAAM;EAAQ,WAAW;EAAY,CAAC;aASjD,QAAQ,EAAE,SAAS,WAAW,CAAC;aAK/B,OAAO;aAyFP,QAAQ,EAAE,SAAS,iBAAiB,CAAC,EACrC,SAAS,EAAE,MAAM,QAAQ,CAAC;aAG1B,SAAS,EAAE,MAAM,QAAQ,CAAC;aAG1B,QAAQ,EAAE,SAAS,cAAc,CAAC;aAyKlC,SAAS;EAAE,MAAM;EAAQ,WAAW;EAAe,CAAC;aAQpD,SAAS;EAAE,MAAM;EAAS,SAAS;EAAM,CAAC;aAU1C,SAAS;EAAE,MAAM;EAAS,SAAS;EAAM,WAAW;EAAQ,CAAC;aAa7D,SAAS,EAAE,MAAM,SAAS,CAAC;aAG3B,SAAS,EAAE,MAAM,QAAQ,CAAC;AAyM7B,QAAO"}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as lit11 from "lit";
|
|
2
2
|
import { LitElement } from "lit";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit_html11 from "lit-html";
|
|
4
4
|
|
|
5
5
|
//#region src/gui/EFConfiguration.d.ts
|
|
6
6
|
declare class EFConfiguration extends LitElement {
|
|
7
|
-
static styles:
|
|
7
|
+
static styles: lit11.CSSResult[];
|
|
8
8
|
efConfiguration: this;
|
|
9
9
|
apiHost?: string;
|
|
10
10
|
signingURL: string;
|
|
11
11
|
mediaEngine?: "cloud" | "local";
|
|
12
|
-
render():
|
|
12
|
+
render(): lit_html11.TemplateResult<1>;
|
|
13
13
|
}
|
|
14
14
|
declare global {
|
|
15
15
|
interface HTMLElementTagNameMap {
|
package/dist/gui/EFControls.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TemporalMixinInterface } from "../elements/EFTemporal.js";
|
|
2
2
|
import { ControllableInterface } from "./Controllable.js";
|
|
3
3
|
import { FocusContext } from "./focusContext.js";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit23 from "lit";
|
|
5
5
|
import { LitElement, PropertyValueMap } from "lit";
|
|
6
6
|
|
|
7
7
|
//#region src/gui/EFControls.d.ts
|
|
@@ -26,7 +26,7 @@ import { LitElement, PropertyValueMap } from "lit";
|
|
|
26
26
|
*/
|
|
27
27
|
declare class EFControls extends LitElement {
|
|
28
28
|
#private;
|
|
29
|
-
static styles:
|
|
29
|
+
static styles: lit23.CSSResult;
|
|
30
30
|
createRenderRoot(): this;
|
|
31
31
|
/**
|
|
32
32
|
* The ID of the ef-preview element to control
|
package/dist/gui/EFDial.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as lit22 from "lit";
|
|
2
2
|
import { LitElement } from "lit";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit_html20 from "lit-html";
|
|
4
4
|
|
|
5
5
|
//#region src/gui/EFDial.d.ts
|
|
6
6
|
interface DialChangeDetail {
|
|
@@ -13,12 +13,12 @@ declare class EFDial extends LitElement {
|
|
|
13
13
|
private isDragging;
|
|
14
14
|
private dragStartAngle;
|
|
15
15
|
private dragStartValue;
|
|
16
|
-
static styles:
|
|
16
|
+
static styles: lit22.CSSResult;
|
|
17
17
|
private getAngleFromPoint;
|
|
18
18
|
private handlePointerDown;
|
|
19
19
|
private handlePointerMove;
|
|
20
20
|
private handlePointerUp;
|
|
21
|
-
render():
|
|
21
|
+
render(): lit_html20.TemplateResult<1>;
|
|
22
22
|
}
|
|
23
23
|
//#endregion
|
|
24
24
|
export { DialChangeDetail, EFDial };
|
package/dist/gui/EFDial.js
CHANGED
|
@@ -78,11 +78,12 @@ let EFDial = class EFDial$1 extends LitElement {
|
|
|
78
78
|
this.dragStartAngle = this.getAngleFromPoint(e.clientX, e.clientY, rect);
|
|
79
79
|
this.dragStartValue = this.value;
|
|
80
80
|
this.setPointerCapture(e.pointerId);
|
|
81
|
-
this.addEventListener("pointermove", this.handlePointerMove);
|
|
82
|
-
this.addEventListener("pointerup", this.handlePointerUp);
|
|
81
|
+
this.addEventListener("pointermove", this.handlePointerMove, { passive: false });
|
|
82
|
+
this.addEventListener("pointerup", this.handlePointerUp, { passive: false });
|
|
83
83
|
}
|
|
84
84
|
handlePointerMove(e) {
|
|
85
85
|
if (!this.isDragging) return;
|
|
86
|
+
e.preventDefault();
|
|
86
87
|
const rect = this.getBoundingClientRect();
|
|
87
88
|
const angleDelta = this.getAngleFromPoint(e.clientX, e.clientY, rect) - this.dragStartAngle;
|
|
88
89
|
let newValue = this.dragStartValue + angleDelta * 180 / Math.PI;
|
|
@@ -94,6 +95,7 @@ let EFDial = class EFDial$1 extends LitElement {
|
|
|
94
95
|
this.dispatchEvent(new CustomEvent("change", { detail: { value: this.value } }));
|
|
95
96
|
}
|
|
96
97
|
handlePointerUp(e) {
|
|
98
|
+
e.preventDefault();
|
|
97
99
|
this.isDragging = false;
|
|
98
100
|
this.releasePointerCapture(e.pointerId);
|
|
99
101
|
this.removeEventListener("pointermove", this.handlePointerMove);
|
package/dist/gui/EFDial.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFDial.js","names":["EFDial"],"sources":["../../src/gui/EFDial.ts"],"sourcesContent":["import { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\n\nexport interface DialChangeDetail {\n value: number;\n}\n\n@customElement(\"ef-dial\")\nexport class EFDial extends LitElement {\n @property({ type: Number })\n set value(newValue: number) {\n // Normalize to 0-360 range\n newValue = newValue % 360;\n if (newValue < 0) {\n newValue += 360;\n }\n // Limit to 6 significant digits\n newValue = Number.parseFloat(newValue.toPrecision(6));\n\n const oldValue = this._value;\n this._value = newValue;\n this.requestUpdate(\"value\", oldValue);\n }\n\n get value() {\n return this._value;\n }\n\n private _value = 0;\n\n @state()\n private isDragging = false;\n\n private dragStartAngle = 0;\n private dragStartValue = 0;\n\n static styles = css`\n :host {\n display: inline-block;\n width: 200px; /* Default size, can be overridden by CSS */\n height: 200px; /* Default size, can be overridden by CSS */\n }\n .dial-container {\n position: relative;\n width: 100%;\n height: 100%;\n border-radius: 50%;\n background-color: #f3f4f6;\n border: 2px solid #d1d5db;\n }\n .handle {\n position: absolute;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n border: 2px solid #3b82f6;\n background-color: white;\n cursor: grab;\n }\n .handle.dragging {\n background-color: #3b82f6;\n cursor: grabbing;\n }\n .center-text {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n background-color: white;\n border: 1px solid #d1d5db;\n padding: 2px 4px;\n border-radius: 4px;\n font-family: monospace;\n font-size: 12px;\n }\n `;\n\n private getAngleFromPoint(clientX: number, clientY: number, rect: DOMRect) {\n const center = this.clientWidth / 2;\n const x = clientX - rect.left - center;\n const y = clientY - rect.top - center;\n return Math.atan2(y, x);\n }\n\n private handlePointerDown(e: PointerEvent) {\n e.preventDefault();\n this.isDragging = true;\n const rect = this.getBoundingClientRect();\n this.dragStartAngle = this.getAngleFromPoint(e.clientX, e.clientY, rect);\n this.dragStartValue = this.value;\n this.setPointerCapture(e.pointerId);\n this.addEventListener(\"pointermove\", this.handlePointerMove);\n this.addEventListener(\"pointerup\", this.handlePointerUp);\n }\n\n private handlePointerMove(e: PointerEvent) {\n if (!this.isDragging) return;\n\n const rect = this.getBoundingClientRect();\n const currentAngle = this.getAngleFromPoint(e.clientX, e.clientY, rect);\n const angleDelta = currentAngle - this.dragStartAngle;\n\n let newValue = this.dragStartValue + (angleDelta * 180) / Math.PI;\n\n if (e.shiftKey) {\n newValue = Math.round(newValue / 15) * 15;\n }\n\n // Normalize to 0-360 range\n newValue = newValue % 360;\n if (newValue < 0) {\n newValue += 360;\n }\n\n // Limit to 6 significant digits\n newValue = Number.parseFloat(newValue.toPrecision(6));\n\n this.value = newValue;\n this.dispatchEvent(\n new CustomEvent<DialChangeDetail>(\"change\", {\n detail: { value: this.value },\n }),\n );\n }\n\n private handlePointerUp(e: PointerEvent) {\n this.isDragging = false;\n this.releasePointerCapture(e.pointerId);\n this.removeEventListener(\"pointermove\", this.handlePointerMove);\n this.removeEventListener(\"pointerup\", this.handlePointerUp);\n }\n\n render() {\n const center = this.clientWidth / 2;\n const radius = center - 20;\n const handleAngle = (this.value * Math.PI) / 180;\n const handleX = center + Math.cos(handleAngle) * radius;\n const handleY = center + Math.sin(handleAngle) * radius;\n\n const handleStyles = {\n left: `${handleX - 8}px`,\n top: `${handleY - 8}px`,\n };\n\n return html`\n <div class=\"dial-container\" @pointerdown=${this.handlePointerDown}>\n <svg class=\"absolute inset-0 w-full h-full\">\n <circle\n cx=${center}\n cy=${center}\n r=${radius}\n fill=\"none\"\n stroke=\"#94a3b8\"\n stroke-width=\"2\"\n stroke-dasharray=\"4 4\"\n />\n ${[0, 90, 180, 270].map((deg) => {\n const angle = (deg * Math.PI) / 180;\n const x1 = center + Math.cos(angle) * (radius - 8);\n const y1 = center + Math.sin(angle) * (radius - 8);\n const x2 = center + Math.cos(angle) * (radius + 8);\n const y2 = center + Math.sin(angle) * (radius + 8);\n return html`<line x1=${x1} y1=${y1} x2=${x2} y2=${y2} stroke=\"#64748b\" stroke-width=\"2\" />`;\n })}\n </svg>\n <div class=\"handle ${this.isDragging ? \"dragging\" : \"\"}\" style=${styleMap(handleStyles)}></div>\n <div class=\"center-text\">${this.value.toFixed(0)}°</div>\n </div>\n `;\n }\n}\n"],"mappings":";;;;;;AASO,mBAAMA,iBAAe,WAAW;;;gBAoBpB;oBAGI;wBAEI;wBACA;;CAzBzB,IACI,MAAM,UAAkB;AAE1B,aAAW,WAAW;AACtB,MAAI,WAAW,EACb,aAAY;AAGd,aAAW,OAAO,WAAW,SAAS,YAAY,EAAE,CAAC;EAErD,MAAM,WAAW,KAAK;AACtB,OAAK,SAAS;AACd,OAAK,cAAc,SAAS,SAAS;;CAGvC,IAAI,QAAQ;AACV,SAAO,KAAK;;;gBAWE,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCnB,AAAQ,kBAAkB,SAAiB,SAAiB,MAAe;EACzE,MAAM,SAAS,KAAK,cAAc;EAClC,MAAM,IAAI,UAAU,KAAK,OAAO;EAChC,MAAM,IAAI,UAAU,KAAK,MAAM;AAC/B,SAAO,KAAK,MAAM,GAAG,EAAE;;CAGzB,AAAQ,kBAAkB,GAAiB;AACzC,IAAE,gBAAgB;AAClB,OAAK,aAAa;EAClB,MAAM,OAAO,KAAK,uBAAuB;AACzC,OAAK,iBAAiB,KAAK,kBAAkB,EAAE,SAAS,EAAE,SAAS,KAAK;AACxE,OAAK,iBAAiB,KAAK;AAC3B,OAAK,kBAAkB,EAAE,UAAU;AACnC,OAAK,iBAAiB,eAAe,KAAK,
|
|
1
|
+
{"version":3,"file":"EFDial.js","names":["EFDial"],"sources":["../../src/gui/EFDial.ts"],"sourcesContent":["import { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\n\nexport interface DialChangeDetail {\n value: number;\n}\n\n@customElement(\"ef-dial\")\nexport class EFDial extends LitElement {\n @property({ type: Number })\n set value(newValue: number) {\n // Normalize to 0-360 range\n newValue = newValue % 360;\n if (newValue < 0) {\n newValue += 360;\n }\n // Limit to 6 significant digits\n newValue = Number.parseFloat(newValue.toPrecision(6));\n\n const oldValue = this._value;\n this._value = newValue;\n this.requestUpdate(\"value\", oldValue);\n }\n\n get value() {\n return this._value;\n }\n\n private _value = 0;\n\n @state()\n private isDragging = false;\n\n private dragStartAngle = 0;\n private dragStartValue = 0;\n\n static styles = css`\n :host {\n display: inline-block;\n width: 200px; /* Default size, can be overridden by CSS */\n height: 200px; /* Default size, can be overridden by CSS */\n }\n .dial-container {\n position: relative;\n width: 100%;\n height: 100%;\n border-radius: 50%;\n background-color: #f3f4f6;\n border: 2px solid #d1d5db;\n }\n .handle {\n position: absolute;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n border: 2px solid #3b82f6;\n background-color: white;\n cursor: grab;\n }\n .handle.dragging {\n background-color: #3b82f6;\n cursor: grabbing;\n }\n .center-text {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n background-color: white;\n border: 1px solid #d1d5db;\n padding: 2px 4px;\n border-radius: 4px;\n font-family: monospace;\n font-size: 12px;\n }\n `;\n\n private getAngleFromPoint(clientX: number, clientY: number, rect: DOMRect) {\n const center = this.clientWidth / 2;\n const x = clientX - rect.left - center;\n const y = clientY - rect.top - center;\n return Math.atan2(y, x);\n }\n\n private handlePointerDown(e: PointerEvent) {\n e.preventDefault();\n this.isDragging = true;\n const rect = this.getBoundingClientRect();\n this.dragStartAngle = this.getAngleFromPoint(e.clientX, e.clientY, rect);\n this.dragStartValue = this.value;\n this.setPointerCapture(e.pointerId);\n this.addEventListener(\"pointermove\", this.handlePointerMove, {\n passive: false,\n });\n this.addEventListener(\"pointerup\", this.handlePointerUp, {\n passive: false,\n });\n }\n\n private handlePointerMove(e: PointerEvent) {\n if (!this.isDragging) return;\n\n e.preventDefault();\n const rect = this.getBoundingClientRect();\n const currentAngle = this.getAngleFromPoint(e.clientX, e.clientY, rect);\n const angleDelta = currentAngle - this.dragStartAngle;\n\n let newValue = this.dragStartValue + (angleDelta * 180) / Math.PI;\n\n if (e.shiftKey) {\n newValue = Math.round(newValue / 15) * 15;\n }\n\n // Normalize to 0-360 range\n newValue = newValue % 360;\n if (newValue < 0) {\n newValue += 360;\n }\n\n // Limit to 6 significant digits\n newValue = Number.parseFloat(newValue.toPrecision(6));\n\n this.value = newValue;\n this.dispatchEvent(\n new CustomEvent<DialChangeDetail>(\"change\", {\n detail: { value: this.value },\n }),\n );\n }\n\n private handlePointerUp(e: PointerEvent) {\n e.preventDefault();\n this.isDragging = false;\n this.releasePointerCapture(e.pointerId);\n this.removeEventListener(\"pointermove\", this.handlePointerMove);\n this.removeEventListener(\"pointerup\", this.handlePointerUp);\n }\n\n render() {\n const center = this.clientWidth / 2;\n const radius = center - 20;\n const handleAngle = (this.value * Math.PI) / 180;\n const handleX = center + Math.cos(handleAngle) * radius;\n const handleY = center + Math.sin(handleAngle) * radius;\n\n const handleStyles = {\n left: `${handleX - 8}px`,\n top: `${handleY - 8}px`,\n };\n\n return html`\n <div class=\"dial-container\" @pointerdown=${this.handlePointerDown}>\n <svg class=\"absolute inset-0 w-full h-full\">\n <circle\n cx=${center}\n cy=${center}\n r=${radius}\n fill=\"none\"\n stroke=\"#94a3b8\"\n stroke-width=\"2\"\n stroke-dasharray=\"4 4\"\n />\n ${[0, 90, 180, 270].map((deg) => {\n const angle = (deg * Math.PI) / 180;\n const x1 = center + Math.cos(angle) * (radius - 8);\n const y1 = center + Math.sin(angle) * (radius - 8);\n const x2 = center + Math.cos(angle) * (radius + 8);\n const y2 = center + Math.sin(angle) * (radius + 8);\n return html`<line x1=${x1} y1=${y1} x2=${x2} y2=${y2} stroke=\"#64748b\" stroke-width=\"2\" />`;\n })}\n </svg>\n <div class=\"handle ${this.isDragging ? \"dragging\" : \"\"}\" style=${styleMap(handleStyles)}></div>\n <div class=\"center-text\">${this.value.toFixed(0)}°</div>\n </div>\n `;\n }\n}\n"],"mappings":"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"}
|