@editframe/elements 0.35.0-beta → 0.36.1-beta

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.
Files changed (72) hide show
  1. package/dist/canvas/EFCanvas.d.ts +4 -4
  2. package/dist/elements/EFAudio.d.ts +4 -4
  3. package/dist/elements/EFCaptions.d.ts +0 -4
  4. package/dist/elements/EFCaptions.js +12 -32
  5. package/dist/elements/EFCaptions.js.map +1 -1
  6. package/dist/elements/EFImage.js +11 -2
  7. package/dist/elements/EFImage.js.map +1 -1
  8. package/dist/elements/EFPanZoom.d.ts +4 -4
  9. package/dist/elements/EFSurface.d.ts +4 -4
  10. package/dist/elements/EFTemporal.js +1 -0
  11. package/dist/elements/EFTemporal.js.map +1 -1
  12. package/dist/elements/EFText.d.ts +4 -4
  13. package/dist/elements/EFTextSegment.d.ts +4 -4
  14. package/dist/elements/EFThumbnailStrip.d.ts +4 -4
  15. package/dist/elements/EFTimegroup.d.ts +40 -6
  16. package/dist/elements/EFTimegroup.js +127 -8
  17. package/dist/elements/EFTimegroup.js.map +1 -1
  18. package/dist/elements/EFVideo.d.ts +6 -6
  19. package/dist/elements/EFWaveform.d.ts +4 -4
  20. package/dist/elements/updateAnimations.js +113 -15
  21. package/dist/elements/updateAnimations.js.map +1 -1
  22. package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
  23. package/dist/gui/EFConfiguration.d.ts +4 -4
  24. package/dist/gui/EFControls.d.ts +2 -2
  25. package/dist/gui/EFDial.d.ts +4 -4
  26. package/dist/gui/EFFilmstrip.d.ts +2 -2
  27. package/dist/gui/EFFitScale.d.ts +3 -3
  28. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  29. package/dist/gui/EFPause.d.ts +4 -4
  30. package/dist/gui/EFPlay.d.ts +4 -4
  31. package/dist/gui/EFPreview.d.ts +4 -4
  32. package/dist/gui/EFResizableBox.d.ts +4 -4
  33. package/dist/gui/EFScrubber.d.ts +4 -4
  34. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  35. package/dist/gui/EFToggleLoop.d.ts +4 -4
  36. package/dist/gui/EFTogglePlay.d.ts +4 -4
  37. package/dist/gui/EFTransformHandles.d.ts +4 -4
  38. package/dist/gui/EFWorkbench.d.ts +6 -6
  39. package/dist/gui/EFWorkbench.js +38 -12
  40. package/dist/gui/EFWorkbench.js.map +1 -1
  41. package/dist/gui/TWMixin.js +1 -1
  42. package/dist/gui/TWMixin.js.map +1 -1
  43. package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
  44. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +2 -2
  45. package/dist/gui/timeline/tracks/ImageTrack.d.ts +2 -2
  46. package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +5 -5
  47. package/dist/gui/timeline/tracks/VideoTrack.d.ts +4 -4
  48. package/dist/gui/tree/EFTree.d.ts +4 -4
  49. package/dist/gui/tree/EFTreeItem.d.ts +4 -4
  50. package/dist/preview/FrameController.js +6 -1
  51. package/dist/preview/FrameController.js.map +1 -1
  52. package/dist/preview/encoding/canvasEncoder.js.map +1 -1
  53. package/dist/preview/encoding/mainThreadEncoder.js +3 -0
  54. package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
  55. package/dist/preview/renderTimegroupPreview.js +57 -55
  56. package/dist/preview/renderTimegroupPreview.js.map +1 -1
  57. package/dist/preview/renderTimegroupToCanvas.js +22 -23
  58. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  59. package/dist/preview/renderTimegroupToVideo.d.ts +2 -1
  60. package/dist/preview/renderTimegroupToVideo.js +77 -40
  61. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  62. package/dist/preview/rendering/renderToImage.d.ts +1 -0
  63. package/dist/preview/rendering/renderToImage.js +1 -26
  64. package/dist/preview/rendering/renderToImage.js.map +1 -1
  65. package/dist/preview/rendering/renderToImageForeignObject.js +34 -6
  66. package/dist/preview/rendering/renderToImageForeignObject.js.map +1 -1
  67. package/dist/preview/rendering/serializeTimelineDirect.js +379 -0
  68. package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -0
  69. package/dist/render/EFRenderAPI.js +45 -0
  70. package/dist/render/EFRenderAPI.js.map +1 -1
  71. package/dist/style.css +45 -0
  72. package/package.json +2 -2
@@ -5,9 +5,9 @@ import { PanZoomTransform } from "../elements/EFPanZoom.js";
5
5
  import "./overlays/SelectionOverlay.js";
6
6
  import "../gui/EFOverlayLayer.js";
7
7
  import "../gui/EFTransformHandles.js";
8
- import * as lit29 from "lit";
8
+ import * as lit3 from "lit";
9
9
  import { LitElement } from "lit";
10
- import * as lit_html28 from "lit-html";
10
+ import * as lit_html2 from "lit-html";
11
11
 
12
12
  //#region src/canvas/EFCanvas.d.ts
13
13
  declare const EFCanvas_base: typeof LitElement;
@@ -87,7 +87,7 @@ declare const EFCanvas_base: typeof LitElement;
87
87
  * Manages existing elements (EF* elements, divs, etc.) and provides selection functionality.
88
88
  */
89
89
  declare class EFCanvas extends EFCanvas_base {
90
- static styles: lit29.CSSResult[];
90
+ static styles: lit3.CSSResult[];
91
91
  panZoomTransform?: PanZoomTransform;
92
92
  elementIdAttribute: string;
93
93
  enableTransformHandles: boolean;
@@ -302,7 +302,7 @@ declare class EFCanvas extends EFCanvas_base {
302
302
  * Cleanup transform handles.
303
303
  */
304
304
  private cleanupTransformHandles;
305
- render(): lit_html28.TemplateResult<1>;
305
+ render(): lit_html2.TemplateResult<1>;
306
306
  }
307
307
  declare global {
308
308
  interface HTMLElementTagNameMap {
@@ -1,7 +1,7 @@
1
1
  import { FrameRenderable, FrameState, FrameTask } from "../preview/FrameController.js";
2
2
  import { EFMedia } from "./EFMedia.js";
3
- import * as lit_html2 from "lit-html";
4
- import * as lit_html_directives_ref_js1 from "lit-html/directives/ref.js";
3
+ import * as lit_html8 from "lit-html";
4
+ import * as lit_html_directives_ref_js2 from "lit-html/directives/ref.js";
5
5
 
6
6
  //#region src/elements/EFAudio.d.ts
7
7
  declare const EFAudio_base: typeof EFMedia;
@@ -17,9 +17,9 @@ declare class EFAudio extends EFAudio_base implements FrameRenderable {
17
17
  * @domAttribute "volume"
18
18
  */
19
19
  volume: number;
20
- audioElementRef: lit_html_directives_ref_js1.Ref<HTMLAudioElement>;
20
+ audioElementRef: lit_html_directives_ref_js2.Ref<HTMLAudioElement>;
21
21
  protected updated(changedProperties: Map<PropertyKey, unknown>): void;
22
- render(): lit_html2.TemplateResult<1>;
22
+ render(): lit_html8.TemplateResult<1>;
23
23
  /**
24
24
  * @deprecated Use FrameRenderable methods (prepareFrame, renderFrame) via FrameController instead.
25
25
  * This is a compatibility wrapper that delegates to the new system.
@@ -30,7 +30,6 @@ interface Caption {
30
30
  */
31
31
  declare class EFCaptionsActiveWord extends HTMLElement {
32
32
  #private;
33
- constructor();
34
33
  set wordText(text: string);
35
34
  get wordText(): string;
36
35
  set wordIndex(index: number);
@@ -42,7 +41,6 @@ declare class EFCaptionsActiveWord extends HTMLElement {
42
41
  */
43
42
  declare class EFCaptionsSegment extends HTMLElement {
44
43
  #private;
45
- constructor();
46
44
  set segmentText(text: string);
47
45
  get segmentText(): string;
48
46
  }
@@ -51,7 +49,6 @@ declare class EFCaptionsSegment extends HTMLElement {
51
49
  * Uses light DOM for simplicity - parent sets textContent directly.
52
50
  */
53
51
  declare class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {
54
- constructor();
55
52
  set segmentText(text: string);
56
53
  }
57
54
  /**
@@ -59,7 +56,6 @@ declare class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {
59
56
  * Uses light DOM for simplicity - parent sets textContent directly.
60
57
  */
61
58
  declare class EFCaptionsAfterActiveWord extends EFCaptionsSegment {
62
- constructor();
63
59
  set segmentText(text: string);
64
60
  }
65
61
  declare const EFCaptions_base: (new (...args: any[]) => EFSourceMixinInterface) & (new (...args: any[]) => TemporalMixinInterface) & (new (...args: any[]) => FetchMixinInterface) & typeof LitElement;
@@ -22,20 +22,14 @@ const stopWords = new Set([
22
22
  let EFCaptionsActiveWord = class EFCaptionsActiveWord$1 extends HTMLElement {
23
23
  #wordText = "";
24
24
  #wordIndex = 0;
25
- constructor() {
26
- super();
27
- this.style.display = "inline-block";
28
- this.style.whiteSpace = "normal";
29
- this.style.lineHeight = "1";
30
- }
31
25
  set wordText(text) {
32
26
  this.#wordText = text;
33
27
  if (!text || stopWords.has(text)) {
34
- this.style.display = "none";
28
+ this.hidden = true;
35
29
  this.textContent = "";
36
30
  } else {
37
- this.style.display = "inline-block";
38
- this.textContent = text;
31
+ this.hidden = false;
32
+ this.textContent = text + " ";
39
33
  }
40
34
  }
41
35
  get wordText() {
@@ -53,19 +47,13 @@ let EFCaptionsActiveWord = class EFCaptionsActiveWord$1 extends HTMLElement {
53
47
  EFCaptionsActiveWord = __decorate([customElement("ef-captions-active-word")], EFCaptionsActiveWord);
54
48
  let EFCaptionsSegment = class EFCaptionsSegment$1 extends HTMLElement {
55
49
  #segmentText = "";
56
- constructor() {
57
- super();
58
- this.style.display = "inline-block";
59
- this.style.whiteSpace = "normal";
60
- this.style.lineHeight = "1";
61
- }
62
50
  set segmentText(text) {
63
51
  this.#segmentText = text;
64
52
  if (!text || stopWords.has(text)) {
65
- this.style.display = "none";
53
+ this.hidden = true;
66
54
  this.textContent = "";
67
55
  } else {
68
- this.style.display = "inline-block";
56
+ this.hidden = false;
69
57
  this.textContent = text;
70
58
  }
71
59
  }
@@ -75,35 +63,27 @@ let EFCaptionsSegment = class EFCaptionsSegment$1 extends HTMLElement {
75
63
  };
76
64
  EFCaptionsSegment = __decorate([customElement("ef-captions-segment")], EFCaptionsSegment);
77
65
  let EFCaptionsBeforeActiveWord = class EFCaptionsBeforeActiveWord$1 extends EFCaptionsSegment {
78
- constructor() {
79
- super();
80
- this.style.whiteSpace = "pre";
81
- }
82
66
  set segmentText(text) {
83
67
  const hasActiveWord = (this.closest("ef-captions")?.querySelector("ef-captions-active-word"))?.wordText;
84
68
  const finalText = text && hasActiveWord ? text + " " : text;
85
69
  if (!finalText || stopWords.has(finalText)) {
86
- this.style.display = "none";
70
+ this.hidden = true;
87
71
  this.textContent = "";
88
72
  } else {
89
- this.style.display = "inline-block";
73
+ this.hidden = false;
90
74
  this.textContent = finalText;
91
75
  }
92
76
  }
93
77
  };
94
78
  EFCaptionsBeforeActiveWord = __decorate([customElement("ef-captions-before-active-word")], EFCaptionsBeforeActiveWord);
95
79
  let EFCaptionsAfterActiveWord = class EFCaptionsAfterActiveWord$1 extends EFCaptionsSegment {
96
- constructor() {
97
- super();
98
- this.style.whiteSpace = "pre";
99
- }
100
80
  set segmentText(text) {
101
- const finalText = text ? " " + text : text;
81
+ const finalText = text;
102
82
  if (!finalText || stopWords.has(finalText)) {
103
- this.style.display = "none";
83
+ this.hidden = true;
104
84
  this.textContent = "";
105
85
  } else {
106
- this.style.display = "inline-block";
86
+ this.hidden = false;
107
87
  this.textContent = finalText;
108
88
  }
109
89
  }
@@ -127,13 +107,13 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
127
107
  static {
128
108
  this.styles = [css`
129
109
  :host {
130
- display: inline-flex;
110
+ display: block;
131
111
  white-space: normal;
132
112
  line-height: 1;
133
113
  gap: 0;
134
114
  }
135
115
  ::slotted(*) {
136
- display: inline-block;
116
+ display: inline;
137
117
  margin: 0;
138
118
  padding: 0;
139
119
  }
@@ -1 +1 @@
1
- {"version":3,"file":"EFCaptions.js","names":["EFCaptionsActiveWord","#wordText","#wordIndex","EFCaptionsSegment","#segmentText","EFCaptionsBeforeActiveWord","EFCaptionsAfterActiveWord","EFCaptions","#captionsDataLoaded","#captionsDataValue","#captionsDataPromise","#doLoadCaptionsData","#transcriptionData","#loadTranscriptionFragment","#rootTimegroupUpdateController","#cachedIntrinsicDurationMs","captionsData: Caption | null","result: number"],"sources":["../../src/elements/EFCaptions.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport type { ReactiveController } from \"lit\";\nimport type { GetISOBMFFFileTranscriptionResult } from \"../../../api/src/index.js\";\nimport {\n type FrameRenderable,\n type FrameState,\n createFrameTaskWrapper,\n PRIORITY_CAPTIONS,\n} from \"../preview/FrameController.js\";\nimport { AsyncValue } from \"./EFMedia.js\";\nimport { CrossUpdateController } from \"./CrossUpdateController.js\";\nimport { EFAudio } from \"./EFAudio.js\";\nimport { EFSourceMixin } from \"./EFSourceMixin.js\";\nimport { EFTemporal, flushStartTimeMsCache } from \"./EFTemporal.js\";\nimport {\n flushSequenceDurationCache,\n EFTimegroup,\n} from \"./EFTimegroup.js\";\nimport { EFVideo } from \"./EFVideo.js\";\nimport { FetchMixin } from \"./FetchMixin.js\";\n\nexport interface WordSegment {\n text: string;\n start: number;\n end: number;\n}\n\nexport interface Segment {\n start: number;\n end: number;\n text: string;\n}\n\nexport interface Caption {\n segments: Segment[];\n word_segments: WordSegment[];\n}\n\nconst stopWords = new Set([\"\", \".\", \"!\", \"?\", \",\"]);\n\n/**\n * Caption active word element - displays the currently spoken word.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-active-word\")\nexport class EFCaptionsActiveWord extends HTMLElement {\n #wordText = \"\";\n #wordIndex = 0;\n \n constructor() {\n super();\n // Apply default styles via inline style\n this.style.display = \"inline-block\";\n this.style.whiteSpace = \"normal\";\n this.style.lineHeight = \"1\";\n }\n \n set wordText(text: string) {\n this.#wordText = text;\n // Hide element if no content or only stop words\n if (!text || stopWords.has(text)) {\n this.style.display = \"none\";\n this.textContent = \"\";\n } else {\n this.style.display = \"inline-block\";\n this.textContent = text;\n }\n }\n \n get wordText(): string {\n return this.#wordText;\n }\n \n set wordIndex(index: number) {\n this.#wordIndex = index;\n // Set deterministic --ef-word-seed value based on word index\n const seed = (index * 9007) % 233; // Prime numbers for better distribution\n const seedValue = seed / 233; // Normalize to 0-1 range\n this.style.setProperty(\"--ef-word-seed\", seedValue.toString());\n }\n \n get wordIndex(): number {\n return this.#wordIndex;\n }\n}\n\n/**\n * Caption segment element - displays a full caption segment.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-segment\")\nexport class EFCaptionsSegment extends HTMLElement {\n #segmentText = \"\";\n \n constructor() {\n super();\n // Apply default styles via inline style\n this.style.display = \"inline-block\";\n this.style.whiteSpace = \"normal\";\n this.style.lineHeight = \"1\";\n }\n \n set segmentText(text: string) {\n this.#segmentText = text;\n // Hide element if no content or only stop words\n if (!text || stopWords.has(text)) {\n this.style.display = \"none\";\n this.textContent = \"\";\n } else {\n this.style.display = \"inline-block\";\n this.textContent = text;\n }\n }\n \n get segmentText(): string {\n return this.#segmentText;\n }\n}\n\n/**\n * Caption before-active-word element - displays words before the current word.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-before-active-word\")\nexport class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {\n constructor() {\n super();\n // Override whiteSpace to preserve spacing\n this.style.whiteSpace = \"pre\";\n }\n \n set segmentText(text: string) {\n // Check if there's an active word by looking for sibling active word element\n const activeWord = this.closest(\"ef-captions\")?.querySelector(\n \"ef-captions-active-word\",\n ) as EFCaptionsActiveWord;\n const hasActiveWord = activeWord?.wordText;\n \n // Add trailing space if there's an active word coming after us\n const finalText = text && hasActiveWord ? text + \" \" : text;\n \n // Hide element if no content or only stop words\n if (!finalText || stopWords.has(finalText)) {\n this.style.display = \"none\";\n this.textContent = \"\";\n } else {\n this.style.display = \"inline-block\";\n this.textContent = finalText;\n }\n }\n}\n\n/**\n * Caption after-active-word element - displays words after the current word.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-after-active-word\")\nexport class EFCaptionsAfterActiveWord extends EFCaptionsSegment {\n constructor() {\n super();\n // Override whiteSpace to preserve spacing\n this.style.whiteSpace = \"pre\";\n }\n \n set segmentText(text: string) {\n // Add leading space if there's text\n const finalText = text ? \" \" + text : text;\n \n // Hide element if no content or only stop words\n if (!finalText || stopWords.has(finalText)) {\n this.style.display = \"none\";\n this.textContent = \"\";\n } else {\n this.style.display = \"inline-block\";\n this.textContent = finalText;\n }\n }\n}\n\n@customElement(\"ef-captions\")\nexport class EFCaptions extends EFSourceMixin(\n EFTemporal(FetchMixin(LitElement)),\n { assetType: \"caption_files\" },\n) implements FrameRenderable {\n static styles = [\n css`\n :host {\n display: inline-flex;\n white-space: normal;\n line-height: 1;\n gap: 0;\n }\n ::slotted(*) {\n display: inline-block;\n margin: 0;\n padding: 0;\n }\n `,\n ];\n\n @property({ type: String, attribute: \"target\", reflect: true })\n targetSelector = \"\";\n\n set target(value: string) {\n this.targetSelector = value;\n }\n\n @property({ attribute: \"word-style\" })\n wordStyle = \"\";\n\n /**\n * URL or path to a JSON file containing custom captions data.\n * The JSON should conform to the Caption interface with 'segments' and 'word_segments' arrays.\n */\n @property({ type: String, attribute: \"captions-src\", reflect: true })\n captionsSrc = \"\";\n\n /**\n * Direct captions data object. Takes priority over captions-src and captions-script.\n * Should conform to the Caption interface with 'segments' and 'word_segments' arrays.\n */\n @property({ type: Object, attribute: false })\n captionsData: Caption | null = null;\n\n /**\n * ID of a <script> element containing JSON captions data.\n * The script's textContent should be valid JSON conforming to the Caption interface.\n */\n @property({ type: String, attribute: \"captions-script\", reflect: true })\n captionsScript = \"\";\n\n activeWordContainers = this.getElementsByTagName(\"ef-captions-active-word\");\n segmentContainers = this.getElementsByTagName(\"ef-captions-segment\");\n beforeActiveWordContainers = this.getElementsByTagName(\n \"ef-captions-before-active-word\",\n );\n afterActiveWordContainers = this.getElementsByTagName(\n \"ef-captions-after-active-word\",\n );\n\n // Cache for intrinsicDurationMs to avoid expensive O(n) recalculation every frame\n #cachedIntrinsicDurationMs: number | undefined | null = null; // null = not computed, undefined = no duration\n\n render() {\n return html`<slot></slot>`;\n }\n\n transcriptionsPath() {\n if (!this.targetElement) {\n return null;\n }\n if (this.targetElement.assetId) {\n return `${this.apiHost}/api/v1/isobmff_files/${this.targetElement.assetId}/transcription`;\n }\n return null;\n }\n\n captionsPath() {\n if (!this.targetElement) {\n return null;\n }\n if (this.targetElement.assetId) {\n return `${this.apiHost}/api/v1/caption_files/${this.targetElement.assetId}`;\n }\n const targetSrc = this.targetElement.src;\n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = targetSrc.startsWith(\"/\")\n ? targetSrc.slice(1)\n : targetSrc;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n // Use production API format for local files\n return `/api/v1/assets/local/captions?src=${encodeURIComponent(normalizedSrc)}`;\n }\n\n // ============================================================================\n // Captions Data Loading - async methods instead of Tasks\n // ============================================================================\n\n #captionsDataLoaded = false;\n #captionsDataPromise: Promise<Caption | null> | null = null;\n #captionsDataValue: Caption | null = null;\n #transcriptionData: GetISOBMFFFileTranscriptionResult | null = null;\n\n /**\n * AsyncValue wrapper for backwards compatibility\n */\n unifiedCaptionsDataTask = new AsyncValue<Caption | null>();\n\n /**\n * Load captions data from all possible sources\n */\n async loadCaptionsData(signal?: AbortSignal): Promise<Caption | null> {\n // Return cached if already loaded\n if (this.#captionsDataLoaded && this.#captionsDataValue) {\n return this.#captionsDataValue;\n }\n\n // Return in-flight promise\n if (this.#captionsDataPromise) {\n return this.#captionsDataPromise;\n }\n\n this.unifiedCaptionsDataTask.startPending();\n this.#captionsDataPromise = this.#doLoadCaptionsData(signal);\n\n try {\n this.#captionsDataValue = await this.#captionsDataPromise;\n this.#captionsDataLoaded = true;\n if (this.#captionsDataValue) {\n this.unifiedCaptionsDataTask.setValue(this.#captionsDataValue);\n }\n return this.#captionsDataValue;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(\"Failed to load captions data:\", error);\n return null;\n } finally {\n this.#captionsDataPromise = null;\n }\n }\n\n async #doLoadCaptionsData(signal?: AbortSignal): Promise<Caption | null> {\n // Priority 1: Direct captionsData property\n if (this.captionsData) {\n return this.captionsData;\n }\n\n // Priority 2: Script element reference\n if (this.captionsScript) {\n const scriptElement = document.getElementById(this.captionsScript);\n if (scriptElement?.textContent) {\n try {\n return JSON.parse(scriptElement.textContent) as Caption;\n } catch (error) {\n console.error(`Failed to parse captions from script #${this.captionsScript}:`, error);\n }\n }\n }\n\n // Priority 3: External captions file\n if (this.captionsSrc) {\n try {\n const response = await this.fetch(this.captionsSrc, { signal });\n return await response.json() as Caption;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(`Failed to load captions from ${this.captionsSrc}:`, error);\n }\n }\n\n // Priority 4: Transcription from target element\n if (this.targetElement && !this.hasCustomCaptionsData) {\n const transcriptionPath = this.transcriptionsPath();\n if (transcriptionPath) {\n try {\n const response = await this.fetch(transcriptionPath, { signal });\n this.#transcriptionData = await response.json() as GetISOBMFFFileTranscriptionResult;\n signal?.throwIfAborted();\n\n // Load fragment for current time\n if (this.#transcriptionData) {\n return this.#loadTranscriptionFragment(signal);\n }\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Transcription not available - not an error\n }\n }\n }\n\n return null;\n }\n\n async #loadTranscriptionFragment(signal?: AbortSignal): Promise<Caption | null> {\n if (!this.#transcriptionData) return null;\n\n const fragmentIndex = Math.floor(this.ownCurrentTimeMs / this.#transcriptionData.work_slice_ms);\n const fragmentPath = `${this.apiHost}/api/v1/transcriptions/${this.#transcriptionData.id}/fragments/${fragmentIndex}`;\n\n try {\n const response = await this.fetch(fragmentPath, { signal });\n return await response.json() as Caption;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(\"Failed to load transcription fragment:\", error);\n return null;\n }\n }\n\n /**\n * @deprecated Use FrameRenderable methods (prepareFrame, renderFrame) via FrameController instead.\n * This is a compatibility wrapper that delegates to the new system.\n */\n frameTask = createFrameTaskWrapper(this);\n\n // ============================================================================\n // FrameRenderable Implementation\n // Centralized frame control - no Lit Tasks\n // ============================================================================\n\n /**\n * Query readiness state for a given time.\n * @implements FrameRenderable\n */\n getFrameState(_timeMs: number): FrameState {\n // Check if captions data is loaded\n const hasData = this.#captionsDataLoaded && this.#captionsDataValue !== null;\n\n return {\n needsPreparation: !hasData,\n isReady: hasData,\n priority: PRIORITY_CAPTIONS,\n };\n }\n\n /**\n * Async preparation - waits for captions data to load.\n * @implements FrameRenderable\n */\n async prepareFrame(_timeMs: number, signal: AbortSignal): Promise<void> {\n await this.loadCaptionsData(signal);\n signal.throwIfAborted();\n }\n\n /**\n * Synchronous render - updates caption text containers.\n * Sets textContent directly on child elements (light DOM).\n * @implements FrameRenderable\n */\n renderFrame(_timeMs: number): void {\n // Update text containers by setting properties\n // Child elements update their textContent directly (light DOM)\n this.updateTextContainers();\n }\n\n // ============================================================================\n // End FrameRenderable Implementation\n // ============================================================================\n\n #rootTimegroupUpdateController?: ReactiveController;\n\n connectedCallback() {\n super.connectedCallback();\n\n // Start loading captions data\n this.loadCaptionsData().catch(() => {});\n\n // Try to get target element safely\n const target = this.targetSelector\n ? document.getElementById(this.targetSelector)\n : null;\n if (target && (target instanceof EFAudio || target instanceof EFVideo)) {\n new CrossUpdateController(target, this);\n }\n // For standalone captions with custom data, ensure proper timeline sync\n else if (this.hasCustomCaptionsData && this.rootTimegroup) {\n new CrossUpdateController(this.rootTimegroup, this);\n }\n\n // Ensure captions update when root timegroup's currentTimeMs changes\n if (this.rootTimegroup) {\n this.#rootTimegroupUpdateController = {\n hostUpdated: () => {\n Promise.resolve().then(() => {\n this.updateTextContainers();\n });\n },\n hostDisconnected: () => {\n this.#rootTimegroupUpdateController = undefined;\n },\n };\n this.rootTimegroup.addController(this.#rootTimegroupUpdateController);\n }\n\n // Prevent display:none from being set on the parent caption element.\n // IMPORTANT: This only applies to the parent <ef-captions> element, NOT to\n // caption child elements (<ef-captions-segment>, <ef-captions-active-word>, etc.).\n // Child elements MUST respect display:none for proper temporal visibility\n // in video rendering. Video export relies on display:none to hide elements\n // outside their time range.\n const observer = new MutationObserver(() => {\n if (this.style.display === \"none\") {\n this.style.removeProperty(\"display\");\n this.style.opacity = \"0\";\n this.style.pointerEvents = \"none\";\n } else if (!this.style.display || this.style.display === \"\") {\n this.style.removeProperty(\"opacity\");\n this.style.removeProperty(\"pointer-events\");\n }\n });\n observer.observe(this, { attributes: true, attributeFilter: [\"style\"] });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n if (this.#rootTimegroupUpdateController && this.rootTimegroup) {\n this.rootTimegroup.removeController(this.#rootTimegroupUpdateController);\n this.#rootTimegroupUpdateController = undefined;\n }\n }\n\n protected updated(\n changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,\n ): void {\n // Set up root timegroup controller if rootTimegroup is now available\n if (this.rootTimegroup && !this.#rootTimegroupUpdateController) {\n this.#rootTimegroupUpdateController = {\n hostUpdated: () => {\n Promise.resolve().then(() => {\n this.updateTextContainers();\n });\n },\n hostDisconnected: () => {\n this.#rootTimegroupUpdateController = undefined;\n },\n };\n this.rootTimegroup.addController(this.#rootTimegroupUpdateController);\n }\n\n // Clean up controller if rootTimegroup changed\n if (\n changedProperties.has(\"rootTimegroup\") &&\n this.#rootTimegroupUpdateController\n ) {\n const oldRootTimegroup = changedProperties.get(\"rootTimegroup\") as\n | EFTimegroup\n | undefined;\n if (oldRootTimegroup && oldRootTimegroup !== this.rootTimegroup) {\n oldRootTimegroup.removeController(this.#rootTimegroupUpdateController);\n this.#rootTimegroupUpdateController = undefined;\n }\n }\n\n this.updateTextContainers();\n\n // Force duration recalculation when custom captions data changes\n if (\n changedProperties.has(\"captionsData\") ||\n changedProperties.has(\"captionsSrc\") ||\n changedProperties.has(\"captionsScript\")\n ) {\n // Invalidate caches and reload\n this.#cachedIntrinsicDurationMs = null;\n this.#captionsDataLoaded = false;\n this.#captionsDataValue = null;\n this.loadCaptionsData().catch(() => {});\n\n this.requestUpdate(\"intrinsicDurationMs\");\n\n flushSequenceDurationCache();\n flushStartTimeMsCache();\n\n if (this.parentTimegroup) {\n this.parentTimegroup.requestUpdate(\"durationMs\");\n this.parentTimegroup.requestUpdate(\"currentTime\");\n }\n }\n\n // Update captions when timeline position changes\n if (changedProperties.has(\"ownCurrentTimeMs\")) {\n this.updateTextContainers();\n }\n }\n\n updateTextContainers() {\n const captionsData = this.#captionsDataValue;\n if (!captionsData) {\n return;\n }\n\n // For captions with custom data, try to use the video's source time\n let currentTimeMs = this.ownCurrentTimeMs;\n if (this.hasCustomCaptionsData && this.parentTimegroup) {\n const videoElement = Array.from(this.parentTimegroup.children).find(\n (child): child is EFVideo => child instanceof EFVideo,\n );\n if (videoElement) {\n const sourceInMs = videoElement.sourceInMs ?? 0;\n currentTimeMs = videoElement.currentSourceTimeMs - sourceInMs;\n currentTimeMs = Math.max(0, Math.min(currentTimeMs, this.durationMs));\n }\n }\n\n const currentTimeSec = currentTimeMs / 1000;\n\n // Find the current word from word_segments\n const currentWord = captionsData.word_segments.find(\n (word) => currentTimeSec >= word.start && currentTimeSec < word.end,\n );\n\n // Find the current segment\n const currentSegment = captionsData.segments.find(\n (segment) =>\n currentTimeSec >= segment.start && currentTimeSec < segment.end,\n );\n\n for (const wordContainer of this.activeWordContainers) {\n if (currentWord) {\n const wordIndex = captionsData.word_segments.findIndex(\n (w) =>\n w.start === currentWord.start &&\n w.end === currentWord.end &&\n w.text === currentWord.text,\n );\n wordContainer.wordIndex = wordIndex >= 0 ? wordIndex : 0;\n wordContainer.wordText = currentWord.text; // Sets textContent directly\n } else {\n wordContainer.wordText = \"\"; // Hides element\n }\n }\n\n for (const segmentContainer of this.segmentContainers) {\n if (currentSegment) {\n segmentContainer.segmentText = currentSegment.text; // Sets textContent directly\n } else {\n segmentContainer.segmentText = \"\"; // Hides element\n }\n }\n\n // Process context for both word and segment cases\n if (currentWord && currentSegment) {\n const segmentWords = captionsData.word_segments.filter(\n (word) =>\n word.start >= currentSegment.start && word.end <= currentSegment.end,\n );\n\n const currentWordIndex = segmentWords.findIndex(\n (word) =>\n word.start === currentWord.start && word.end === currentWord.end,\n );\n\n if (currentWordIndex !== -1) {\n const beforeWords = segmentWords\n .slice(0, currentWordIndex)\n .map((w) => w.text.trim())\n .join(\" \");\n\n const afterWords = segmentWords\n .slice(currentWordIndex + 1)\n .map((w) => w.text.trim())\n .join(\" \");\n\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = beforeWords; // Sets textContent directly\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = afterWords; // Sets textContent directly\n }\n }\n } else if (currentSegment) {\n const segmentWords = captionsData.word_segments.filter(\n (word) =>\n word.start >= currentSegment.start && word.end <= currentSegment.end,\n );\n\n const firstWord = segmentWords[0];\n const isBeforeFirstWord = firstWord && currentTimeSec < firstWord.start;\n\n if (isBeforeFirstWord) {\n const allWords = segmentWords.map((w) => w.text.trim()).join(\" \");\n\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = allWords; // Sets textContent directly\n }\n } else {\n const allCompletedWords = segmentWords\n .map((w) => w.text.trim())\n .join(\" \");\n\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = allCompletedWords; // Sets textContent directly\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n }\n } else {\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n }\n }\n\n get targetElement() {\n const target = document.getElementById(this.targetSelector ?? \"\");\n if (target instanceof EFAudio || target instanceof EFVideo) {\n return target;\n }\n if (this.hasCustomCaptionsData) {\n return null;\n }\n return null;\n }\n\n get hasCustomCaptionsData(): boolean {\n return !!(this.captionsData || this.captionsSrc || this.captionsScript);\n }\n\n get intrinsicDurationMs(): number | undefined {\n if (this.#cachedIntrinsicDurationMs !== null) {\n return this.#cachedIntrinsicDurationMs;\n }\n\n let captionsData: Caption | null = null;\n\n if (this.captionsData) {\n captionsData = this.captionsData;\n } else if (this.captionsScript) {\n const scriptElement = document.getElementById(this.captionsScript);\n if (scriptElement?.textContent) {\n try {\n captionsData = JSON.parse(scriptElement.textContent) as Caption;\n } catch {\n // Invalid JSON\n }\n }\n } else if (this.#captionsDataValue) {\n captionsData = this.#captionsDataValue;\n }\n\n if (!captionsData) {\n if (!this.captionsData && !this.captionsScript && !this.captionsSrc) {\n this.#cachedIntrinsicDurationMs = undefined;\n }\n return undefined;\n }\n\n let result: number;\n if (\n captionsData.segments.length === 0 &&\n captionsData.word_segments.length === 0\n ) {\n result = 0;\n } else {\n const maxSegmentEnd =\n captionsData.segments.length > 0\n ? captionsData.segments.reduce(\n (max, s) => (s.end > max ? s.end : max),\n 0,\n )\n : 0;\n const maxWordEnd =\n captionsData.word_segments.length > 0\n ? captionsData.word_segments.reduce(\n (max, w) => (w.end > max ? w.end : max),\n 0,\n )\n : 0;\n\n result = Math.max(maxSegmentEnd, maxWordEnd) * 1000;\n }\n\n this.#cachedIntrinsicDurationMs = result;\n return result;\n }\n\n get hasOwnDuration(): boolean {\n return !!(\n this.captionsData ||\n this.captionsScript ||\n this.#captionsDataValue\n );\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-captions\": EFCaptions;\n \"ef-captions-active-word\": EFCaptionsActiveWord;\n \"ef-captions-segment\": EFCaptionsSegment;\n \"ef-captions-before-active-word\": EFCaptionsBeforeActiveWord;\n \"ef-captions-after-active-word\": EFCaptionsAfterActiveWord;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"EFCaptions.js","names":["EFCaptionsActiveWord","#wordText","#wordIndex","EFCaptionsSegment","#segmentText","EFCaptionsBeforeActiveWord","EFCaptionsAfterActiveWord","EFCaptions","#captionsDataLoaded","#captionsDataValue","#captionsDataPromise","#doLoadCaptionsData","#transcriptionData","#loadTranscriptionFragment","#rootTimegroupUpdateController","#cachedIntrinsicDurationMs","captionsData: Caption | null","result: number"],"sources":["../../src/elements/EFCaptions.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport type { ReactiveController } from \"lit\";\nimport type { GetISOBMFFFileTranscriptionResult } from \"../../../api/src/index.js\";\nimport {\n type FrameRenderable,\n type FrameState,\n createFrameTaskWrapper,\n PRIORITY_CAPTIONS,\n} from \"../preview/FrameController.js\";\nimport { AsyncValue } from \"./EFMedia.js\";\nimport { CrossUpdateController } from \"./CrossUpdateController.js\";\nimport { EFAudio } from \"./EFAudio.js\";\nimport { EFSourceMixin } from \"./EFSourceMixin.js\";\nimport { EFTemporal, flushStartTimeMsCache } from \"./EFTemporal.js\";\nimport {\n flushSequenceDurationCache,\n EFTimegroup,\n} from \"./EFTimegroup.js\";\nimport { EFVideo } from \"./EFVideo.js\";\nimport { FetchMixin } from \"./FetchMixin.js\";\n\nexport interface WordSegment {\n text: string;\n start: number;\n end: number;\n}\n\nexport interface Segment {\n start: number;\n end: number;\n text: string;\n}\n\nexport interface Caption {\n segments: Segment[];\n word_segments: WordSegment[];\n}\n\nconst stopWords = new Set([\"\", \".\", \"!\", \"?\", \",\"]);\n\n/**\n * Caption active word element - displays the currently spoken word.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-active-word\")\nexport class EFCaptionsActiveWord extends HTMLElement {\n #wordText = \"\";\n #wordIndex = 0;\n \n set wordText(text: string) {\n this.#wordText = text;\n // Hide element if no content or only stop words\n if (!text || stopWords.has(text)) {\n this.hidden = true;\n this.textContent = \"\";\n } else {\n this.hidden = false;\n // Add trailing space to maintain consistent spacing with surrounding words\n this.textContent = text + \" \";\n }\n }\n \n get wordText(): string {\n return this.#wordText;\n }\n \n set wordIndex(index: number) {\n this.#wordIndex = index;\n // Set deterministic --ef-word-seed value based on word index\n const seed = (index * 9007) % 233; // Prime numbers for better distribution\n const seedValue = seed / 233; // Normalize to 0-1 range\n this.style.setProperty(\"--ef-word-seed\", seedValue.toString());\n }\n \n get wordIndex(): number {\n return this.#wordIndex;\n }\n}\n\n/**\n * Caption segment element - displays a full caption segment.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-segment\")\nexport class EFCaptionsSegment extends HTMLElement {\n #segmentText = \"\";\n \n set segmentText(text: string) {\n this.#segmentText = text;\n // Hide element if no content or only stop words\n if (!text || stopWords.has(text)) {\n this.hidden = true;\n this.textContent = \"\";\n } else {\n this.hidden = false;\n this.textContent = text;\n }\n }\n \n get segmentText(): string {\n return this.#segmentText;\n }\n}\n\n/**\n * Caption before-active-word element - displays words before the current word.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-before-active-word\")\nexport class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {\n set segmentText(text: string) {\n // Check if there's an active word by looking for sibling active word element\n const activeWord = this.closest(\"ef-captions\")?.querySelector(\n \"ef-captions-active-word\",\n ) as EFCaptionsActiveWord;\n const hasActiveWord = activeWord?.wordText;\n \n // Add trailing space if there's an active word coming after us\n const finalText = text && hasActiveWord ? text + \" \" : text;\n \n // Hide element if no content or only stop words\n if (!finalText || stopWords.has(finalText)) {\n this.hidden = true;\n this.textContent = \"\";\n } else {\n this.hidden = false;\n this.textContent = finalText;\n }\n }\n}\n\n/**\n * Caption after-active-word element - displays words after the current word.\n * Uses light DOM for simplicity - parent sets textContent directly.\n */\n@customElement(\"ef-captions-after-active-word\")\nexport class EFCaptionsAfterActiveWord extends EFCaptionsSegment {\n set segmentText(text: string) {\n // No leading space - active word will add trailing space\n const finalText = text;\n \n // Hide element if no content or only stop words\n if (!finalText || stopWords.has(finalText)) {\n this.hidden = true;\n this.textContent = \"\";\n } else {\n this.hidden = false;\n this.textContent = finalText;\n }\n }\n}\n\n@customElement(\"ef-captions\")\nexport class EFCaptions extends EFSourceMixin(\n EFTemporal(FetchMixin(LitElement)),\n { assetType: \"caption_files\" },\n) implements FrameRenderable {\n static styles = [\n css`\n :host {\n display: block;\n white-space: normal;\n line-height: 1;\n gap: 0;\n }\n ::slotted(*) {\n display: inline;\n margin: 0;\n padding: 0;\n }\n `,\n ];\n\n @property({ type: String, attribute: \"target\", reflect: true })\n targetSelector = \"\";\n\n set target(value: string) {\n this.targetSelector = value;\n }\n\n @property({ attribute: \"word-style\" })\n wordStyle = \"\";\n\n /**\n * URL or path to a JSON file containing custom captions data.\n * The JSON should conform to the Caption interface with 'segments' and 'word_segments' arrays.\n */\n @property({ type: String, attribute: \"captions-src\", reflect: true })\n captionsSrc = \"\";\n\n /**\n * Direct captions data object. Takes priority over captions-src and captions-script.\n * Should conform to the Caption interface with 'segments' and 'word_segments' arrays.\n */\n @property({ type: Object, attribute: false })\n captionsData: Caption | null = null;\n\n /**\n * ID of a <script> element containing JSON captions data.\n * The script's textContent should be valid JSON conforming to the Caption interface.\n */\n @property({ type: String, attribute: \"captions-script\", reflect: true })\n captionsScript = \"\";\n\n activeWordContainers = this.getElementsByTagName(\"ef-captions-active-word\");\n segmentContainers = this.getElementsByTagName(\"ef-captions-segment\");\n beforeActiveWordContainers = this.getElementsByTagName(\n \"ef-captions-before-active-word\",\n );\n afterActiveWordContainers = this.getElementsByTagName(\n \"ef-captions-after-active-word\",\n );\n\n // Cache for intrinsicDurationMs to avoid expensive O(n) recalculation every frame\n #cachedIntrinsicDurationMs: number | undefined | null = null; // null = not computed, undefined = no duration\n\n render() {\n return html`<slot></slot>`;\n }\n\n transcriptionsPath() {\n if (!this.targetElement) {\n return null;\n }\n if (this.targetElement.assetId) {\n return `${this.apiHost}/api/v1/isobmff_files/${this.targetElement.assetId}/transcription`;\n }\n return null;\n }\n\n captionsPath() {\n if (!this.targetElement) {\n return null;\n }\n if (this.targetElement.assetId) {\n return `${this.apiHost}/api/v1/caption_files/${this.targetElement.assetId}`;\n }\n const targetSrc = this.targetElement.src;\n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = targetSrc.startsWith(\"/\")\n ? targetSrc.slice(1)\n : targetSrc;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n // Use production API format for local files\n return `/api/v1/assets/local/captions?src=${encodeURIComponent(normalizedSrc)}`;\n }\n\n // ============================================================================\n // Captions Data Loading - async methods instead of Tasks\n // ============================================================================\n\n #captionsDataLoaded = false;\n #captionsDataPromise: Promise<Caption | null> | null = null;\n #captionsDataValue: Caption | null = null;\n #transcriptionData: GetISOBMFFFileTranscriptionResult | null = null;\n\n /**\n * AsyncValue wrapper for backwards compatibility\n */\n unifiedCaptionsDataTask = new AsyncValue<Caption | null>();\n\n /**\n * Load captions data from all possible sources\n */\n async loadCaptionsData(signal?: AbortSignal): Promise<Caption | null> {\n // Return cached if already loaded\n if (this.#captionsDataLoaded && this.#captionsDataValue) {\n return this.#captionsDataValue;\n }\n\n // Return in-flight promise\n if (this.#captionsDataPromise) {\n return this.#captionsDataPromise;\n }\n\n this.unifiedCaptionsDataTask.startPending();\n this.#captionsDataPromise = this.#doLoadCaptionsData(signal);\n\n try {\n this.#captionsDataValue = await this.#captionsDataPromise;\n this.#captionsDataLoaded = true;\n if (this.#captionsDataValue) {\n this.unifiedCaptionsDataTask.setValue(this.#captionsDataValue);\n }\n return this.#captionsDataValue;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(\"Failed to load captions data:\", error);\n return null;\n } finally {\n this.#captionsDataPromise = null;\n }\n }\n\n async #doLoadCaptionsData(signal?: AbortSignal): Promise<Caption | null> {\n // Priority 1: Direct captionsData property\n if (this.captionsData) {\n return this.captionsData;\n }\n\n // Priority 2: Script element reference\n if (this.captionsScript) {\n const scriptElement = document.getElementById(this.captionsScript);\n if (scriptElement?.textContent) {\n try {\n return JSON.parse(scriptElement.textContent) as Caption;\n } catch (error) {\n console.error(`Failed to parse captions from script #${this.captionsScript}:`, error);\n }\n }\n }\n\n // Priority 3: External captions file\n if (this.captionsSrc) {\n try {\n const response = await this.fetch(this.captionsSrc, { signal });\n return await response.json() as Caption;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(`Failed to load captions from ${this.captionsSrc}:`, error);\n }\n }\n\n // Priority 4: Transcription from target element\n if (this.targetElement && !this.hasCustomCaptionsData) {\n const transcriptionPath = this.transcriptionsPath();\n if (transcriptionPath) {\n try {\n const response = await this.fetch(transcriptionPath, { signal });\n this.#transcriptionData = await response.json() as GetISOBMFFFileTranscriptionResult;\n signal?.throwIfAborted();\n\n // Load fragment for current time\n if (this.#transcriptionData) {\n return this.#loadTranscriptionFragment(signal);\n }\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Transcription not available - not an error\n }\n }\n }\n\n return null;\n }\n\n async #loadTranscriptionFragment(signal?: AbortSignal): Promise<Caption | null> {\n if (!this.#transcriptionData) return null;\n\n const fragmentIndex = Math.floor(this.ownCurrentTimeMs / this.#transcriptionData.work_slice_ms);\n const fragmentPath = `${this.apiHost}/api/v1/transcriptions/${this.#transcriptionData.id}/fragments/${fragmentIndex}`;\n\n try {\n const response = await this.fetch(fragmentPath, { signal });\n return await response.json() as Caption;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.error(\"Failed to load transcription fragment:\", error);\n return null;\n }\n }\n\n /**\n * @deprecated Use FrameRenderable methods (prepareFrame, renderFrame) via FrameController instead.\n * This is a compatibility wrapper that delegates to the new system.\n */\n frameTask = createFrameTaskWrapper(this);\n\n // ============================================================================\n // FrameRenderable Implementation\n // Centralized frame control - no Lit Tasks\n // ============================================================================\n\n /**\n * Query readiness state for a given time.\n * @implements FrameRenderable\n */\n getFrameState(_timeMs: number): FrameState {\n // Check if captions data is loaded\n const hasData = this.#captionsDataLoaded && this.#captionsDataValue !== null;\n\n return {\n needsPreparation: !hasData,\n isReady: hasData,\n priority: PRIORITY_CAPTIONS,\n };\n }\n\n /**\n * Async preparation - waits for captions data to load.\n * @implements FrameRenderable\n */\n async prepareFrame(_timeMs: number, signal: AbortSignal): Promise<void> {\n await this.loadCaptionsData(signal);\n signal.throwIfAborted();\n }\n\n /**\n * Synchronous render - updates caption text containers.\n * Sets textContent directly on child elements (light DOM).\n * @implements FrameRenderable\n */\n renderFrame(_timeMs: number): void {\n // Update text containers by setting properties\n // Child elements update their textContent directly (light DOM)\n this.updateTextContainers();\n }\n\n // ============================================================================\n // End FrameRenderable Implementation\n // ============================================================================\n\n #rootTimegroupUpdateController?: ReactiveController;\n\n connectedCallback() {\n super.connectedCallback();\n\n // Start loading captions data\n this.loadCaptionsData().catch(() => {});\n\n // Try to get target element safely\n const target = this.targetSelector\n ? document.getElementById(this.targetSelector)\n : null;\n if (target && (target instanceof EFAudio || target instanceof EFVideo)) {\n new CrossUpdateController(target, this);\n }\n // For standalone captions with custom data, ensure proper timeline sync\n else if (this.hasCustomCaptionsData && this.rootTimegroup) {\n new CrossUpdateController(this.rootTimegroup, this);\n }\n\n // Ensure captions update when root timegroup's currentTimeMs changes\n if (this.rootTimegroup) {\n this.#rootTimegroupUpdateController = {\n hostUpdated: () => {\n Promise.resolve().then(() => {\n this.updateTextContainers();\n });\n },\n hostDisconnected: () => {\n this.#rootTimegroupUpdateController = undefined;\n },\n };\n this.rootTimegroup.addController(this.#rootTimegroupUpdateController);\n }\n\n // Prevent display:none from being set on the parent caption element.\n // IMPORTANT: This only applies to the parent <ef-captions> element, NOT to\n // caption child elements (<ef-captions-segment>, <ef-captions-active-word>, etc.).\n // Child elements MUST respect display:none for proper temporal visibility\n // in video rendering. Video export relies on display:none to hide elements\n // outside their time range.\n const observer = new MutationObserver(() => {\n if (this.style.display === \"none\") {\n this.style.removeProperty(\"display\");\n this.style.opacity = \"0\";\n this.style.pointerEvents = \"none\";\n } else if (!this.style.display || this.style.display === \"\") {\n this.style.removeProperty(\"opacity\");\n this.style.removeProperty(\"pointer-events\");\n }\n });\n observer.observe(this, { attributes: true, attributeFilter: [\"style\"] });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n if (this.#rootTimegroupUpdateController && this.rootTimegroup) {\n this.rootTimegroup.removeController(this.#rootTimegroupUpdateController);\n this.#rootTimegroupUpdateController = undefined;\n }\n }\n\n protected updated(\n changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,\n ): void {\n // Set up root timegroup controller if rootTimegroup is now available\n if (this.rootTimegroup && !this.#rootTimegroupUpdateController) {\n this.#rootTimegroupUpdateController = {\n hostUpdated: () => {\n Promise.resolve().then(() => {\n this.updateTextContainers();\n });\n },\n hostDisconnected: () => {\n this.#rootTimegroupUpdateController = undefined;\n },\n };\n this.rootTimegroup.addController(this.#rootTimegroupUpdateController);\n }\n\n // Clean up controller if rootTimegroup changed\n if (\n changedProperties.has(\"rootTimegroup\") &&\n this.#rootTimegroupUpdateController\n ) {\n const oldRootTimegroup = changedProperties.get(\"rootTimegroup\") as\n | EFTimegroup\n | undefined;\n if (oldRootTimegroup && oldRootTimegroup !== this.rootTimegroup) {\n oldRootTimegroup.removeController(this.#rootTimegroupUpdateController);\n this.#rootTimegroupUpdateController = undefined;\n }\n }\n\n this.updateTextContainers();\n\n // Force duration recalculation when custom captions data changes\n if (\n changedProperties.has(\"captionsData\") ||\n changedProperties.has(\"captionsSrc\") ||\n changedProperties.has(\"captionsScript\")\n ) {\n // Invalidate caches and reload\n this.#cachedIntrinsicDurationMs = null;\n this.#captionsDataLoaded = false;\n this.#captionsDataValue = null;\n this.loadCaptionsData().catch(() => {});\n\n this.requestUpdate(\"intrinsicDurationMs\");\n\n flushSequenceDurationCache();\n flushStartTimeMsCache();\n\n if (this.parentTimegroup) {\n this.parentTimegroup.requestUpdate(\"durationMs\");\n this.parentTimegroup.requestUpdate(\"currentTime\");\n }\n }\n\n // Update captions when timeline position changes\n if (changedProperties.has(\"ownCurrentTimeMs\")) {\n this.updateTextContainers();\n }\n }\n\n updateTextContainers() {\n const captionsData = this.#captionsDataValue;\n if (!captionsData) {\n return;\n }\n\n // For captions with custom data, try to use the video's source time\n let currentTimeMs = this.ownCurrentTimeMs;\n if (this.hasCustomCaptionsData && this.parentTimegroup) {\n const videoElement = Array.from(this.parentTimegroup.children).find(\n (child): child is EFVideo => child instanceof EFVideo,\n );\n if (videoElement) {\n const sourceInMs = videoElement.sourceInMs ?? 0;\n currentTimeMs = videoElement.currentSourceTimeMs - sourceInMs;\n currentTimeMs = Math.max(0, Math.min(currentTimeMs, this.durationMs));\n }\n }\n\n const currentTimeSec = currentTimeMs / 1000;\n\n // Find the current word from word_segments\n const currentWord = captionsData.word_segments.find(\n (word) => currentTimeSec >= word.start && currentTimeSec < word.end,\n );\n\n // Find the current segment\n const currentSegment = captionsData.segments.find(\n (segment) =>\n currentTimeSec >= segment.start && currentTimeSec < segment.end,\n );\n\n for (const wordContainer of this.activeWordContainers) {\n if (currentWord) {\n const wordIndex = captionsData.word_segments.findIndex(\n (w) =>\n w.start === currentWord.start &&\n w.end === currentWord.end &&\n w.text === currentWord.text,\n );\n wordContainer.wordIndex = wordIndex >= 0 ? wordIndex : 0;\n wordContainer.wordText = currentWord.text; // Sets textContent directly\n } else {\n wordContainer.wordText = \"\"; // Hides element\n }\n }\n\n for (const segmentContainer of this.segmentContainers) {\n if (currentSegment) {\n segmentContainer.segmentText = currentSegment.text; // Sets textContent directly\n } else {\n segmentContainer.segmentText = \"\"; // Hides element\n }\n }\n\n // Process context for both word and segment cases\n if (currentWord && currentSegment) {\n const segmentWords = captionsData.word_segments.filter(\n (word) =>\n word.start >= currentSegment.start && word.end <= currentSegment.end,\n );\n\n const currentWordIndex = segmentWords.findIndex(\n (word) =>\n word.start === currentWord.start && word.end === currentWord.end,\n );\n\n if (currentWordIndex !== -1) {\n const beforeWords = segmentWords\n .slice(0, currentWordIndex)\n .map((w) => w.text.trim())\n .join(\" \");\n\n const afterWords = segmentWords\n .slice(currentWordIndex + 1)\n .map((w) => w.text.trim())\n .join(\" \");\n\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = beforeWords; // Sets textContent directly\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = afterWords; // Sets textContent directly\n }\n }\n } else if (currentSegment) {\n const segmentWords = captionsData.word_segments.filter(\n (word) =>\n word.start >= currentSegment.start && word.end <= currentSegment.end,\n );\n\n const firstWord = segmentWords[0];\n const isBeforeFirstWord = firstWord && currentTimeSec < firstWord.start;\n\n if (isBeforeFirstWord) {\n const allWords = segmentWords.map((w) => w.text.trim()).join(\" \");\n\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = allWords; // Sets textContent directly\n }\n } else {\n const allCompletedWords = segmentWords\n .map((w) => w.text.trim())\n .join(\" \");\n\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = allCompletedWords; // Sets textContent directly\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n }\n } else {\n for (const container of this.beforeActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n\n for (const container of this.afterActiveWordContainers) {\n container.segmentText = \"\"; // Hides element\n }\n }\n }\n\n get targetElement() {\n const target = document.getElementById(this.targetSelector ?? \"\");\n if (target instanceof EFAudio || target instanceof EFVideo) {\n return target;\n }\n if (this.hasCustomCaptionsData) {\n return null;\n }\n return null;\n }\n\n get hasCustomCaptionsData(): boolean {\n return !!(this.captionsData || this.captionsSrc || this.captionsScript);\n }\n\n get intrinsicDurationMs(): number | undefined {\n if (this.#cachedIntrinsicDurationMs !== null) {\n return this.#cachedIntrinsicDurationMs;\n }\n\n let captionsData: Caption | null = null;\n\n if (this.captionsData) {\n captionsData = this.captionsData;\n } else if (this.captionsScript) {\n const scriptElement = document.getElementById(this.captionsScript);\n if (scriptElement?.textContent) {\n try {\n captionsData = JSON.parse(scriptElement.textContent) as Caption;\n } catch {\n // Invalid JSON\n }\n }\n } else if (this.#captionsDataValue) {\n captionsData = this.#captionsDataValue;\n }\n\n if (!captionsData) {\n if (!this.captionsData && !this.captionsScript && !this.captionsSrc) {\n this.#cachedIntrinsicDurationMs = undefined;\n }\n return undefined;\n }\n\n let result: number;\n if (\n captionsData.segments.length === 0 &&\n captionsData.word_segments.length === 0\n ) {\n result = 0;\n } else {\n const maxSegmentEnd =\n captionsData.segments.length > 0\n ? captionsData.segments.reduce(\n (max, s) => (s.end > max ? s.end : max),\n 0,\n )\n : 0;\n const maxWordEnd =\n captionsData.word_segments.length > 0\n ? captionsData.word_segments.reduce(\n (max, w) => (w.end > max ? w.end : max),\n 0,\n )\n : 0;\n\n result = Math.max(maxSegmentEnd, maxWordEnd) * 1000;\n }\n\n this.#cachedIntrinsicDurationMs = result;\n return result;\n }\n\n get hasOwnDuration(): boolean {\n return !!(\n this.captionsData ||\n this.captionsScript ||\n this.#captionsDataValue\n );\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-captions\": EFCaptions;\n \"ef-captions-active-word\": EFCaptionsActiveWord;\n \"ef-captions-segment\": EFCaptionsSegment;\n \"ef-captions-before-active-word\": EFCaptionsBeforeActiveWord;\n \"ef-captions-after-active-word\": EFCaptionsAfterActiveWord;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAuCA,MAAM,YAAY,IAAI,IAAI;CAAC;CAAI;CAAK;CAAK;CAAK;CAAI,CAAC;AAO5C,iCAAMA,+BAA6B,YAAY;CACpD,YAAY;CACZ,aAAa;CAEb,IAAI,SAAS,MAAc;AACzB,QAAKC,WAAY;AAEjB,MAAI,CAAC,QAAQ,UAAU,IAAI,KAAK,EAAE;AAChC,QAAK,SAAS;AACd,QAAK,cAAc;SACd;AACL,QAAK,SAAS;AAEd,QAAK,cAAc,OAAO;;;CAI9B,IAAI,WAAmB;AACrB,SAAO,MAAKA;;CAGd,IAAI,UAAU,OAAe;AAC3B,QAAKC,YAAa;EAGlB,MAAM,YADQ,QAAQ,OAAQ,MACL;AACzB,OAAK,MAAM,YAAY,kBAAkB,UAAU,UAAU,CAAC;;CAGhE,IAAI,YAAoB;AACtB,SAAO,MAAKA;;;mCA/Bf,cAAc,0BAA0B;AAwClC,8BAAMC,4BAA0B,YAAY;CACjD,eAAe;CAEf,IAAI,YAAY,MAAc;AAC5B,QAAKC,cAAe;AAEpB,MAAI,CAAC,QAAQ,UAAU,IAAI,KAAK,EAAE;AAChC,QAAK,SAAS;AACd,QAAK,cAAc;SACd;AACL,QAAK,SAAS;AACd,QAAK,cAAc;;;CAIvB,IAAI,cAAsB;AACxB,SAAO,MAAKA;;;gCAjBf,cAAc,sBAAsB;AA0B9B,uCAAMC,qCAAmC,kBAAkB;CAChE,IAAI,YAAY,MAAc;EAK5B,MAAM,iBAHa,KAAK,QAAQ,cAAc,EAAE,cAC9C,0BACD,GACiC;EAGlC,MAAM,YAAY,QAAQ,gBAAgB,OAAO,MAAM;AAGvD,MAAI,CAAC,aAAa,UAAU,IAAI,UAAU,EAAE;AAC1C,QAAK,SAAS;AACd,QAAK,cAAc;SACd;AACL,QAAK,SAAS;AACd,QAAK,cAAc;;;;yCAlBxB,cAAc,iCAAiC;AA4BzC,sCAAMC,oCAAkC,kBAAkB;CAC/D,IAAI,YAAY,MAAc;EAE5B,MAAM,YAAY;AAGlB,MAAI,CAAC,aAAa,UAAU,IAAI,UAAU,EAAE;AAC1C,QAAK,SAAS;AACd,QAAK,cAAc;SACd;AACL,QAAK,SAAS;AACd,QAAK,cAAc;;;;wCAZxB,cAAc,gCAAgC;AAkBxC,uBAAMC,qBAAmB,cAC9B,WAAW,WAAW,WAAW,CAAC,EAClC,EAAE,WAAW,iBAAiB,CAC/B,CAA4B;;;wBAkBV;mBAOL;qBAOE;sBAOiB;wBAOd;8BAEM,KAAK,qBAAqB,0BAA0B;2BACvD,KAAK,qBAAqB,sBAAsB;oCACvC,KAAK,qBAChC,iCACD;mCAC2B,KAAK,qBAC/B,gCACD;iCAgDyB,IAAI,YAA4B;mBAmH9C,uBAAuB,KAAK;;;gBAzNxB,CACd,GAAG;;;;;;;;;;;;MAaJ;;CAKD,IAAI,OAAO,OAAe;AACxB,OAAK,iBAAiB;;CAqCxB,6BAAwD;CAExD,SAAS;AACP,SAAO,IAAI;;CAGb,qBAAqB;AACnB,MAAI,CAAC,KAAK,cACR,QAAO;AAET,MAAI,KAAK,cAAc,QACrB,QAAO,GAAG,KAAK,QAAQ,wBAAwB,KAAK,cAAc,QAAQ;AAE5E,SAAO;;CAGT,eAAe;AACb,MAAI,CAAC,KAAK,cACR,QAAO;AAET,MAAI,KAAK,cAAc,QACrB,QAAO,GAAG,KAAK,QAAQ,wBAAwB,KAAK,cAAc;EAEpE,MAAM,YAAY,KAAK,cAAc;EAErC,IAAI,gBAAgB,UAAU,WAAW,IAAI,GACzC,UAAU,MAAM,EAAE,GAClB;AACJ,kBAAgB,cAAc,QAAQ,QAAQ,GAAG;AAEjD,SAAO,qCAAqC,mBAAmB,cAAc;;CAO/E,sBAAsB;CACtB,uBAAuD;CACvD,qBAAqC;CACrC,qBAA+D;;;;CAU/D,MAAM,iBAAiB,QAA+C;AAEpE,MAAI,MAAKC,sBAAuB,MAAKC,kBACnC,QAAO,MAAKA;AAId,MAAI,MAAKC,oBACP,QAAO,MAAKA;AAGd,OAAK,wBAAwB,cAAc;AAC3C,QAAKA,sBAAuB,MAAKC,mBAAoB,OAAO;AAE5D,MAAI;AACF,SAAKF,oBAAqB,MAAM,MAAKC;AACrC,SAAKF,qBAAsB;AAC3B,OAAI,MAAKC,kBACP,MAAK,wBAAwB,SAAS,MAAKA,kBAAmB;AAEhE,UAAO,MAAKA;WACL,OAAO;AACd,OAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAER,WAAQ,MAAM,iCAAiC,MAAM;AACrD,UAAO;YACC;AACR,SAAKC,sBAAuB;;;CAIhC,OAAMC,mBAAoB,QAA+C;AAEvE,MAAI,KAAK,aACP,QAAO,KAAK;AAId,MAAI,KAAK,gBAAgB;GACvB,MAAM,gBAAgB,SAAS,eAAe,KAAK,eAAe;AAClE,OAAI,eAAe,YACjB,KAAI;AACF,WAAO,KAAK,MAAM,cAAc,YAAY;YACrC,OAAO;AACd,YAAQ,MAAM,yCAAyC,KAAK,eAAe,IAAI,MAAM;;;AAM3F,MAAI,KAAK,YACP,KAAI;AAEF,UAAO,OADU,MAAM,KAAK,MAAM,KAAK,aAAa,EAAE,QAAQ,CAAC,EACzC,MAAM;WACrB,OAAO;AACd,OAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAER,WAAQ,MAAM,gCAAgC,KAAK,YAAY,IAAI,MAAM;;AAK7E,MAAI,KAAK,iBAAiB,CAAC,KAAK,uBAAuB;GACrD,MAAM,oBAAoB,KAAK,oBAAoB;AACnD,OAAI,kBACF,KAAI;AAEF,UAAKC,oBAAqB,OADT,MAAM,KAAK,MAAM,mBAAmB,EAAE,QAAQ,CAAC,EACvB,MAAM;AAC/C,YAAQ,gBAAgB;AAGxB,QAAI,MAAKA,kBACP,QAAO,MAAKC,0BAA2B,OAAO;YAEzC,OAAO;AACd,QAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;;;AAOd,SAAO;;CAGT,OAAMA,0BAA2B,QAA+C;AAC9E,MAAI,CAAC,MAAKD,kBAAoB,QAAO;EAErC,MAAM,gBAAgB,KAAK,MAAM,KAAK,mBAAmB,MAAKA,kBAAmB,cAAc;EAC/F,MAAM,eAAe,GAAG,KAAK,QAAQ,yBAAyB,MAAKA,kBAAmB,GAAG,aAAa;AAEtG,MAAI;AAEF,UAAO,OADU,MAAM,KAAK,MAAM,cAAc,EAAE,QAAQ,CAAC,EACrC,MAAM;WACrB,OAAO;AACd,OAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAER,WAAQ,MAAM,0CAA0C,MAAM;AAC9D,UAAO;;;;;;;CAmBX,cAAc,SAA6B;EAEzC,MAAM,UAAU,MAAKJ,sBAAuB,MAAKC,sBAAuB;AAExE,SAAO;GACL,kBAAkB,CAAC;GACnB,SAAS;GACT,UAAU;GACX;;;;;;CAOH,MAAM,aAAa,SAAiB,QAAoC;AACtE,QAAM,KAAK,iBAAiB,OAAO;AACnC,SAAO,gBAAgB;;;;;;;CAQzB,YAAY,SAAuB;AAGjC,OAAK,sBAAsB;;CAO7B;CAEA,oBAAoB;AAClB,QAAM,mBAAmB;AAGzB,OAAK,kBAAkB,CAAC,YAAY,GAAG;EAGvC,MAAM,SAAS,KAAK,iBAChB,SAAS,eAAe,KAAK,eAAe,GAC5C;AACJ,MAAI,WAAW,kBAAkB,WAAW,kBAAkB,SAC5D,KAAI,sBAAsB,QAAQ,KAAK;WAGhC,KAAK,yBAAyB,KAAK,cAC1C,KAAI,sBAAsB,KAAK,eAAe,KAAK;AAIrD,MAAI,KAAK,eAAe;AACtB,SAAKK,gCAAiC;IACpC,mBAAmB;AACjB,aAAQ,SAAS,CAAC,WAAW;AAC3B,WAAK,sBAAsB;OAC3B;;IAEJ,wBAAwB;AACtB,WAAKA,gCAAiC;;IAEzC;AACD,QAAK,cAAc,cAAc,MAAKA,8BAA+B;;AAmBvE,EAViB,IAAI,uBAAuB;AAC1C,OAAI,KAAK,MAAM,YAAY,QAAQ;AACjC,SAAK,MAAM,eAAe,UAAU;AACpC,SAAK,MAAM,UAAU;AACrB,SAAK,MAAM,gBAAgB;cAClB,CAAC,KAAK,MAAM,WAAW,KAAK,MAAM,YAAY,IAAI;AAC3D,SAAK,MAAM,eAAe,UAAU;AACpC,SAAK,MAAM,eAAe,iBAAiB;;IAE7C,CACO,QAAQ,MAAM;GAAE,YAAY;GAAM,iBAAiB,CAAC,QAAQ;GAAE,CAAC;;CAG1E,uBAAuB;AACrB,QAAM,sBAAsB;AAC5B,MAAI,MAAKA,iCAAkC,KAAK,eAAe;AAC7D,QAAK,cAAc,iBAAiB,MAAKA,8BAA+B;AACxE,SAAKA,gCAAiC;;;CAI1C,AAAU,QACR,mBACM;AAEN,MAAI,KAAK,iBAAiB,CAAC,MAAKA,+BAAgC;AAC9D,SAAKA,gCAAiC;IACpC,mBAAmB;AACjB,aAAQ,SAAS,CAAC,WAAW;AAC3B,WAAK,sBAAsB;OAC3B;;IAEJ,wBAAwB;AACtB,WAAKA,gCAAiC;;IAEzC;AACD,QAAK,cAAc,cAAc,MAAKA,8BAA+B;;AAIvE,MACE,kBAAkB,IAAI,gBAAgB,IACtC,MAAKA,+BACL;GACA,MAAM,mBAAmB,kBAAkB,IAAI,gBAAgB;AAG/D,OAAI,oBAAoB,qBAAqB,KAAK,eAAe;AAC/D,qBAAiB,iBAAiB,MAAKA,8BAA+B;AACtE,UAAKA,gCAAiC;;;AAI1C,OAAK,sBAAsB;AAG3B,MACE,kBAAkB,IAAI,eAAe,IACrC,kBAAkB,IAAI,cAAc,IACpC,kBAAkB,IAAI,iBAAiB,EACvC;AAEA,SAAKC,4BAA6B;AAClC,SAAKP,qBAAsB;AAC3B,SAAKC,oBAAqB;AAC1B,QAAK,kBAAkB,CAAC,YAAY,GAAG;AAEvC,QAAK,cAAc,sBAAsB;AAEzC,+BAA4B;AAC5B,0BAAuB;AAEvB,OAAI,KAAK,iBAAiB;AACxB,SAAK,gBAAgB,cAAc,aAAa;AAChD,SAAK,gBAAgB,cAAc,cAAc;;;AAKrD,MAAI,kBAAkB,IAAI,mBAAmB,CAC3C,MAAK,sBAAsB;;CAI/B,uBAAuB;EACrB,MAAM,eAAe,MAAKA;AAC1B,MAAI,CAAC,aACH;EAIF,IAAI,gBAAgB,KAAK;AACzB,MAAI,KAAK,yBAAyB,KAAK,iBAAiB;GACtD,MAAM,eAAe,MAAM,KAAK,KAAK,gBAAgB,SAAS,CAAC,MAC5D,UAA4B,iBAAiB,QAC/C;AACD,OAAI,cAAc;IAChB,MAAM,aAAa,aAAa,cAAc;AAC9C,oBAAgB,aAAa,sBAAsB;AACnD,oBAAgB,KAAK,IAAI,GAAG,KAAK,IAAI,eAAe,KAAK,WAAW,CAAC;;;EAIzE,MAAM,iBAAiB,gBAAgB;EAGvC,MAAM,cAAc,aAAa,cAAc,MAC5C,SAAS,kBAAkB,KAAK,SAAS,iBAAiB,KAAK,IACjE;EAGD,MAAM,iBAAiB,aAAa,SAAS,MAC1C,YACC,kBAAkB,QAAQ,SAAS,iBAAiB,QAAQ,IAC/D;AAED,OAAK,MAAM,iBAAiB,KAAK,qBAC/B,KAAI,aAAa;GACf,MAAM,YAAY,aAAa,cAAc,WAC1C,MACC,EAAE,UAAU,YAAY,SACxB,EAAE,QAAQ,YAAY,OACtB,EAAE,SAAS,YAAY,KAC1B;AACD,iBAAc,YAAY,aAAa,IAAI,YAAY;AACvD,iBAAc,WAAW,YAAY;QAErC,eAAc,WAAW;AAI7B,OAAK,MAAM,oBAAoB,KAAK,kBAClC,KAAI,eACF,kBAAiB,cAAc,eAAe;MAE9C,kBAAiB,cAAc;AAKnC,MAAI,eAAe,gBAAgB;GACjC,MAAM,eAAe,aAAa,cAAc,QAC7C,SACC,KAAK,SAAS,eAAe,SAAS,KAAK,OAAO,eAAe,IACpE;GAED,MAAM,mBAAmB,aAAa,WACnC,SACC,KAAK,UAAU,YAAY,SAAS,KAAK,QAAQ,YAAY,IAChE;AAED,OAAI,qBAAqB,IAAI;IAC3B,MAAM,cAAc,aACjB,MAAM,GAAG,iBAAiB,CAC1B,KAAK,MAAM,EAAE,KAAK,MAAM,CAAC,CACzB,KAAK,IAAI;IAEZ,MAAM,aAAa,aAChB,MAAM,mBAAmB,EAAE,CAC3B,KAAK,MAAM,EAAE,KAAK,MAAM,CAAC,CACzB,KAAK,IAAI;AAEZ,SAAK,MAAM,aAAa,KAAK,2BAC3B,WAAU,cAAc;AAG1B,SAAK,MAAM,aAAa,KAAK,0BAC3B,WAAU,cAAc;;aAGnB,gBAAgB;GACzB,MAAM,eAAe,aAAa,cAAc,QAC7C,SACC,KAAK,SAAS,eAAe,SAAS,KAAK,OAAO,eAAe,IACpE;GAED,MAAM,YAAY,aAAa;AAG/B,OAF0B,aAAa,iBAAiB,UAAU,OAE3C;IACrB,MAAM,WAAW,aAAa,KAAK,MAAM,EAAE,KAAK,MAAM,CAAC,CAAC,KAAK,IAAI;AAEjE,SAAK,MAAM,aAAa,KAAK,2BAC3B,WAAU,cAAc;AAG1B,SAAK,MAAM,aAAa,KAAK,0BAC3B,WAAU,cAAc;UAErB;IACL,MAAM,oBAAoB,aACvB,KAAK,MAAM,EAAE,KAAK,MAAM,CAAC,CACzB,KAAK,IAAI;AAEZ,SAAK,MAAM,aAAa,KAAK,2BAC3B,WAAU,cAAc;AAG1B,SAAK,MAAM,aAAa,KAAK,0BAC3B,WAAU,cAAc;;SAGvB;AACL,QAAK,MAAM,aAAa,KAAK,2BAC3B,WAAU,cAAc;AAG1B,QAAK,MAAM,aAAa,KAAK,0BAC3B,WAAU,cAAc;;;CAK9B,IAAI,gBAAgB;EAClB,MAAM,SAAS,SAAS,eAAe,KAAK,kBAAkB,GAAG;AACjE,MAAI,kBAAkB,WAAW,kBAAkB,QACjD,QAAO;AAET,MAAI,KAAK,sBACP,QAAO;AAET,SAAO;;CAGT,IAAI,wBAAiC;AACnC,SAAO,CAAC,EAAE,KAAK,gBAAgB,KAAK,eAAe,KAAK;;CAG1D,IAAI,sBAA0C;AAC5C,MAAI,MAAKM,8BAA+B,KACtC,QAAO,MAAKA;EAGd,IAAIC,eAA+B;AAEnC,MAAI,KAAK,aACP,gBAAe,KAAK;WACX,KAAK,gBAAgB;GAC9B,MAAM,gBAAgB,SAAS,eAAe,KAAK,eAAe;AAClE,OAAI,eAAe,YACjB,KAAI;AACF,mBAAe,KAAK,MAAM,cAAc,YAAY;WAC9C;aAID,MAAKP,kBACd,gBAAe,MAAKA;AAGtB,MAAI,CAAC,cAAc;AACjB,OAAI,CAAC,KAAK,gBAAgB,CAAC,KAAK,kBAAkB,CAAC,KAAK,YACtD,OAAKM,4BAA6B;AAEpC;;EAGF,IAAIE;AACJ,MACE,aAAa,SAAS,WAAW,KACjC,aAAa,cAAc,WAAW,EAEtC,UAAS;OACJ;GACL,MAAM,gBACJ,aAAa,SAAS,SAAS,IAC3B,aAAa,SAAS,QACnB,KAAK,MAAO,EAAE,MAAM,MAAM,EAAE,MAAM,KACnC,EACD,GACD;GACN,MAAM,aACJ,aAAa,cAAc,SAAS,IAChC,aAAa,cAAc,QACxB,KAAK,MAAO,EAAE,MAAM,MAAM,EAAE,MAAM,KACnC,EACD,GACD;AAEN,YAAS,KAAK,IAAI,eAAe,WAAW,GAAG;;AAGjD,QAAKF,4BAA6B;AAClC,SAAO;;CAGT,IAAI,iBAA0B;AAC5B,SAAO,CAAC,EACN,KAAK,gBACL,KAAK,kBACL,MAAKN;;;YAlkBR,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAU,SAAS;CAAM,CAAC;YAO9D,SAAS,EAAE,WAAW,cAAc,CAAC;YAOrC,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAgB,SAAS;CAAM,CAAC;YAOpE,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAO5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAmB,SAAS;CAAM,CAAC;yBAjDzE,cAAc,cAAc"}
@@ -24,10 +24,10 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
24
24
  justify-content: center;
25
25
  }
26
26
  canvas, img {
27
- position: static;
28
- all: initial;
29
27
  width: 100%;
30
28
  height: 100%;
29
+ object-fit: var(--object-fit, contain);
30
+ object-position: var(--object-position, center);
31
31
  }
32
32
  `];
33
33
  }
@@ -118,6 +118,7 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
118
118
  signal?.throwIfAborted();
119
119
  const mimeType = blob.type.toLowerCase();
120
120
  this.#hasAlpha = !mimeType.includes("jpeg") && !mimeType.includes("jpg");
121
+ console.log(`[EFImage] Loaded image: mimeType=${mimeType}, hasAlpha=${this.#hasAlpha}`);
121
122
  image.src = URL.createObjectURL(blob);
122
123
  await new Promise((resolve, reject) => {
123
124
  if (signal?.aborted) {
@@ -175,12 +176,20 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
175
176
  renderFrame(_timeMs) {}
176
177
  updated(changedProperties) {
177
178
  super.updated(changedProperties);
179
+ this.#syncObjectFitStyles();
178
180
  if (changedProperties.has("src") || changedProperties.has("assetId")) {
179
181
  this.#imageLoaded = false;
180
182
  this.loadImage().catch(() => {});
181
183
  this.#renderVersion++;
182
184
  }
183
185
  }
186
+ #syncObjectFitStyles() {
187
+ const computedStyle = getComputedStyle(this);
188
+ const objectFit = computedStyle.objectFit;
189
+ const objectPosition = computedStyle.objectPosition;
190
+ if (objectFit && objectFit !== "fill") this.style.setProperty("--object-fit", objectFit);
191
+ if (objectPosition) this.style.setProperty("--object-position", objectPosition);
192
+ }
184
193
  /**
185
194
  * Get the natural dimensions of the image.
186
195
  * Returns null if the image hasn't loaded yet.
@@ -1 +1 @@
1
- {"version":3,"file":"EFImage.js","names":["EFImage","#renderVersion","#hasAlpha","#assetId","#imageLoaded","#lastLoadedPath","#imageLoadPromise","#doLoadImage"],"sources":["../../src/elements/EFImage.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport {\n type FrameRenderable,\n type FrameState,\n createFrameTaskWrapper,\n PRIORITY_IMAGE,\n} from \"../preview/FrameController.js\";\nimport { EFSourceMixin } from \"./EFSourceMixin.js\";\nimport { EFTemporal } from \"./EFTemporal.js\";\nimport { FetchMixin } from \"./FetchMixin.js\";\n\n@customElement(\"ef-image\")\nexport class EFImage extends EFTemporal(\n EFSourceMixin(FetchMixin(LitElement), {\n assetType: \"image_files\",\n }),\n) implements FrameRenderable {\n static styles = [\n css`\n :host {\n display: block;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n canvas, img {\n position: static;\n all: initial;\n width: 100%;\n height: 100%;\n }\n `,\n ];\n\n imageRef = createRef<HTMLImageElement>();\n canvasRef = createRef<HTMLCanvasElement>();\n\n /**\n * Render version counter - increments when visual content changes.\n * Used by RenderContext to cache rendered dataURLs.\n */\n #renderVersion = 0;\n\n /**\n * Get the current render version.\n * Version increments when src or assetId changes.\n * @public\n */\n get renderVersion(): number {\n return this.#renderVersion;\n }\n\n /**\n * Whether the loaded image has an alpha channel.\n * JPEG images don't have alpha, PNG/WebP may have alpha.\n */\n #hasAlpha = true; // Default to true (preserve alpha) until we know otherwise\n\n /**\n * Get whether the image has an alpha channel.\n * Used to determine if we should encode as PNG (alpha) or JPEG (no alpha).\n * @public\n */\n get hasAlpha(): boolean {\n return this.#hasAlpha;\n }\n\n #assetId: string | null = null;\n @property({ type: String, attribute: \"asset-id\", reflect: true })\n set assetId(value: string | null) {\n this.#assetId = value;\n }\n\n get assetId() {\n return this.#assetId ?? this.getAttribute(\"asset-id\");\n }\n\n render() {\n const assetPath = this.assetPath();\n const isDirectUrl = this.isDirectUrl(assetPath);\n return isDirectUrl\n ? html`<img ${ref(this.imageRef)} src=${assetPath} />`\n : html`<canvas ${ref(this.canvasRef)}></canvas>`;\n }\n\n private isDirectUrl(src: string): boolean {\n return src.startsWith(\"http://\") || src.startsWith(\"https://\") || src.startsWith(\"data:\");\n }\n\n assetPath() {\n if (this.assetId) {\n return `${this.apiHost}/api/v1/image_files/${this.assetId}`;\n }\n if (this.isDirectUrl(this.src)) {\n return this.src;\n }\n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = this.src.startsWith(\"/\")\n ? this.src.slice(1)\n : this.src;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n // Use production API format for local files\n return `/api/v1/assets/local/image?src=${encodeURIComponent(normalizedSrc)}`;\n }\n\n get hasOwnDuration() {\n return this.hasExplicitDuration;\n }\n\n // ============================================================================\n // Image Loading - async method instead of Task\n // ============================================================================\n\n #imageLoaded = false;\n #imageLoadPromise: Promise<void> | null = null;\n #lastLoadedPath: string | null = null;\n\n /**\n * Load image from the configured source\n */\n async loadImage(signal?: AbortSignal): Promise<void> {\n const assetPath = this.assetPath();\n\n // Skip if no source\n if (!this.src && !this.assetId) {\n return;\n }\n\n // Return cached if path hasn't changed\n if (this.#imageLoaded && this.#lastLoadedPath === assetPath) {\n return;\n }\n\n // Return in-flight promise\n if (this.#imageLoadPromise && this.#lastLoadedPath === assetPath) {\n return this.#imageLoadPromise;\n }\n\n // For direct URLs, the img element handles loading\n if (this.isDirectUrl(assetPath)) {\n this.#imageLoaded = true;\n this.#lastLoadedPath = assetPath;\n return;\n }\n\n this.#lastLoadedPath = assetPath;\n this.#imageLoadPromise = this.#doLoadImage(assetPath, signal);\n\n try {\n await this.#imageLoadPromise;\n this.#imageLoaded = true;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Canvas not ready errors are expected during lifecycle\n if (error instanceof Error && error.message === \"Canvas not ready\") {\n return;\n }\n console.error(\"EFImage load error\", error);\n } finally {\n this.#imageLoadPromise = null;\n }\n }\n\n async #doLoadImage(assetPath: string, signal?: AbortSignal): Promise<void> {\n const response = await this.fetch(assetPath, { signal });\n signal?.throwIfAborted();\n \n const image = new Image();\n const blob = await response.blob();\n signal?.throwIfAborted();\n \n // Detect if image has alpha channel based on MIME type\n // JPEG images don't have alpha, PNG/WebP may have alpha\n const mimeType = blob.type.toLowerCase();\n this.#hasAlpha = !mimeType.includes(\"jpeg\") && !mimeType.includes(\"jpg\");\n \n image.src = URL.createObjectURL(blob);\n\n await new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n URL.revokeObjectURL(image.src);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n \n const abortHandler = () => {\n URL.revokeObjectURL(image.src);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n signal?.addEventListener(\"abort\", abortHandler, { once: true });\n \n image.onload = () => {\n signal?.removeEventListener(\"abort\", abortHandler);\n resolve();\n };\n image.onerror = (error) => {\n signal?.removeEventListener(\"abort\", abortHandler);\n URL.revokeObjectURL(image.src);\n reject(error);\n };\n });\n\n signal?.throwIfAborted();\n\n if (!this.canvasRef.value) throw new Error(\"Canvas not ready\");\n const ctx = this.canvasRef.value.getContext(\"2d\");\n if (!ctx) throw new Error(\"Canvas 2d context not ready\");\n this.canvasRef.value.width = image.width;\n this.canvasRef.value.height = image.height;\n ctx.drawImage(image, 0, 0);\n \n URL.revokeObjectURL(image.src);\n }\n\n /**\n * @deprecated Use FrameRenderable methods (prepareFrame, renderFrame) via FrameController instead.\n * This is a compatibility wrapper that delegates to the new system.\n */\n frameTask = createFrameTaskWrapper(this);\n\n // ============================================================================\n // FrameRenderable Implementation\n // Centralized frame control - no Lit Tasks\n // ============================================================================\n\n /**\n * Query readiness state for a given time.\n * @implements FrameRenderable\n */\n getFrameState(_timeMs: number): FrameState {\n return {\n needsPreparation: !this.#imageLoaded,\n isReady: this.#imageLoaded,\n priority: PRIORITY_IMAGE,\n };\n }\n\n /**\n * Async preparation - waits for image to load.\n * @implements FrameRenderable\n */\n async prepareFrame(_timeMs: number, signal: AbortSignal): Promise<void> {\n await this.loadImage(signal);\n signal.throwIfAborted();\n }\n\n /**\n * Synchronous render - image is already displayed via img element or canvas.\n * @implements FrameRenderable\n */\n renderFrame(_timeMs: number): void {\n // Image is already displayed - no explicit render action needed\n }\n\n // ============================================================================\n // End FrameRenderable Implementation\n // ============================================================================\n\n protected updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {\n super.updated(changedProperties);\n \n // Trigger image load when src or assetId changes\n if (changedProperties.has(\"src\") || changedProperties.has(\"assetId\")) {\n this.#imageLoaded = false;\n this.loadImage().catch(() => {});\n // Increment render version only when actual image content changes\n this.#renderVersion++;\n }\n }\n\n /**\n * Get the natural dimensions of the image.\n * Returns null if the image hasn't loaded yet.\n *\n * @public\n */\n getNaturalDimensions(): { width: number; height: number } | null {\n // For direct URLs, check img element\n const img = this.imageRef.value;\n if (img && img.naturalWidth > 0 && img.naturalHeight > 0) {\n return {\n width: img.naturalWidth,\n height: img.naturalHeight,\n };\n }\n\n // For canvas-based images, check canvas dimensions\n const canvas = this.canvasRef.value;\n if (canvas && canvas.width > 0 && canvas.height > 0) {\n return {\n width: canvas.width,\n height: canvas.height,\n };\n }\n\n return null;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-image\": EFImage;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"EFImage.js","names":["EFImage","#renderVersion","#hasAlpha","#assetId","#imageLoaded","#lastLoadedPath","#imageLoadPromise","#doLoadImage","#syncObjectFitStyles"],"sources":["../../src/elements/EFImage.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport {\n type FrameRenderable,\n type FrameState,\n createFrameTaskWrapper,\n PRIORITY_IMAGE,\n} from \"../preview/FrameController.js\";\nimport { EFSourceMixin } from \"./EFSourceMixin.js\";\nimport { EFTemporal } from \"./EFTemporal.js\";\nimport { FetchMixin } from \"./FetchMixin.js\";\n\n@customElement(\"ef-image\")\nexport class EFImage extends EFTemporal(\n EFSourceMixin(FetchMixin(LitElement), {\n assetType: \"image_files\",\n }),\n) implements FrameRenderable {\n static styles = [\n css`\n :host {\n display: block;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n canvas, img {\n width: 100%;\n height: 100%;\n object-fit: var(--object-fit, contain);\n object-position: var(--object-position, center);\n }\n `,\n ];\n\n imageRef = createRef<HTMLImageElement>();\n canvasRef = createRef<HTMLCanvasElement>();\n\n /**\n * Render version counter - increments when visual content changes.\n * Used by RenderContext to cache rendered dataURLs.\n */\n #renderVersion = 0;\n\n /**\n * Get the current render version.\n * Version increments when src or assetId changes.\n * @public\n */\n get renderVersion(): number {\n return this.#renderVersion;\n }\n\n /**\n * Whether the loaded image has an alpha channel.\n * JPEG images don't have alpha, PNG/WebP may have alpha.\n */\n #hasAlpha = true; // Default to true (preserve alpha) until we know otherwise\n\n /**\n * Get whether the image has an alpha channel.\n * Used to determine if we should encode as PNG (alpha) or JPEG (no alpha).\n * @public\n */\n get hasAlpha(): boolean {\n return this.#hasAlpha;\n }\n\n #assetId: string | null = null;\n @property({ type: String, attribute: \"asset-id\", reflect: true })\n set assetId(value: string | null) {\n this.#assetId = value;\n }\n\n get assetId() {\n return this.#assetId ?? this.getAttribute(\"asset-id\");\n }\n\n render() {\n const assetPath = this.assetPath();\n const isDirectUrl = this.isDirectUrl(assetPath);\n return isDirectUrl\n ? html`<img ${ref(this.imageRef)} src=${assetPath} />`\n : html`<canvas ${ref(this.canvasRef)}></canvas>`;\n }\n\n private isDirectUrl(src: string): boolean {\n return src.startsWith(\"http://\") || src.startsWith(\"https://\") || src.startsWith(\"data:\");\n }\n\n assetPath() {\n if (this.assetId) {\n return `${this.apiHost}/api/v1/image_files/${this.assetId}`;\n }\n if (this.isDirectUrl(this.src)) {\n return this.src;\n }\n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = this.src.startsWith(\"/\")\n ? this.src.slice(1)\n : this.src;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n // Use production API format for local files\n return `/api/v1/assets/local/image?src=${encodeURIComponent(normalizedSrc)}`;\n }\n\n get hasOwnDuration() {\n return this.hasExplicitDuration;\n }\n\n // ============================================================================\n // Image Loading - async method instead of Task\n // ============================================================================\n\n #imageLoaded = false;\n #imageLoadPromise: Promise<void> | null = null;\n #lastLoadedPath: string | null = null;\n\n /**\n * Load image from the configured source\n */\n async loadImage(signal?: AbortSignal): Promise<void> {\n const assetPath = this.assetPath();\n\n // Skip if no source\n if (!this.src && !this.assetId) {\n return;\n }\n\n // Return cached if path hasn't changed\n if (this.#imageLoaded && this.#lastLoadedPath === assetPath) {\n return;\n }\n\n // Return in-flight promise\n if (this.#imageLoadPromise && this.#lastLoadedPath === assetPath) {\n return this.#imageLoadPromise;\n }\n\n // For direct URLs, the img element handles loading\n if (this.isDirectUrl(assetPath)) {\n this.#imageLoaded = true;\n this.#lastLoadedPath = assetPath;\n return;\n }\n\n this.#lastLoadedPath = assetPath;\n this.#imageLoadPromise = this.#doLoadImage(assetPath, signal);\n\n try {\n await this.#imageLoadPromise;\n this.#imageLoaded = true;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Canvas not ready errors are expected during lifecycle\n if (error instanceof Error && error.message === \"Canvas not ready\") {\n return;\n }\n console.error(\"EFImage load error\", error);\n } finally {\n this.#imageLoadPromise = null;\n }\n }\n\n async #doLoadImage(assetPath: string, signal?: AbortSignal): Promise<void> {\n const response = await this.fetch(assetPath, { signal });\n signal?.throwIfAborted();\n \n const image = new Image();\n const blob = await response.blob();\n signal?.throwIfAborted();\n \n // Detect if image has alpha channel based on MIME type\n // JPEG images don't have alpha, PNG/WebP may have alpha\n const mimeType = blob.type.toLowerCase();\n this.#hasAlpha = !mimeType.includes(\"jpeg\") && !mimeType.includes(\"jpg\");\n console.log(`[EFImage] Loaded image: mimeType=${mimeType}, hasAlpha=${this.#hasAlpha}`);\n \n image.src = URL.createObjectURL(blob);\n\n await new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n URL.revokeObjectURL(image.src);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n \n const abortHandler = () => {\n URL.revokeObjectURL(image.src);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n signal?.addEventListener(\"abort\", abortHandler, { once: true });\n \n image.onload = () => {\n signal?.removeEventListener(\"abort\", abortHandler);\n resolve();\n };\n image.onerror = (error) => {\n signal?.removeEventListener(\"abort\", abortHandler);\n URL.revokeObjectURL(image.src);\n reject(error);\n };\n });\n\n signal?.throwIfAborted();\n\n if (!this.canvasRef.value) throw new Error(\"Canvas not ready\");\n const ctx = this.canvasRef.value.getContext(\"2d\");\n if (!ctx) throw new Error(\"Canvas 2d context not ready\");\n this.canvasRef.value.width = image.width;\n this.canvasRef.value.height = image.height;\n ctx.drawImage(image, 0, 0);\n \n URL.revokeObjectURL(image.src);\n }\n\n /**\n * @deprecated Use FrameRenderable methods (prepareFrame, renderFrame) via FrameController instead.\n * This is a compatibility wrapper that delegates to the new system.\n */\n frameTask = createFrameTaskWrapper(this);\n\n // ============================================================================\n // FrameRenderable Implementation\n // Centralized frame control - no Lit Tasks\n // ============================================================================\n\n /**\n * Query readiness state for a given time.\n * @implements FrameRenderable\n */\n getFrameState(_timeMs: number): FrameState {\n return {\n needsPreparation: !this.#imageLoaded,\n isReady: this.#imageLoaded,\n priority: PRIORITY_IMAGE,\n };\n }\n\n /**\n * Async preparation - waits for image to load.\n * @implements FrameRenderable\n */\n async prepareFrame(_timeMs: number, signal: AbortSignal): Promise<void> {\n await this.loadImage(signal);\n signal.throwIfAborted();\n }\n\n /**\n * Synchronous render - image is already displayed via img element or canvas.\n * @implements FrameRenderable\n */\n renderFrame(_timeMs: number): void {\n // Image is already displayed - no explicit render action needed\n }\n\n // ============================================================================\n // End FrameRenderable Implementation\n // ============================================================================\n\n protected updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {\n super.updated(changedProperties);\n \n // Sync object-fit styles from host element\n this.#syncObjectFitStyles();\n \n // Trigger image load when src or assetId changes\n if (changedProperties.has(\"src\") || changedProperties.has(\"assetId\")) {\n this.#imageLoaded = false;\n this.loadImage().catch(() => {});\n // Increment render version only when actual image content changes\n this.#renderVersion++;\n }\n }\n\n #syncObjectFitStyles() {\n const computedStyle = getComputedStyle(this);\n const objectFit = computedStyle.objectFit;\n const objectPosition = computedStyle.objectPosition;\n \n if (objectFit && objectFit !== 'fill') {\n this.style.setProperty('--object-fit', objectFit);\n }\n if (objectPosition) {\n this.style.setProperty('--object-position', objectPosition);\n }\n }\n\n /**\n * Get the natural dimensions of the image.\n * Returns null if the image hasn't loaded yet.\n *\n * @public\n */\n getNaturalDimensions(): { width: number; height: number } | null {\n // For direct URLs, check img element\n const img = this.imageRef.value;\n if (img && img.naturalWidth > 0 && img.naturalHeight > 0) {\n return {\n width: img.naturalWidth,\n height: img.naturalHeight,\n };\n }\n\n // For canvas-based images, check canvas dimensions\n const canvas = this.canvasRef.value;\n if (canvas && canvas.width > 0 && canvas.height > 0) {\n return {\n width: canvas.width,\n height: canvas.height,\n };\n }\n\n return null;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-image\": EFImage;\n }\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
- import * as lit28 from "lit";
1
+ import * as lit6 from "lit";
2
2
  import { LitElement } from "lit";
3
- import * as lit_html27 from "lit-html";
3
+ import * as lit_html6 from "lit-html";
4
4
 
5
5
  //#region src/elements/EFPanZoom.d.ts
6
6
  interface PanZoomTransform {
@@ -9,7 +9,7 @@ interface PanZoomTransform {
9
9
  scale: number;
10
10
  }
11
11
  declare class EFPanZoom extends LitElement {
12
- static styles: lit28.CSSResult[];
12
+ static styles: lit6.CSSResult[];
13
13
  x: number;
14
14
  y: number;
15
15
  scale: number;
@@ -89,7 +89,7 @@ declare class EFPanZoom extends LitElement {
89
89
  * @param padding - Padding factor (0-1), e.g., 0.1 = 10% padding on each side. Default: 0.05
90
90
  */
91
91
  fitToContent(padding?: number): void;
92
- render(): lit_html27.TemplateResult<1>;
92
+ render(): lit_html6.TemplateResult<1>;
93
93
  }
94
94
  //#endregion
95
95
  export { EFPanZoom, PanZoomTransform };
@@ -1,18 +1,18 @@
1
1
  import { FrameRenderable, FrameState } from "../preview/FrameController.js";
2
2
  import { ContextMixinInterface } from "../gui/ContextMixin.js";
3
- import * as lit26 from "lit";
3
+ import * as lit28 from "lit";
4
4
  import { LitElement } from "lit";
5
- import * as lit_html25 from "lit-html";
5
+ import * as lit_html27 from "lit-html";
6
6
  import * as lit_html_directives_ref3 from "lit-html/directives/ref";
7
7
 
8
8
  //#region src/elements/EFSurface.d.ts
9
9
  declare class EFSurface extends LitElement implements FrameRenderable {
10
10
  #private;
11
- static styles: lit26.CSSResult[];
11
+ static styles: lit28.CSSResult[];
12
12
  canvasRef: lit_html_directives_ref3.Ref<HTMLCanvasElement>;
13
13
  targetElement: ContextMixinInterface | null;
14
14
  target: string;
15
- render(): lit_html25.TemplateResult<1>;
15
+ render(): lit_html27.TemplateResult<1>;
16
16
  get rootTimegroup(): any;
17
17
  get currentTimeMs(): number;
18
18
  get durationMs(): number;
@@ -447,6 +447,7 @@ const EFTemporal = (superClass) => {
447
447
  */
448
448
  #frameTaskPromise = Promise.resolve();
449
449
  didBecomeRoot() {
450
+ if (this.closest?.(".ef-render-clone-container")) return;
450
451
  if (!this.playbackController) {
451
452
  this.playbackController = new PlaybackController(this);
452
453
  if (this.#loop) this.playbackController.setLoop(this.#loop);